Merge branch 'staging' into 202411-backend-maxctx

This commit is contained in:
Cohee
2024-12-01 14:39:01 +02:00
27 changed files with 503 additions and 441 deletions

View File

@ -627,6 +627,14 @@
"filename": "presets/instruct/Synthia.json", "filename": "presets/instruct/Synthia.json",
"type": "instruct" "type": "instruct"
}, },
{
"filename": "presets/instruct/Tulu.json",
"type": "instruct"
},
{
"filename": "presets/context/Tulu.json",
"type": "context"
},
{ {
"filename": "presets/instruct/Vicuna 1.0.json", "filename": "presets/instruct/Vicuna 1.0.json",
"type": "instruct" "type": "instruct"

View File

@ -0,0 +1,11 @@
{
"story_string": "<|system|>\n{{#if system}}{{system}}\n{{/if}}{{#if wiBefore}}{{wiBefore}}\n{{/if}}{{#if description}}{{description}}\n{{/if}}{{#if personality}}{{char}}'s personality: {{personality}}\n{{/if}}{{#if scenario}}Scenario: {{scenario}}\n{{/if}}{{#if wiAfter}}{{wiAfter}}\n{{/if}}{{#if persona}}{{persona}}\n{{/if}}{{trim}}\n",
"example_separator": "",
"chat_start": "",
"use_stop_strings": false,
"allow_jailbreak": false,
"always_force_name2": true,
"trim_sentences": false,
"single_line": false,
"name": "Tulu"
}

View File

@ -0,0 +1,22 @@
{
"input_sequence": "<|user|>\n",
"output_sequence": "<|assistant|>\n",
"first_output_sequence": "",
"last_output_sequence": "",
"system_sequence_prefix": "",
"system_sequence_suffix": "",
"stop_sequence": "<|end_of_text|>",
"wrap": false,
"macro": true,
"names_behavior": "always",
"activation_regex": "",
"skip_examples": false,
"output_suffix": "<|end_of_text|>\n",
"input_suffix": "\n",
"system_sequence": "<|system|>\n",
"system_suffix": "\n",
"user_alignment_message": "",
"last_system_sequence": "",
"system_same_as_user": false,
"name": "Tulu"
}

View File

@ -230,7 +230,6 @@
"show_external_models": false, "show_external_models": false,
"assistant_prefill": "", "assistant_prefill": "",
"assistant_impersonation": "", "assistant_impersonation": "",
"human_sysprompt_message": "Let's get started. Please generate your response based on the information and instructions provided above.",
"claude_use_sysprompt": false, "claude_use_sysprompt": false,
"use_alt_scale": false, "use_alt_scale": false,
"squash_system_messages": false, "squash_system_messages": false,

257
package-lock.json generated
View File

@ -1,12 +1,12 @@
{ {
"name": "sillytavern", "name": "sillytavern",
"version": "1.12.7", "version": "1.12.8",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "sillytavern", "name": "sillytavern",
"version": "1.12.7", "version": "1.12.8",
"hasInstallScript": true, "hasInstallScript": true,
"license": "AGPL-3.0", "license": "AGPL-3.0",
"dependencies": { "dependencies": {
@ -67,7 +67,6 @@
"vectra": "^0.2.2", "vectra": "^0.2.2",
"wavefile": "^11.0.0", "wavefile": "^11.0.0",
"webpack": "^5.95.0", "webpack": "^5.95.0",
"webpack-dev-middleware": "^7.4.2",
"write-file-atomic": "^5.0.1", "write-file-atomic": "^5.0.1",
"ws": "^8.17.1", "ws": "^8.17.1",
"yaml": "^2.3.4", "yaml": "^2.3.4",
@ -866,60 +865,6 @@
"@jridgewell/sourcemap-codec": "^1.4.14" "@jridgewell/sourcemap-codec": "^1.4.14"
} }
}, },
"node_modules/@jsonjoy.com/base64": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@jsonjoy.com/base64/-/base64-1.1.2.tgz",
"integrity": "sha512-q6XAnWQDIMA3+FTiOYajoYqySkO+JSat0ytXGSuRdq9uXE7o92gzuQwQM14xaCRlBLGq3v5miDGC4vkVTn54xA==",
"license": "Apache-2.0",
"engines": {
"node": ">=10.0"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/streamich"
},
"peerDependencies": {
"tslib": "2"
}
},
"node_modules/@jsonjoy.com/json-pack": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pack/-/json-pack-1.1.0.tgz",
"integrity": "sha512-zlQONA+msXPPwHWZMKFVS78ewFczIll5lXiVPwFPCZUsrOKdxc2AvxU1HoNBmMRhqDZUR9HkC3UOm+6pME6Xsg==",
"license": "Apache-2.0",
"dependencies": {
"@jsonjoy.com/base64": "^1.1.1",
"@jsonjoy.com/util": "^1.1.2",
"hyperdyperid": "^1.2.0",
"thingies": "^1.20.0"
},
"engines": {
"node": ">=10.0"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/streamich"
},
"peerDependencies": {
"tslib": "2"
}
},
"node_modules/@jsonjoy.com/util": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/@jsonjoy.com/util/-/util-1.5.0.tgz",
"integrity": "sha512-ojoNsrIuPI9g6o8UxhraZQSyF2ByJanAY4cTFbc8Mf2AXEF4aQRGY1dJxyJpuyav8r9FGflEt/Ff3u5Nt6YMPA==",
"license": "Apache-2.0",
"engines": {
"node": ">=10.0"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/streamich"
},
"peerDependencies": {
"tslib": "2"
}
},
"node_modules/@kwsites/file-exists": { "node_modules/@kwsites/file-exists": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/@kwsites/file-exists/-/file-exists-1.1.1.tgz", "resolved": "https://registry.npmjs.org/@kwsites/file-exists/-/file-exists-1.1.1.tgz",
@ -1803,45 +1748,6 @@
"url": "https://github.com/sponsors/epoberezkin" "url": "https://github.com/sponsors/epoberezkin"
} }
}, },
"node_modules/ajv-formats": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz",
"integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==",
"license": "MIT",
"dependencies": {
"ajv": "^8.0.0"
},
"peerDependencies": {
"ajv": "^8.0.0"
},
"peerDependenciesMeta": {
"ajv": {
"optional": true
}
}
},
"node_modules/ajv-formats/node_modules/ajv": {
"version": "8.17.1",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz",
"integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
"license": "MIT",
"dependencies": {
"fast-deep-equal": "^3.1.3",
"fast-uri": "^3.0.1",
"json-schema-traverse": "^1.0.0",
"require-from-string": "^2.0.2"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/epoberezkin"
}
},
"node_modules/ajv-formats/node_modules/json-schema-traverse": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
"license": "MIT"
},
"node_modules/ajv-keywords": { "node_modules/ajv-keywords": {
"version": "3.5.2", "version": "3.5.2",
"resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz",
@ -2602,12 +2508,6 @@
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/colorette": {
"version": "2.0.20",
"resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz",
"integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==",
"license": "MIT"
},
"node_modules/combined-stream": { "node_modules/combined-stream": {
"version": "1.0.8", "version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
@ -3785,12 +3685,6 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/fast-uri": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.3.tgz",
"integrity": "sha512-aLrHthzCjH5He4Z2H9YZ+v6Ujb9ocRuW6ZzkJQOrTxleEijANq4v1TsaPaVG1PZcuurEzrLcWRyYBYXD5cEiaw==",
"license": "BSD-3-Clause"
},
"node_modules/fastq": { "node_modules/fastq": {
"version": "1.15.0", "version": "1.15.0",
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz",
@ -4541,15 +4435,6 @@
"ms": "^2.0.0" "ms": "^2.0.0"
} }
}, },
"node_modules/hyperdyperid": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/hyperdyperid/-/hyperdyperid-1.2.0.tgz",
"integrity": "sha512-Y93lCzHYgGWdrJ66yIktxiaGULYc6oGiABxhcO5AufBeOyoIdZF7bIfLaOrbM0iGIOXQQgxxRrFEnb+Y6w1n4A==",
"license": "MIT",
"engines": {
"node": ">=10.18"
}
},
"node_modules/iconv-lite": { "node_modules/iconv-lite": {
"version": "0.6.3", "version": "0.6.3",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
@ -5176,25 +5061,6 @@
"node": ">= 0.6" "node": ">= 0.6"
} }
}, },
"node_modules/memfs": {
"version": "4.14.0",
"resolved": "https://registry.npmjs.org/memfs/-/memfs-4.14.0.tgz",
"integrity": "sha512-JUeY0F/fQZgIod31Ja1eJgiSxLn7BfQlCnqhwXFBzFHEw63OdLK7VJUJ7bnzNsWgCyoUP5tEp1VRY8rDaYzqOA==",
"license": "Apache-2.0",
"dependencies": {
"@jsonjoy.com/json-pack": "^1.0.3",
"@jsonjoy.com/util": "^1.3.0",
"tree-dump": "^1.0.1",
"tslib": "^2.0.0"
},
"engines": {
"node": ">= 4.0.0"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/streamich"
}
},
"node_modules/merge-descriptors": { "node_modules/merge-descriptors": {
"version": "1.0.3", "version": "1.0.3",
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz",
@ -6310,15 +6176,6 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/require-from-string": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
"integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/resolve-alpn": { "node_modules/resolve-alpn": {
"version": "1.2.1", "version": "1.2.1",
"resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz", "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz",
@ -6447,59 +6304,6 @@
"integrity": "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==", "integrity": "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==",
"license": "ISC" "license": "ISC"
}, },
"node_modules/schema-utils": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.2.0.tgz",
"integrity": "sha512-L0jRsrPpjdckP3oPug3/VxNKt2trR8TcabrM6FOAAlvC/9Phcmm+cuAgTlxBqdBR1WJx7Naj9WHw+aOmheSVbw==",
"license": "MIT",
"dependencies": {
"@types/json-schema": "^7.0.9",
"ajv": "^8.9.0",
"ajv-formats": "^2.1.1",
"ajv-keywords": "^5.1.0"
},
"engines": {
"node": ">= 12.13.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/webpack"
}
},
"node_modules/schema-utils/node_modules/ajv": {
"version": "8.17.1",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz",
"integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
"license": "MIT",
"dependencies": {
"fast-deep-equal": "^3.1.3",
"fast-uri": "^3.0.1",
"json-schema-traverse": "^1.0.0",
"require-from-string": "^2.0.2"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/epoberezkin"
}
},
"node_modules/schema-utils/node_modules/ajv-keywords": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz",
"integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==",
"license": "MIT",
"dependencies": {
"fast-deep-equal": "^3.1.3"
},
"peerDependencies": {
"ajv": "^8.8.2"
}
},
"node_modules/schema-utils/node_modules/json-schema-traverse": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
"license": "MIT"
},
"node_modules/seedrandom": { "node_modules/seedrandom": {
"version": "3.0.5", "version": "3.0.5",
"resolved": "https://registry.npmjs.org/seedrandom/-/seedrandom-3.0.5.tgz", "resolved": "https://registry.npmjs.org/seedrandom/-/seedrandom-3.0.5.tgz",
@ -7038,18 +6842,6 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/thingies": {
"version": "1.21.0",
"resolved": "https://registry.npmjs.org/thingies/-/thingies-1.21.0.tgz",
"integrity": "sha512-hsqsJsFMsV+aD4s3CWKk85ep/3I9XzYV/IXaSouJMYIoDlgyi11cBhsqYe9/geRfB0YIikBQg6raRaM+nIMP9g==",
"license": "Unlicense",
"engines": {
"node": ">=10.18"
},
"peerDependencies": {
"tslib": "^2"
}
},
"node_modules/tiktoken": { "node_modules/tiktoken": {
"version": "1.0.16", "version": "1.0.16",
"resolved": "https://registry.npmjs.org/tiktoken/-/tiktoken-1.0.16.tgz", "resolved": "https://registry.npmjs.org/tiktoken/-/tiktoken-1.0.16.tgz",
@ -7106,22 +6898,6 @@
"node": ">=18" "node": ">=18"
} }
}, },
"node_modules/tree-dump": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/tree-dump/-/tree-dump-1.0.2.tgz",
"integrity": "sha512-dpev9ABuLWdEubk+cIaI9cHwRNNDjkBBLXTwI4UCUFdQ5xXKqNXoK4FEciw/vxf+NQ7Cb7sGUyeUtORvHIdRXQ==",
"license": "Apache-2.0",
"engines": {
"node": ">=10.0"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/streamich"
},
"peerDependencies": {
"tslib": "2"
}
},
"node_modules/truncate-utf8-bytes": { "node_modules/truncate-utf8-bytes": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/truncate-utf8-bytes/-/truncate-utf8-bytes-1.0.2.tgz", "resolved": "https://registry.npmjs.org/truncate-utf8-bytes/-/truncate-utf8-bytes-1.0.2.tgz",
@ -7425,35 +7201,6 @@
} }
} }
}, },
"node_modules/webpack-dev-middleware": {
"version": "7.4.2",
"resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-7.4.2.tgz",
"integrity": "sha512-xOO8n6eggxnwYpy1NlzUKpvrjfJTvae5/D6WOK0S2LSo7vjmo5gCM1DbLUmFqrMTJP+W/0YZNctm7jasWvLuBA==",
"license": "MIT",
"dependencies": {
"colorette": "^2.0.10",
"memfs": "^4.6.0",
"mime-types": "^2.1.31",
"on-finished": "^2.4.1",
"range-parser": "^1.2.1",
"schema-utils": "^4.0.0"
},
"engines": {
"node": ">= 18.12.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/webpack"
},
"peerDependencies": {
"webpack": "^5.0.0"
},
"peerDependenciesMeta": {
"webpack": {
"optional": true
}
}
},
"node_modules/webpack-sources": { "node_modules/webpack-sources": {
"version": "3.2.3", "version": "3.2.3",
"resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz", "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz",

View File

@ -57,7 +57,6 @@
"vectra": "^0.2.2", "vectra": "^0.2.2",
"wavefile": "^11.0.0", "wavefile": "^11.0.0",
"webpack": "^5.95.0", "webpack": "^5.95.0",
"webpack-dev-middleware": "^7.4.2",
"write-file-atomic": "^5.0.1", "write-file-atomic": "^5.0.1",
"ws": "^8.17.1", "ws": "^8.17.1",
"yaml": "^2.3.4", "yaml": "^2.3.4",
@ -85,7 +84,7 @@
"type": "git", "type": "git",
"url": "https://github.com/SillyTavern/SillyTavern.git" "url": "https://github.com/SillyTavern/SillyTavern.git"
}, },
"version": "1.12.7", "version": "1.12.8",
"scripts": { "scripts": {
"start": "node server.js", "start": "node server.js",
"start:deno": "deno run --allow-run --allow-net --allow-read --allow-write --allow-sys --allow-env server.js", "start:deno": "deno run --allow-run --allow-net --allow-read --allow-write --allow-sys --allow-env server.js",

View File

@ -42,3 +42,9 @@ body.login .userSelect .userHandle {
body.login .userSelect:hover { body.login .userSelect:hover {
background-color: var(--black30a); background-color: var(--black30a);
} }
body.login #handleEntryBlock,
body.login #passwordEntryBlock,
body.login #passwordRecoveryBlock {
margin: 2px;
}

View File

@ -1342,12 +1342,12 @@
<input class="neo-range-slider" type="range" id="presence_pen_textgenerationwebui" name="volume" min="-2" max="2" step="0.01" /> <input class="neo-range-slider" type="range" id="presence_pen_textgenerationwebui" name="volume" min="-2" max="2" step="0.01" />
<input class="neo-range-input" type="number" min="-2" max="2" step="0.01" data-for="presence_pen_textgenerationwebui" id="presence_pen_counter_textgenerationwebui"> <input class="neo-range-input" type="number" min="-2" max="2" step="0.01" data-for="presence_pen_textgenerationwebui" id="presence_pen_counter_textgenerationwebui">
</div> </div>
<div data-tg-type="ooba" class="alignitemscenter flex-container flexFlowColumn flexBasis30p flexGrow flexShrink gap0"> <div data-tg-type="aphrodite, ooba" class="alignitemscenter flex-container flexFlowColumn flexBasis30p flexGrow flexShrink gap0">
<small data-i18n="No Repeat Ngram Size">No Repeat Ngram Size</small> <small data-i18n="No Repeat Ngram Size">No Repeat Ngram Size</small>
<input class="neo-range-slider" type="range" id="no_repeat_ngram_size_textgenerationwebui" name="volume" min="0" max="20" step="1"> <input class="neo-range-slider" type="range" id="no_repeat_ngram_size_textgenerationwebui" name="volume" min="0" max="20" step="1">
<input class="neo-range-input" type="number" min="0" max="20" step="1" data-for="no_repeat_ngram_size_textgenerationwebui" id="no_repeat_ngram_size_counter_textgenerationwebui"> <input class="neo-range-input" type="number" min="0" max="20" step="1" data-for="no_repeat_ngram_size_textgenerationwebui" id="no_repeat_ngram_size_counter_textgenerationwebui">
</div> </div>
<div data-tg-type="tabby" class="alignitemscenter flex-container flexFlowColumn flexBasis30p flexGrow flexShrink gap0"> <div data-tg-type="tabby, aphrodite" class="alignitemscenter flex-container flexFlowColumn flexBasis30p flexGrow flexShrink gap0">
<small data-i18n="Skew">Skew</small> <small data-i18n="Skew">Skew</small>
<input class="neo-range-slider" type="range" id="skew_textgenerationwebui" name="volume" min="-5" max="5" step="0.01" /> <input class="neo-range-slider" type="range" id="skew_textgenerationwebui" name="volume" min="-5" max="5" step="0.01" />
<input class="neo-range-input" type="number" min="-5" max="5" step="0.01" data-for="skew_textgenerationwebui" id="skew_counter_textgenerationwebui"> <input class="neo-range-input" type="number" min="-5" max="5" step="0.01" data-for="skew_textgenerationwebui" id="skew_counter_textgenerationwebui">
@ -1402,7 +1402,7 @@
</div> </div>
</div> </div>
<div data-tg-type="ooba, koboldcpp, tabby, llamacpp" id="dryBlock" class="wide100p"> <div data-tg-type="aphrodite, ooba, koboldcpp, tabby, llamacpp" id="dryBlock" class="wide100p">
<h4 class="wide100p textAlignCenter" title="DRY penalizes tokens that would extend the end of the input into a sequence that has previously occurred in the input. Set multiplier to 0 to disable." data-i18n="[title]DRY_Repetition_Penalty_desc"> <h4 class="wide100p textAlignCenter" title="DRY penalizes tokens that would extend the end of the input into a sequence that has previously occurred in the input. Set multiplier to 0 to disable." data-i18n="[title]DRY_Repetition_Penalty_desc">
<label data-i18n="DRY Repetition Penalty">DRY Repetition Penalty</label> <label data-i18n="DRY Repetition Penalty">DRY Repetition Penalty</label>
<a href="https://github.com/oobabooga/text-generation-webui/pull/5677" target="_blank"> <a href="https://github.com/oobabooga/text-generation-webui/pull/5677" target="_blank">
@ -1959,15 +1959,6 @@
Send the system prompt for supported models. If disabled, the user message is added to the beginning of the prompt. Send the system prompt for supported models. If disabled, the user message is added to the beginning of the prompt.
</span> </span>
</div> </div>
<div id="claude_human_sysprompt_message_block" class="wide100p">
<div class="range-block-title openai_restorable">
<span data-i18n="User first message">User first message</span>
<div id="claude_human_sysprompt_message_restore" title="Restore User first message" data-i18n="[title]Restore User first message" class="right_menu_button">
<div class="fa-solid fa-clock-rotate-left"></div>
</div>
</div>
<textarea id="claude_human_sysprompt_textarea" class="text_pole textarea_compact autoSetHeight" rows="2" data-i18n="[placeholder]Human message" placeholder="Human message, instruction, etc.&#10;Adds nothing when empty, i.e. requires a new prompt with the role 'user'."></textarea>
</div>
</div> </div>
</div> </div>
<div class="range-block m-t-1" data-source="openai,openrouter,scale,custom"> <div class="range-block m-t-1" data-source="openai,openrouter,scale,custom">
@ -3287,7 +3278,7 @@
Advanced Formatting Advanced Formatting
</span> </span>
<a href="https://docs.sillytavern.app/usage/prompts/" class="notes-link" target="_blank"> <a href="https://docs.sillytavern.app/usage/core-concepts/advancedformatting/" class="notes-link" target="_blank">
<span class="fa-solid fa-circle-question note-link-span"></span> <span class="fa-solid fa-circle-question note-link-span"></span>
</a> </a>
</h3> </h3>
@ -3749,7 +3740,7 @@
</div> </div>
<h3 class="margin0"> <h3 class="margin0">
<span data-i18n="Worlds/Lorebooks">Worlds/Lorebooks</span> <span data-i18n="Worlds/Lorebooks">Worlds/Lorebooks</span>
<a href="https://docs.sillytavern.app/usage/worldinfo/" class="notes-link" target="_blank"> <a href="https://docs.sillytavern.app/usage/core-concepts/worldinfo/" class="notes-link" target="_blank">
<span class="fa-solid fa-circle-question note-link-span"></span> <span class="fa-solid fa-circle-question note-link-span"></span>
</a> </a>
</h3> </h3>
@ -3943,7 +3934,13 @@
<div name="userSettingsRowOne" class="flex-container flexFlowRow alignitemscenter spaceBetween"> <div name="userSettingsRowOne" class="flex-container flexFlowRow alignitemscenter spaceBetween">
<div class="flex-container"> <div class="flex-container">
<div class="flex-container flexnowrap alignItemsBaseline"> <div class="flex-container flexnowrap alignItemsBaseline">
<h3 class="margin0"><span data-i18n="User Settings">User Settings</span></h3> <h3 class="margin0">
<span data-i18n="User Settings">User Settings</span>
<a href="https://docs.sillytavern.app/usage/user_settings/" class="notes-link" target="_blank">
<span class="fa-solid fa-circle-question note-link-span"></span>
</a>
</h3>
</div> </div>
</div> </div>
<div id="UI-language-block" class="flex-container alignItemsBaseline"> <div id="UI-language-block" class="flex-container alignItemsBaseline">
@ -4276,9 +4273,6 @@
<audio id="audio_message_sound" src="sounds/message.mp3" hidden></audio> <audio id="audio_message_sound" src="sounds/message.mp3" hidden></audio>
<span> <span>
<small data-i18n="Message Sound">Message Sound</small> <small data-i18n="Message Sound">Message Sound</small>
<a href="https://docs.sillytavern.app/usage/user_settings/uicustomization/#message-sound" class="notes-link" target="_blank">
<span class="fa-solid fa-circle-question note-link-span"></span>
</a>
</span> </span>
</label> </label>
<label for="play_sound_unfocused" class="checkbox_label" title="Only play a sound when ST's browser tab is unfocused." data-i18n="[title]Only play a sound when ST's browser tab is unfocused"> <label for="play_sound_unfocused" class="checkbox_label" title="Only play a sound when ST's browser tab is unfocused." data-i18n="[title]Only play a sound when ST's browser tab is unfocused">

View File

@ -1633,7 +1633,7 @@ export function getEntitiesList({ doFilter = false, doSort = true } = {}) {
subEntities = filterByTagState(entities, { subForEntity: entity }); subEntities = filterByTagState(entities, { subForEntity: entity });
if (doFilter) { if (doFilter) {
// sub entities filter "hacked" because folder filter should not be applied there, so even in "only folders" mode characters show up // sub entities filter "hacked" because folder filter should not be applied there, so even in "only folders" mode characters show up
subEntities = entitiesFilter.applyFilters(subEntities, { clearScoreCache: false, tempOverrides: { [FILTER_TYPES.FOLDER]: FILTER_STATES.UNDEFINED } }); subEntities = entitiesFilter.applyFilters(subEntities, { clearScoreCache: false, tempOverrides: { [FILTER_TYPES.FOLDER]: FILTER_STATES.UNDEFINED }, clearFuzzySearchCaches: false });
} }
if (doSort) { if (doSort) {
sortEntitiesList(subEntities); sortEntitiesList(subEntities);
@ -1646,11 +1646,11 @@ export function getEntitiesList({ doFilter = false, doSort = true } = {}) {
// Second run filters, hiding whatever should be filtered later // Second run filters, hiding whatever should be filtered later
if (doFilter) { if (doFilter) {
const beforeFinalEntities = filterByTagState(entities, { globalDisplayFilters: true }); const beforeFinalEntities = filterByTagState(entities, { globalDisplayFilters: true });
entities = entitiesFilter.applyFilters(beforeFinalEntities); entities = entitiesFilter.applyFilters(beforeFinalEntities, { clearFuzzySearchCaches: false });
// Magic for folder filter. If that one is enabled, and no folders are display anymore, we remove that filter to actually show the characters. // Magic for folder filter. If that one is enabled, and no folders are display anymore, we remove that filter to actually show the characters.
if (isFilterState(entitiesFilter.getFilterData(FILTER_TYPES.FOLDER), FILTER_STATES.SELECTED) && entities.filter(x => x.type == 'tag').length == 0) { if (isFilterState(entitiesFilter.getFilterData(FILTER_TYPES.FOLDER), FILTER_STATES.SELECTED) && entities.filter(x => x.type == 'tag').length == 0) {
entities = entitiesFilter.applyFilters(beforeFinalEntities, { tempOverrides: { [FILTER_TYPES.FOLDER]: FILTER_STATES.UNDEFINED } }); entities = entitiesFilter.applyFilters(beforeFinalEntities, { tempOverrides: { [FILTER_TYPES.FOLDER]: FILTER_STATES.UNDEFINED }, clearFuzzySearchCaches: false });
} }
} }
@ -1666,6 +1666,7 @@ export function getEntitiesList({ doFilter = false, doSort = true } = {}) {
if (doSort) { if (doSort) {
sortEntitiesList(entities); sortEntitiesList(entities);
} }
entitiesFilter.clearFuzzySearchCaches();
return entities; return entities;
} }

View File

@ -53,6 +53,12 @@ const hash_derivations = {
// command-r-08-2024 // command-r-08-2024
'Command R' 'Command R'
, ,
// Tulu
'ac7498a36a719da630e99d48e6ebc4409de85a77556c2b6159eeb735bcbd11df':
// Tulu-3-8B
// Tulu-3-70B
'Tulu'
}; };
const substr_derivations = { const substr_derivations = {

View File

@ -419,30 +419,35 @@ export class SlashCommandHandler {
namedArgumentList: [ namedArgumentList: [
SlashCommandNamedArgument.fromProps({ SlashCommandNamedArgument.fromProps({
name: 'set', name: 'set',
description: 'QR set name', description: 'Name of QR set to add the context menu to',
typeList: [ARGUMENT_TYPE.STRING], typeList: [ARGUMENT_TYPE.STRING],
isRequired: true, isRequired: true,
enumProvider: localEnumProviders.qrSets, enumProvider: localEnumProviders.qrSets,
}), }),
SlashCommandNamedArgument.fromProps({ SlashCommandNamedArgument.fromProps({
name: 'label', name: 'label',
description: 'Quick Reply label', description: 'Label of Quick Reply to add the context menu to',
typeList: [ARGUMENT_TYPE.STRING], typeList: [ARGUMENT_TYPE.STRING],
enumProvider: localEnumProviders.qrEntries, enumProvider: localEnumProviders.qrEntries,
}), }),
SlashCommandNamedArgument.fromProps({ SlashCommandNamedArgument.fromProps({
name: 'id', name: 'id',
description: 'numeric ID of the QR, e.g., id=42', description: 'Numeric ID of Quick Reply to add the context menu to, e.g. id=42',
typeList: [ARGUMENT_TYPE.NUMBER], typeList: [ARGUMENT_TYPE.NUMBER],
enumProvider: localEnumProviders.qrIds, enumProvider: localEnumProviders.qrIds,
}), }),
new SlashCommandNamedArgument( new SlashCommandNamedArgument(
'chain', 'boolean', [ARGUMENT_TYPE.BOOLEAN], false, false, 'false', 'chain',
'If true, button QR is sent together with (before) the clicked QR from the context menu',
[ARGUMENT_TYPE.BOOLEAN],
false,
false,
'false',
), ),
], ],
unnamedArgumentList: [ unnamedArgumentList: [
SlashCommandArgument.fromProps({ SlashCommandArgument.fromProps({
description: 'QR set name', description: 'Name of QR set to add as a context menu',
typeList: [ARGUMENT_TYPE.STRING], typeList: [ARGUMENT_TYPE.STRING],
isRequired: true, isRequired: true,
enumProvider: localEnumProviders.qrSets, enumProvider: localEnumProviders.qrSets,
@ -450,13 +455,16 @@ export class SlashCommandHandler {
], ],
helpString: ` helpString: `
<div> <div>
Add context menu preset to a QR. Add a context menu preset to a QR.
</div>
<div>
If <code>id</code> and <code>label</code> are both provided, <code>id</code> will be used.
</div> </div>
<div> <div>
<strong>Example:</strong> <strong>Example:</strong>
<ul> <ul>
<li> <li>
<pre><code>/qr-contextadd set=MyPreset label=MyButton chain=true MyOtherPreset</code></pre> <pre><code>/qr-contextadd set=MyQRSetWithTheButton label=MyButton chain=true MyQRSetWithContextItems</code></pre>
</li> </li>
</ul> </ul>
</div> </div>
@ -470,27 +478,27 @@ export class SlashCommandHandler {
namedArgumentList: [ namedArgumentList: [
SlashCommandNamedArgument.fromProps({ SlashCommandNamedArgument.fromProps({
name: 'set', name: 'set',
description: 'QR set name', description: 'Name of QR set to remove the context menu from',
typeList: [ARGUMENT_TYPE.STRING], typeList: [ARGUMENT_TYPE.STRING],
isRequired: true, isRequired: true,
enumProvider: localEnumProviders.qrSets, enumProvider: localEnumProviders.qrSets,
}), }),
SlashCommandNamedArgument.fromProps({ SlashCommandNamedArgument.fromProps({
name: 'label', name: 'label',
description: 'Quick Reply label', description: 'Label of Quick Reply to remove the context menu from',
typeList: [ARGUMENT_TYPE.STRING], typeList: [ARGUMENT_TYPE.STRING],
enumProvider: localEnumProviders.qrEntries, enumProvider: localEnumProviders.qrEntries,
}), }),
SlashCommandNamedArgument.fromProps({ SlashCommandNamedArgument.fromProps({
name: 'id', name: 'id',
description: 'numeric ID of the QR, e.g., id=42', description: 'Numeric ID of Quick Reply to remove the context menu from, e.g. id=42',
typeList: [ARGUMENT_TYPE.NUMBER], typeList: [ARGUMENT_TYPE.NUMBER],
enumProvider: localEnumProviders.qrIds, enumProvider: localEnumProviders.qrIds,
}), }),
], ],
unnamedArgumentList: [ unnamedArgumentList: [
SlashCommandArgument.fromProps({ SlashCommandArgument.fromProps({
description: 'QR set name', description: 'Name of QR set to remove',
typeList: [ARGUMENT_TYPE.STRING], typeList: [ARGUMENT_TYPE.STRING],
isRequired: true, isRequired: true,
enumProvider: localEnumProviders.qrSets, enumProvider: localEnumProviders.qrSets,
@ -500,6 +508,9 @@ export class SlashCommandHandler {
<div> <div>
Remove context menu preset from a QR. Remove context menu preset from a QR.
</div> </div>
<div>
If <code>id</code> and <code>label</code> are both provided, <code>id</code> will be used.
</div>
<div> <div>
<strong>Example:</strong> <strong>Example:</strong>
<ul> <ul>
@ -541,6 +552,9 @@ export class SlashCommandHandler {
<div> <div>
Remove all context menu presets from a QR. Remove all context menu presets from a QR.
</div> </div>
<div>
If <code>id</code> and a label are both provided, <code>id</code> will be used.
</div>
<div> <div>
<strong>Example:</strong> <strong>Example:</strong>
<ul> <ul>
@ -908,12 +922,11 @@ export class SlashCommandHandler {
} }
} }
createContextItem(args, name) { createContextItem(args, name) {
try { try {
this.api.createContextItem( this.api.createContextItem(
args.set, args.set,
args.label, args.id !== undefined ? Number(args.id) : args.label,
name, name,
isTrueBoolean(args.chain), isTrueBoolean(args.chain),
); );
@ -923,14 +936,14 @@ export class SlashCommandHandler {
} }
deleteContextItem(args, name) { deleteContextItem(args, name) {
try { try {
this.api.deleteContextItem(args.set, args.label, name); this.api.deleteContextItem(args.set, args.id !== undefined ? Number(args.id) : args.label, name);
} catch (ex) { } catch (ex) {
toastr.error(ex.message); toastr.error(ex.message);
} }
} }
clearContextMenu(args, label) { clearContextMenu(args, label) {
try { try {
this.api.clearContextMenu(args.set, args.label ?? label); this.api.clearContextMenu(args.set, args.id !== undefined ? Number(args.id) : args.label ?? label);
} catch (ex) { } catch (ex) {
toastr.error(ex.message); toastr.error(ex.message);
} }

View File

@ -19,7 +19,7 @@ export class ContextMenu {
this.itemList = this.build(qr).children; this.itemList = this.build(qr).children;
this.itemList.forEach(item => { this.itemList.forEach(item => {
item.onExpand = () => { item.onExpand = () => {
this.itemList.filter(it => it != item) this.itemList.filter(it => it !== item)
.forEach(it => it.collapse()); .forEach(it => it.collapse());
}; };
}); });
@ -36,6 +36,7 @@ export class ContextMenu {
icon: qr.icon, icon: qr.icon,
showLabel: qr.showLabel, showLabel: qr.showLabel,
label: qr.label, label: qr.label,
title: qr.title,
message: (chainedMessage && qr.message ? `${chainedMessage} | ` : '') + qr.message, message: (chainedMessage && qr.message ? `${chainedMessage} | ` : '') + qr.message,
children: [], children: [],
}; };
@ -45,12 +46,29 @@ export class ContextMenu {
const nextHierarchy = [...hierarchy, cl.set]; const nextHierarchy = [...hierarchy, cl.set];
const nextLabelHierarchy = [...labelHierarchy, tree.label]; const nextLabelHierarchy = [...labelHierarchy, tree.label];
tree.children.push(new MenuHeader(cl.set.name)); tree.children.push(new MenuHeader(cl.set.name));
cl.set.qrList.forEach(subQr => {
// If the Quick Reply's own set is added as a context menu,
// show only the sub-QRs that are Invisible but have an icon
// intent: allow a QR set to be assigned to one of its own QR buttons for a "burger" menu
// with "UI" QRs either in the bar or in the menu, and "library function" QRs still hidden.
// - QRs already visible on the bar are filtered out,
// - hidden QRs without an icon are filtered out,
// - hidden QRs **with an icon** are shown in the menu
// so everybody is happy
const qrsOwnSetAddedAsContextMenu = cl.set.qrList.includes(qr);
const visible = (subQr) => {
return qrsOwnSetAddedAsContextMenu
? subQr.isHidden && !!subQr.icon // yes .isHidden gets inverted here
: !subQr.isHidden;
};
cl.set.qrList.filter(visible).forEach(subQr => {
const subTree = this.build(subQr, cl.isChained ? tree.message : null, nextHierarchy, nextLabelHierarchy); const subTree = this.build(subQr, cl.isChained ? tree.message : null, nextHierarchy, nextLabelHierarchy);
tree.children.push(new MenuItem( tree.children.push(new MenuItem(
subTree.icon, subTree.icon,
subTree.showLabel, subTree.showLabel,
subTree.label, subTree.label,
subTree.title,
subTree.message, subTree.message,
(evt) => { (evt) => {
evt.stopPropagation(); evt.stopPropagation();

View File

@ -2,7 +2,7 @@ import { MenuItem } from './MenuItem.js';
export class MenuHeader extends MenuItem { export class MenuHeader extends MenuItem {
constructor(/**@type {String}*/label) { constructor(/**@type {String}*/label) {
super(null, null, label, null, null); super(null, null, label, null, null, null, []);
} }

View File

@ -4,11 +4,11 @@ export class MenuItem {
/**@type {string}*/ icon; /**@type {string}*/ icon;
/**@type {boolean}*/ showLabel; /**@type {boolean}*/ showLabel;
/**@type {string}*/ label; /**@type {string}*/ label;
/**@type {string}*/ title;
/**@type {object}*/ value; /**@type {object}*/ value;
/**@type {function}*/ callback; /**@type {function}*/ callback;
/**@type {MenuItem[]}*/ childList = []; /**@type {MenuItem[]}*/ childList = [];
/**@type {SubMenu}*/ subMenu; /**@type {SubMenu}*/ subMenu;
/**@type {boolean}*/ isForceExpanded = false;
/**@type {HTMLElement}*/ root; /**@type {HTMLElement}*/ root;
@ -19,17 +19,19 @@ export class MenuItem {
/** /**
* *
* @param {string} icon * @param {?string} icon
* @param {boolean} showLabel * @param {?boolean} showLabel
* @param {string} label * @param {string} label
* @param {?string} title Tooltip
* @param {object} value * @param {object} value
* @param {function} callback * @param {function} callback
* @param {MenuItem[]} children * @param {MenuItem[]} children
*/ */
constructor(icon, showLabel, label, value, callback, children = []) { constructor(icon, showLabel, label, title, value, callback, children = []) {
this.icon = icon; this.icon = icon;
this.showLabel = showLabel; this.showLabel = showLabel;
this.label = label; this.label = label;
this.title = title;
this.value = value; this.value = value;
this.callback = callback; this.callback = callback;
this.childList = children; this.childList = children;
@ -42,12 +44,15 @@ export class MenuItem {
this.root = item; this.root = item;
item.classList.add('list-group-item'); item.classList.add('list-group-item');
item.classList.add('ctx-item'); item.classList.add('ctx-item');
item.title = this.value;
// if a title/tooltip is set, add it, otherwise use the QR content
// same as for the main QR list
item.title = this.title || this.value;
if (this.callback) { if (this.callback) {
item.addEventListener('click', (evt) => this.callback(evt, this)); item.addEventListener('click', (evt) => this.callback(evt, this));
} }
const icon = document.createElement('div'); { const icon = document.createElement('div'); {
this.domIcon = icon;
icon.classList.add('qr--button-icon'); icon.classList.add('qr--button-icon');
icon.classList.add('fa-solid'); icon.classList.add('fa-solid');
if (!this.icon) icon.classList.add('qr--hidden'); if (!this.icon) icon.classList.add('qr--hidden');
@ -55,7 +60,6 @@ export class MenuItem {
item.append(icon); item.append(icon);
} }
const lbl = document.createElement('div'); { const lbl = document.createElement('div'); {
this.domLabel = lbl;
lbl.classList.add('qr--button-label'); lbl.classList.add('qr--button-label');
if (this.icon && !this.showLabel) lbl.classList.add('qr--hidden'); if (this.icon && !this.showLabel) lbl.classList.add('qr--hidden');
lbl.textContent = this.label; lbl.textContent = this.label;

View File

@ -174,6 +174,9 @@
position: absolute; position: absolute;
overflow: visible; overflow: visible;
} }
.ctx-menu .ctx-item .qr--hidden {
display: none;
}
.list-group .list-group-item.ctx-header { .list-group .list-group-item.ctx-header {
font-weight: bold; font-weight: bold;
cursor: default; cursor: default;

View File

@ -176,6 +176,10 @@
overflow: visible; overflow: visible;
} }
.ctx-menu .ctx-item .qr--hidden {
display: none;
}
.list-group .list-group-item.ctx-header { .list-group .list-group-item.ctx-header {
font-weight: bold; font-weight: bold;
cursor: default; cursor: default;

View File

@ -55,6 +55,19 @@ export function isFilterState(a, b) {
return aKey === bKey; return aKey === bKey;
} }
/**
* The fuzzy search categories
* @type {{ characters: string, worldInfo: string, personas: string, tags: string, groups: string }}
*/
export const fuzzySearchCategories = Object.freeze({
characters: 'characters',
worldInfo: 'worldInfo',
personas: 'personas',
tags: 'tags',
groups: 'groups',
});
/** /**
* Helper class for filtering data. * Helper class for filtering data.
* @example * @example
@ -72,6 +85,12 @@ export class FilterHelper {
*/ */
scoreCache; scoreCache;
/**
* Cache for fuzzy search results per category.
* @type {Object.<string, { resultMap: Map<string, any> }>}
*/
fuzzySearchCaches;
/** /**
* Creates a new FilterHelper * Creates a new FilterHelper
* @param {Function} onDataChanged Callback to trigger when the filter data changes * @param {Function} onDataChanged Callback to trigger when the filter data changes
@ -79,6 +98,13 @@ export class FilterHelper {
constructor(onDataChanged) { constructor(onDataChanged) {
this.onDataChanged = onDataChanged; this.onDataChanged = onDataChanged;
this.scoreCache = new Map(); this.scoreCache = new Map();
this.fuzzySearchCaches = {
[fuzzySearchCategories.characters]: { resultMap: new Map() },
[fuzzySearchCategories.worldInfo]: { resultMap: new Map() },
[fuzzySearchCategories.personas]: { resultMap: new Map() },
[fuzzySearchCategories.tags]: { resultMap: new Map() },
[fuzzySearchCategories.groups]: { resultMap: new Map() },
};
} }
/** /**
@ -151,7 +177,7 @@ export class FilterHelper {
return data; return data;
} }
const fuzzySearchResults = fuzzySearchWorldInfo(data, term); const fuzzySearchResults = fuzzySearchWorldInfo(data, term, this.fuzzySearchCaches);
this.cacheScores(FILTER_TYPES.WORLD_INFO_SEARCH, new Map(fuzzySearchResults.map(i => [i.item?.uid, i.score]))); this.cacheScores(FILTER_TYPES.WORLD_INFO_SEARCH, new Map(fuzzySearchResults.map(i => [i.item?.uid, i.score])));
const filteredData = data.filter(entity => fuzzySearchResults.find(x => x.item === entity)); const filteredData = data.filter(entity => fuzzySearchResults.find(x => x.item === entity));
@ -170,7 +196,7 @@ export class FilterHelper {
return data; return data;
} }
const fuzzySearchResults = fuzzySearchPersonas(data, term); const fuzzySearchResults = fuzzySearchPersonas(data, term, this.fuzzySearchCaches);
this.cacheScores(FILTER_TYPES.PERSONA_SEARCH, new Map(fuzzySearchResults.map(i => [i.item.key, i.score]))); this.cacheScores(FILTER_TYPES.PERSONA_SEARCH, new Map(fuzzySearchResults.map(i => [i.item.key, i.score])));
const filteredData = data.filter(name => fuzzySearchResults.find(x => x.item.key === name)); const filteredData = data.filter(name => fuzzySearchResults.find(x => x.item.key === name));
@ -289,9 +315,9 @@ export class FilterHelper {
// Save fuzzy search results and scores if enabled // Save fuzzy search results and scores if enabled
if (power_user.fuzzy_search) { if (power_user.fuzzy_search) {
const fuzzySearchCharactersResults = fuzzySearchCharacters(searchValue); const fuzzySearchCharactersResults = fuzzySearchCharacters(searchValue, this.fuzzySearchCaches);
const fuzzySearchGroupsResults = fuzzySearchGroups(searchValue); const fuzzySearchGroupsResults = fuzzySearchGroups(searchValue, this.fuzzySearchCaches);
const fuzzySearchTagsResult = fuzzySearchTags(searchValue); const fuzzySearchTagsResult = fuzzySearchTags(searchValue, this.fuzzySearchCaches);
this.cacheScores(FILTER_TYPES.SEARCH, new Map(fuzzySearchCharactersResults.map(i => [`character.${i.refIndex}`, i.score]))); this.cacheScores(FILTER_TYPES.SEARCH, new Map(fuzzySearchCharactersResults.map(i => [`character.${i.refIndex}`, i.score])));
this.cacheScores(FILTER_TYPES.SEARCH, new Map(fuzzySearchGroupsResults.map(i => [`group.${i.item.id}`, i.score]))); this.cacheScores(FILTER_TYPES.SEARCH, new Map(fuzzySearchGroupsResults.map(i => [`group.${i.item.id}`, i.score])));
this.cacheScores(FILTER_TYPES.SEARCH, new Map(fuzzySearchTagsResult.map(i => [`tag.${i.item.id}`, i.score]))); this.cacheScores(FILTER_TYPES.SEARCH, new Map(fuzzySearchTagsResult.map(i => [`tag.${i.item.id}`, i.score])));
@ -343,11 +369,14 @@ export class FilterHelper {
* @param {object} options - Optional call parameters * @param {object} options - Optional call parameters
* @param {boolean} [options.clearScoreCache=true] - Whether the score cache should be cleared. * @param {boolean} [options.clearScoreCache=true] - Whether the score cache should be cleared.
* @param {Object.<FilterType, any>} [options.tempOverrides={}] - Temporarily override specific filters for this filter application * @param {Object.<FilterType, any>} [options.tempOverrides={}] - Temporarily override specific filters for this filter application
* @param {boolean} [options.clearFuzzySearchCaches=true] - Whether the fuzzy search caches should be cleared.
* @returns {any[]} The filtered data. * @returns {any[]} The filtered data.
*/ */
applyFilters(data, { clearScoreCache = true, tempOverrides = {} } = {}) { applyFilters(data, { clearScoreCache = true, tempOverrides = {}, clearFuzzySearchCaches = true } = {}) {
if (clearScoreCache) this.clearScoreCache(); if (clearScoreCache) this.clearScoreCache();
if (clearFuzzySearchCaches) this.clearFuzzySearchCaches();
// Save original filter states // Save original filter states
const originalStates = {}; const originalStates = {};
for (const key in tempOverrides) { for (const key in tempOverrides) {
@ -411,4 +440,14 @@ export class FilterHelper {
this.scoreCache = new Map(); this.scoreCache = new Map();
} }
} }
/**
* Clears fuzzy search caches
*/
clearFuzzySearchCaches() {
for (const cache of Object.values(this.fuzzySearchCaches)) {
cache.resultMap.clear();
}
console.log('All fuzzy search caches cleared');
}
} }

View File

@ -99,7 +99,6 @@ const default_wi_format = '{0}';
const default_new_chat_prompt = '[Start a new Chat]'; const default_new_chat_prompt = '[Start a new Chat]';
const default_new_group_chat_prompt = '[Start a new group chat. Group members: {{group}}]'; const default_new_group_chat_prompt = '[Start a new group chat. Group members: {{group}}]';
const default_new_example_chat_prompt = '[Example Chat]'; const default_new_example_chat_prompt = '[Example Chat]';
const default_claude_human_sysprompt_message = 'Let\'s get started. Please generate your response based on the information and instructions provided above.';
const default_continue_nudge_prompt = '[Continue the following message. Do not include ANY parts of the original message. Use capitalization and punctuation as if your reply is a part of the original message: {{lastChatMessage}}]'; const default_continue_nudge_prompt = '[Continue the following message. Do not include ANY parts of the original message. Use capitalization and punctuation as if your reply is a part of the original message: {{lastChatMessage}}]';
const default_bias = 'Default (none)'; const default_bias = 'Default (none)';
const default_personality_format = '[{{char}}\'s personality: {{personality}}]'; const default_personality_format = '[{{char}}\'s personality: {{personality}}]';
@ -276,7 +275,6 @@ const default_settings = {
proxy_password: '', proxy_password: '',
assistant_prefill: '', assistant_prefill: '',
assistant_impersonation: '', assistant_impersonation: '',
human_sysprompt_message: default_claude_human_sysprompt_message,
claude_use_sysprompt: false, claude_use_sysprompt: false,
use_makersuite_sysprompt: true, use_makersuite_sysprompt: true,
use_alt_scale: false, use_alt_scale: false,
@ -353,7 +351,6 @@ const oai_settings = {
proxy_password: '', proxy_password: '',
assistant_prefill: '', assistant_prefill: '',
assistant_impersonation: '', assistant_impersonation: '',
human_sysprompt_message: default_claude_human_sysprompt_message,
claude_use_sysprompt: false, claude_use_sysprompt: false,
use_makersuite_sysprompt: true, use_makersuite_sysprompt: true,
use_alt_scale: false, use_alt_scale: false,
@ -1892,7 +1889,6 @@ async function sendOpenAIRequest(type, messages, signal) {
generate_data['top_k'] = Number(oai_settings.top_k_openai); generate_data['top_k'] = Number(oai_settings.top_k_openai);
generate_data['claude_use_sysprompt'] = oai_settings.claude_use_sysprompt; generate_data['claude_use_sysprompt'] = oai_settings.claude_use_sysprompt;
generate_data['stop'] = getCustomStoppingStrings(); // Claude shouldn't have limits on stop strings. generate_data['stop'] = getCustomStoppingStrings(); // Claude shouldn't have limits on stop strings.
generate_data['human_sysprompt_message'] = substituteParams(oai_settings.human_sysprompt_message);
// Don't add a prefill on quiet gens (summarization) and when using continue prefill. // Don't add a prefill on quiet gens (summarization) and when using continue prefill.
if (!isQuiet && !(isContinue && oai_settings.continue_prefill)) { if (!isQuiet && !(isContinue && oai_settings.continue_prefill)) {
generate_data['assistant_prefill'] = isImpersonate ? substituteParams(oai_settings.assistant_impersonation) : substituteParams(oai_settings.assistant_prefill); generate_data['assistant_prefill'] = isImpersonate ? substituteParams(oai_settings.assistant_impersonation) : substituteParams(oai_settings.assistant_prefill);
@ -3030,7 +3026,6 @@ function loadOpenAISettings(data, settings) {
oai_settings.proxy_password = settings.proxy_password ?? default_settings.proxy_password; oai_settings.proxy_password = settings.proxy_password ?? default_settings.proxy_password;
oai_settings.assistant_prefill = settings.assistant_prefill ?? default_settings.assistant_prefill; oai_settings.assistant_prefill = settings.assistant_prefill ?? default_settings.assistant_prefill;
oai_settings.assistant_impersonation = settings.assistant_impersonation ?? default_settings.assistant_impersonation; oai_settings.assistant_impersonation = settings.assistant_impersonation ?? default_settings.assistant_impersonation;
oai_settings.human_sysprompt_message = settings.human_sysprompt_message ?? default_settings.human_sysprompt_message;
oai_settings.image_inlining = settings.image_inlining ?? default_settings.image_inlining; oai_settings.image_inlining = settings.image_inlining ?? default_settings.image_inlining;
oai_settings.inline_image_quality = settings.inline_image_quality ?? default_settings.inline_image_quality; oai_settings.inline_image_quality = settings.inline_image_quality ?? default_settings.inline_image_quality;
oai_settings.bypass_status_check = settings.bypass_status_check ?? default_settings.bypass_status_check; oai_settings.bypass_status_check = settings.bypass_status_check ?? default_settings.bypass_status_check;
@ -3070,7 +3065,6 @@ function loadOpenAISettings(data, settings) {
$('#openai_proxy_password').val(oai_settings.proxy_password); $('#openai_proxy_password').val(oai_settings.proxy_password);
$('#claude_assistant_prefill').val(oai_settings.assistant_prefill); $('#claude_assistant_prefill').val(oai_settings.assistant_prefill);
$('#claude_assistant_impersonation').val(oai_settings.assistant_impersonation); $('#claude_assistant_impersonation').val(oai_settings.assistant_impersonation);
$('#claude_human_sysprompt_textarea').val(oai_settings.human_sysprompt_message);
$('#openai_image_inlining').prop('checked', oai_settings.image_inlining); $('#openai_image_inlining').prop('checked', oai_settings.image_inlining);
$('#openai_bypass_status_check').prop('checked', oai_settings.bypass_status_check); $('#openai_bypass_status_check').prop('checked', oai_settings.bypass_status_check);
@ -3400,7 +3394,6 @@ async function saveOpenAIPreset(name, settings, triggerUi = true) {
show_external_models: settings.show_external_models, show_external_models: settings.show_external_models,
assistant_prefill: settings.assistant_prefill, assistant_prefill: settings.assistant_prefill,
assistant_impersonation: settings.assistant_impersonation, assistant_impersonation: settings.assistant_impersonation,
human_sysprompt_message: settings.human_sysprompt_message,
claude_use_sysprompt: settings.claude_use_sysprompt, claude_use_sysprompt: settings.claude_use_sysprompt,
use_makersuite_sysprompt: settings.use_makersuite_sysprompt, use_makersuite_sysprompt: settings.use_makersuite_sysprompt,
use_alt_scale: settings.use_alt_scale, use_alt_scale: settings.use_alt_scale,
@ -3825,7 +3818,6 @@ function onSettingsPresetChange() {
proxy_password: ['#openai_proxy_password', 'proxy_password', false], proxy_password: ['#openai_proxy_password', 'proxy_password', false],
assistant_prefill: ['#claude_assistant_prefill', 'assistant_prefill', false], assistant_prefill: ['#claude_assistant_prefill', 'assistant_prefill', false],
assistant_impersonation: ['#claude_assistant_impersonation', 'assistant_impersonation', false], assistant_impersonation: ['#claude_assistant_impersonation', 'assistant_impersonation', false],
human_sysprompt_message: ['#claude_human_sysprompt_textarea', 'human_sysprompt_message', false],
claude_use_sysprompt: ['#claude_use_sysprompt', 'claude_use_sysprompt', true], claude_use_sysprompt: ['#claude_use_sysprompt', 'claude_use_sysprompt', true],
use_makersuite_sysprompt: ['#use_makersuite_sysprompt', 'use_makersuite_sysprompt', true], use_makersuite_sysprompt: ['#use_makersuite_sysprompt', 'use_makersuite_sysprompt', true],
use_alt_scale: ['#use_alt_scale', 'use_alt_scale', true], use_alt_scale: ['#use_alt_scale', 'use_alt_scale', true],
@ -4677,10 +4669,6 @@ function toggleChatCompletionForms() {
const validSources = $(this).data('source').split(','); const validSources = $(this).data('source').split(',');
$(this).toggle(validSources.includes(oai_settings.chat_completion_source)); $(this).toggle(validSources.includes(oai_settings.chat_completion_source));
}); });
if (chat_completion_sources.CLAUDE == oai_settings.chat_completion_source) {
$('#claude_human_sysprompt_message_block').toggle(oai_settings.claude_use_sysprompt);
}
} }
async function testApiConnection() { async function testApiConnection() {
@ -5036,7 +5024,6 @@ export function initOpenAI() {
$('#claude_use_sysprompt').on('change', function () { $('#claude_use_sysprompt').on('change', function () {
oai_settings.claude_use_sysprompt = !!$('#claude_use_sysprompt').prop('checked'); oai_settings.claude_use_sysprompt = !!$('#claude_use_sysprompt').prop('checked');
$('#claude_human_sysprompt_message_block').toggle(oai_settings.claude_use_sysprompt);
saveSettingsDebounced(); saveSettingsDebounced();
}); });
@ -5113,12 +5100,6 @@ export function initOpenAI() {
saveSettingsDebounced(); saveSettingsDebounced();
}); });
$('#claude_human_sysprompt_message_restore').on('click', function () {
oai_settings.human_sysprompt_message = default_claude_human_sysprompt_message;
$('#claude_human_sysprompt_textarea').val(oai_settings.human_sysprompt_message);
saveSettingsDebounced();
});
$('#newgroupchat_prompt_restore').on('click', function () { $('#newgroupchat_prompt_restore').on('click', function () {
oai_settings.new_group_chat_prompt = default_new_group_chat_prompt; oai_settings.new_group_chat_prompt = default_new_group_chat_prompt;
$('#newgroupchat_prompt_textarea').val(oai_settings.new_group_chat_prompt); $('#newgroupchat_prompt_textarea').val(oai_settings.new_group_chat_prompt);
@ -5210,11 +5191,6 @@ export function initOpenAI() {
saveSettingsDebounced(); saveSettingsDebounced();
}); });
$('#claude_human_sysprompt_textarea').on('input', function () {
oai_settings.human_sysprompt_message = String($('#claude_human_sysprompt_textarea').val());
saveSettingsDebounced();
});
$('#openrouter_use_fallback').on('input', function () { $('#openrouter_use_fallback').on('input', function () {
oai_settings.openrouter_use_fallback = !!$(this).prop('checked'); oai_settings.openrouter_use_fallback = !!$(this).prop('checked');
saveSettingsDebounced(); saveSettingsDebounced();

View File

@ -53,6 +53,7 @@ import { SlashCommandEnumValue, enumTypes } from './slash-commands/SlashCommandE
import { commonEnumProviders, enumIcons } from './slash-commands/SlashCommandCommonEnumsProvider.js'; import { commonEnumProviders, enumIcons } from './slash-commands/SlashCommandCommonEnumsProvider.js';
import { POPUP_TYPE, callGenericPopup } from './popup.js'; import { POPUP_TYPE, callGenericPopup } from './popup.js';
import { loadSystemPrompts } from './sysprompt.js'; import { loadSystemPrompts } from './sysprompt.js';
import { fuzzySearchCategories } from './filters.js';
export { export {
loadPowerUserSettings, loadPowerUserSettings,
@ -1831,27 +1832,28 @@ async function loadContextSettings() {
}); });
} }
/** /**
* Fuzzy search characters by a search term * Common function to perform fuzzy search with optional caching
* @param {string} type - Type of search from fuzzySearchCategories
* @param {any[]} data - Data array to search in
* @param {Array<{name: string, weight: number, getFn?: Function}>} keys - Fuse.js keys configuration
* @param {string} searchValue - The search term * @param {string} searchValue - The search term
* @param {Object.<string, { resultMap: Map<string, any> }>} [fuzzySearchCaches=null] - Optional fuzzy search caches
* @returns {import('fuse.js').FuseResult<any>[]} Results as items with their score * @returns {import('fuse.js').FuseResult<any>[]} Results as items with their score
*/ */
export function fuzzySearchCharacters(searchValue) { function performFuzzySearch(type, data, keys, searchValue, fuzzySearchCaches = null) {
// Check cache if provided
if (fuzzySearchCaches) {
const cache = fuzzySearchCaches[type];
if (cache?.resultMap.has(searchValue)) {
return cache.resultMap.get(searchValue);
}
}
// @ts-ignore // @ts-ignore
const fuse = new Fuse(characters, { const fuse = new Fuse(data, {
keys: [ keys: keys,
{ name: 'data.name', weight: 20 },
{ name: '#tags', weight: 10, getFn: (character) => getTagsList(character.avatar).map(x => x.name).join('||') },
{ name: 'data.description', weight: 3 },
{ name: 'data.mes_example', weight: 3 },
{ name: 'data.scenario', weight: 2 },
{ name: 'data.personality', weight: 2 },
{ name: 'data.first_mes', weight: 2 },
{ name: 'data.creator_notes', weight: 2 },
{ name: 'data.creator', weight: 1 },
{ name: 'data.tags', weight: 1 },
{ name: 'data.alternate_greetings', weight: 1 },
],
includeScore: true, includeScore: true,
ignoreLocation: true, ignoreLocation: true,
useExtendedSearch: true, useExtendedSearch: true,
@ -1859,109 +1861,110 @@ export function fuzzySearchCharacters(searchValue) {
}); });
const results = fuse.search(searchValue); const results = fuse.search(searchValue);
console.debug('Characters fuzzy search results for ' + searchValue, results);
// Store in cache if provided
if (fuzzySearchCaches) {
fuzzySearchCaches[type].resultMap.set(searchValue, results);
}
return results; return results;
} }
/**
* Fuzzy search characters by a search term
* @param {string} searchValue - The search term
* @param {Object.<string, { resultMap: Map<string, any> }>} [fuzzySearchCaches=null] - Optional fuzzy search caches
* @returns {import('fuse.js').FuseResult<any>[]} Results as items with their score
*/
export function fuzzySearchCharacters(searchValue, fuzzySearchCaches = null) {
const keys = [
{ name: 'data.name', weight: 20 },
{ name: '#tags', weight: 10, getFn: (character) => getTagsList(character.avatar).map(x => x.name).join('||') },
{ name: 'data.description', weight: 3 },
{ name: 'data.mes_example', weight: 3 },
{ name: 'data.scenario', weight: 2 },
{ name: 'data.personality', weight: 2 },
{ name: 'data.first_mes', weight: 2 },
{ name: 'data.creator_notes', weight: 2 },
{ name: 'data.creator', weight: 1 },
{ name: 'data.tags', weight: 1 },
{ name: 'data.alternate_greetings', weight: 1 },
];
return performFuzzySearch(fuzzySearchCategories.characters, characters, keys, searchValue, fuzzySearchCaches);
}
/** /**
* Fuzzy search world info entries by a search term * Fuzzy search world info entries by a search term
* @param {*[]} data - WI items data array * @param {*[]} data - WI items data array
* @param {string} searchValue - The search term * @param {string} searchValue - The search term
* @param {Object.<string, { resultMap: Map<string, any> }>} [fuzzySearchCaches=null] - Optional fuzzy search caches
* @returns {import('fuse.js').FuseResult<any>[]} Results as items with their score * @returns {import('fuse.js').FuseResult<any>[]} Results as items with their score
*/ */
export function fuzzySearchWorldInfo(data, searchValue) { export function fuzzySearchWorldInfo(data, searchValue, fuzzySearchCaches = null) {
// @ts-ignore const keys = [
const fuse = new Fuse(data, { { name: 'key', weight: 20 },
keys: [ { name: 'group', weight: 15 },
{ name: 'key', weight: 20 }, { name: 'comment', weight: 10 },
{ name: 'group', weight: 15 }, { name: 'keysecondary', weight: 10 },
{ name: 'comment', weight: 10 }, { name: 'content', weight: 3 },
{ name: 'keysecondary', weight: 10 }, { name: 'uid', weight: 1 },
{ name: 'content', weight: 3 }, { name: 'automationId', weight: 1 },
{ name: 'uid', weight: 1 }, ];
{ name: 'automationId', weight: 1 },
],
includeScore: true,
ignoreLocation: true,
useExtendedSearch: true,
threshold: 0.2,
});
const results = fuse.search(searchValue); return performFuzzySearch(fuzzySearchCategories.worldInfo, data, keys, searchValue, fuzzySearchCaches);
console.debug('World Info fuzzy search results for ' + searchValue, results);
return results;
} }
/** /**
* Fuzzy search persona entries by a search term * Fuzzy search persona entries by a search term
* @param {*[]} data - persona data array * @param {*[]} data - persona data array
* @param {string} searchValue - The search term * @param {string} searchValue - The search term
* @param {Object.<string, { resultMap: Map<string, any> }>} [fuzzySearchCaches=null] - Optional fuzzy search caches
* @returns {import('fuse.js').FuseResult<any>[]} Results as items with their score * @returns {import('fuse.js').FuseResult<any>[]} Results as items with their score
*/ */
export function fuzzySearchPersonas(data, searchValue) { export function fuzzySearchPersonas(data, searchValue, fuzzySearchCaches = null) {
data = data.map(x => ({ key: x, name: power_user.personas[x] ?? '', description: power_user.persona_descriptions[x]?.description ?? '' })); const mappedData = data.map(x => ({
// @ts-ignore key: x,
const fuse = new Fuse(data, { name: power_user.personas[x] ?? '',
keys: [ description: power_user.persona_descriptions[x]?.description ?? ''
{ name: 'name', weight: 20 }, }));
{ name: 'description', weight: 3 },
],
includeScore: true,
ignoreLocation: true,
useExtendedSearch: true,
threshold: 0.2,
});
const results = fuse.search(searchValue); const keys = [
console.debug('Personas fuzzy search results for ' + searchValue, results); { name: 'name', weight: 20 },
return results; { name: 'description', weight: 3 },
];
return performFuzzySearch(fuzzySearchCategories.personas, mappedData, keys, searchValue, fuzzySearchCaches);
} }
/** /**
* Fuzzy search tags by a search term * Fuzzy search tags by a search term
* @param {string} searchValue - The search term * @param {string} searchValue - The search term
* @param {Object.<string, { resultMap: Map<string, any> }>} [fuzzySearchCaches=null] - Optional fuzzy search caches
* @returns {import('fuse.js').FuseResult<any>[]} Results as items with their score * @returns {import('fuse.js').FuseResult<any>[]} Results as items with their score
*/ */
export function fuzzySearchTags(searchValue) { export function fuzzySearchTags(searchValue, fuzzySearchCaches = null) {
// @ts-ignore const keys = [
const fuse = new Fuse(tags, { { name: 'name', weight: 1 },
keys: [ ];
{ name: 'name', weight: 1 },
],
includeScore: true,
ignoreLocation: true,
useExtendedSearch: true,
threshold: 0.2,
});
const results = fuse.search(searchValue); return performFuzzySearch(fuzzySearchCategories.tags, tags, keys, searchValue, fuzzySearchCaches);
console.debug('Tags fuzzy search results for ' + searchValue, results);
return results;
} }
/** /**
* Fuzzy search groups by a search term * Fuzzy search groups by a search term
* @param {string} searchValue - The search term * @param {string} searchValue - The search term
* @param {Object.<string, { resultMap: Map<string, any> }>} [fuzzySearchCaches=null] - Optional fuzzy search caches
* @returns {import('fuse.js').FuseResult<any>[]} Results as items with their score * @returns {import('fuse.js').FuseResult<any>[]} Results as items with their score
*/ */
export function fuzzySearchGroups(searchValue) { export function fuzzySearchGroups(searchValue, fuzzySearchCaches = null) {
// @ts-ignore const keys = [
const fuse = new Fuse(groups, { { name: 'name', weight: 20 },
keys: [ { name: 'members', weight: 15 },
{ name: 'name', weight: 20 }, { name: '#tags', weight: 10, getFn: (group) => getTagsList(group.id).map(x => x.name).join('||') },
{ name: 'members', weight: 15 }, { name: 'id', weight: 1 },
{ name: '#tags', weight: 10, getFn: (group) => getTagsList(group.id).map(x => x.name).join('||') }, ];
{ name: 'id', weight: 1 },
],
includeScore: true,
ignoreLocation: true,
useExtendedSearch: true,
threshold: 0.2,
});
const results = fuse.search(searchValue); return performFuzzySearch(fuzzySearchCategories.groups, groups, keys, searchValue, fuzzySearchCaches);
console.debug('Groups fuzzy search results for ' + searchValue, results);
return results;
} }
/** /**

View File

@ -1258,6 +1258,7 @@ export function getTextGenGenerationData(finalPrompt, maxTokens, isImpersonate,
'xtc_probability': settings.xtc_probability, 'xtc_probability': settings.xtc_probability,
'nsigma': settings.nsigma, 'nsigma': settings.nsigma,
'custom_token_bans': toIntArray(banned_tokens), 'custom_token_bans': toIntArray(banned_tokens),
'no_repeat_ngram_size': settings.no_repeat_ngram_size,
}; };
if (settings.type === OPENROUTER) { if (settings.type === OPENROUTER) {

View File

@ -627,10 +627,6 @@ const tavernUrl = new URL(
(':' + server_port), (':' + server_port),
); );
function prepareFrontendBundle() {
return new Promise((resolve) => webpackMiddleware.waitUntilValid(resolve));
}
/** /**
* Tasks that need to be run before the server starts listening. * Tasks that need to be run before the server starts listening.
*/ */
@ -682,7 +678,7 @@ const preSetupTasks = async function () {
initRequestProxy({ enabled: proxyEnabled, url: proxyUrl, bypass: proxyBypass }); initRequestProxy({ enabled: proxyEnabled, url: proxyUrl, bypass: proxyBypass });
// Wait for frontend libs to compile // Wait for frontend libs to compile
await prepareFrontendBundle(); await webpackMiddleware.runWebpackCompiler();
}; };
/** /**

View File

@ -102,7 +102,7 @@ async function sendClaudeRequest(request, response) {
const additionalHeaders = {}; const additionalHeaders = {};
const useTools = request.body.model.startsWith('claude-3') && Array.isArray(request.body.tools) && request.body.tools.length > 0; const useTools = request.body.model.startsWith('claude-3') && Array.isArray(request.body.tools) && request.body.tools.length > 0;
const useSystemPrompt = (request.body.model.startsWith('claude-2') || request.body.model.startsWith('claude-3')) && request.body.claude_use_sysprompt; const useSystemPrompt = (request.body.model.startsWith('claude-2') || request.body.model.startsWith('claude-3')) && request.body.claude_use_sysprompt;
const convertedPrompt = convertClaudeMessages(request.body.messages, request.body.assistant_prefill, useSystemPrompt, useTools, request.body.human_sysprompt_message, request.body.char_name, request.body.user_name); const convertedPrompt = convertClaudeMessages(request.body.messages, request.body.assistant_prefill, useSystemPrompt, useTools, request.body.char_name, request.body.user_name);
// Add custom stop sequences // Add custom stop sequences
const stopSequences = []; const stopSequences = [];
if (Array.isArray(request.body.stop)) { if (Array.isArray(request.body.stop)) {

View File

@ -14,7 +14,7 @@ import jimp from 'jimp';
import { AVATAR_WIDTH, AVATAR_HEIGHT } from '../constants.js'; import { AVATAR_WIDTH, AVATAR_HEIGHT } from '../constants.js';
import { jsonParser, urlencodedParser } from '../express-common.js'; import { jsonParser, urlencodedParser } from '../express-common.js';
import { deepMerge, humanizedISO8601DateTime, tryParse, extractFileFromZipBuffer } from '../util.js'; import { deepMerge, humanizedISO8601DateTime, tryParse, extractFileFromZipBuffer, MemoryLimitedMap } from '../util.js';
import { TavernCardValidator } from '../validator/TavernCardValidator.js'; import { TavernCardValidator } from '../validator/TavernCardValidator.js';
import { parse, write } from '../character-card-parser.js'; import { parse, write } from '../character-card-parser.js';
import { readWorldInfoFile } from './worldinfo.js'; import { readWorldInfoFile } from './worldinfo.js';
@ -23,7 +23,8 @@ import { importRisuSprites } from './sprites.js';
const defaultAvatarPath = './public/img/ai4.png'; const defaultAvatarPath = './public/img/ai4.png';
// KV-store for parsed character data // KV-store for parsed character data
const characterDataCache = new Map(); // 100 MB limit. Would take roughly 3000 characters to reach this limit
const characterDataCache = new MemoryLimitedMap(1024 * 1024 * 100);
// Some Android devices require tighter memory management // Some Android devices require tighter memory management
const isAndroid = process.platform === 'android'; const isAndroid = process.platform === 'android';
@ -58,6 +59,9 @@ async function writeCharacterData(inputFile, data, outputFile, request, crop = u
try { try {
// Reset the cache // Reset the cache
for (const key of characterDataCache.keys()) { for (const key of characterDataCache.keys()) {
if (Buffer.isBuffer(inputFile)) {
break;
}
if (key.startsWith(inputFile)) { if (key.startsWith(inputFile)) {
characterDataCache.delete(key); characterDataCache.delete(key);
break; break;

View File

@ -1,20 +1,45 @@
import process from 'node:process'; import path from 'node:path';
import webpack from 'webpack'; import webpack from 'webpack';
import middleware from 'webpack-dev-middleware';
import { publicLibConfig } from '../../webpack.config.js'; import { publicLibConfig } from '../../webpack.config.js';
export default function getWebpackServeMiddleware() { export default function getWebpackServeMiddleware() {
const outputPath = publicLibConfig.output?.path;
const outputFile = publicLibConfig.output?.filename;
const compiler = webpack(publicLibConfig); const compiler = webpack(publicLibConfig);
if (process.env.NODE_ENV === 'production' || process.platform === 'android') { /**
compiler.hooks.done.tap('serve', () => { * A very spartan recreation of webpack-dev-middleware.
if (compiler.watching) { * @param {import('express').Request} req Request object.
compiler.watching.close(() => { }); * @param {import('express').Response} res Response object.
} * @param {import('express').NextFunction} next Next function.
compiler.watchFileSystem = null; * @type {import('express').RequestHandler}
compiler.watchMode = false; */
}); function devMiddleware(req, res, next) {
if (req.method === 'GET' && path.parse(req.path).base === outputFile) {
return res.sendFile(outputFile, { root: outputPath });
}
next();
} }
return middleware(compiler, {}); /**
* Wait until Webpack is done compiling.
* @returns {Promise<void>}
*/
devMiddleware.runWebpackCompiler = () => {
return new Promise((resolve) => {
console.log();
console.log('Compiling frontend libraries...');
compiler.run((_error, stats) => {
const output = stats?.toString(publicLibConfig.stats);
if (output) {
console.log(output);
console.log();
}
resolve();
});
});
};
return devMiddleware;
} }

View File

@ -91,11 +91,10 @@ export function convertClaudePrompt(messages, addAssistantPostfix, addAssistantP
* @param {string} prefillString User determined prefill string * @param {string} prefillString User determined prefill string
* @param {boolean} useSysPrompt See if we want to use a system prompt * @param {boolean} useSysPrompt See if we want to use a system prompt
* @param {boolean} useTools See if we want to use tools * @param {boolean} useTools See if we want to use tools
* @param {string} humanMsgFix Add Human message between system prompt and assistant.
* @param {string} charName Character name * @param {string} charName Character name
* @param {string} userName User name * @param {string} userName User name
*/ */
export function convertClaudeMessages(messages, prefillString, useSysPrompt, useTools, humanMsgFix, charName = '', userName = '') { export function convertClaudeMessages(messages, prefillString, useSysPrompt, useTools, charName, userName) {
let systemPrompt = []; let systemPrompt = [];
if (useSysPrompt) { if (useSysPrompt) {
// Collect all the system messages up until the first instance of a non-system message, and then remove them from the messages array. // Collect all the system messages up until the first instance of a non-system message, and then remove them from the messages array.
@ -122,10 +121,10 @@ export function convertClaudeMessages(messages, prefillString, useSysPrompt, use
// Check if the first message in the array is of type user, if not, interject with humanMsgFix or a blank message. // Check if the first message in the array is of type user, if not, interject with humanMsgFix or a blank message.
// Also prevents erroring out if the messages array is empty. // Also prevents erroring out if the messages array is empty.
if (messages.length === 0 || (messages.length > 0 && messages[0].role !== 'user')) { if (messages.length === 0) {
messages.unshift({ messages.unshift({
role: 'user', role: 'user',
content: humanMsgFix || PROMPT_PLACEHOLDER, content: PROMPT_PLACEHOLDER,
}); });
} }
} }

View File

@ -670,3 +670,182 @@ export function isValidUrl(url) {
return false; return false;
} }
} }
/**
* MemoryLimitedMap class that limits the memory usage of string values.
*/
export class MemoryLimitedMap {
/**
* Creates an instance of MemoryLimitedMap.
* @param {number} maxMemoryInBytes - The maximum allowed memory in bytes for string values.
*/
constructor(maxMemoryInBytes) {
if (typeof maxMemoryInBytes !== 'number' || maxMemoryInBytes <= 0) {
throw new Error('maxMemoryInBytes must be a positive number');
}
this.maxMemory = maxMemoryInBytes;
this.currentMemory = 0;
this.map = new Map();
this.queue = [];
}
/**
* Estimates the memory usage of a string in bytes.
* Assumes each character occupies 2 bytes (UTF-16).
* @param {string} str
* @returns {number}
*/
static estimateStringSize(str) {
return str ? str.length * 2 : 0;
}
/**
* Adds or updates a key-value pair in the map.
* If adding the new value exceeds the memory limit, evicts oldest entries.
* @param {string} key
* @param {string} value
*/
set(key, value) {
if (typeof key !== 'string' || typeof value !== 'string') {
return;
}
const newValueSize = MemoryLimitedMap.estimateStringSize(value);
// If the new value itself exceeds the max memory, reject it
if (newValueSize > this.maxMemory) {
return;
}
// Check if the key already exists to adjust memory accordingly
if (this.map.has(key)) {
const oldValue = this.map.get(key);
const oldValueSize = MemoryLimitedMap.estimateStringSize(oldValue);
this.currentMemory -= oldValueSize;
// Remove the key from its current position in the queue
const index = this.queue.indexOf(key);
if (index > -1) {
this.queue.splice(index, 1);
}
}
// Evict oldest entries until there's enough space
while (this.currentMemory + newValueSize > this.maxMemory && this.queue.length > 0) {
const oldestKey = this.queue.shift();
const oldestValue = this.map.get(oldestKey);
const oldestValueSize = MemoryLimitedMap.estimateStringSize(oldestValue);
this.map.delete(oldestKey);
this.currentMemory -= oldestValueSize;
}
// After eviction, check again if there's enough space
if (this.currentMemory + newValueSize > this.maxMemory) {
return;
}
// Add the new key-value pair
this.map.set(key, value);
this.queue.push(key);
this.currentMemory += newValueSize;
}
/**
* Retrieves the value associated with the given key.
* @param {string} key
* @returns {string | undefined}
*/
get(key) {
return this.map.get(key);
}
/**
* Checks if the map contains the given key.
* @param {string} key
* @returns {boolean}
*/
has(key) {
return this.map.has(key);
}
/**
* Deletes the key-value pair associated with the given key.
* @param {string} key
* @returns {boolean} - Returns true if the key was found and deleted, else false.
*/
delete(key) {
if (!this.map.has(key)) {
return false;
}
const value = this.map.get(key);
const valueSize = MemoryLimitedMap.estimateStringSize(value);
this.map.delete(key);
this.currentMemory -= valueSize;
// Remove the key from the queue
const index = this.queue.indexOf(key);
if (index > -1) {
this.queue.splice(index, 1);
}
return true;
}
/**
* Clears all entries from the map.
*/
clear() {
this.map.clear();
this.queue = [];
this.currentMemory = 0;
}
/**
* Returns the number of key-value pairs in the map.
* @returns {number}
*/
size() {
return this.map.size;
}
/**
* Returns the current memory usage in bytes.
* @returns {number}
*/
totalMemory() {
return this.currentMemory;
}
/**
* Returns an iterator over the keys in the map.
* @returns {IterableIterator<string>}
*/
keys() {
return this.map.keys();
}
/**
* Returns an iterator over the values in the map.
* @returns {IterableIterator<string>}
*/
values() {
return this.map.values();
}
/**
* Iterates over the map in insertion order.
* @param {Function} callback - Function to execute for each element.
*/
forEach(callback) {
this.map.forEach((value, key) => {
callback(value, key, this);
});
}
/**
* Makes the MemoryLimitedMap iterable.
* @returns {Iterator} - Iterator over [key, value] pairs.
*/
[Symbol.iterator]() {
return this.map[Symbol.iterator]();
}
}

View File

@ -8,6 +8,8 @@ export const publicLibConfig = {
cache: { cache: {
type: 'filesystem', type: 'filesystem',
cacheDirectory: path.resolve(process.cwd(), 'dist/webpack'), cacheDirectory: path.resolve(process.cwd(), 'dist/webpack'),
store: 'pack',
compression: 'gzip',
}, },
devtool: false, devtool: false,
watch: false, watch: false,
@ -16,6 +18,8 @@ export const publicLibConfig = {
preset: 'minimal', preset: 'minimal',
assets: false, assets: false,
modules: false, modules: false,
colors: true,
timings: true,
}, },
experiments: { experiments: {
outputModule: true, outputModule: true,
@ -24,6 +28,7 @@ export const publicLibConfig = {
hints: false, hints: false,
}, },
output: { output: {
path: path.resolve(process.cwd(), 'dist'),
filename: 'lib.js', filename: 'lib.js',
libraryTarget: 'module', libraryTarget: 'module',
}, },