From fe355c5d4f0ab44da1c75e19aa22abcaae8ec0ed Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Fri, 1 Dec 2023 01:50:10 +0200 Subject: [PATCH] Add MVP set of WI manipulation commands --- public/scripts/variables.js | 4 + public/scripts/world-info.js | 152 ++++++++++++++++++++++++++++++++++- 2 files changed, 154 insertions(+), 2 deletions(-) diff --git a/public/scripts/variables.js b/public/scripts/variables.js index 5cf2f06e2..26480befa 100644 --- a/public/scripts/variables.js +++ b/public/scripts/variables.js @@ -95,6 +95,10 @@ function decrementGlobalVariable(name) { * @returns {string} Variable value or the string literal */ export function resolveVariable(name) { + if (name === undefined) { + return ''; + } + if (existsLocalVariable(name)) { return getLocalVariable(name); } diff --git a/public/scripts/world-info.js b/public/scripts/world-info.js index af0aa29a8..0df3ff95b 100644 --- a/public/scripts/world-info.js +++ b/public/scripts/world-info.js @@ -8,6 +8,7 @@ import { FILTER_TYPES, FilterHelper } from "./filters.js"; import { getTokenCount } from "./tokenizers.js"; import { power_user } from "./power-user.js"; import { getTagKeyForCharacter } from "./tags.js"; +import { resolveVariable } from "./variables.js"; export { world_info, @@ -189,6 +190,149 @@ function setWorldInfoSettings(settings, data) { const hasWorldInfo = !!chat_metadata[METADATA_KEY] && world_names.includes(chat_metadata[METADATA_KEY]); $('.chat_lorebook_button').toggleClass('world_set', hasWorldInfo); }); + + // Add slash commands + registerWorldInfoSlashCommands(); +} + +function registerWorldInfoSlashCommands() { + async function getEntriesFromFile(file) { + if (!file || !world_names.includes(file)) { + return ''; + } + + const data = await loadWorldInfoData(file); + + if (!data || !("entries" in data)) { + return ''; + } + + const entries = Object.values(data.entries); + + if (!entries || entries.length === 0) { + return ''; + } + + return entries; + } + + async function getChatBookCallback() { + const chatId = getCurrentChatId(); + + if (!chatId) { + return ''; + } + + if (chat_metadata[METADATA_KEY] && world_names.includes(chat_metadata[METADATA_KEY])) { + return chat_metadata[METADATA_KEY]; + } + + // Replace non-alphanumeric characters with underscores, cut to 64 characters + const name = `Chat Book ${getCurrentChatId()}`.replace(/[^a-z0-9]/gi, '_').replace(/_{2,}/g, '_').substring(0, 64); + await createNewWorldInfo(name); + + chat_metadata[METADATA_KEY] = name; + await saveMetadata(); + $('.chat_lorebook_button').addClass('world_set'); + return name; + } + + async function findBookEntryCallback(args, value) { + const file = resolveVariable(args.file); + const field = args.field || 'key'; + + const entries = await getEntriesFromFile(file); + + if (!entries) { + return ''; + } + + const fuse = new Fuse(entries, { + keys: [{ name: field, weight: 1 }], + includeScore: true, + threshold: 0.3, + }); + + const results = fuse.search(value); + + if (!results || results.length === 0) { + return ''; + } + + const result = results[0]?.item?.uid; + + if (result === undefined) { + return ''; + } + + return result; + } + + async function getEntryFieldCallback(args, uid) { + const file = resolveVariable(args.file); + const field = args.field || 'content'; + + const entries = await getEntriesFromFile(file); + + if (!entries) { + return ''; + } + + const entry = entries.find(x => x.uid === uid); + + if (!entry) { + return ''; + } + + const fieldValue = entry[field]; + + if (fieldValue === undefined) { + return ''; + } + + if (Array.isArray(fieldValue)) { + return fieldValue.map(x => substituteParams(x)).join(', '); + } + + return substituteParams(fieldValue); + } + + async function createEntryCallback(args, content) { + const file = resolveVariable(args.file); + const key = args.key; + + const data = await loadWorldInfoData(file); + + if (!data || !("entries" in data)) { + return ''; + } + + const entry = createWorldInfoEntry(file, data, true); + + if (key) { + entry.key.push(key); + entry.addMemo = true; + entry.comment = key; + } + + if (content) { + entry.content = content; + } + + await saveWorldInfo(file, data, true); + + const selectedIndex = world_names.indexOf(file); + if (selectedIndex !== -1) { + $('#world_editor_select').val(selectedIndex).trigger('change'); + } + + return entry.uid; + } + + registerSlashCommand('getchatbook', getChatBookCallback, ['getchatlore', 'getchatwi'], '– get a name of the chat-bound lorebook or create a new one if was unbound, and pass it down the pipe', true, true); + registerSlashCommand('findentry', findBookEntryCallback, ['findlore', 'findwi'], `(file=bookName by=field [texts]) – find a UID of the record from the specified book using the fuzzy match of a field value (default: key) and pass it down the pipe, e.g. /findentry file=chatLore by=key Shadowfang`, true, true); + registerSlashCommand('getentryfield', getEntryFieldCallback, ['getlorefield', 'getwifield'], '(file=bookName field=field [UID]) – get a field value (default: content) of the record with the UID from the specified book and pass it down the pipe, e.g. /getentryfield file=chatLore field=content 123', true, true); + registerSlashCommand('createentry', createEntryCallback, ['createlore', 'createwi'], '(file=bookName key=key [content]) – create a new record in the specified book with the key and content (both are optional) and pass the UID down the pipe, e.g. /createentry file=chatLore key=Shadowfang The sword of the king', true, true); } // World Info Editor @@ -1134,7 +1278,7 @@ async function deleteWorldInfoEntry(data, uid) { delete data.entries[uid]; } -function createWorldInfoEntry(name, data) { +function createWorldInfoEntry(name, data, fromSlashCommand = false) { const newEntryTemplate = { key: [], keysecondary: [], @@ -1161,7 +1305,11 @@ function createWorldInfoEntry(name, data) { const newEntry = { uid: newUid, ...newEntryTemplate }; data.entries[newUid] = newEntry; - updateEditor(newUid); + if (!fromSlashCommand) { + updateEditor(newUid); + } + + return newEntry; } async function _save(name, data) {