diff --git a/.gitignore b/.gitignore index d6a5061bb..fbef3d33f 100644 --- a/.gitignore +++ b/.gitignore @@ -47,3 +47,4 @@ access.log public/css/user.css /plugins/ /data +/default/scaffold diff --git a/default/content/presets/openai/Default.json b/default/content/presets/openai/Default.json index dbf3b9619..132abeea0 100644 --- a/default/content/presets/openai/Default.json +++ b/default/content/presets/openai/Default.json @@ -231,6 +231,7 @@ "api_url_scale": "", "show_external_models": false, "assistant_prefill": "", + "assistant_impersonation": "", "human_sysprompt_message": "Let's get started. Please generate your response based on the information and instructions provided above.", "use_ai21_tokenizer": false, "use_google_tokenizer": false, diff --git a/default/content/settings.json b/default/content/settings.json index 3f4c25865..c04b88bfb 100644 --- a/default/content/settings.json +++ b/default/content/settings.json @@ -624,6 +624,7 @@ "show_external_models": false, "proxy_password": "", "assistant_prefill": "", + "assistant_impersonation": "", "use_ai21_tokenizer": false } } diff --git a/default/scaffold/README.md b/default/scaffold/README.md new file mode 100644 index 000000000..b1272cf8f --- /dev/null +++ b/default/scaffold/README.md @@ -0,0 +1,26 @@ +# Content Scaffolding + +Content files in this folder will be copied for all users (old and new) on the server startup. + +1. You **must** create an `index.json` file in `/default/scaffold` for it to work. The syntax is the same as for default content. +2. All file paths should be relative to `/default/scaffold`, the use of subdirectories is allowed. +3. Scaffolded files are copied first, so they override any of the default files (presets/settings/etc.) that have the same file name. + +## Example + +```json +[ + { + "filename": "themes/Midnight.json", + "type": "theme" + }, + { + "filename": "backgrounds/city.png", + "type": "background" + }, + { + "filename": "characters/Charlie.png", + "type": "character" + } +] +``` diff --git a/public/index.html b/public/index.html index 0c2232ff1..c0d8dead8 100644 --- a/public/index.html +++ b/public/index.html @@ -116,7 +116,7 @@
@@ -134,7 +134,7 @@

Chat Completion Presets

@@ -246,7 +246,7 @@
-
+
Temperature
@@ -1268,13 +1268,12 @@
-

-
+

+
-
- Dynamic Temperature + Dynamic Temperature

