diff --git a/package-lock.json b/package-lock.json index 93c8ed8ee..4cef87f54 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,6 @@ "dependencies": { "@agnai/sentencepiece-js": "^1.1.1", "@agnai/web-tokenizers": "^0.1.3", - "@dqbd/tiktoken": "^1.0.13", "@zeldafan0225/ai_horde": "^4.0.1", "archiver": "^7.0.1", "bing-translate-api": "^2.9.1", @@ -46,6 +45,7 @@ "sanitize-filename": "^1.6.3", "sillytavern-transformers": "^2.14.6", "simple-git": "^3.19.1", + "tiktoken": "^1.0.15", "vectra": "^0.2.2", "wavefile": "^11.0.0", "write-file-atomic": "^5.0.1", @@ -82,10 +82,6 @@ "version": "0.1.3", "license": "Apache-2.0" }, - "node_modules/@dqbd/tiktoken": { - "version": "1.0.13", - "license": "MIT" - }, "node_modules/@eslint-community/eslint-utils": { "version": "4.4.0", "dev": true, @@ -4403,6 +4399,11 @@ "dev": true, "license": "MIT" }, + "node_modules/tiktoken": { + "version": "1.0.15", + "resolved": "https://registry.npmjs.org/tiktoken/-/tiktoken-1.0.15.tgz", + "integrity": "sha512-sCsrq/vMWUSEW29CJLNmPvWxlVp7yh2tlkAjpJltIKqp5CKf98ZNpdeHRmAlPVFlGEbswDc6SmI8vz64W/qErw==" + }, "node_modules/timm": { "version": "1.7.1", "license": "MIT" diff --git a/package.json b/package.json index ba6fb92af..2ab67ede2 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,6 @@ "dependencies": { "@agnai/sentencepiece-js": "^1.1.1", "@agnai/web-tokenizers": "^0.1.3", - "@dqbd/tiktoken": "^1.0.13", "@zeldafan0225/ai_horde": "^4.0.1", "archiver": "^7.0.1", "bing-translate-api": "^2.9.1", @@ -36,6 +35,7 @@ "sanitize-filename": "^1.6.3", "sillytavern-transformers": "^2.14.6", "simple-git": "^3.19.1", + "tiktoken": "^1.0.15", "vectra": "^0.2.2", "wavefile": "^11.0.0", "write-file-atomic": "^5.0.1", diff --git a/public/css/world-info.css b/public/css/world-info.css index 6244d9c2e..0f0a304c8 100644 --- a/public/css/world-info.css +++ b/public/css/world-info.css @@ -102,7 +102,7 @@ height: auto; margin-top: 0; margin-bottom: 0; - min-height: calc(var(--mainFontSize) + 13px); + min-height: calc(var(--mainFontSize) + 14px); } .delete_entry_button { diff --git a/public/index.html b/public/index.html index bbf0f8515..da5d8ab11 100644 --- a/public/index.html +++ b/public/index.html @@ -5292,6 +5292,12 @@ Prevent further recursion (this entry will not activate others) + @@ -5332,7 +5338,7 @@
Inclusion Group - + diff --git a/public/script.js b/public/script.js index f9e781b85..8dd9b427e 100644 --- a/public/script.js +++ b/public/script.js @@ -415,6 +415,7 @@ export const event_types = { GROUP_MEMBER_DRAFTED: 'group_member_drafted', WORLD_INFO_ACTIVATED: 'world_info_activated', TEXT_COMPLETION_SETTINGS_READY: 'text_completion_settings_ready', + CHAT_COMPLETION_SETTINGS_READY: 'chat_completion_settings_ready', CHARACTER_FIRST_MESSAGE_SELECTED: 'character_first_message_selected', // TODO: Naming convention is inconsistent with other events CHARACTER_DELETED: 'characterDeleted', @@ -7566,6 +7567,7 @@ window['SillyTavern'].getContext = function () { getCurrentChatId: getCurrentChatId, getRequestHeaders: getRequestHeaders, reloadCurrentChat: reloadCurrentChat, + renameChat: renameChat, saveSettingsDebounced: saveSettingsDebounced, onlineStatus: online_status, maxContext: Number(max_context), @@ -8288,6 +8290,58 @@ async function doDeleteChat() { $('#dialogue_popup_ok').trigger('click', { fromSlashCommand: true }); } +/** + * Renames the currently selected chat. + * @param {string} oldFileName Old name of the chat (no JSONL extension) + * @param {string} newName New name for the chat (no JSONL extension) + */ +export async function renameChat(oldFileName, newName) { + const body = { + is_group: !!selected_group, + avatar_url: characters[this_chid]?.avatar, + original_file: `${oldFileName}.jsonl`, + renamed_file: `${newName}.jsonl`, + }; + + try { + showLoader(); + const response = await fetch('/api/chats/rename', { + method: 'POST', + body: JSON.stringify(body), + headers: getRequestHeaders(), + }); + + if (!response.ok) { + throw new Error('Unsuccessful request.'); + } + + const data = await response.json(); + + if (data.error) { + throw new Error('Server returned an error.'); + } + + if (selected_group) { + await renameGroupChat(selected_group, oldFileName, newName); + } + else { + if (characters[this_chid].chat == oldFileName) { + characters[this_chid].chat = newName; + $('#selected_chat_pole').val(characters[this_chid].chat); + await createOrEditCharacter(); + } + } + + await reloadCurrentChat(); + } catch { + hideLoader(); + await delay(500); + await callPopup('An error has occurred. Chat was not renamed.', 'text'); + } finally { + hideLoader(); + } +} + /** * /getchatname` slash command */ @@ -8966,69 +9020,26 @@ jQuery(async function () { $(document).on('click', '.renameChatButton', async function (e) { e.stopPropagation(); - const old_filenamefull = $(this).closest('.select_chat_block_wrapper').find('.select_chat_block_filename').text(); - const old_filename = old_filenamefull.replace('.jsonl', ''); + const oldFileNameFull = $(this).closest('.select_chat_block_wrapper').find('.select_chat_block_filename').text(); + const oldFileName = oldFileNameFull.replace('.jsonl', ''); const popupText = `

Enter the new name for the chat:

!!Using an existing filename will produce an error!!
This will break the link between checkpoint chats.
No need to add '.jsonl' at the end.
`; - const newName = await callPopup(popupText, 'input', old_filename); + const newName = await callPopup(popupText, 'input', oldFileName); - if (!newName || newName == old_filename) { + if (!newName || newName == oldFileName) { console.log('no new name found, aborting'); return; } - const body = { - is_group: !!selected_group, - avatar_url: characters[this_chid]?.avatar, - original_file: `${old_filename}.jsonl`, - renamed_file: `${newName}.jsonl`, - }; + await renameChat(oldFileName, newName); - try { - showLoader(); - const response = await fetch('/api/chats/rename', { - method: 'POST', - body: JSON.stringify(body), - headers: getRequestHeaders(), - }); - - if (!response.ok) { - throw new Error('Unsuccessful request.'); - } - - const data = await response.json(); - - if (data.error) { - throw new Error('Server returned an error.'); - } - - if (selected_group) { - await renameGroupChat(selected_group, old_filename, newName); - } - else { - if (characters[this_chid].chat == old_filename) { - characters[this_chid].chat = newName; - $('#selected_chat_pole').val(characters[this_chid].chat); - await createOrEditCharacter(); - } - } - - await reloadCurrentChat(); - - await delay(250); - $('#option_select_chat').trigger('click'); - $('#options').hide(); - } catch { - hideLoader(); - await delay(500); - await callPopup('An error has occurred. Chat was not renamed.', 'text'); - } finally { - hideLoader(); - } + await delay(250); + $('#option_select_chat').trigger('click'); + $('#options').hide(); }); $(document).on('click', '.exportChatButton, .exportRawChatButton', async function (e) { diff --git a/public/scripts/RossAscends-mods.js b/public/scripts/RossAscends-mods.js index 826f968ea..c08378ee9 100644 --- a/public/scripts/RossAscends-mods.js +++ b/public/scripts/RossAscends-mods.js @@ -1133,6 +1133,11 @@ export function initRossMods() { return; } + if ($('#dialogue_del_mes_cancel').is(':visible')) { + $('#dialogue_del_mes_cancel').trigger('click'); + return; + } + if ($('.drawer-content') .not('#WorldInfo') .not('#left-nav-panel') diff --git a/public/scripts/char-data.js b/public/scripts/char-data.js index fa2f03382..20e484f31 100644 --- a/public/scripts/char-data.js +++ b/public/scripts/char-data.js @@ -24,6 +24,7 @@ * @property {boolean} group_override - Overrides any existing group assignment for the extension. * @property {number} group_weight - A value used for prioritizing extensions within the same group. * @property {boolean} prevent_recursion - Completely disallows recursive application of the extension. + * @property {boolean} delay_until_recursion - Will only be checked during recursion. * @property {number} scan_depth - The maximum depth to search for matches when applying the extension. * @property {boolean} match_whole_words - Specifies if only entire words should be matched during extension application. * @property {boolean} use_group_scoring - Indicates if group weight is considered when selecting extensions. diff --git a/public/scripts/openai.js b/public/scripts/openai.js index 479df8cb0..2e35ecae7 100644 --- a/public/scripts/openai.js +++ b/public/scripts/openai.js @@ -1847,6 +1847,8 @@ async function sendOpenAIRequest(type, messages, signal) { generate_data['seed'] = oai_settings.seed; } + await eventSource.emit(event_types.CHAT_COMPLETION_SETTINGS_READY, generate_data); + const generate_url = '/api/backends/chat-completions/generate'; const response = await fetch(generate_url, { method: 'POST', diff --git a/public/scripts/slash-commands/SlashCommandAutoCompleteNameResult.js b/public/scripts/slash-commands/SlashCommandAutoCompleteNameResult.js index 80ed27a37..ba163fcd5 100644 --- a/public/scripts/slash-commands/SlashCommandAutoCompleteNameResult.js +++ b/public/scripts/slash-commands/SlashCommandAutoCompleteNameResult.js @@ -50,6 +50,14 @@ export class SlashCommandAutoCompleteNameResult extends AutoCompleteNameResult { } getNamedArgumentAt(text, index, isSelect) { + function getSplitRegex() { + try { + return new RegExp('(?<==)'); + } catch { + // For browsers that don't support lookbehind + return new RegExp('=(.*)'); + } + } const notProvidedNamedArguments = this.executor.command.namedArgumentList.filter(arg=>!this.executor.namedArgumentList.find(it=>it.name == arg.name)); let name; let value; @@ -62,7 +70,7 @@ export class SlashCommandAutoCompleteNameResult extends AutoCompleteNameResult { // cursor is somewhere within the named arguments (including final space) argAssign = this.executor.namedArgumentList.find(it=>it.start <= index && it.end >= index); if (argAssign) { - const [argName, ...v] = text.slice(argAssign.start, index).split(/(?<==)/); + const [argName, ...v] = text.slice(argAssign.start, index).split(getSplitRegex()); name = argName; value = v.join(''); start = argAssign.start; diff --git a/public/scripts/slash-commands/SlashCommandParser.js b/public/scripts/slash-commands/SlashCommandParser.js index f1b88b73d..75bb0338d 100644 --- a/public/scripts/slash-commands/SlashCommandParser.js +++ b/public/scripts/slash-commands/SlashCommandParser.js @@ -186,6 +186,15 @@ export class SlashCommandParser { relevance: 0, }; + function getQuotedRunRegex() { + try { + return new RegExp('(".+?(? { keywordsAndRegexes.push(item.text); - } + }; const { term } = customTokenizer({ _type: 'custom_call', term: input }, undefined, addFindCallback); const finalTerm = term.trim(); @@ -1501,7 +1502,7 @@ function getWorldEntry(name, data, entry) { const isRegex = isValidRegex(item.text); if (isRegex) { content.html(highlightRegex(item.text)); - content.addClass('regex_item').prepend($('').addClass('regex_icon').text("•*").attr('title', 'Regex')); + content.addClass('regex_item').prepend($('').addClass('regex_icon').text('•*').attr('title', 'Regex')); } if (searchStyle && item.count) { @@ -1551,7 +1552,7 @@ function getWorldEntry(name, data, entry) { if (index > -1) selected.splice(index, 1); input.val(selected).trigger('change'); // Manually update the cache, that change event is not gonna trigger it - updateWorldEntryKeyOptionsCache([key], { remove: true }) + updateWorldEntryKeyOptionsCache([key], { remove: true }); // We need to "hack" the actual text input into the currently open textarea input.next('span.select2-container').find('textarea') @@ -1580,10 +1581,10 @@ function getWorldEntry(name, data, entry) { } // key - enableKeysInput("key", "keys"); + enableKeysInput('key', 'keys'); // keysecondary - enableKeysInput("keysecondary", "secondary_keys"); + enableKeysInput('keysecondary', 'secondary_keys'); // draw key input switch button template.find('.switch_input_type_icon').on('click', function () { @@ -1871,7 +1872,7 @@ function getWorldEntry(name, data, entry) { saveWorldInfo(name, data); }); groupInput.val(entry.group ?? '').trigger('input'); - setTimeout(() => createEntryInputAutocomplete(groupInput, getInclusionGroupCallback(data)), 1); + setTimeout(() => createEntryInputAutocomplete(groupInput, getInclusionGroupCallback(data), { allowMultiple: true }), 1); // inclusion priority const groupOverrideInput = template.find('input[name="groupOverride"]'); @@ -2143,6 +2144,18 @@ function getWorldEntry(name, data, entry) { }); preventRecursionInput.prop('checked', entry.preventRecursion).trigger('input'); + // delay until recursion + const delayUntilRecursionInput = template.find('input[name="delay_until_recursion"]'); + delayUntilRecursionInput.data('uid', entry.uid); + delayUntilRecursionInput.on('input', function () { + const uid = $(this).data('uid'); + const value = $(this).prop('checked'); + data.entries[uid].delayUntilRecursion = value; + setOriginalDataValue(data, uid, 'extensions.delay_until_recursion', data.entries[uid].delayUntilRecursion); + saveWorldInfo(name, data); + }); + delayUntilRecursionInput.prop('checked', entry.delayUntilRecursion).trigger('input'); + // duplicate button const duplicateButton = template.find('.duplicate_entry_button'); duplicateButton.data('uid', entry.uid); @@ -2281,11 +2294,15 @@ function getWorldEntry(name, data, entry) { * @returns {(input: any, output: any) => any} Callback function for the autocomplete */ function getInclusionGroupCallback(data) { - return function (input, output) { + return function (control, input, output) { + const uid = $(control).data('uid'); + const thisGroups = String($(control).val()).split(/,\s*/).filter(x => x).map(x => x.toLowerCase()); const groups = new Set(); for (const entry of Object.values(data.entries)) { + // Skip the groups of this entry, because auto-complete should only suggest the ones that are already available on other entries + if (entry.uid == uid) continue; if (entry.group) { - groups.add(String(entry.group)); + entry.group.split(/,\s*/).filter(x => x).forEach(x => groups.add(x)); } } @@ -2293,20 +2310,19 @@ function getInclusionGroupCallback(data) { haystack.sort((a, b) => a.localeCompare(b)); const needle = input.term.toLowerCase(); const hasExactMatch = haystack.findIndex(x => x.toLowerCase() == needle) !== -1; - const result = haystack.filter(x => x.toLowerCase().includes(needle)); - - if (input.term && !hasExactMatch) { - result.unshift(input.term); - } + const result = haystack.filter(x => x.toLowerCase().includes(needle) && (!thisGroups.includes(x) || hasExactMatch && thisGroups.filter(g => g == x).length == 1)); output(result); }; } function getAutomationIdCallback(data) { - return function (input, output) { + return function (control, input, output) { + const uid = $(control).data('uid'); const ids = new Set(); for (const entry of Object.values(data.entries)) { + // Skip automation id of this entry, because auto-complete should only suggest the ones that are already available on other entries + if (entry.uid == uid) continue; if (entry.automationId) { ids.add(String(entry.automationId)); } @@ -2322,36 +2338,53 @@ function getAutomationIdCallback(data) { const haystack = Array.from(ids); haystack.sort((a, b) => a.localeCompare(b)); const needle = input.term.toLowerCase(); - const hasExactMatch = haystack.findIndex(x => x.toLowerCase() == needle) !== -1; const result = haystack.filter(x => x.toLowerCase().includes(needle)); - if (input.term && !hasExactMatch) { - result.unshift(input.term); - } - output(result); }; } /** * Create an autocomplete for the inclusion group. - * @param {JQuery} input Input element to attach the autocomplete to - * @param {(input: any, output: any) => any} callback Source data callbacks + * @param {JQuery} input - Input element to attach the autocomplete to + * @param {(control: JQuery, input: any, output: any) => any} callback - Source data callbacks + * @param {object} [options={}] - Optional arguments + * @param {boolean} [options.allowMultiple=false] - Whether to allow multiple comma-separated values */ -function createEntryInputAutocomplete(input, callback) { +function createEntryInputAutocomplete(input, callback, { allowMultiple = false } = {}) { + const handleSelect = (event, ui) => { + // Prevent default autocomplete select, so we can manually set the value + event.preventDefault(); + if (!allowMultiple) { + $(input).val(ui.item.value).trigger('input').trigger('blur'); + } else { + var terms = String($(input).val()).split(/,\s*/); + terms.pop(); // remove the current input + terms.push(ui.item.value); // add the selected item + $(input).val(terms.filter(x => x).join(', ')).trigger('input').trigger('blur'); + } + }; + $(input).autocomplete({ minLength: 0, - source: callback, - select: function (_event, ui) { - $(input).val(ui.item.value).trigger('input').trigger('blur'); + source: function (request, response) { + if (!allowMultiple) { + callback(input, request, response); + } else { + const term = request.term.split(/,\s*/).pop(); + request.term = term; + callback(input, request, response); + } }, + select: handleSelect, }); $(input).on('focus click', function () { - $(input).autocomplete('search', String($(input).val())); + $(input).autocomplete('search', allowMultiple ? String($(input).val()).split(/,\s*/).pop() : $(input).val()); }); } + /** * Duplicated a WI entry by copying all of its properties and assigning a new uid * @param {*} data - The data of the book @@ -2404,6 +2437,8 @@ const newEntryTemplate = { position: 0, disable: false, excludeRecursion: false, + preventRecursion: false, + delayUntilRecursion: false, probability: 100, useProbability: true, depth: DEFAULT_DEPTH, @@ -2771,7 +2806,7 @@ async function checkWorldInfo(chat, maxContext) { continue; } - if (allActivatedEntries.has(entry) || entry.disable == true || (count > 1 && world_info_recursive && entry.excludeRecursion)) { + if (allActivatedEntries.has(entry) || entry.disable == true || (count > 1 && world_info_recursive && entry.excludeRecursion) || (count == 1 && entry.delayUntilRecursion)) { continue; } @@ -3044,10 +3079,12 @@ function filterGroupsByScoring(groups, buffer, removeEntry) { function filterByInclusionGroups(newEntries, allActivatedEntries, buffer) { console.debug('-- INCLUSION GROUP CHECKS BEGIN --'); const grouped = newEntries.filter(x => x.group).reduce((acc, item) => { - if (!acc[item.group]) { - acc[item.group] = []; - } - acc[item.group].push(item); + item.group.split(/,\s*/).filter(x => x).forEach(group => { + if (!acc[group]) { + acc[group] = []; + } + acc[group].push(item); + }); return acc; }, {}); @@ -3139,6 +3176,7 @@ function convertAgnaiMemoryBook(inputObj) { disable: !entry.enabled, addMemo: !!entry.name, excludeRecursion: false, + delayUntilRecursion: false, displayIndex: index, probability: 100, useProbability: true, @@ -3177,6 +3215,7 @@ function convertRisuLorebook(inputObj) { disable: false, addMemo: true, excludeRecursion: false, + delayUntilRecursion: false, displayIndex: index, probability: entry.activationPercent ?? 100, useProbability: entry.activationPercent ?? true, @@ -3220,6 +3259,7 @@ function convertNovelLorebook(inputObj) { disable: !entry.enabled, addMemo: addMemo, excludeRecursion: false, + delayUntilRecursion: false, displayIndex: index, probability: 100, useProbability: true, @@ -3260,6 +3300,7 @@ function convertCharacterBook(characterBook) { position: entry.extensions?.position ?? (entry.position === 'before_char' ? world_info_position.before : world_info_position.after), excludeRecursion: entry.extensions?.exclude_recursion ?? false, preventRecursion: entry.extensions?.prevent_recursion ?? false, + delayUntilRecursion: entry.extensions?.delay_until_recursion ?? false, disable: !entry.enabled, addMemo: entry.comment ? true : false, displayIndex: entry.extensions?.display_index ?? index, diff --git a/public/style.css b/public/style.css index 9e576a33e..3d44507c4 100644 --- a/public/style.css +++ b/public/style.css @@ -129,7 +129,7 @@ body { height: 100vh; height: 100svh; /*defaults as 100%, then reassigned via JS as pixels, will work on PC and Android*/ - height: calc(var(--doc-height) - 1px); + /*height: calc(var(--doc-height) - 1px);*/ background-color: var(--greyCAIbg); background-repeat: no-repeat; background-attachment: fixed; @@ -872,7 +872,8 @@ body .panelControlBar { } #chat .mes.selected{ - background-color: rgb(from var(--SmartThemeQuoteColor) r g b / .5); + /* background-color: rgb(from var(--SmartThemeQuoteColor) r g b / .5); */ + background-color: rgb(102, 0, 0); } .mes q:before, diff --git a/src/endpoints/characters.js b/src/endpoints/characters.js index 2e3171880..fc7ae3ef8 100644 --- a/src/endpoints/characters.js +++ b/src/endpoints/characters.js @@ -436,6 +436,7 @@ function convertWorldInfoToCharacterBook(name, entries) { group_override: entry.groupOverride ?? false, group_weight: entry.groupWeight ?? null, prevent_recursion: entry.preventRecursion ?? false, + delay_until_recursion: entry.delayUntilRecursion ?? false, scan_depth: entry.scanDepth ?? null, match_whole_words: entry.matchWholeWords ?? null, use_group_scoring: entry.useGroupScoring ?? false, diff --git a/src/endpoints/tokenizers.js b/src/endpoints/tokenizers.js index 321267d05..82bab1439 100644 --- a/src/endpoints/tokenizers.js +++ b/src/endpoints/tokenizers.js @@ -2,7 +2,7 @@ const fs = require('fs'); const path = require('path'); const express = require('express'); const { SentencePieceProcessor } = require('@agnai/sentencepiece-js'); -const tiktoken = require('@dqbd/tiktoken'); +const tiktoken = require('tiktoken'); const { Tokenizer } = require('@agnai/web-tokenizers'); const { convertClaudePrompt, convertGooglePrompt } = require('../prompt-converters'); const { readSecret, SECRET_KEYS } = require('./secrets'); @@ -15,7 +15,7 @@ const { setAdditionalHeaders } = require('../additional-headers'); */ /** - * @type {{[key: string]: import("@dqbd/tiktoken").Tiktoken}} Tokenizers cache + * @type {{[key: string]: import('tiktoken').Tiktoken}} Tokenizers cache */ const tokenizersCache = {}; @@ -262,6 +262,10 @@ function getWebTokenizersChunks(tokenizer, ids) { * @returns {string} Tokenizer model to use */ function getTokenizerModel(requestModel) { + if (requestModel.includes('gpt-4o')) { + return 'gpt-4o'; + } + if (requestModel.includes('gpt-4-32k')) { return 'gpt-4-32k'; }