assets/themes/j1/adapter/js/gemini.js in j1-template-2024.1.5 vs assets/themes/j1/adapter/js/gemini.js in j1-template-2024.2.0

- old
+ new

@@ -25,34 +25,37 @@ {% comment %} Liquid procedures -------------------------------------------------------------------------------- {% endcomment %} {% comment %} Set global settings -------------------------------------------------------------------------------- {% endcomment %} -{% assign environment = site.environment %} -{% assign asset_path = "/assets/themes/j1" %} +{% assign environment = site.environment %} +{% assign asset_path = "/assets/themes/j1" %} {% comment %} Process YML config data ================================================================================ {% endcomment %} {% comment %} Set config files -------------------------------------------------------------------------------- {% endcomment %} -{% assign template_config = site.data.j1_config %} -{% assign blocks = site.data.blocks %} -{% assign modules = site.data.modules %} +{% assign template_config = site.data.j1_config %} +{% assign blocks = site.data.blocks %} +{% assign modules = site.data.modules %} {% comment %} Set config data (settings only) -------------------------------------------------------------------------------- {% endcomment %} -{% assign gemini_defaults = modules.defaults.gemini.defaults %} -{% assign gemini_settings = modules.gemini.settings %} +{% assign slim_select_defaults = modules.defaults.slim_select.defaults %} +{% assign slim_select_settings = modules.slim_select.settings %} +{% assign gemini_defaults = modules.defaults.gemini.defaults %} +{% assign gemini_settings = modules.gemini.settings %} {% comment %} Set config options (settings only) -------------------------------------------------------------------------------- {% endcomment %} -{% assign gemini_options = gemini_defaults | merge: gemini_settings %} +{% assign slim_select_options = slim_select_defaults | merge: slim_select_settings %} +{% assign gemini_options = gemini_defaults | merge: gemini_settings %} {% comment %} Variables -------------------------------------------------------------------------------- {% endcomment %} -{% assign comments = gemini_options.enabled %} +{% assign comments = gemini_options.enabled %} {% comment %} Detect prod mode -------------------------------------------------------------------------------- {% endcomment %} {% assign production = false %} {% if environment == 'prod' or environment == 'production' %} @@ -82,385 +85,630 @@ // ESLint shimming // ----------------------------------------------------------------------------- /* eslint indent: "off" */ // ----------------------------------------------------------------------------- 'use strict'; -j1.adapter.gemini = (function (j1, window) { +j1.adapter.gemini = ((j1, window) => { -{% comment %} Set global variables --------------------------------------------------------------------------------- {% endcomment %} -var environment = '{{environment}}'; -var state = 'not_started'; -var leafletScript = document.createElement('script'); -var geocoderScript = document.createElement('script'); -var safetySettings = []; -var genAIError = false; -var genAIErrorType = ''; -var response = ''; -var modal_error_text = ''; -var moduleLoaded = false; -var moduleLoaded = false; -var apiKey; -var validApiKey; -var genAI; -var frontmatterOptions; -var result; -var _this; -var logger; -var logText; -var HarmCategory, HarmBlockThreshold; + {% comment %} Set global variables + ------------------------------------------------------------------------------ {% endcomment %} + var environment = '{{environment}}'; + var state = 'not_started'; + var leafletScript = document.createElement('script'); + var geocoderScript = document.createElement('script'); + var safetySettings = []; + var generationConfig = {} ; + var genAIError = false; + var genAIErrorType = ''; + var response = ''; + var modal_error_text = ''; + var modulesLoaded = false; + var textHistory = []; // Array to store the history of entered text + var historyIndex = -1; // Index to keep track of the current position in the history + var chat_prompt = {}; + var maxRetries = 3; + var logStartOnce = false; -// ----------------------------------------------------------------------- -// Module variable settings -// ----------------------------------------------------------------------- -var geminiDefaults = $.extend({}, {{gemini_defaults | replace: 'nil', 'null' | replace: '=>', ':' }}); -var geminiSettings = $.extend({}, {{gemini_settings | replace: 'nil', 'null' | replace: '=>', ':' }}); -var geminiOptions = $.extend(true, {}, geminiDefaults, geminiSettings, frontmatterOptions); + var url; + var baseUrl; + var cookie_names; + var cookie_written; + var hostname; + var auto_domain; + var check_cookie_option_domain; + var cookie_domain; + var secure; -const defaultPrompt = geminiOptions.prompt.default; -const httpError400 = geminiOptions.errors.http400; -const httpError500 = geminiOptions.errors.http500; + var gemini_model; + var apiKey; + var validApiKey; + var genAI; + var result; + var retryCount; -// ----------------------------------------------------------------------------- -// Helper functions -// ----------------------------------------------------------------------------- + var latitude; + var longitude; + var country; + var city; + var newItem; + var itemExists; + + var selectList; + var $slimSelect; + var textarea; + var promptHistoryMax; + var promptHistoryEnabled; + var promptHistoryFromCookie; + var allowPromptHistoryUpdatesOnMax; + + var _this; + var logger; + var logText; + + // values taken from API + var HarmCategory, HarmBlockThreshold; + + // date|time + var startTime; + var endTime; + var startTimeModule; + var endTimeModule; + var timeSeconds; + + var eventListenersReady; + + // --------------------------------------------------------------------------- + // module variable settings + // --------------------------------------------------------------------------- + + // create settings object from module options + // + var slimSelectDefaults = $.extend({}, {{slim_select_defaults | replace: 'nil', 'null' | replace: '=>', ':' }}); + var slimSelectSettings = $.extend({}, {{slim_select_settings | replace: 'nil', 'null' | replace: '=>', ':' }}); + var slimSelectOptions = $.extend(true, {}, slimSelectDefaults, slimSelectSettings); + + var geminiDefaults = $.extend({}, {{gemini_defaults | replace: 'nil', 'null' | replace: '=>', ':' }}); + var geminiSettings = $.extend({}, {{gemini_settings | replace: 'nil', 'null' | replace: '=>', ':' }}); + var geminiOptions = $.extend(true, {}, geminiDefaults, geminiSettings); + + const defaultPrompt = geminiOptions.prompt.default; + const httpError400 = geminiOptions.errors.http400; + const httpError500 = geminiOptions.errors.http500; + + // --------------------------------------------------------------------------- + // helper functions + // --------------------------------------------------------------------------- + + function addPromptHistoryEventListeners(slimSelectData) { + var index = 1; + slimSelectData.forEach (() => { + var span = 'opt_prompt_history_' + index; + var spanElement = document.getElementById(span); + + var dependencies_met_span_ready = setInterval (() => { + var spanElementReady = (($(spanElement).length) !== 0) ? true : false; + if (spanElementReady) { + logger.debug('\n' + 'add eventListener to: ' + span); + spanElement.addEventListener('click', spanElementEventListener); + + clearInterval(dependencies_met_span_ready); + } + }, 10); + index++; + }); // END forEach data + } // END addPromptHistoryEventListeners + + function spanElementEventListener(event) { + var optionText = event.currentTarget.nextSibling.data; + var slimData = $slimSelect.getData(); + var textHistory = []; + var chatHistory = j1.existsCookie(cookie_names.chat_prompt) + ? j1.readCookie(cookie_names.chat_prompt) + : {}; + var foundItem; + var newHistory; + var newData; + + // suppress default actions|bubble up + event.preventDefault(); + event.stopPropagation(); + + // update slimSelect data + foundItem = -1; + for (var i = 0; i < slimData.length; i++) { + if (slimData[i].text === optionText) { + foundItem = i; + break; + } + } + + if (foundItem !== -1) { + delete slimData[foundItem]; + + // create new reindexed data object + newData = Object.values(slimData); + // update the select + $slimSelect.setData(newData); + } + + // update prompt history data + foundItem = -1; + // convert chat prompt object to array + textHistory = Object.values(chatHistory); + for (var i = 0; i < textHistory.length; i++) { + if (textHistory[i] === optionText) { + foundItem = i; + break; + } + } + + if (foundItem !== -1) { + delete textHistory[foundItem]; + + // create new reindexed data object + newHistory = Object.values(textHistory); + + // remove duplicates from history + if (newHistory.length > 1) { + // create a 'Set' from the history array to automatically remove duplicates + var uniqueArray = [...new Set(newHistory)]; + newHistory = Object.values(uniqueArray); + } // END if allowHistoryDupicates + + // update the prompt history + if (promptHistoryFromCookie) { + logger.debug('\n' + 'save prompt history to cookie'); + j1.removeCookie({ + name: cookie_names.chat_prompt, + domain: auto_domain, + secure: secure + }); + + if (newHistory.length > 0) { + cookie_written = j1.writeCookie({ + name: cookie_names.chat_prompt, + data: newHistory, + secure: secure + }); + } else { + cookie_written = j1.writeCookie({ + name: cookie_names.chat_prompt, + data: {}, + secure: secure + }); + logger.info('\n' + 'spanElementEventListener, hide prompt history on last element'); + $("#prompt_history_container").hide(); + } // END if length + } // END if promptHistoryFromCookie + } + + logger.info('\n' + 'spanElementEventListener, option deleted:\n' + optionText); + + // close currently required to re-add history prompt events on next beforeOpen + $slimSelect.close(); + } // END spanElementEventListener + // Log the geolocation position function showPosition(position) { - var latitude = position.coords.latitude; - var longitude = position.coords.longitude; - console.debug("Detected geocode (lat:long): " + latitude + ':' + longitude); - } //END function showPosition + latitude = position.coords.latitude; + longitude = position.coords.longitude; + logger.debug('\n' + 'detected geocode (lat:long): ' + latitude + ':' + longitude); + } // END function showPosition + function locateCountry(position) { - const latitude = position.coords.latitude; - const longitude = position.coords.longitude; + latitude = position.coords.latitude; + longitude = position.coords.longitude; // Reverse geocode to find the country fetch(`//nominatim.openstreetmap.org/reverse?format=jsonv2&lat=${latitude}&lon=${longitude}`) .then(response => response.json()) - .then(data => { - const country = '<b>' + data.address.country; - const city = data.address.city; - $("#modal_error").html(modal_error_text + '<br>' + country); + .then((data) => { + country = data.address.country; + city = data.address.city; + $("#modal_error").html(modal_error_text + '<br>' + '<b>' + country + '</b>'); + logger.warn('\n' + 'location is not supported: ' + country + ':' + city); }) - .catch(error => { - console.warn('Error:', error); + .catch((error) => { + logger.error('\n' + 'error detect location: ' + error); }); - } //END function locateCountry + } // END function locateCountry function geoFindMe() { function success(position) { - const latitude = position.coords.latitude; - const longitude = position.coords.longitude; + latitude = position.coords.latitude; + longitude = position.coords.longitude; locateCountry(position); - } //END function success + } // END function success function error() { logger.warn('\n' + 'Unable to retrieve the location'); - } //END function error + } // END function error if (!navigator.geolocation) { logger.warn('\n' + 'Geolocation API is not supported by the browser'); } else { navigator.geolocation.getCurrentPosition(success, error); } - } //END function geoFindMe + } // END function geoFindMe async function runner() { - let input = document.getElementById("name"); + var input = document.getElementById("name"); - // For text-only input, use the gemini-pro model + // For text-only input, use the selected model const model = genAI.getGenerativeModel({ - model: "gemini-pro", - safetySettings + model: gemini_model, + safetySettings, + generationConfig }); var prompt = $('textarea#prompt').val(); - if (prompt.length == 0) { - prompt = defaultPrompt; + if (prompt.length === 0) { + // use default prompt + prompt = defaultPrompt.replace(/\s+$/g, ''); + logger.debug('\n' + 'use default prompt: ' + prompt); document.getElementById('prompt').value = prompt; } - try { - result = await model.generateContent(prompt); - } catch (e) { - var error = e.toString(); - if (error.includes("400")) { - genAIErrorType = 400; + // run a request + startTime = Date.now(); + retryCount = 1; + logger.info('\n' + 'processing request: started'); + while (retryCount <= maxRetries) { + try { + logger.debug('\n' + 'processing request: #' + retryCount + '|' + maxRetries); + result = await model.generateContent(prompt); + + // exit the loop on success + break; + } catch (e) { + var error = e.toString(); + if (error.includes('400')) { + genAIErrorType = 400; modal_error_text = httpError400; - $("#modal_error").html(modal_error_text); - logger.warn('\n' + 'Location is not supported'); - } else if (error.includes("50")) { - genAIErrorType = 500; + if (geminiOptions.detect_geo_location) { + geoFindMe(); + $("#modal_error").html(modal_error_text); + } else { + $("#modal_error").html(modal_error_text); + logger.warn('\n' + 'location not supported'); + } + } else if (error.includes('50')) { + genAIErrorType = 500; modal_error_text = httpError500; $("#modal_error").html(modal_error_text); - logger.warn('\n' + 'Service currently not available'); + logger.warn('\n' + 'service not available'); } genAIError = true; - } finally { - if (!genAIError) { - try { - response = await result.response; - } catch (e) { - logger.warn('\n' + e); - } finally { - $("#spinner").hide(); + } finally { + if (!genAIError) { + try { + logger.debug('\n' + 'collecting results ...'); + response = await result.response; + } catch (e) { + logger.warn('\n' + e); + } finally { + $("#spinner").hide(); - // Evaluate|Process feedback returned from API - var candidateRatings = geminiOptions.candidateRatings; - var responseText = ''; - var safetyRatings; - var safetyRating; - var safetyCategory; - var ratingCategory; - var ratingProbability; - var responseFinishReason; + // Evaluate|Process feedback returned from API + var candidateRatings = geminiOptions.api_options.candidateRatings; + var responseText = ''; + var safetyRatings; + var safetyRating; + var safetyCategory; + var ratingCategory; + var ratingProbability; + var responseFinishReason; - if (response.promptFeedback !== undefined) { - safetyRatings = response.promptFeedback.safetyRatings; - responseFinishReason = response.promptFeedback.blockReason; - if (responseFinishReason == 'SAFETY') { - safetyRatings.forEach(rating => { - if (rating.probability !== undefined && rating.probability !== 'NEGLIGIBLE' && rating.probability !== 'LOW') { - if (rating.category !== undefined) { - ratingCategory = rating.category; - ratingProbability = rating.probability; + if (response.promptFeedback !== undefined) { + safetyRatings = response.promptFeedback.safetyRatings; + responseFinishReason = response.promptFeedback.blockReason; + if (responseFinishReason === 'SAFETY') { + safetyRatings.forEach(rating => { + if (rating.probability !== undefined && rating.probability !== 'NEGLIGIBLE' && rating.probability !== 'LOW') { + if (rating.category !== undefined) { + ratingCategory = rating.category; + ratingProbability = rating.probability; + } } + }); + if (ratingCategory !== undefined && ratingCategory !== '' && ratingProbability !== undefined && ratingProbability !== '') { + logger.warn('\n' + 'Security issue detected, reason: ' + ratingCategory + ' = ' + ratingProbability); } - }); - if (ratingCategory !== undefined && ratingCategory !== '' && ratingProbability !== undefined && ratingProbability !== '') { - logger.warn('\n' + 'Security issue detected, reason: ' + ratingCategory + ' = ' + ratingProbability); + var ratingCategoryText = ratingCategory.replace("HARM_CATEGORY_", '').toLowerCase(); + var ratingProbabilityText = ratingProbability.toLowerCase(); + responseText = 'Response disabled due to security reasons (<b>' + ratingCategoryText + ': ' + ratingProbabilityText + '</b>). Please modify your prompt.'; } - responseText = 'Response disabled due to security reasons. You need to <b>change your prompt</b> to get proper results.'; + if (response.text !== undefined && response.text.length > 0) { + responseText = response.text; + } } - if (response.text !== undefined && response.text.length > 0) { - responseText = response.text; - } - } - if (response.candidates !== undefined) { - safetyRatings = response.candidates[0].safetyRatings; - responseFinishReason = response.candidates[0].finishReason; + if (response.candidates !== undefined) { + safetyRatings = response.candidates[0].safetyRatings; + responseFinishReason = response.candidates[0].finishReason; - if (responseFinishReason == 'STOP') { - for (const [key, value] of Object.entries(candidateRatings)) { - safetyRatings.forEach(rating => { - if (rating == 'HARM_CATEGORY_DANGEROUS_CONTENT' || rating.category == 'HARM_CATEGORY_HARASSMENT' || rating.category == 'HARM_CATEGORY_HATE_SPEECH' || rating.category == 'HARM_CATEGORY_SEXUALLY_EXPLICIT') { - if (rating.probability !== "NEGLIGIBLE") { - if (candidateRatings.HARM_CATEGORY_DANGEROUS_CONTENT == "BLOCK_NONE") { - safetyCategory = rating.category; - safetyRating = candidateRatings.HARM_CATEGORY_DANGEROUS_CONTENT; - responseText = response.candidates[0].content.parts[0].text; - } - if (candidateRatings.HARM_CATEGORY_HARASSMENT == "BLOCK_NONE") { - safetyCategory = rating.category; - safetyRating = candidateRatings.HARM_CATEGORY_HARASSMENT; - responseText = response.candidates[0].content.parts[0].text; - } - if (candidateRatings.HARM_CATEGORY_HATE_SPEECH == "BLOCK_NONE") { - safetyCategory = rating.category; - safetyRating = candidateRatings.HARM_CATEGORY_HATE_SPEECH; - responseText = response.candidates[0].content.parts[0].text; - } - if (candidateRatings.HARM_CATEGORY_SEXUALLY_EXPLICIT == "BLOCK_NONE") { - safetyCategory = rating.category; - safetyRating = candidateRatings.HARM_CATEGORY_SEXUALLY_EXPLICIT; + if (responseFinishReason === 'STOP') { + for (const [key, value] of Object.entries(candidateRatings)) { + safetyRatings.forEach(rating => { + if (rating === 'HARM_CATEGORY_DANGEROUS_CONTENT' || rating.category === 'HARM_CATEGORY_HARASSMENT' || rating.category === 'HARM_CATEGORY_HATE_SPEECH' || rating.category === 'HARM_CATEGORY_SEXUALLY_EXPLICIT') { + if (rating.probability !== "NEGLIGIBLE") { + if (candidateRatings.HARM_CATEGORY_DANGEROUS_CONTENT === "BLOCK_NONE") { + safetyCategory = rating.category; + safetyRating = candidateRatings.HARM_CATEGORY_DANGEROUS_CONTENT; + responseText = response.candidates[0].content.parts[0].text; + } + if (candidateRatings.HARM_CATEGORY_HARASSMENT === "BLOCK_NONE") { + safetyCategory = rating.category; + safetyRating = candidateRatings.HARM_CATEGORY_HARASSMENT; + responseText = response.candidates[0].content.parts[0].text; + } + if (candidateRatings.HARM_CATEGORY_HATE_SPEECH === "BLOCK_NONE") { + safetyCategory = rating.category; + safetyRating = candidateRatings.HARM_CATEGORY_HATE_SPEECH; + responseText = response.candidates[0].content.parts[0].text; + } + if (candidateRatings.HARM_CATEGORY_SEXUALLY_EXPLICIT === "BLOCK_NONE") { + safetyCategory = rating.category; + safetyRating = candidateRatings.HARM_CATEGORY_SEXUALLY_EXPLICIT; + responseText = response.candidates[0].content.parts[0].text; + } + } else { responseText = response.candidates[0].content.parts[0].text; - } - } else { - responseText = response.candidates[0].content.parts[0].text; - } //END if rating.probability - } //END if rating.category - }); //END forEach - } //END for + } // END if rating.probability + } // END if rating.category + }); // END forEach + } // END for - if (safetyCategory !== undefined) { - logger.debug('\n' + safetyCategory + ': ' + safetyRating); - } - if (response.candidates[0].finishReason == 'SAFETY') { + if (safetyCategory !== undefined) { + logger.debug('\n' + safetyCategory + ': ' + safetyRating); + } + } // END responseFinishReason STOP + + if (response.candidates[0].finishReason === 'MAX_TOKENS') { + responseText = 'Response disabled due to model settings (<b>maxOutputTokens: ' + geminiOptions.api_options.generationConfig.maxOutputTokens + '</b>). You need to increase your settings to get full response.'; + } // END responseFinishReason MAX_TOKENS + + if (response.candidates[0].finishReason === 'SAFETY') { responseText = 'Response disabled due to security reasons. You need to <b>change your prompt</b> to get proper results.'; + console.warn('Response disabled due to security reasons'); + } // END responseFinishReason SAFETY + + if (response.candidates[0].finishReason === 'RECITATION') { + responseText = 'Response flagged "RECITATION". Resposne currently not supported'; + console.warn('finishReason "RECITATION" currently not supported'); + } // END responseFinishReason RECITATION + + if (response.candidates[0].finishReason === 'OTHER') { + responseText = 'Response disabled due to unknown reasons.'; + console.warn('Response disabled due to unknown reasons'); + } // END responseFinishReason OTHER + + } // END if response.candidates + + if (responseText.length > 0) { + // Set|Show UI elements + if (responseText.length < geminiOptions.api_options.responseLengthMin) { + logger.warn('\n' + 'Response generated too short: <' + geminiOptions.api_options.responseLengthMin + ' characters'); + document.getElementById('md_result').innerHTML = 'Response generated too short (less than ' + geminiOptions.api_options.responseLengthMin + ' characters). Please re-run the generation for better results'; + } else { + document.getElementById('md_result').innerHTML = marked.parse(responseText); } - } //END if finishReason - } + $("#result").show(); + $("#response").show(); + } // END responseText length + } // END finally + } else { + if (retryCount === 3) { + logger.debug('\n' + 'requests failed after max retries: ' + maxRetries); - if (responseText.length > 0) { - // Set|Show UI elements - if (responseText.length < geminiOptions.responseLengthMin) { - logger.warn('\n' + 'Response generated too short: <' + geminiOptions.responseLengthMin + ' characters'); - document.getElementById('md_result').innerHTML = 'Response generated too short (less than ' + geminiOptions.responseLengthMin + ' characters). Please re-run the generation for better results'; - } else { - document.getElementById('md_result').innerHTML = marked.parse(responseText); + $("#spinner").hide(); + if (geminiOptions.detectGeoLocation) { + geoFindMe(); } - $("#result").show(); - $("#response").show(); + setTimeout (() => { + $('#confirmError').modal('show'); + }, 1000); } - } //END finally - } else { - if (geminiOptions.detectGeoLocation) { - geoFindMe(); - } - $("#spinner").hide(); - setTimeout (function() { - $('#errorModal').modal('show'); - }, 1000); - } //END else - } //END finally - } //END async function runner() + // increment retry counter + retryCount++; + } // END else + } // END finally + } // END while (retry) + endTime = Date.now(); + logger.debug('\n' + 'request execution time: ' + (endTime-startTime) + 'ms'); + logger.info('\n' + 'processing request: finished'); + + } // END async function runner() + // --------------------------------------------------------------------------- - // Main object + // main // --------------------------------------------------------------------------- + // return { // ------------------------------------------------------------------------- - // init() - // adapter initializer + // module initializer // ------------------------------------------------------------------------- - init: function (options) { - var logStartOnce = false; + init: (options) => { // ----------------------------------------------------------------------- - // Default module settings + // default module settings // ----------------------------------------------------------------------- var settings = $.extend({ module_name: 'j1.adapter.gemini', generated: '{{site.time}}' }, options); // ----------------------------------------------------------------------- - // Module variable settings + // module variable settings // ----------------------------------------------------------------------- + _this = j1.adapter.gemini; + logger = log4javascript.getLogger('j1.adapter.gemini'); + cookie_names = j1.getCookieNames(); + url = new liteURL(window.location.href); + baseUrl = url.origin; + hostname = url.hostname; + auto_domain = hostname.substring(hostname.lastIndexOf('.', hostname.lastIndexOf('.') - 1) + 1); + secure = (url.protocol.includes('https')) ? true : false; + promptHistoryEnabled = geminiOptions.prompt_history_enabled; + promptHistoryFromCookie = geminiOptions.prompt_history_from_cookie; - // create settings object from frontmatter - frontmatterOptions = options != null ? $.extend({}, options) : {}; + var data; + var option; - _this = j1.adapter.gemini; - logger = log4javascript.getLogger('j1.adapter.gemini'); - - // Module loader + // module loader _this.loadModules(); - // UI loader + // ui loader _this.loadUI(); - // Module initializer - var dependencies_met_page_ready = setInterval (function (options) { - var pageState = $('#no_flicker').css("display"); - var pageVisible = (pageState == 'block') ? true : false; - var uiLoaded = (j1.xhrDOMState['#gemini_ui'] == 'success') ? true : false; + // ----------------------------------------------------------------------- + // module initializer + // ----------------------------------------------------------------------- + var dependencies_met_page_ready = setInterval (() => { + var pageState = $('#content').css("display"); + var pageVisible = (pageState === 'block') ? true : false; + var j1CoreFinished = (j1.getState() === 'finished') ? true : false; +// var slimSelectFinished = (j1.adapter.slimSelect.getState() === 'finished') ? true : false; + var slimSelectFinished = (Object.keys(j1.adapter.slimSelect.select).length) ? true : false; + var uiLoaded = (j1.xhrDOMState['#gemini_ui'] === 'success') ? true : false; - if (!logStartOnce) { + // check page ready state + if (j1CoreFinished && pageVisible && slimSelectFinished && uiLoaded && modulesLoaded) { + startTimeModule = Date.now(); + _this.setState('started'); - logger.info('\n' + 'set module state to: ' + _this.getState()); - logger.info('\n' + 'module is being initialized'); - logStartOnce = true; - } + logger.debug('\n' + 'set module state to: ' + _this.getState()); + logger.info('\n' + 'initializing module: started'); - if (j1.getState() === 'finished' && pageVisible && moduleLoaded && uiLoaded) { + if (!validApiKey) { + logger.warn('\n' + 'Invalid API key detected: ' + apiKey); + logger.debug('\n' + 'disable|hide all UI buttons'); + // disable all UI buttons + $("#send").hide(); + $("#reset").hide(); + $("#clear").hide(); + } - // Initialize|Hide UI Components + // initialize|hide Chatbot UI $("#gemini_ui_container").show(); $("#spinner").hide(); $("#response").hide(); - // Initialize|Empty the prompt (textarea) - document.getElementById('prompt').value = ''; + // get|clear textarea element (prompt) + textarea = document.getElementById(geminiOptions.prompt_id); + textarea.value = ''; - if (!validApiKey) { - logger.warn('\n' + 'Invalid API key detected: ' + apiKey); - $("#send").hide(); - $("#reset").hide(); - } + var dependencies_met_select_ready = setInterval(() => { + var selectState = $('#container_prompt_history_select_wrapper').length; + var selectReady = (selectState > 0) ? true : false; - const sendButton = document.getElementById('send'); - sendButton.addEventListener('click', (event) => { - // Prevent default actions - event.preventDefault(); + if (selectReady) { + logger.debug('\n' + 'initializing select data'); - // Clear UI elements - document.getElementById('md_result').innerHTML = ''; - $("#result").hide(); - $("#spinner").show(); + // initialize history array from cookie + if (promptHistoryEnabled && promptHistoryFromCookie) { + // get slimSelect object for the history (placed by slimSelect adapter) + // selectList = document.getElementById('prompt_history'); + $slimSelect = j1.adapter.slimSelect.select[geminiOptions.prompt_history_id]; - // Run main processing - runner(); - }); //END sendButton (click) + // limit the prompt history + promptHistoryMax = geminiOptions.prompt_history_max; - // Clear input form|spinner|responses - const resetButton = document.getElementById('reset'); - resetButton.addEventListener('click', (event) => { - // Prevent default actions - event.preventDefault(); - document.getElementById("prompt").value = ''; - document.getElementById("response").value = ''; - $("#spinner").hide(); - $("#response").hide(); - }); //END resetButton (click) + // allow|reject history updates if promptHistoryMax reached + allowPromptHistoryUpdatesOnMax = geminiOptions.allow_prompt_history_updates_on_max; - _this.setState('finished'); - logger.debug('\n' + 'state: ' + _this.getState()); - logger.info('\n' + 'module initialized successfully'); + logger.debug('\n' + 'read prompt history from cookie'); + var data = []; + var option = {}; + chat_prompt = j1.existsCookie(cookie_names.chat_prompt) + ? j1.readCookie(cookie_names.chat_prompt) + : {}; - clearInterval(dependencies_met_page_ready); - } - }, 10); + // convert chat prompt object to array + textHistory = Object.values(chat_prompt); - }, // END init + // remove duplicates from history + if (textHistory.length > 1) { + var textHistoryLenght = textHistory.length; + var uniqueArray = [...new Set(textHistory)]; // create a 'Set' from the history array to automatically remove duplicates - // ------------------------------------------------------------------------- - // messageHandler() - // manage messages send from other J1 modules - // ------------------------------------------------------------------------- - messageHandler: function (sender, message) { - var json_message = JSON.stringify(message, undefined, 2); + textHistory = uniqueArray; + if (textHistoryLenght > textHistory.length) { + logger.debug('\n' + 'removed duplicates from history array: ' + (textHistoryLenght - textHistory.length) + ' element|s'); + } + } // END if !allowHistoryDupicates - logText = '\n' + 'received message from ' + sender + ': ' + json_message; - logger.debug(logText); + // update|set slimSelect data elements + var index = 1; + var data = []; + var option = {}; + var html; + textHistory.forEach((historyText) => { + html = '<span id="opt_' + geminiOptions.prompt_history_id + '_' + index + '" class="ss-option-delete">' + '<i class="mdib mdib-close mdib-16px ml-1 mr-2"></i></span>' + historyText; + option = { + text: historyText, + html: html, + display: true, + selected: false, + disabled: false + } + data.push(option); + index++ + }); // END forEach + $slimSelect.setData(data); - // ----------------------------------------------------------------------- - // Process commands|actions - // ----------------------------------------------------------------------- - if (message.type === 'command' && message.action === 'module_initialized') { - // - // Place handling of command|action here - // - logger.info('\n' + message.text); - } + // display history container + if (textHistory.length > 0) { + $("#prompt_history_container").show(); + } - // - // Place handling of other command|action here - // + // ------------------------------------------------------------- + // setup Slim select eventHandlers + // ------------------------------------------------------------- + // + _this.setupSlimSelectEventHandlers(); - return true; - }, // END messageHandler + } else { + // disable|hide clear history button + $("#clear").hide(); + } // if promptHistoryEnabled - // ------------------------------------------------------------------------- - // setState() - // Sets the current (processing) state of the module - // ------------------------------------------------------------------------- - setState: function (stat) { - _this.state = stat; - }, // END setState + clearInterval(dependencies_met_select_ready); + } // END if modules loaded + }, 10); // END dependencies_met_select_ready - // ------------------------------------------------------------------------- - // getState() - // Returns the current (processing) state of the module - // ------------------------------------------------------------------------- - getState: function () { - return _this.state; - }, // END getState + // ------------------------------------------------------------------- + // setup UI button eventHandlers + // ------------------------------------------------------------------- + // + _this.setupUIButtonEventHandlers() + _this.setState('finished'); + logger.debug('\n' + 'state: ' + _this.getState()); + logger.info('\n' + 'initializing module: finished'); + + endTimeModule = Date.now(); + logger.info('\n' + 'module initializing time: ' + (endTimeModule-startTimeModule) + 'ms'); + + clearInterval(dependencies_met_page_ready); + } // END slimSelectFinished && uiLoaded && modulesLoaded + }, 10); // END dependencies_met_page_ready + }, // END init + // ------------------------------------------------------------------------- // loadModules() - // Module loader + // load required modules // ------------------------------------------------------------------------- - loadModules: function () { + loadModules: () => { - if (geminiOptions.detectGeoLocation) { + if (geminiOptions.detect_geo_location) { leafletScript.async = true; leafletScript.type = "script"; leafletScript.id = 'leaflet-api'; leafletScript.src = '//unpkg.com/leaflet/dist/leaflet.js' document.head.appendChild(leafletScript); @@ -470,70 +718,456 @@ geocoderScript.id = 'geocoder-api'; geocoderScript.src = '//unpkg.com/leaflet-control-geocoder/dist/Control.Geocoder.js' document.head.appendChild(geocoderScript); } + // https://github.com/google/generative-ai-js/blob/main/docs/reference/generative-ai.md import('//esm.run/@google/generative-ai') .then((module) => { // Module is imported successfully - apiKey = geminiOptions.apiKey; + logger = log4javascript.getLogger('j1.adapter.gemini'); + apiKey = geminiOptions.api_options.apiKey; validApiKey = (apiKey.includes('your-')) ? false : true; genAI = new module.GoogleGenerativeAI(apiKey); HarmCategory = module.HarmCategory; HarmBlockThreshold = module.HarmBlockThreshold; + gemini_model = geminiOptions.api_options.model; + generationConfig = { + candidateCount: geminiOptions.api_options.generationConfig.candidateCount, + maxOutputTokens: geminiOptions.api_options.generationConfig.maxOutputTokens, + temperature: geminiOptions.api_options.generationConfig.temperature, + topK: geminiOptions.api_options.generationConfig.topK, + topP: geminiOptions.api_options.generationConfig.topP + }; + safetySettings = [ { category: HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT, - threshold: geminiOptions.safetyRatings.HARM_CATEGORY_DANGEROUS_CONTENT + threshold: geminiOptions.api_options.safetyRatings.HARM_CATEGORY_DANGEROUS_CONTENT }, { category: HarmCategory.HARM_CATEGORY_HARASSMENT, - threshold: geminiOptions.safetyRatings.HARM_CATEGORY_HARASSMENT + threshold: geminiOptions.api_options.safetyRatings.HARM_CATEGORY_HARASSMENT }, { category: HarmCategory.HARM_CATEGORY_HATE_SPEECH, - threshold: geminiOptions.safetyRatings.HARM_CATEGORY_HATE_SPEECH + threshold: geminiOptions.api_options.safetyRatings.HARM_CATEGORY_HATE_SPEECH }, { category: HarmCategory.HARM_CATEGORY_SEXUALLY_EXPLICIT, - threshold: geminiOptions.safetyRatings.HARM_CATEGORY_SEXUALLY_EXPLICIT + threshold: geminiOptions.api_options.safetyRatings.HARM_CATEGORY_SEXUALLY_EXPLICIT } ]; - console.debug('gemini: Importing module: successful'); - moduleLoaded = true; + logger.debug('\n' + 'Importing Gemini module: successful'); + modulesLoaded = true; }) .catch((error) => { + logger = log4javascript.getLogger('j1.adapter.gemini'); // An error occurred during module import - console.warn('gemini: Importing module failed: ', error); + logger.warn('\n' + 'Importing Gemini module failed: ' + error); }); }, // END loadModules // ------------------------------------------------------------------------- // loadUI() // UI loader // ------------------------------------------------------------------------- - loadUI: function () { + loadUI: () => { j1.loadHTML ({ xhr_container_id: geminiOptions.xhr_container_id, xhr_data_path: geminiOptions.xhr_data_path, xhr_data_element: geminiOptions.xhr_data_element }, 'j1.adapter.gemini', 'null' ); - var dependencies_met_data_loaded = setInterval(function() { - if (j1.xhrDOMState['#gemini_ui'] == 'success') { - console.debug('gemini: Loading UI: successful'); + var dependencies_met_data_loaded = setInterval(() => { + if (j1.xhrDOMState['#gemini_ui'] === 'success') { + logger.debug('\n' + 'Loading UI: successful'); + clearInterval(dependencies_met_data_loaded); } // END if xhrDOMState }, 10); - } // END loadUI + }, // END loadUI - }; // END return + // ------------------------------------------------------------------------- + // setupSlimSelectEventHandlers() + // sel all used select events + // see: https://slimselectjs.com/ + // ------------------------------------------------------------------------- + setupSlimSelectEventHandlers: () => { + var select = document.getElementById(geminiOptions.prompt_history_id); + var $select = select.slim; + var slimValues; + var data; + var prompt; + + $select.events.beforeOpen = (e) => { + // get all options + const slimValues = $select.getData(); + eventListenersReady = false; + + logger.debug('\n' + 'slimSelect.beforeOpen, processing: started'); + + // re-read current history from cookie for initial values + if (promptHistoryFromCookie) { + var chatHistory = j1.existsCookie(cookie_names.chat_prompt) + ? j1.readCookie(cookie_names.chat_prompt) + : {}; + + // set textHistory array + textHistory = Object.values(chatHistory); + + // create|set current slimSelect data elements + var index = 1; + var data = []; + var option = {}; + var html; + textHistory.forEach ((historyText) => { + html = '<span id="opt_' + geminiOptions.prompt_history_id + '_' + index + '" class="ss-option-delete">' + '<i class="mdib mdib-close mdib-16px ml-1 mr-2"></i></span>' + historyText; + option = { + text: historyText, + html: html, + display: true, + selected: false, + disabled: false + } + data.push(option); + index++; + }); // END forEach + $slimSelect.setData(data); + + } // END re-read current history from cookie + + // set prompt history EventListeners (for option deletion) + if (slimValues.length) { + logger.debug('\n' + 'slimSelect.beforeOpen, number of eventListeners to process: #' + slimValues.length); + addPromptHistoryEventListeners(slimValues); + } + + // wait until prompt history eventListener|s is|are placed + var listenerIndex = 1; + slimValues.forEach( () => { + var span = 'opt_prompt_history_' + listenerIndex; + var spanElement = document.getElementById(span); + var dependencies_met_listeners_ready = setInterval (() => { + var spanElementReady = (($(spanElement).length) !== 0) ? true : false; + if (spanElementReady) { + if (listenerIndex === slimValues.length) { + eventListenersReady = true; + logger.debug('\n' + 'slimSelect.beforeOpen, all eventListeners ready'); + } // END if listenerIndex + } // END if spanElementReady + if (!eventListenersReady) { + listenerIndex++; + } else { + clearInterval(dependencies_met_listeners_ready); + } + }, 10); + }); // END forEach data + + var dependencies_beforeOpen_met_ready = setInterval (() => { + if (eventListenersReady) { + logger.debug('\n' + 'slimSelect.beforeOpen, processing: finished'); + + clearInterval(dependencies_beforeOpen_met_ready); + } + }, 10); + } // END event beforeOpen + + $select.events.afterClose = (e) => { + // get selected value (NOTE: one||no selection possible) + const slimValue = $select.getSelected(); + + // set prompt on selection + if (slimValue.length) { + prompt = slimValue[0]; + document.getElementById('prompt').value = prompt; + logger.debug('\n' + 'slimSelect.afterClose, selection from history: ' + prompt); + } else { + logger.debug('\n' + 'slimSelect.afterClose, selection from history: empty'); + document.getElementById('prompt').value = ''; + } + + // remove selection from select + $slimSelect.setSelected('', false); + } // END event afterClose + + }, // END setupSlimSelectEventHandlers() + + // ------------------------------------------------------------------------- + // setupUIButtonEventHandlers()) + // add events for all history elements for deletion + // ------------------------------------------------------------------------- + setupUIButtonEventHandlers: () => { + + // send request to generate results + const sendButton = document.getElementById('{{gemini_options.buttons.generate.id}}'); + sendButton.addEventListener('click', (event) => { + // suppress default actions|bubble up + event.preventDefault(); + event.stopPropagation(); + + if (promptHistoryEnabled) { + var historySet = false; + + // re-read current history from cookie for initial values + if (promptHistoryFromCookie) { + var chatHistory = j1.existsCookie(cookie_names.chat_prompt) + ? j1.readCookie(cookie_names.chat_prompt) + : {}; + + // set textHistory array + textHistory = Object.values(chatHistory); + } // END re-read current history from cookie + + // set initial prompt from input (textarea) + if (textarea.value.length === 0) { + // use default prompt + prompt = defaultPrompt.replace(/\s+$/g, ''); + logger.debug('\n' + 'sendButton, use default prompt: ' + prompt); + } else { + prompt = textarea.value.replace(/\s+$/g, ''); + } + + // check if current prompt alreay exists in history + index = textHistory.indexOf(prompt); + itemExists = (index !== -1) ? true : false; + if (itemExists) { + logText = '\n' + `sendButton, prompt: "${prompt}"\n` + `already exists in history at index: ${index}`; + logger.debug(logText); + } + + // update history on promptHistoryMax + if (textHistory.length === promptHistoryMax && allowPromptHistoryUpdatesOnMax && !itemExists && !historySet) { + // place the CURRENT history element FIRST for replacement + textHistory.reverse(); + if (textarea.value.length > 0) { + // cleanup textarea value for trailing whitespaces + newItem = textarea.value.replace(/\s+$/g, ''); + } else if (textarea.value.length === 0) { + // use default prompt + newItem = defaultPrompt.replace(/\s+$/g, ''); + logger.debug('\n' + 'sendButton, use default prompt:\n' + newItem); + } + + logger.debug('\n' + 'sendButton, update item in history:\n' + textHistory[0]); + // replace FIRST history element by NEW item + textHistory[0] = newItem; + logger.debug('\n' + 'sendButton, add new item to history:\n' + textHistory[0]); + + historySet = true; + } // END update history on promptHistoryMax + + // add new item to history + if (textHistory.length < promptHistoryMax && !itemExists && !historySet) { + if (textarea.value.length > 0) { + // cleanup textarea value for trailing whitespaces + newItem = textarea.value.replace(/\s+$/g, ''); + } else if (textarea.value.length === 0) { + // use default prompt + newItem = defaultPrompt.replace(/\s+$/g, ''); + logger.debug('\n' + 'sendButton, use default prompt:\n' + newItem); + } + logger.debug('\n' + 'sendButton, add new item to history:\n' + newItem); + textHistory.push(newItem); + + historySet = true; + } // END add new item to history + + // failsafe, cleanup history + if (textHistory.length > 0) { + // cleanup|add selected value + var p = 0; + textHistory.forEach ((elm) => { + prompt = elm.replace(/\s+$/g, ''); + textHistory[p] = prompt; + p++; + }); // END forEach + logger.debug('\n' + 'sendButton, cleaned history for trailing whitespaces'); + } // END failsafe, cleanup history + + // remove duplicates from history + if (textHistory.length > 1) { + var textHistoryLenght = textHistory.length; + var uniqueArray = [...new Set(textHistory)]; // create a 'Set' from the history array to automatically remove duplicates + + textHistory = uniqueArray; + if (textHistoryLenght > textHistory.length) { + logger.debug('\n' + 'sendButton, removed duplicates from history array: ' + (textHistoryLenght - textHistory.length) + ' element|s'); + } + } // END remove duplicates from history + + // create|set slimSelect data elements + var index = 1; + var data = []; + var option = {}; + var html; + textHistory.forEach ((historyText) => { + html = '<span id="opt_' + geminiOptions.prompt_history_id + '_' + index + '" class="ss-option-delete">' + '<i class="mdib mdib-close mdib-16px ml-1 mr-2"></i></span>' + historyText; + option = { + text: historyText, + html: html, + display: true, + selected: false, + disabled: false + } + data.push(option); + index++; + }); // END forEach + $slimSelect.setData(data); + // END create|set slimSelect data elements + + // display history container + if (textHistory.length > 0) { + $("#prompt_history_container").show(); + } + + // write current history to cookie + if (promptHistoryFromCookie) { + logger.debug('\n' + 'sendButton, save prompt history to cookie'); + j1.removeCookie({ + name: cookie_names.chat_prompt, + domain: auto_domain, + secure: secure + }); + cookie_written = j1.writeCookie({ + name: cookie_names.chat_prompt, + data: textHistory, + secure: secure + }); + } // END write current history to cookie + } // END if promptHistoryEnabled + + // clear results + document.getElementById('md_result').innerHTML = ''; + $("#result").hide(); + $("#spinner").show(); + + // call Gemini API for processing + runner(); + }); // END click sendButton + + // clear input prompt and the spinner|responses + const resetButton = document.getElementById('{{gemini_options.buttons.reset.id}}'); + resetButton.addEventListener('click', (event) => { + // suppress default actions|bubble up + event.preventDefault(); + event.stopPropagation(); + + logger.debug('\n' + 'resetButton, clear input prompt|response'); + document.getElementById("prompt").value = ''; + document.getElementById("response").value = ''; + $("#spinner").hide(); + $("#response").hide(); + }); // END click resetButton + + // Clear history|cookie + const clearButton = document.getElementById('{{gemini_options.buttons.clear.id}}'); + clearButton.addEventListener('click', (event) => { + // suppress default actions|bubble up + event.preventDefault(); + event.stopPropagation(); + + logStartOnce = false; + $('#clearHistory').modal('show'); + + const confirmClearHistory = document.getElementById('clearHistory'); + const accecptClearHistory = document.getElementById('accecptClearHistory'); + const dismissClearHistory = document.getElementById('dismissClearHistory'); + + accecptClearHistory.addEventListener('click', (event) => { + logStartOnce = false; + + // suppress default actions|bubble up + event.preventDefault(); + event.stopPropagation(); + + // clear history + if (!logStartOnce) { + logger.warn('\n' + 'resetButton, perform clearHistory'); + logStartOnce = true; + } + + // write empty history to cookie + textHistory = []; + if (promptHistoryFromCookie) { + j1.removeCookie({ + name: cookie_names.chat_prompt, + domain: auto_domain, + secure: secure + }); + cookie_written = j1.writeCookie({ + name: cookie_names.chat_prompt, + data: {}, + secure: secure + }); + } + $("#prompt_history_container").hide(); + }); // END click accecptClearHistory + + // skip clear history + dismissClearHistory.addEventListener('click', (event) => { + // suppress default actions|bubble up + event.preventDefault(); + event.stopPropagation(); + + logger.debug('\n' + 'resetButton, skipped clearHistory'); + }); // END click dismissClearHistoryButton + + }); // END click clearButton + }, // END setupUIButtonEventHandlers + + // ------------------------------------------------------------------------- + // messageHandler() + // manage messages send from other J1 modules + // ------------------------------------------------------------------------- + messageHandler: (sender, message) => { + var json_message = JSON.stringify(message, undefined, 2); + + logText = '\n' + 'received message from ' + sender + ': ' + json_message; + logger.debug(logText); + + // ----------------------------------------------------------------------- + // process commands|actions + // ----------------------------------------------------------------------- + if (message.type === 'command' && message.action === 'module_initialized') { + + // + // place handling of command|action here + // + + logger.info('\n' + message.text); + } + + // + // place handling of other command|action here + // + + return true; + }, // END messageHandler + + // ------------------------------------------------------------------------- + // setState() + // sets the current (processing) state of the module + // ------------------------------------------------------------------------- + setState: (stat) => { + _this.state = stat; + }, // END setState + + // ------------------------------------------------------------------------- + // getState() + // Returns the current (processing) state of the module + // ------------------------------------------------------------------------- + getState: () => { + return _this.state; + } // END getState + + }; // END main (return) })(j1, window); {% endcapture %} {% if production %} {{ cache | minifyJS }}