@@ -1734,6 +1733,8 @@
Assistant Prefill + Assistant Impersonation Prefill +
`, })); -SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'memberup', +SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'member-up', callback: moveGroupMemberUpCallback, - aliases: ['upmember'], + aliases: ['upmember', 'memberup'], unnamedArgumentList: [ new SlashCommandArgument( 'member index or name', [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.STRING], true, @@ -520,9 +522,9 @@ SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'memberup', ], helpString: 'Moves a group member up in the group chat list.', })); -SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'memberdown', +SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'member-down', callback: moveGroupMemberDownCallback, - aliases: ['downmember'], + aliases: ['downmember', 'memberdown'], unnamedArgumentList: [ new SlashCommandArgument( 'member index or name', [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.STRING], true, @@ -1421,7 +1423,7 @@ async function runCallback(args, name) { function abortCallback() { $('#send_textarea').val('')[0].dispatchEvent(new Event('input', { bubbles:true })); - throw new Error('/abort command executed'); + throw new Error('/abort command executed', { cause: 'abort' }); } async function delayCallback(_, amount) { @@ -2747,10 +2749,12 @@ export async function executeSlashCommandsOnChatInput(text, options = {}) { } } catch (e) { document.querySelector('#form_sheld').classList.add('script_error'); - toastr.error(e.message); result = new SlashCommandClosureResult(); result.isError = true; result.errorMessage = e.message; + if (e.cause !== 'abort') { + toastr.error(e.message); + } } finally { delay(1000).then(()=>clearCommandProgressDebounced()); diff --git a/public/scripts/slash-commands/SlashCommandAutoCompleteNameResult.js b/public/scripts/slash-commands/SlashCommandAutoCompleteNameResult.js index ba163fcd5..8a9c08653 100644 --- a/public/scripts/slash-commands/SlashCommandAutoCompleteNameResult.js +++ b/public/scripts/slash-commands/SlashCommandAutoCompleteNameResult.js @@ -58,6 +58,9 @@ export class SlashCommandAutoCompleteNameResult extends AutoCompleteNameResult { return new RegExp('=(.*)'); } } + if (!Array.isArray(this.executor.command?.namedArgumentList)) { + return null; + } const notProvidedNamedArguments = this.executor.command.namedArgumentList.filter(arg=>!this.executor.namedArgumentList.find(it=>it.name == arg.name)); let name; let value; @@ -130,6 +133,9 @@ export class SlashCommandAutoCompleteNameResult extends AutoCompleteNameResult { } getUnnamedArgumentAt(text, index, isSelect) { + if (!Array.isArray(this.executor.command?.unnamedArgumentList)) { + return null; + } const lastArgIsBlank = this.executor.unnamedArgumentList.slice(-1)[0]?.value == ''; const notProvidedArguments = this.executor.command.unnamedArgumentList.slice(this.executor.unnamedArgumentList.length - (lastArgIsBlank ? 1 : 0)); let value; diff --git a/public/scripts/templates/welcome.html b/public/scripts/templates/welcome.html index 6082b2cde..a950c5a11 100644 --- a/public/scripts/templates/welcome.html +++ b/public/scripts/templates/welcome.html @@ -11,9 +11,10 @@ Click and select a Chat API.
  • - Click and pick a character + Click and pick a character.
  • +You can browse a list of bundled characters in the Download Extensions & Assets menu within .

    Confused or lost?

      diff --git a/public/scripts/variables.js b/public/scripts/variables.js index f4d753ba6..ebaa7ebca 100644 --- a/public/scripts/variables.js +++ b/public/scripts/variables.js @@ -518,7 +518,7 @@ async function executeSubCommands(command, scope = null, parserFlags = null) { command = command.slice(1, -1); } - const result = await executeSlashCommands(command, true, scope, true, parserFlags); + const result = await executeSlashCommands(command, true, scope, false, parserFlags); if (!result || typeof result !== 'object') { return ''; diff --git a/public/scripts/world-info.js b/public/scripts/world-info.js index d21b36847..10a9cc47e 100644 --- a/public/scripts/world-info.js +++ b/public/scripts/world-info.js @@ -1,5 +1,5 @@ import { saveSettings, callPopup, substituteParams, getRequestHeaders, chat_metadata, this_chid, characters, saveCharacterDebounced, menu_type, eventSource, event_types, getExtensionPromptByName, saveMetadata, getCurrentChatId, extension_prompt_roles } from '../script.js'; -import { download, debounce, initScrollHeight, resetScrollHeight, parseJsonFile, extractDataFromPng, getFileBuffer, getCharaFilename, getSortableDelay, escapeRegex, PAGINATION_TEMPLATE, navigation_option, waitUntilCondition, isTrueBoolean, setValueByPath, flashHighlight, select2ModifyOptions, getSelect2OptionId, dynamicSelect2DataViaAjax, highlightRegex, select2ChoiceClickSubscribe } from './utils.js'; +import { download, debounce, initScrollHeight, resetScrollHeight, parseJsonFile, extractDataFromPng, getFileBuffer, getCharaFilename, getSortableDelay, escapeRegex, PAGINATION_TEMPLATE, navigation_option, waitUntilCondition, isTrueBoolean, setValueByPath, flashHighlight, select2ModifyOptions, getSelect2OptionId, dynamicSelect2DataViaAjax, highlightRegex, select2ChoiceClickSubscribe, isFalseBoolean } from './utils.js'; import { extension_settings, getContext } from './extensions.js'; import { NOTE_MODULE_NAME, metadata_keys, shouldWIAddPrompt } from './authors-note.js'; import { isMobile } from './RossAscends-mods.js'; @@ -548,6 +548,19 @@ function registerWorldInfoSlashCommands() { return ''; } + if (typeof newEntryTemplate[field] === 'boolean') { + const isTrue = isTrueBoolean(value); + const isFalse = isFalseBoolean(value); + + if (isTrue) { + value = String(true); + } + + if (isFalse) { + value = String(false); + } + } + const fuse = new Fuse(entries, { keys: [{ name: field, weight: 1 }], includeScore: true, @@ -1244,6 +1257,7 @@ function displayWorldEntries(name, data, navigation = navigation_option.none, fl } worldEntriesList.sortable({ + items: '.world_entry', delay: getSortableDelay(), handle: '.drag-handle', stop: async function (_event, _ui) { diff --git a/src/endpoints/content-manager.js b/src/endpoints/content-manager.js index d91319051..c282dba36 100644 --- a/src/endpoints/content-manager.js +++ b/src/endpoints/content-manager.js @@ -7,7 +7,9 @@ const { getConfigValue, color } = require('../util'); const { jsonParser } = require('../express-common'); const writeFileAtomicSync = require('write-file-atomic').sync; const contentDirectory = path.join(process.cwd(), 'default/content'); +const scaffoldDirectory = path.join(process.cwd(), 'default/scaffold'); const contentIndexPath = path.join(contentDirectory, 'index.json'); +const scaffoldIndexPath = path.join(scaffoldDirectory, 'index.json'); const characterCardParser = require('../character-card-parser.js'); const WHITELIST_GENERIC_URL_DOWNLOAD_SOURCES = getConfigValue('whitelistImportDomains', []); @@ -16,6 +18,8 @@ const WHITELIST_GENERIC_URL_DOWNLOAD_SOURCES = getConfigValue('whitelistImportDo * @typedef {Object} ContentItem * @property {string} filename * @property {string} type + * @property {string} [name] + * @property {string|null} [folder] */ /** @@ -48,9 +52,7 @@ const CONTENT_TYPES = { */ function getDefaultPresets(directories) { try { - const contentIndexText = fs.readFileSync(contentIndexPath, 'utf8'); - const contentIndex = JSON.parse(contentIndexText); - + const contentIndex = getContentIndex(); const presets = []; for (const contentItem of contentIndex) { @@ -112,8 +114,12 @@ async function seedContentForUser(contentIndex, directories, forceCategories) { continue; } - contentLog.push(contentItem.filename); - const contentPath = path.join(contentDirectory, contentItem.filename); + if (!contentItem.folder) { + console.log(`Content file ${contentItem.filename} has no parent folder`); + continue; + } + + const contentPath = path.join(contentItem.folder, contentItem.filename); if (!fs.existsSync(contentPath)) { console.log(`Content file ${contentItem.filename} is missing`); @@ -129,6 +135,7 @@ async function seedContentForUser(contentIndex, directories, forceCategories) { const basePath = path.parse(contentItem.filename).base; const targetPath = path.join(contentTarget, basePath); + contentLog.push(contentItem.filename); if (fs.existsSync(targetPath)) { console.log(`Content file ${contentItem.filename} already exists in ${contentTarget}`); @@ -157,8 +164,7 @@ async function checkForNewContent(directoriesList, forceCategories = []) { return; } - const contentIndexText = fs.readFileSync(contentIndexPath, 'utf8'); - const contentIndex = JSON.parse(contentIndexText); + const contentIndex = getContentIndex(); let anyContentAdded = false; for (const directories of directoriesList) { @@ -179,6 +185,38 @@ async function checkForNewContent(directoriesList, forceCategories = []) { } } +/** + * Gets combined content index from the content and scaffold directories. + * @returns {ContentItem[]} Array of content index + */ +function getContentIndex() { + const result = []; + + if (fs.existsSync(scaffoldIndexPath)) { + const scaffoldIndexText = fs.readFileSync(scaffoldIndexPath, 'utf8'); + const scaffoldIndex = JSON.parse(scaffoldIndexText); + if (Array.isArray(scaffoldIndex)) { + scaffoldIndex.forEach((item) => { + item.folder = scaffoldDirectory; + }); + result.push(...scaffoldIndex); + } + } + + if (fs.existsSync(contentIndexPath)) { + const contentIndexText = fs.readFileSync(contentIndexPath, 'utf8'); + const contentIndex = JSON.parse(contentIndexText); + if (Array.isArray(contentIndex)) { + contentIndex.forEach((item) => { + item.folder = contentDirectory; + }); + result.push(...contentIndex); + } + } + + return result; +} + /** * Gets the target directory for the specified asset type. * @param {ContentType} type Asset type