From 0f21eabb6ec72b93bc6479dc7ff81a56c1b02735 Mon Sep 17 00:00:00 2001 From: based Date: Sun, 20 Aug 2023 01:20:42 +1000 Subject: [PATCH 01/12] AI21 Adapter + Tokenization implementation --- public/index.html | 57 ++++++++++-- public/script.js | 10 ++- public/scripts/RossAscends-mods.js | 1 + public/scripts/openai.js | 137 +++++++++++++++++++++++++++-- public/scripts/secrets.js | 2 + server.js | 95 ++++++++++++++++++++ 6 files changed, 286 insertions(+), 16 deletions(-) diff --git a/public/index.html b/public/index.html index 1fbc98f55..53abced5b 100644 --- a/public/index.html +++ b/public/index.html @@ -654,7 +654,7 @@ Max prompt cost: Unknown
-
+
Temperature
@@ -669,7 +669,7 @@
-
+
Frequency Penalty
@@ -684,7 +684,7 @@
-
+
Presence Penalty
@@ -699,7 +699,22 @@
-
+
+
+ Count Penalty +
+
+
+ +
+
+
+ select +
+
+
+
+
Top K
@@ -714,7 +729,7 @@
-
+
Top P
@@ -1418,6 +1433,14 @@ Helps the model to associate messages in group chats. Names must only contain letters or numbers without whitespaces.
+
+ +
+ Use the appropriate tokenizer for Jurassic models, which is more efficient than GPT's. +
+
Quick Edit @@ -1618,7 +1641,7 @@ - +
@@ -1804,6 +1827,7 @@ +

OpenAI API key

@@ -1972,6 +1996,27 @@
+
+

AI21 API Key

+
+ + +
+
+ For privacy reasons, your API key will be hidden after you reload the page. +
+
+

AI21 Model

+ +
+
+
diff --git a/public/script.js b/public/script.js index 634eae314..4eb1695f1 100644 --- a/public/script.js +++ b/public/script.js @@ -2131,7 +2131,7 @@ function baseChatReplace(value, name1, name2) { } function isStreamingEnabled() { - return ((main_api == 'openai' && oai_settings.stream_openai && oai_settings.chat_completion_source !== chat_completion_sources.SCALE) + return ((main_api == 'openai' && oai_settings.stream_openai && oai_settings.chat_completion_source !== chat_completion_sources.SCALE && oai_settings.chat_completion_source !== chat_completion_sources.AI21) || (main_api == 'kobold' && kai_settings.streaming_kobold && kai_settings.can_use_streaming) || (main_api == 'novel' && nai_settings.streaming_novel) || (main_api == 'textgenerationwebui' && textgenerationwebui_settings.streaming)) @@ -4738,6 +4738,7 @@ function changeMainAPI() { case chat_completion_sources.WINDOWAI: case chat_completion_sources.CLAUDE: case chat_completion_sources.OPENAI: + case chat_completion_sources.AI21: default: setupChatCompletionPromptManager(oai_settings); break; @@ -7178,6 +7179,11 @@ function connectAPISlash(_, text) { source: 'openrouter', button: '#api_button_openai', }, + 'ai21': { + selected: 'openai', + source: 'ai21', + button: '#api_button_openai', + } }; const apiConfig = apiMap[text]; @@ -7396,7 +7402,7 @@ $(document).ready(function () { } registerSlashCommand('dupe', DupeChar, [], "– duplicates the currently selected character", true, true); - registerSlashCommand('api', connectAPISlash, [], "(kobold, horde, novel, ooba, oai, claude, windowai) – connect to an API", true, true); + registerSlashCommand('api', connectAPISlash, [], "(kobold, horde, novel, ooba, oai, claude, windowai, ai21) – connect to an API", true, true); registerSlashCommand('impersonate', doImpersonate, ['imp'], "- calls an impersonation response", true, true); registerSlashCommand('delchat', doDeleteChat, [], "- deletes the current chat", true, true); registerSlashCommand('closechat', doCloseChat, [], "- closes the current chat", true, true); diff --git a/public/scripts/RossAscends-mods.js b/public/scripts/RossAscends-mods.js index 87abe5569..2ddd81880 100644 --- a/public/scripts/RossAscends-mods.js +++ b/public/scripts/RossAscends-mods.js @@ -478,6 +478,7 @@ function RA_autoconnect(PrevApi) { || (secret_state[SECRET_KEYS.SCALE] && oai_settings.chat_completion_source == chat_completion_sources.SCALE) || (oai_settings.chat_completion_source == chat_completion_sources.WINDOWAI) || (secret_state[SECRET_KEYS.OPENROUTER] && oai_settings.chat_completion_source == chat_completion_sources.OPENROUTER) + || (secret_state[SECRET_KEYS.AI21] && oai_settings.chat_completion_source == chat_completion_sources.AI21) ) { $("#api_button_openai").click(); } diff --git a/public/scripts/openai.js b/public/scripts/openai.js index 188a70777..827ac5a37 100644 --- a/public/scripts/openai.js +++ b/public/scripts/openai.js @@ -113,9 +113,13 @@ const scale_max = 7900; // Probably more. Save some for the system prompt define const claude_max = 8000; // We have a proper tokenizer, so theoretically could be larger (up to 9k) const palm2_max = 7500; // The real context window is 8192, spare some for padding due to using turbo tokenizer const claude_100k_max = 99000; +let ai21_max = 9200; //can easily fit 9k gpt tokens because j2's tokenizer is efficient af const unlocked_max = 100 * 1024; const oai_max_temp = 2.0; -const claude_max_temp = 1.0; +const claude_max_temp = 1.0; //same as j2 +const j2_max_topk = 10.0; +const j2_max_freq = 5.0; +const j2_max_pres = 5.0; const openrouter_website_model = 'OR_Website'; let biasCache = undefined; @@ -161,13 +165,26 @@ export const chat_completion_sources = { CLAUDE: 'claude', SCALE: 'scale', OPENROUTER: 'openrouter', + AI21: 'ai21', }; +const prefixMap = selected_group ? { + assistant: "", + user: "", + system: "OOC: " + } + : { + assistant: "{{char}}:", + user: "{{user}}:", + system: "" + }; + const default_settings = { preset_settings_openai: 'Default', temp_openai: 0.9, freq_pen_openai: 0.7, pres_pen_openai: 0.7, + count_pen: 0.0, top_p_openai: 1.0, top_k_openai: 0, stream_openai: false, @@ -188,6 +205,7 @@ const default_settings = { wi_format: default_wi_format, openai_model: 'gpt-3.5-turbo', claude_model: 'claude-instant-v1', + ai21_model: 'j2-ultra', windowai_model: '', openrouter_model: openrouter_website_model, jailbreak_system: false, @@ -199,6 +217,7 @@ const default_settings = { show_external_models: false, proxy_password: '', assistant_prefill: '', + use_ai21_tokenizer: false, }; const oai_settings = { @@ -206,6 +225,7 @@ const oai_settings = { temp_openai: 1.0, freq_pen_openai: 0, pres_pen_openai: 0, + count_pen: 0.0, top_p_openai: 1.0, top_k_openai: 0, stream_openai: false, @@ -226,6 +246,7 @@ const oai_settings = { wi_format: default_wi_format, openai_model: 'gpt-3.5-turbo', claude_model: 'claude-instant-v1', + ai21_model: 'j2-ultra', windowai_model: '', openrouter_model: openrouter_website_model, jailbreak_system: false, @@ -237,6 +258,7 @@ const oai_settings = { show_external_models: false, proxy_password: '', assistant_prefill: '', + use_ai21_tokenizer: false, }; let openai_setting_names; @@ -967,6 +989,8 @@ function getChatCompletionModel() { return ''; case chat_completion_sources.OPENROUTER: return oai_settings.openrouter_model !== openrouter_website_model ? oai_settings.openrouter_model : null; + case chat_completion_sources.AI21: + return oai_settings.ai21_model; default: throw new Error(`Unknown chat completion source: ${oai_settings.chat_completion_source}`); } @@ -1048,10 +1072,19 @@ async function sendOpenAIRequest(type, openai_msgs_tosend, signal) { const isClaude = oai_settings.chat_completion_source == chat_completion_sources.CLAUDE; const isOpenRouter = oai_settings.chat_completion_source == chat_completion_sources.OPENROUTER; const isScale = oai_settings.chat_completion_source == chat_completion_sources.SCALE; + const isAI21 = oai_settings.chat_completion_source == chat_completion_sources.AI21; const isTextCompletion = oai_settings.chat_completion_source == chat_completion_sources.OPENAI && (oai_settings.openai_model.startsWith('text-') || oai_settings.openai_model.startsWith('code-')); - const stream = type !== 'quiet' && oai_settings.stream_openai && !isScale; + const stream = type !== 'quiet' && oai_settings.stream_openai && !isScale && !isAI21; const isQuiet = type === 'quiet'; + if(isAI21) { + const joinedMsgs = openai_msgs_tosend.reduce((acc, obj) => { + const prefix = prefixMap[obj.role]; + return acc + (prefix ? (selected_group ? "\n" : prefix + " ") : "") + obj.content + "\n"; + }, ""); + openai_msgs_tosend = substituteParams(joinedMsgs); + } + // If we're using the window.ai extension, use that instead // Doesn't support logit bias yet if (oai_settings.chat_completion_source == chat_completion_sources.WINDOWAI) { @@ -1107,6 +1140,13 @@ async function sendOpenAIRequest(type, openai_msgs_tosend, signal) { generate_data['api_url_scale'] = oai_settings.api_url_scale; } + if (isAI21) { + generate_data['use_ai21'] = true; + generate_data['top_k'] = parseFloat(oai_settings.top_k_openai); + generate_data['count_pen'] = parseFloat(oai_settings.count_pen); + generate_data['stop_tokens'] = [name1 + ':', 'prompt: [Start a new chat]']; + } + const generate_url = '/generate_openai'; const response = await fetch(generate_url, { method: 'POST', @@ -1297,6 +1337,7 @@ class TokenHandler { } function countTokens(messages, full = false) { + let shouldTokenizeAI21 = oai_settings.chat_completion_source === chat_completion_sources.AI21 && oai_settings.use_ai21_tokenizer; let chatId = 'undefined'; try { @@ -1329,12 +1370,13 @@ function countTokens(messages, full = false) { if (typeof cachedCount === 'number') { token_count += cachedCount; } - else { + else { + console.log(JSON.stringify([message])); jQuery.ajax({ async: false, type: 'POST', // - url: `/tokenize_openai?model=${model}`, + url: shouldTokenizeAI21 ? '/tokenize_ai21' : `/tokenize_openai?model=${model}`, data: JSON.stringify([message]), dataType: "json", contentType: "application/json", @@ -1850,6 +1892,7 @@ function loadOpenAISettings(data, settings) { oai_settings.temp_openai = settings.temp_openai ?? default_settings.temp_openai; oai_settings.freq_pen_openai = settings.freq_pen_openai ?? default_settings.freq_pen_openai; oai_settings.pres_pen_openai = settings.pres_pen_openai ?? default_settings.pres_pen_openai; + oai_settings.count_pen = settings.count_pen ?? default_settings.count_pen; oai_settings.top_p_openai = settings.top_p_openai ?? default_settings.top_p_openai; oai_settings.top_k_openai = settings.top_k_openai ?? default_settings.top_k_openai; oai_settings.stream_openai = settings.stream_openai ?? default_settings.stream_openai; @@ -1865,6 +1908,7 @@ function loadOpenAISettings(data, settings) { oai_settings.claude_model = settings.claude_model ?? default_settings.claude_model; oai_settings.windowai_model = settings.windowai_model ?? default_settings.windowai_model; oai_settings.openrouter_model = settings.openrouter_model ?? default_settings.openrouter_model; + oai_settings.ai21_model = settings.ai21_model ?? default_settings.ai21_model; oai_settings.chat_completion_source = settings.chat_completion_source ?? default_settings.chat_completion_source; oai_settings.api_url_scale = settings.api_url_scale ?? default_settings.api_url_scale; oai_settings.show_external_models = settings.show_external_models ?? default_settings.show_external_models; @@ -1883,7 +1927,7 @@ function loadOpenAISettings(data, settings) { if (settings.wrap_in_quotes !== undefined) oai_settings.wrap_in_quotes = !!settings.wrap_in_quotes; if (settings.names_in_completion !== undefined) oai_settings.names_in_completion = !!settings.names_in_completion; if (settings.openai_model !== undefined) oai_settings.openai_model = settings.openai_model; - + if (settings.use_ai21_tokenizer !== undefined) oai_settings.use_ai21_tokenizer = !!settings.use_ai21_tokenizer; $('#stream_toggle').prop('checked', oai_settings.stream_openai); $('#api_url_scale').val(oai_settings.api_url_scale); $('#openai_proxy_password').val(oai_settings.proxy_password); @@ -1895,6 +1939,8 @@ function loadOpenAISettings(data, settings) { $(`#model_claude_select option[value="${oai_settings.claude_model}"`).attr('selected', true); $('#model_windowai_select').val(oai_settings.windowai_model); $(`#model_windowai_select option[value="${oai_settings.windowai_model}"`).attr('selected', true); + $('#model_ai21_select').val(oai_settings.ai21_model); + $(`#model_ai21_select option[value="${oai_settings.ai21_model}"`).attr('selected', true); $('#openai_max_context').val(oai_settings.openai_max_context); $('#openai_max_context_counter').text(`${oai_settings.openai_max_context}`); $('#model_openrouter_select').val(oai_settings.openrouter_model); @@ -1910,7 +1956,7 @@ function loadOpenAISettings(data, settings) { $('#legacy_streaming').prop('checked', oai_settings.legacy_streaming); $('#openai_show_external_models').prop('checked', oai_settings.show_external_models); $('#openai_external_category').toggle(oai_settings.show_external_models); - + $('#use_ai21_tokenizer').prop('checked', oai_settings.use_ai21_tokenizer); if (settings.impersonation_prompt !== undefined) oai_settings.impersonation_prompt = settings.impersonation_prompt; $('#impersonation_prompt_textarea').val(oai_settings.impersonation_prompt); @@ -1933,6 +1979,9 @@ function loadOpenAISettings(data, settings) { $('#pres_pen_openai').val(oai_settings.pres_pen_openai); $('#pres_pen_counter_openai').text(Number(oai_settings.pres_pen_openai).toFixed(2)); + $('#count_pen').val(oai_settings.count_pen); + $('#count_pen_counter').text(Number(oai_settings.count_pen).toFixed(2)); + $('#top_p_openai').val(oai_settings.top_p_openai); $('#top_p_counter_openai').text(Number(oai_settings.top_p_openai).toFixed(2)); @@ -1975,7 +2024,7 @@ async function getStatusOpen() { return resultCheckStatusOpen(); } - if (oai_settings.chat_completion_source == chat_completion_sources.SCALE || oai_settings.chat_completion_source == chat_completion_sources.CLAUDE) { + if (oai_settings.chat_completion_source == chat_completion_sources.SCALE || oai_settings.chat_completion_source == chat_completion_sources.CLAUDE || oai_settings.chat_completion_source == chat_completion_sources.AI21) { let status = 'Unable to verify key; press "Test Message" to validate.'; setOnlineStatus(status); return resultCheckStatusOpen(); @@ -2072,9 +2121,11 @@ async function saveOpenAIPreset(name, settings, triggerUi = true) { claude_model: settings.claude_model, windowai_model: settings.windowai_model, openrouter_model: settings.openrouter_model, + ai21_model: settings.ai21_model, temperature: settings.temp_openai, frequency_penalty: settings.freq_pen_openai, presence_penalty: settings.pres_pen_openai, + count_penalty: settings.count_pen, top_p: settings.top_p_openai, top_k: settings.top_k_openai, openai_max_context: settings.openai_max_context, @@ -2102,6 +2153,7 @@ async function saveOpenAIPreset(name, settings, triggerUi = true) { api_url_scale: settings.api_url_scale, show_external_models: settings.show_external_models, assistant_prefill: settings.assistant_prefill, + use_ai21_tokenizer: settings.use_ai21_tokenizer, }; const savePresetSettings = await fetch(`/savepreset_openai?name=${name}`, { @@ -2401,6 +2453,7 @@ function onSettingsPresetChange() { temperature: ['#temp_openai', 'temp_openai', false], frequency_penalty: ['#freq_pen_openai', 'freq_pen_openai', false], presence_penalty: ['#pres_pen_openai', 'pres_pen_openai', false], + count_penalty: ['#count_pen', 'count_pen', false], top_p: ['#top_p_openai', 'top_p_openai', false], top_k: ['#top_k_openai', 'top_k_openai', false], max_context_unlocked: ['#oai_max_context_unlocked', 'max_context_unlocked', true], @@ -2408,6 +2461,7 @@ function onSettingsPresetChange() { claude_model: ['#model_claude_select', 'claude_model', false], windowai_model: ['#model_windowai_select', 'windowai_model', false], openrouter_model: ['#model_openrouter_select', 'openrouter_model', false], + ai21_model: ['#model_ai21_select', 'ai21_model', false], openai_max_context: ['#openai_max_context', 'openai_max_context', false], openai_max_tokens: ['#openai_max_tokens', 'openai_max_tokens', false], wrap_in_quotes: ['#wrap_in_quotes', 'wrap_in_quotes', true], @@ -2431,6 +2485,7 @@ function onSettingsPresetChange() { show_external_models: ['#openai_show_external_models', 'show_external_models', true], proxy_password: ['#openai_proxy_password', 'proxy_password', false], assistant_prefill: ['#claude_assistant_prefill', 'assistant_prefill', false], + use_ai21_tokenizer: ['#use_ai21_tokenizer', 'use_ai21_tokenizer', false], }; const presetName = $('#settings_perset_openai').find(":selected").text(); @@ -2555,6 +2610,11 @@ async function onModelChange() { oai_settings.openrouter_model = value; } + if ($(this).is('#model_ai21_select')) { + console.log('AI21 model changed to', value); + oai_settings.ai21_model = value; + } + if (oai_settings.chat_completion_source == chat_completion_sources.SCALE) { if (oai_settings.max_context_unlocked) { $('#openai_max_context').attr('max', unlocked_max); @@ -2641,6 +2701,38 @@ async function onModelChange() { $('#temp_openai').attr('max', oai_max_temp).val(oai_settings.temp_openai).trigger('input'); } + if (oai_settings.chat_completion_source == chat_completion_sources.AI21) { + if (oai_settings.max_context_unlocked) { + $('#openai_max_context').attr('max', unlocked_max); + } else { + $('#openai_max_context').attr('max', ai21_max); + } + + oai_settings.openai_max_context = Math.min(oai_settings.openai_max_context, Number($('#openai_max_context').attr('max'))); + $('#openai_max_context').val(oai_settings.openai_max_context).trigger('input'); + + oai_settings.temp_openai = Math.min(claude_max_temp, oai_settings.temp_openai); + $('#temp_openai').attr('max', claude_max_temp).val(oai_settings.temp_openai).trigger('input'); + + oai_settings.freq_pen_openai = Math.min(j2_max_freq, oai_settings.freq_pen_openai < 0 ? 0 : oai_settings.freq_pen_openai); + $('#freq_pen_openai').attr('min', 0).attr('max', j2_max_freq).val(oai_settings.freq_pen_openai).trigger('input'); + + oai_settings.pres_pen_openai = Math.min(j2_max_pres, oai_settings.pres_pen_openai < 0 ? 0 : oai_settings.pres_pen_openai); + $('#pres_pen_openai').attr('min', 0).attr('max', j2_max_pres).val(oai_settings.pres_pen_openai).trigger('input'); + + oai_settings.top_k_openai = Math.min(j2_max_topk, oai_settings.top_k_openai); + $('#top_k_openai').attr('max', j2_max_topk).val(oai_settings.top_k_openai).trigger('input'); + } else if (oai_settings.chat_completion_source != chat_completion_sources.AI21) { + oai_settings.freq_pen_openai = Math.min(2.0, oai_settings.freq_pen_openai); + $('#freq_pen_openai').attr('min', -2.0).attr('max', 2.0).val(oai_settings.freq_pen_openai).trigger('input'); + + oai_settings.freq_pen_openai = Math.min(2.0, oai_settings.pres_pen_openai); + $('#pres_pen_openai').attr('min', -2.0).attr('max', 2.0).val(oai_settings.freq_pen_openai).trigger('input'); + + oai_settings.top_k_openai = Math.min(200, oai_settings.top_k_openai); + $('#top_k_openai').attr('max', 200).val(oai_settings.top_k_openai).trigger('input'); + } + saveSettingsDebounced(); eventSource.emit(event_types.CHATCOMPLETION_MODEL_CHANGED, value); } @@ -2731,6 +2823,19 @@ async function onConnectButtonClick(e) { } } + if (oai_settings.chat_completion_source == chat_completion_sources.AI21) { + const api_key_ai21 = $('#api_key_ai21').val().trim(); + + if (api_key_ai21.length) { + await writeSecret(SECRET_KEYS.AI21, api_key_ai21); + } + + if (!secret_state[SECRET_KEYS.AI21] && !oai_settings.reverse_proxy) { + console.log('No secret key saved for Claude'); + return; + } + } + $("#api_loading_openai").css("display", 'inline-block'); $("#api_button_openai").css("display", 'none'); saveSettingsDebounced(); @@ -2760,7 +2865,9 @@ function toggleChatCompletionForms() { else if (oai_settings.chat_completion_source == chat_completion_sources.OPENROUTER) { $('#model_openrouter_select').trigger('change'); } - + else if (oai_settings.chat_completion_source == chat_completion_sources.AI21) { + $('#model_ai21_select').trigger('change'); + } $('[data-source]').each(function () { const validSources = $(this).data('source').split(','); $(this).toggle(validSources.includes(oai_settings.chat_completion_source)); @@ -2818,7 +2925,12 @@ $(document).ready(async function () { oai_settings.pres_pen_openai = $(this).val(); $('#pres_pen_counter_openai').text(Number($(this).val()).toFixed(2)); saveSettingsDebounced(); + }); + $(document).on('input', '#count_pen', function () { + oai_settings.count_pen = $(this).val(); + $('#count_pen_counter').text(Number($(this).val()).toFixed(2)); + saveSettingsDebounced(); }); $(document).on('input', '#top_p_openai', function () { @@ -2856,6 +2968,14 @@ $(document).ready(async function () { saveSettingsDebounced(); }); + $('#use_ai21_tokenizer').on('change', function () { + oai_settings.use_ai21_tokenizer = !!$('#use_ai21_tokenizer').prop('checked'); + oai_settings.use_ai21_tokenizer ? ai21_max = 8191: ai21_max = 9200; + oai_settings.openai_max_context = Math.min(ai21_max, oai_settings.openai_max_context); + $('#openai_max_context').attr('max', ai21_max).val(oai_settings.openai_max_context).trigger('input'); + saveSettingsDebounced(); + }); + $('#names_in_completion').on('change', function () { oai_settings.names_in_completion = !!$('#names_in_completion').prop('checked'); saveSettingsDebounced(); @@ -3023,6 +3143,7 @@ $(document).ready(async function () { $("#model_windowai_select").on("change", onModelChange); $("#model_scale_select").on("change", onModelChange); $("#model_openrouter_select").on("change", onModelChange); + $("#model_ai21_select").on("change", onModelChange); $("#settings_perset_openai").on("change", onSettingsPresetChange); $("#new_oai_preset").on("click", onNewPresetClick); $("#delete_oai_preset").on("click", onDeletePresetClick); diff --git a/public/scripts/secrets.js b/public/scripts/secrets.js index 00203a106..202870c50 100644 --- a/public/scripts/secrets.js +++ b/public/scripts/secrets.js @@ -8,6 +8,7 @@ export const SECRET_KEYS = { CLAUDE: 'api_key_claude', OPENROUTER: 'api_key_openrouter', SCALE: 'api_key_scale', + AI21: 'api_key_ai21', } const INPUT_MAP = { @@ -18,6 +19,7 @@ const INPUT_MAP = { [SECRET_KEYS.CLAUDE]: '#api_key_claude', [SECRET_KEYS.OPENROUTER]: '#api_key_openrouter', [SECRET_KEYS.SCALE]: '#api_key_scale', + [SECRET_KEYS.AI21]: '#api_key_ai21', } async function clearSecret() { diff --git a/server.js b/server.js index f495f27e2..d8873d6c1 100644 --- a/server.js +++ b/server.js @@ -3290,6 +3290,10 @@ app.post("/generate_openai", jsonParser, function (request, response_generate_op return sendScaleRequest(request, response_generate_openai); } + if (request.body.use_ai21) { + return sendAI21Request(request, response_generate_openai); + } + let api_url; let api_key_openai; let headers; @@ -3479,6 +3483,96 @@ app.post("/tokenize_openai", jsonParser, function (request, response_tokenize_op response_tokenize_openai.send({ "token_count": num_tokens }); }); +async function sendAI21Request(request, response) { + if (!request.body) return response.sendStatus(400); + const controller = new AbortController(); + console.log(request.body.messages) + request.socket.removeAllListeners('close'); + request.socket.on('close', function () { + controller.abort(); + }); + //console.log(request.body) + const options = { + method: 'POST', + headers: { + accept: 'application/json', + 'content-type': 'application/json', + Authorization: `Bearer ${readSecret(SECRET_KEYS.AI21)}` + }, + body: JSON.stringify({ + numResults: 1, + maxTokens: request.body.max_tokens, + minTokens: 0, + temperature: request.body.temperature, + topP: request.body.top_p, + stopSequences: request.body.stop_tokens, + topKReturn: request.body.top_k, + frequencyPenalty: { + scale: request.body.frequency_penalty * 100, + applyToWhitespaces: false, + applyToPunctuations: false, + applyToNumbers: false, + applyToStopwords: false, + applyToEmojis: false + }, + presencePenalty: { + scale: request.body.presence_penalty, + applyToWhitespaces: false, + applyToPunctuations: false, + applyToNumbers: false, + applyToStopwords: false, + applyToEmojis: false + }, + countPenalty: { + scale: request.body.count_pen, + applyToWhitespaces: false, + applyToPunctuations: false, + applyToNumbers: false, + applyToStopwords: false, + applyToEmojis: false + }, + prompt: request.body.messages + }), + signal: controller.signal, + }; + + fetch(`https://api.ai21.com/studio/v1/${request.body.model}/complete`, options) + .then(r => r.json()) + .then(r => { + if (r.completions === undefined) { + console.log(r) + } else { + console.log(r.completions[0].data.text) + } + const reply = { choices: [{ "message": { "content": r.completions[0].data.text, } }] }; + return response.send(reply) + }) + .catch(err => { + console.error(err) + return response.send({error: true}) + }); + +} + +app.post("/tokenize_ai21", jsonParser, function (request, response_tokenize_ai21 = response) { + if (!request.body) return response_tokenize_ai21.sendStatus(400); + console.log(request.body[0].content) + const options = { + method: 'POST', + headers: { + accept: 'application/json', + 'content-type': 'application/json', + Authorization: `Bearer ${readSecret(SECRET_KEYS.AI21)}` + }, + body: JSON.stringify({text: request.body[0].content}) + }; + + fetch('https://api.ai21.com/studio/v1/tokenize', options) + .then(response => response.json()) + .then(response => response_tokenize_ai21.send({"token_count": response.tokens.length})) + .catch(err => console.error(err)); +}); + app.post("/save_preset", jsonParser, function (request, response) { const name = sanitize(request.body.name); if (!request.body.preset || !name) { @@ -3791,6 +3885,7 @@ const SECRET_KEYS = { DEEPL: 'deepl', OPENROUTER: 'api_key_openrouter', SCALE: 'api_key_scale', + AI21: 'api_key_ai21' } function migrateSecrets() { From e77cded357b9221a52c518d539a35c809da1fbe0 Mon Sep 17 00:00:00 2001 From: Cohee1207 <18619528+Cohee1207@users.noreply.github.com> Date: Sat, 19 Aug 2023 18:51:20 +0300 Subject: [PATCH 02/12] Code clean-up --- public/scripts/openai.js | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/public/scripts/openai.js b/public/scripts/openai.js index 95067827e..5adfd13f3 100644 --- a/public/scripts/openai.js +++ b/public/scripts/openai.js @@ -169,10 +169,10 @@ export const chat_completion_sources = { }; const prefixMap = selected_group ? { - assistant: "", - user: "", - system: "OOC: " - } + assistant: "", + user: "", + system: "OOC: " +} : { assistant: "{{char}}:", user: "{{user}}:", @@ -1080,10 +1080,10 @@ async function sendOpenAIRequest(type, openai_msgs_tosend, signal) { const isScale = oai_settings.chat_completion_source == chat_completion_sources.SCALE; const isAI21 = oai_settings.chat_completion_source == chat_completion_sources.AI21; const isTextCompletion = oai_settings.chat_completion_source == chat_completion_sources.OPENAI && (oai_settings.openai_model.startsWith('text-') || oai_settings.openai_model.startsWith('code-')); - const stream = type !== 'quiet' && oai_settings.stream_openai && !isScale && !isAI21; const isQuiet = type === 'quiet'; + const stream = oai_settings.stream_openai && !isQuiet && !isScale && !isAI21; - if(isAI21) { + if (isAI21) { const joinedMsgs = openai_msgs_tosend.reduce((acc, obj) => { const prefix = prefixMap[obj.role]; return acc + (prefix ? (selected_group ? "\n" : prefix + " ") : "") + obj.content + "\n"; @@ -2836,8 +2836,8 @@ async function onConnectButtonClick(e) { await writeSecret(SECRET_KEYS.AI21, api_key_ai21); } - if (!secret_state[SECRET_KEYS.AI21] && !oai_settings.reverse_proxy) { - console.log('No secret key saved for Claude'); + if (!secret_state[SECRET_KEYS.AI21]) { + console.log('No secret key saved for AI21'); return; } } @@ -2976,7 +2976,7 @@ $(document).ready(async function () { $('#use_ai21_tokenizer').on('change', function () { oai_settings.use_ai21_tokenizer = !!$('#use_ai21_tokenizer').prop('checked'); - oai_settings.use_ai21_tokenizer ? ai21_max = 8191: ai21_max = 9200; + oai_settings.use_ai21_tokenizer ? ai21_max = 8191 : ai21_max = 9200; oai_settings.openai_max_context = Math.min(ai21_max, oai_settings.openai_max_context); $('#openai_max_context').attr('max', ai21_max).val(oai_settings.openai_max_context).trigger('input'); saveSettingsDebounced(); From 2cd23182982626cdefe9870683030d0d715c8423 Mon Sep 17 00:00:00 2001 From: Cohee1207 <18619528+Cohee1207@users.noreply.github.com> Date: Sat, 19 Aug 2023 18:52:06 +0300 Subject: [PATCH 03/12] Code clean-up --- server.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/server.js b/server.js index 1da4ea291..87e41e3ef 100644 --- a/server.js +++ b/server.js @@ -633,7 +633,7 @@ app.post("/generate_textgenerationwebui", jsonParser, async function (request, r resolve(data, isBinary); }); }); - } catch(err) { + } catch (err) { console.error("Socket error:", err); websocket.close(); yield "[SillyTavern] Streaming failed:\n" + err; @@ -3582,7 +3582,7 @@ async function sendAI21Request(request, response) { }) .catch(err => { console.error(err) - return response.send({error: true}) + return response.send({ error: true }) }); } @@ -3597,12 +3597,12 @@ app.post("/tokenize_ai21", jsonParser, function (request, response_tokenize_ai21 'content-type': 'application/json', Authorization: `Bearer ${readSecret(SECRET_KEYS.AI21)}` }, - body: JSON.stringify({text: request.body[0].content}) + body: JSON.stringify({ text: request.body[0].content }) }; fetch('https://api.ai21.com/studio/v1/tokenize', options) .then(response => response.json()) - .then(response => response_tokenize_ai21.send({"token_count": response.tokens.length})) + .then(response => response_tokenize_ai21.send({ "token_count": response.tokens.length })) .catch(err => console.error(err)); }); From 5a68cd61a1fbc936902f85ecbb2b3e3b9d51b188 Mon Sep 17 00:00:00 2001 From: Cohee1207 <18619528+Cohee1207@users.noreply.github.com> Date: Sat, 19 Aug 2023 18:58:37 +0300 Subject: [PATCH 04/12] Remove unnecessary log --- server.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/server.js b/server.js index 87e41e3ef..4dfa9d741 100644 --- a/server.js +++ b/server.js @@ -3524,7 +3524,6 @@ async function sendAI21Request(request, response) { request.socket.on('close', function () { controller.abort(); }); - //console.log(request.body) const options = { method: 'POST', headers: { @@ -3589,7 +3588,6 @@ async function sendAI21Request(request, response) { app.post("/tokenize_ai21", jsonParser, function (request, response_tokenize_ai21 = response) { if (!request.body) return response_tokenize_ai21.sendStatus(400); - console.log(request.body[0].content) const options = { method: 'POST', headers: { From 143ebec4c6db64a91e904a371ea2f82c87d3b6e9 Mon Sep 17 00:00:00 2001 From: Cohee1207 <18619528+Cohee1207@users.noreply.github.com> Date: Sat, 19 Aug 2023 19:08:35 +0300 Subject: [PATCH 05/12] Remove console log --- public/scripts/openai.js | 1 - 1 file changed, 1 deletion(-) diff --git a/public/scripts/openai.js b/public/scripts/openai.js index 5adfd13f3..7ecd88ea1 100644 --- a/public/scripts/openai.js +++ b/public/scripts/openai.js @@ -1378,7 +1378,6 @@ function countTokens(messages, full = false) { } else { - console.log(JSON.stringify([message])); jQuery.ajax({ async: false, type: 'POST', // From 771c9d6165062c9fef0a14565d7a74b7abe0f0ed Mon Sep 17 00:00:00 2001 From: Cohee1207 <18619528+Cohee1207@users.noreply.github.com> Date: Sat, 19 Aug 2023 19:48:33 +0300 Subject: [PATCH 06/12] Optimize SVG loader. Add ai21 icon --- public/img/ai21.svg | 20 ++ public/index.html | 1 + public/lib/svg-inject.js | 697 +++++++++++++++++++++++++++++++++++++++ public/script.js | 20 +- 4 files changed, 728 insertions(+), 10 deletions(-) create mode 100644 public/img/ai21.svg create mode 100644 public/lib/svg-inject.js diff --git a/public/img/ai21.svg b/public/img/ai21.svg new file mode 100644 index 000000000..891beda5a --- /dev/null +++ b/public/img/ai21.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + diff --git a/public/index.html b/public/index.html index 512ca6368..7be7bf3e7 100644 --- a/public/index.html +++ b/public/index.html @@ -44,6 +44,7 @@ + diff --git a/public/lib/svg-inject.js b/public/lib/svg-inject.js new file mode 100644 index 000000000..4e74af07c --- /dev/null +++ b/public/lib/svg-inject.js @@ -0,0 +1,697 @@ +/** + * SVGInject - Version 1.2.3 + * A tiny, intuitive, robust, caching solution for injecting SVG files inline into the DOM. + * + * https://github.com/iconfu/svg-inject + * + * Copyright (c) 2018 INCORS, the creators of iconfu.com + * @license MIT License - https://github.com/iconfu/svg-inject/blob/master/LICENSE + */ + +(function(window, document) { + // constants for better minification + var _CREATE_ELEMENT_ = 'createElement'; + var _GET_ELEMENTS_BY_TAG_NAME_ = 'getElementsByTagName'; + var _LENGTH_ = 'length'; + var _STYLE_ = 'style'; + var _TITLE_ = 'title'; + var _UNDEFINED_ = 'undefined'; + var _SET_ATTRIBUTE_ = 'setAttribute'; + var _GET_ATTRIBUTE_ = 'getAttribute'; + + var NULL = null; + + // constants + var __SVGINJECT = '__svgInject'; + var ID_SUFFIX = '--inject-'; + var ID_SUFFIX_REGEX = new RegExp(ID_SUFFIX + '\\d+', "g"); + var LOAD_FAIL = 'LOAD_FAIL'; + var SVG_NOT_SUPPORTED = 'SVG_NOT_SUPPORTED'; + var SVG_INVALID = 'SVG_INVALID'; + var ATTRIBUTE_EXCLUSION_NAMES = ['src', 'alt', 'onload', 'onerror']; + var A_ELEMENT = document[_CREATE_ELEMENT_]('a'); + var IS_SVG_SUPPORTED = typeof SVGRect != _UNDEFINED_; + var DEFAULT_OPTIONS = { + useCache: true, + copyAttributes: true, + makeIdsUnique: true + }; + // Map of IRI referenceable tag names to properties that can reference them. This is defined in + // https://www.w3.org/TR/SVG11/linking.html#processingIRI + var IRI_TAG_PROPERTIES_MAP = { + clipPath: ['clip-path'], + 'color-profile': NULL, + cursor: NULL, + filter: NULL, + linearGradient: ['fill', 'stroke'], + marker: ['marker', 'marker-end', 'marker-mid', 'marker-start'], + mask: NULL, + pattern: ['fill', 'stroke'], + radialGradient: ['fill', 'stroke'] + }; + var INJECTED = 1; + var FAIL = 2; + + var uniqueIdCounter = 1; + var xmlSerializer; + var domParser; + + + // creates an SVG document from an SVG string + function svgStringToSvgDoc(svgStr) { + domParser = domParser || new DOMParser(); + return domParser.parseFromString(svgStr, 'text/xml'); + } + + + // searializes an SVG element to an SVG string + function svgElemToSvgString(svgElement) { + xmlSerializer = xmlSerializer || new XMLSerializer(); + return xmlSerializer.serializeToString(svgElement); + } + + + // Returns the absolute url for the specified url + function getAbsoluteUrl(url) { + A_ELEMENT.href = url; + return A_ELEMENT.href; + } + + + // Load svg with an XHR request + function loadSvg(url, callback, errorCallback) { + if (url) { + var req = new XMLHttpRequest(); + req.onreadystatechange = function() { + if (req.readyState == 4) { + // readyState is DONE + var status = req.status; + if (status == 200) { + // request status is OK + callback(req.responseXML, req.responseText.trim()); + } else if (status >= 400) { + // request status is error (4xx or 5xx) + errorCallback(); + } else if (status == 0) { + // request status 0 can indicate a failed cross-domain call + errorCallback(); + } + } + }; + req.open('GET', url, true); + req.send(); + } + } + + + // Copy attributes from img element to svg element + function copyAttributes(imgElem, svgElem) { + var attribute; + var attributeName; + var attributeValue; + var attributes = imgElem.attributes; + for (var i = 0; i < attributes[_LENGTH_]; i++) { + attribute = attributes[i]; + attributeName = attribute.name; + // Only copy attributes not explicitly excluded from copying + if (ATTRIBUTE_EXCLUSION_NAMES.indexOf(attributeName) == -1) { + attributeValue = attribute.value; + // If img attribute is "title", insert a title element into SVG element + if (attributeName == _TITLE_) { + var titleElem; + var firstElementChild = svgElem.firstElementChild; + if (firstElementChild && firstElementChild.localName.toLowerCase() == _TITLE_) { + // If the SVG element's first child is a title element, keep it as the title element + titleElem = firstElementChild; + } else { + // If the SVG element's first child element is not a title element, create a new title + // ele,emt and set it as the first child + titleElem = document[_CREATE_ELEMENT_ + 'NS']('http://www.w3.org/2000/svg', _TITLE_); + svgElem.insertBefore(titleElem, firstElementChild); + } + // Set new title content + titleElem.textContent = attributeValue; + } else { + // Set img attribute to svg element + svgElem[_SET_ATTRIBUTE_](attributeName, attributeValue); + } + } + } + } + + + // This function appends a suffix to IDs of referenced elements in the in order to to avoid ID collision + // between multiple injected SVGs. The suffix has the form "--inject-X", where X is a running number which is + // incremented with each injection. References to the IDs are adjusted accordingly. + // We assume tha all IDs within the injected SVG are unique, therefore the same suffix can be used for all IDs of one + // injected SVG. + // If the onlyReferenced argument is set to true, only those IDs will be made unique that are referenced from within the SVG + function makeIdsUnique(svgElem, onlyReferenced) { + var idSuffix = ID_SUFFIX + uniqueIdCounter++; + // Regular expression for functional notations of an IRI references. This will find occurences in the form + // url(#anyId) or url("#anyId") (for Internet Explorer) and capture the referenced ID + var funcIriRegex = /url\("?#([a-zA-Z][\w:.-]*)"?\)/g; + // Get all elements with an ID. The SVG spec recommends to put referenced elements inside elements, but + // this is not a requirement, therefore we have to search for IDs in the whole SVG. + var idElements = svgElem.querySelectorAll('[id]'); + var idElem; + // An object containing referenced IDs as keys is used if only referenced IDs should be uniquified. + // If this object does not exist, all IDs will be uniquified. + var referencedIds = onlyReferenced ? [] : NULL; + var tagName; + var iriTagNames = {}; + var iriProperties = []; + var changed = false; + var i, j; + + if (idElements[_LENGTH_]) { + // Make all IDs unique by adding the ID suffix and collect all encountered tag names + // that are IRI referenceable from properities. + for (i = 0; i < idElements[_LENGTH_]; i++) { + tagName = idElements[i].localName; // Use non-namespaced tag name + // Make ID unique if tag name is IRI referenceable + if (tagName in IRI_TAG_PROPERTIES_MAP) { + iriTagNames[tagName] = 1; + } + } + // Get all properties that are mapped to the found IRI referenceable tags + for (tagName in iriTagNames) { + (IRI_TAG_PROPERTIES_MAP[tagName] || [tagName]).forEach(function (mappedProperty) { + // Add mapped properties to array of iri referencing properties. + // Use linear search here because the number of possible entries is very small (maximum 11) + if (iriProperties.indexOf(mappedProperty) < 0) { + iriProperties.push(mappedProperty); + } + }); + } + if (iriProperties[_LENGTH_]) { + // Add "style" to properties, because it may contain references in the form 'style="fill:url(#myFill)"' + iriProperties.push(_STYLE_); + } + // Run through all elements of the SVG and replace IDs in references. + // To get all descending elements, getElementsByTagName('*') seems to perform faster than querySelectorAll('*'). + // Since svgElem.getElementsByTagName('*') does not return the svg element itself, we have to handle it separately. + var descElements = svgElem[_GET_ELEMENTS_BY_TAG_NAME_]('*'); + var element = svgElem; + var propertyName; + var value; + var newValue; + for (i = -1; element != NULL;) { + if (element.localName == _STYLE_) { + // If element is a style element, replace IDs in all occurences of "url(#anyId)" in text content + value = element.textContent; + newValue = value && value.replace(funcIriRegex, function(match, id) { + if (referencedIds) { + referencedIds[id] = 1; + } + return 'url(#' + id + idSuffix + ')'; + }); + if (newValue !== value) { + element.textContent = newValue; + } + } else if (element.hasAttributes()) { + // Run through all property names for which IDs were found + for (j = 0; j < iriProperties[_LENGTH_]; j++) { + propertyName = iriProperties[j]; + value = element[_GET_ATTRIBUTE_](propertyName); + newValue = value && value.replace(funcIriRegex, function(match, id) { + if (referencedIds) { + referencedIds[id] = 1; + } + return 'url(#' + id + idSuffix + ')'; + }); + if (newValue !== value) { + element[_SET_ATTRIBUTE_](propertyName, newValue); + } + } + // Replace IDs in xlink:ref and href attributes + ['xlink:href', 'href'].forEach(function(refAttrName) { + var iri = element[_GET_ATTRIBUTE_](refAttrName); + if (/^\s*#/.test(iri)) { // Check if iri is non-null and internal reference + iri = iri.trim(); + element[_SET_ATTRIBUTE_](refAttrName, iri + idSuffix); + if (referencedIds) { + // Add ID to referenced IDs + referencedIds[iri.substring(1)] = 1; + } + } + }); + } + element = descElements[++i]; + } + for (i = 0; i < idElements[_LENGTH_]; i++) { + idElem = idElements[i]; + // If set of referenced IDs exists, make only referenced IDs unique, + // otherwise make all IDs unique. + if (!referencedIds || referencedIds[idElem.id]) { + // Add suffix to element's ID + idElem.id += idSuffix; + changed = true; + } + } + } + // return true if SVG element has changed + return changed; + } + + + // For cached SVGs the IDs are made unique by simply replacing the already inserted unique IDs with a + // higher ID counter. This is much more performant than a call to makeIdsUnique(). + function makeIdsUniqueCached(svgString) { + return svgString.replace(ID_SUFFIX_REGEX, ID_SUFFIX + uniqueIdCounter++); + } + + + // Inject SVG by replacing the img element with the SVG element in the DOM + function inject(imgElem, svgElem, absUrl, options) { + if (svgElem) { + svgElem[_SET_ATTRIBUTE_]('data-inject-url', absUrl); + var parentNode = imgElem.parentNode; + if (parentNode) { + if (options.copyAttributes) { + copyAttributes(imgElem, svgElem); + } + // Invoke beforeInject hook if set + var beforeInject = options.beforeInject; + var injectElem = (beforeInject && beforeInject(imgElem, svgElem)) || svgElem; + // Replace img element with new element. This is the actual injection. + parentNode.replaceChild(injectElem, imgElem); + // Mark img element as injected + imgElem[__SVGINJECT] = INJECTED; + removeOnLoadAttribute(imgElem); + // Invoke afterInject hook if set + var afterInject = options.afterInject; + if (afterInject) { + afterInject(imgElem, injectElem); + } + } + } else { + svgInvalid(imgElem, options); + } + } + + + // Merges any number of options objects into a new object + function mergeOptions() { + var mergedOptions = {}; + var args = arguments; + // Iterate over all specified options objects and add all properties to the new options object + for (var i = 0; i < args[_LENGTH_]; i++) { + var argument = args[i]; + for (var key in argument) { + if (argument.hasOwnProperty(key)) { + mergedOptions[key] = argument[key]; + } + } + } + return mergedOptions; + } + + + // Adds the specified CSS to the document's element + function addStyleToHead(css) { + var head = document[_GET_ELEMENTS_BY_TAG_NAME_]('head')[0]; + if (head) { + var style = document[_CREATE_ELEMENT_](_STYLE_); + style.type = 'text/css'; + style.appendChild(document.createTextNode(css)); + head.appendChild(style); + } + } + + + // Builds an SVG element from the specified SVG string + function buildSvgElement(svgStr, verify) { + if (verify) { + var svgDoc; + try { + // Parse the SVG string with DOMParser + svgDoc = svgStringToSvgDoc(svgStr); + } catch(e) { + return NULL; + } + if (svgDoc[_GET_ELEMENTS_BY_TAG_NAME_]('parsererror')[_LENGTH_]) { + // DOMParser does not throw an exception, but instead puts parsererror tags in the document + return NULL; + } + return svgDoc.documentElement; + } else { + var div = document.createElement('div'); + div.innerHTML = svgStr; + return div.firstElementChild; + } + } + + + function removeOnLoadAttribute(imgElem) { + // Remove the onload attribute. Should only be used to remove the unstyled image flash protection and + // make the element visible, not for removing the event listener. + imgElem.removeAttribute('onload'); + } + + + function errorMessage(msg) { + console.error('SVGInject: ' + msg); + } + + + function fail(imgElem, status, options) { + imgElem[__SVGINJECT] = FAIL; + if (options.onFail) { + options.onFail(imgElem, status); + } else { + errorMessage(status); + } + } + + + function svgInvalid(imgElem, options) { + removeOnLoadAttribute(imgElem); + fail(imgElem, SVG_INVALID, options); + } + + + function svgNotSupported(imgElem, options) { + removeOnLoadAttribute(imgElem); + fail(imgElem, SVG_NOT_SUPPORTED, options); + } + + + function loadFail(imgElem, options) { + fail(imgElem, LOAD_FAIL, options); + } + + + function removeEventListeners(imgElem) { + imgElem.onload = NULL; + imgElem.onerror = NULL; + } + + + function imgNotSet(msg) { + errorMessage('no img element'); + } + + + function createSVGInject(globalName, options) { + var defaultOptions = mergeOptions(DEFAULT_OPTIONS, options); + var svgLoadCache = {}; + + if (IS_SVG_SUPPORTED) { + // If the browser supports SVG, add a small stylesheet that hides the elements until + // injection is finished. This avoids showing the unstyled SVGs before style is applied. + addStyleToHead('img[onload^="' + globalName + '("]{visibility:hidden;}'); + } + + + /** + * SVGInject + * + * Injects the SVG specified in the `src` attribute of the specified `img` element or array of `img` + * elements. Returns a Promise object which resolves if all passed in `img` elements have either been + * injected or failed to inject (Only if a global Promise object is available like in all modern browsers + * or through a polyfill). + * + * Options: + * useCache: If set to `true` the SVG will be cached using the absolute URL. Default value is `true`. + * copyAttributes: If set to `true` the attributes will be copied from `img` to `svg`. Dfault value + * is `true`. + * makeIdsUnique: If set to `true` the ID of elements in the `` element that can be references by + * property values (for example 'clipPath') are made unique by appending "--inject-X", where X is a + * running number which increases with each injection. This is done to avoid duplicate IDs in the DOM. + * beforeLoad: Hook before SVG is loaded. The `img` element is passed as a parameter. If the hook returns + * a string it is used as the URL instead of the `img` element's `src` attribute. + * afterLoad: Hook after SVG is loaded. The loaded `svg` element and `svg` string are passed as a + * parameters. If caching is active this hook will only get called once for injected SVGs with the + * same absolute path. Changes to the `svg` element in this hook will be applied to all injected SVGs + * with the same absolute path. It's also possible to return an `svg` string or `svg` element which + * will then be used for the injection. + * beforeInject: Hook before SVG is injected. The `img` and `svg` elements are passed as parameters. If + * any html element is returned it gets injected instead of applying the default SVG injection. + * afterInject: Hook after SVG is injected. The `img` and `svg` elements are passed as parameters. + * onAllFinish: Hook after all `img` elements passed to an SVGInject() call have either been injected or + * failed to inject. + * onFail: Hook after injection fails. The `img` element and a `status` string are passed as an parameter. + * The `status` can be either `'SVG_NOT_SUPPORTED'` (the browser does not support SVG), + * `'SVG_INVALID'` (the SVG is not in a valid format) or `'LOAD_FAILED'` (loading of the SVG failed). + * + * @param {HTMLImageElement} img - an img element or an array of img elements + * @param {Object} [options] - optional parameter with [options](#options) for this injection. + */ + function SVGInject(img, options) { + options = mergeOptions(defaultOptions, options); + + var run = function(resolve) { + var allFinish = function() { + var onAllFinish = options.onAllFinish; + if (onAllFinish) { + onAllFinish(); + } + resolve && resolve(); + }; + + if (img && typeof img[_LENGTH_] != _UNDEFINED_) { + // an array like structure of img elements + var injectIndex = 0; + var injectCount = img[_LENGTH_]; + + if (injectCount == 0) { + allFinish(); + } else { + var finish = function() { + if (++injectIndex == injectCount) { + allFinish(); + } + }; + + for (var i = 0; i < injectCount; i++) { + SVGInjectElement(img[i], options, finish); + } + } + } else { + // only one img element + SVGInjectElement(img, options, allFinish); + } + }; + + // return a Promise object if globally available + return typeof Promise == _UNDEFINED_ ? run() : new Promise(run); + } + + + // Injects a single svg element. Options must be already merged with the default options. + function SVGInjectElement(imgElem, options, callback) { + if (imgElem) { + var svgInjectAttributeValue = imgElem[__SVGINJECT]; + if (!svgInjectAttributeValue) { + removeEventListeners(imgElem); + + if (!IS_SVG_SUPPORTED) { + svgNotSupported(imgElem, options); + callback(); + return; + } + // Invoke beforeLoad hook if set. If the beforeLoad returns a value use it as the src for the load + // URL path. Else use the imgElem's src attribute value. + var beforeLoad = options.beforeLoad; + var src = (beforeLoad && beforeLoad(imgElem)) || imgElem[_GET_ATTRIBUTE_]('src'); + + if (!src) { + // If no image src attribute is set do no injection. This can only be reached by using javascript + // because if no src attribute is set the onload and onerror events do not get called + if (src === '') { + loadFail(imgElem, options); + } + callback(); + return; + } + + // set array so later calls can register callbacks + var onFinishCallbacks = []; + imgElem[__SVGINJECT] = onFinishCallbacks; + + var onFinish = function() { + callback(); + onFinishCallbacks.forEach(function(onFinishCallback) { + onFinishCallback(); + }); + }; + + var absUrl = getAbsoluteUrl(src); + var useCacheOption = options.useCache; + var makeIdsUniqueOption = options.makeIdsUnique; + + var setSvgLoadCacheValue = function(val) { + if (useCacheOption) { + svgLoadCache[absUrl].forEach(function(svgLoad) { + svgLoad(val); + }); + svgLoadCache[absUrl] = val; + } + }; + + if (useCacheOption) { + var svgLoad = svgLoadCache[absUrl]; + + var handleLoadValue = function(loadValue) { + if (loadValue === LOAD_FAIL) { + loadFail(imgElem, options); + } else if (loadValue === SVG_INVALID) { + svgInvalid(imgElem, options); + } else { + var hasUniqueIds = loadValue[0]; + var svgString = loadValue[1]; + var uniqueIdsSvgString = loadValue[2]; + var svgElem; + + if (makeIdsUniqueOption) { + if (hasUniqueIds === NULL) { + // IDs for the SVG string have not been made unique before. This may happen if previous + // injection of a cached SVG have been run with the option makedIdsUnique set to false + svgElem = buildSvgElement(svgString, false); + hasUniqueIds = makeIdsUnique(svgElem, false); + + loadValue[0] = hasUniqueIds; + loadValue[2] = hasUniqueIds && svgElemToSvgString(svgElem); + } else if (hasUniqueIds) { + // Make IDs unique for already cached SVGs with better performance + svgString = makeIdsUniqueCached(uniqueIdsSvgString); + } + } + + svgElem = svgElem || buildSvgElement(svgString, false); + + inject(imgElem, svgElem, absUrl, options); + } + onFinish(); + }; + + if (typeof svgLoad != _UNDEFINED_) { + // Value for url exists in cache + if (svgLoad.isCallbackQueue) { + // Same url has been cached, but value has not been loaded yet, so add to callbacks + svgLoad.push(handleLoadValue); + } else { + handleLoadValue(svgLoad); + } + return; + } else { + var svgLoad = []; + // set property isCallbackQueue to Array to differentiate from array with cached loaded values + svgLoad.isCallbackQueue = true; + svgLoadCache[absUrl] = svgLoad; + } + } + + // Load the SVG because it is not cached or caching is disabled + loadSvg(absUrl, function(svgXml, svgString) { + // Use the XML from the XHR request if it is an instance of Document. Otherwise + // (for example of IE9), create the svg document from the svg string. + var svgElem = svgXml instanceof Document ? svgXml.documentElement : buildSvgElement(svgString, true); + + var afterLoad = options.afterLoad; + if (afterLoad) { + // Invoke afterLoad hook which may modify the SVG element. After load may also return a new + // svg element or svg string + var svgElemOrSvgString = afterLoad(svgElem, svgString) || svgElem; + if (svgElemOrSvgString) { + // Update svgElem and svgString because of modifications to the SVG element or SVG string in + // the afterLoad hook, so the modified SVG is also used for all later cached injections + var isString = typeof svgElemOrSvgString == 'string'; + svgString = isString ? svgElemOrSvgString : svgElemToSvgString(svgElem); + svgElem = isString ? buildSvgElement(svgElemOrSvgString, true) : svgElemOrSvgString; + } + } + + if (svgElem instanceof SVGElement) { + var hasUniqueIds = NULL; + if (makeIdsUniqueOption) { + hasUniqueIds = makeIdsUnique(svgElem, false); + } + + if (useCacheOption) { + var uniqueIdsSvgString = hasUniqueIds && svgElemToSvgString(svgElem); + // set an array with three entries to the load cache + setSvgLoadCacheValue([hasUniqueIds, svgString, uniqueIdsSvgString]); + } + + inject(imgElem, svgElem, absUrl, options); + } else { + svgInvalid(imgElem, options); + setSvgLoadCacheValue(SVG_INVALID); + } + onFinish(); + }, function() { + loadFail(imgElem, options); + setSvgLoadCacheValue(LOAD_FAIL); + onFinish(); + }); + } else { + if (Array.isArray(svgInjectAttributeValue)) { + // svgInjectAttributeValue is an array. Injection is not complete so register callback + svgInjectAttributeValue.push(callback); + } else { + callback(); + } + } + } else { + imgNotSet(); + } + } + + + /** + * Sets the default [options](#options) for SVGInject. + * + * @param {Object} [options] - default [options](#options) for an injection. + */ + SVGInject.setOptions = function(options) { + defaultOptions = mergeOptions(defaultOptions, options); + }; + + + // Create a new instance of SVGInject + SVGInject.create = createSVGInject; + + + /** + * Used in onerror Event of an `` element to handle cases when the loading the original src fails + * (for example if file is not found or if the browser does not support SVG). This triggers a call to the + * options onFail hook if available. The optional second parameter will be set as the new src attribute + * for the img element. + * + * @param {HTMLImageElement} img - an img element + * @param {String} [fallbackSrc] - optional parameter fallback src + */ + SVGInject.err = function(img, fallbackSrc) { + if (img) { + if (img[__SVGINJECT] != FAIL) { + removeEventListeners(img); + + if (!IS_SVG_SUPPORTED) { + svgNotSupported(img, defaultOptions); + } else { + removeOnLoadAttribute(img); + loadFail(img, defaultOptions); + } + if (fallbackSrc) { + removeOnLoadAttribute(img); + img.src = fallbackSrc; + } + } + } else { + imgNotSet(); + } + }; + + window[globalName] = SVGInject; + + return SVGInject; + } + + var SVGInjectInstance = createSVGInject('SVGInject'); + + if (typeof module == 'object' && typeof module.exports == 'object') { + module.exports = SVGInjectInstance; + } +})(window, document); \ No newline at end of file diff --git a/public/script.js b/public/script.js index 36582390c..1348e9381 100644 --- a/public/script.js +++ b/public/script.js @@ -1446,25 +1446,25 @@ function insertSVGIcon(mes, extra) { modelName = extra.api; } - // Fetch the SVG based on the modelName - $.get(`/img/${modelName}.svg`, function (data) { - // Extract the SVG content from the XML data - let svg = $(data).find('svg'); - - // Add classes for styling and identification - svg.addClass('icon-svg timestamp-icon'); + const image = new Image(); + // Add classes for styling and identification + image.classList.add('icon-svg', 'timestamp-icon'); + image.src = `/img/${modelName}.svg`; + image.onload = async function () { // Check if an SVG already exists adjacent to the timestamp let existingSVG = mes.find('.timestamp').next('.timestamp-icon'); if (existingSVG.length) { // Replace existing SVG - existingSVG.replaceWith(svg); + existingSVG.replaceWith(image); } else { // Append the new SVG if none exists - mes.find('.timestamp').after(svg); + mes.find('.timestamp').after(image); } - }); + + await SVGInject(this); + }; } From 06c7b8d7d6a309f0c80afd9363b50e2238b8ef16 Mon Sep 17 00:00:00 2001 From: Cohee1207 <18619528+Cohee1207@users.noreply.github.com> Date: Sat, 19 Aug 2023 20:18:39 +0300 Subject: [PATCH 07/12] [WIP] Add UI for auto-select instruct --- public/index.html | 28 +++++++++++++++++++++------- public/scripts/power-user.js | 18 ++++++++++++++++++ public/style.css | 23 ++++++++++++++++++----- 3 files changed, 57 insertions(+), 12 deletions(-) diff --git a/public/index.html b/public/index.html index 7be7bf3e7..44c1dafd2 100644 --- a/public/index.html +++ b/public/index.html @@ -2061,14 +2061,14 @@
-