Compare commits

..

56 Commits

Author SHA1 Message Date
Cohee
de3f340a55 Merge pull request #3135 from SillyTavern/staging
Staging
2024-12-01 17:31:30 +02:00
Cohee
39c3924b3f Merge pull request #3112 from kallewoof/202411-backend-maxctx
feature: allow auto-use of max context size given by backend
2024-12-01 15:15:15 +02:00
Cohee
8e94589f83 Revert keepContextLock flag in setGenerationParamsFromPreset 2024-12-01 15:13:17 +02:00
Cohee
66a862f797 Lint the use of data-i18n attributes 2024-12-01 15:10:22 +02:00
Cohee
c930a66d81 Merge branch 'staging' into 202411-backend-maxctx 2024-12-01 14:39:01 +02:00
Cohee
89ec8fd23a Bump package version 2024-12-01 14:38:17 +02:00
Cohee
a86735d743 Fix sendless statuses 2024-11-30 19:30:14 +02:00
Cohee
56c99000c4 Split auto-switch toast 2024-11-30 18:21:15 +02:00
Cohee
4a7a11dfd5 Add JSdoc for function 2024-11-30 18:19:31 +02:00
Cohee
8e20ebb534 Run eslint 2024-11-30 18:14:46 +02:00
Cohee
04b68d2cce Move derive context size to TC settings 2024-11-30 18:08:12 +02:00
Cohee
c3c16ea0d6 Merge pull request #3106 from joenunezb/optimize/improve-search
perf(search): improve fuzzy character search performance by ~13x (4.5s → 350ms)
2024-11-30 17:50:40 +02:00
Cohee
8b7a14f895 Merge pull request #3124 from ceruleandeep/feature/actuallyCompactContextMenus
Filter out hidden items in context menus
2024-11-30 17:39:50 +02:00
ceruleandeep
2f7bc7ca8d Filter out hidden items in context menus
Add CSS to apply label show/hide settings to QRs in context menus

Add provision for QR set applied to one of its own buttons as "burger" menu
2024-11-30 00:46:04 +11:00
Cohee
a444a782e2 Fix input outline on discreet login page 2024-11-29 13:32:53 +00:00
Cohee
0bebf02c97 Merge pull request #3119 from SillyTavern/webpack-memory
Replace webpack-dev-middleware with a statically compiled file
2024-11-29 15:24:53 +02:00
Cohee
7fbff41329 Unasync route handler 2024-11-29 13:14:40 +00:00
Cohee
53514b5e1a Prettify compilation console logs 2024-11-29 13:12:46 +00:00
Cohee
c0b37631bc Merge branch 'staging' into webpack-memory 2024-11-29 13:07:17 +00:00
Cohee
e124a22ffd Merge pull request #3120 from SillyTavern/char-cache-limit
(perf) Add 100MB limit to parsed characters cache
2024-11-29 15:04:03 +02:00
Cohee
095d19cda7 Rename LimitedMap => MemoryLimitedMap 2024-11-29 12:33:48 +00:00
Cohee
176ef77624 Merge branch 'staging' into char-cache-limit 2024-11-29 12:30:26 +00:00
Cohee
2384031d09 Merge pull request #3121 from ceruleandeep/fix/handleIdForQRMenuAdd
Wire up id= parameter for /qr-context*
2024-11-29 14:27:57 +02:00
Cohee
710a4ee867 Merge pull request #3122 from ceruleandeep/feature/compactererQRContextMenus
Make QR context menu display options more consistent with QR bar
2024-11-29 14:17:24 +02:00
ceruleandeep
3a1a955164 Make QR context menu display options more consistent with QR bar
Use QR title as tooltip if set on the QR

Add qr--hidden class to "invisible" context items to allow hiding with CSS

Add title and isHidden props to MenuItem

Remove domIcon and domLabel props: not needed for ctx menu rendering; isForceExpanded: unimplemented
2024-11-29 17:44:51 +11:00
ceruleandeep
e8004b5b56 Wire up id= parameter for /qr-context*
Parameter is in the named arguments but was not handled in the handler. Added `args.id !== undefined ? Number(args.id) : args.label` etc, as used elsewhere.
2024-11-29 11:52:57 +11:00
Cohee
c873362d01 Safer estimation of possibly undefined values 2024-11-29 01:28:10 +02:00
Cohee
52606616c4 Add 100MB limit to parsed characters cache 2024-11-29 01:06:10 +02:00
Cohee
768b3e48f7 Remove webpack watch unhook 2024-11-29 00:25:31 +02:00
Cohee
afccb8517a Ditch webpack-dev-middleware 2024-11-29 00:13:43 +02:00
Cohee
b5c2ecdfcc Webpack: cache lib.js to disk to prevent occasional OOM 2024-11-28 22:47:27 +02:00
Cohee
afb4acc19b Revert "Webpack: cache lib.js to disk to prevent occasional OOM"
This reverts commit f630c8892a.
2024-11-28 22:45:45 +02:00
Cohee
f630c8892a Webpack: cache lib.js to disk to prevent occasional OOM 2024-11-28 22:39:20 +02:00
Cohee
4f24f8078d Merge pull request #3118 from kallewoof/202411-tulu-nl
tulu template: added \n to input/system_suffix to match model card
2024-11-28 14:38:45 +02:00
Cohee
8bfb695536 Adjust documentation links 2024-11-28 11:38:10 +00:00
Karl-Johan Alm
ef35adb9e4 tulu template: added \n to input_suffix and system_suffix to match model card 2024-11-28 13:04:40 +09:00
Cohee
76c2789587 Merge pull request #3117 from kallewoof/202411-tulu
Add Tulu templates
2024-11-27 12:31:44 +02:00
Joe
f4ef9697e9 Account for optional cache and remove timing code 2024-11-26 20:07:30 -08:00
Joe
309157dd89 Remove unnecesary key only clearing for cache clearing 2024-11-26 19:52:21 -08:00
Joe
1395c0b8c6 Encapsulate logic into filters instead of spreading around 2024-11-26 19:47:09 -08:00
Karl-Johan Alm
8d67bdee84 Add Tulu templates 2024-11-27 11:58:12 +09:00
Joe
78c55558af Merge branch 'staging' of https://github.com/joenunezb/SillyTavern into optimize/improve-search 2024-11-26 18:00:28 -08:00
Joe
dd4a1bf072 Attempt to allow preview for github 2024-11-26 13:32:40 -08:00
Joe
16ba8331b5 Document Fuzzy Search process 2024-11-26 13:27:27 -08:00
Cohee
8dbd78f560 Merge pull request #3108 from SillyTavern/claude-no-filler
Claude: remove user filler from prompt converter
2024-11-26 21:41:33 +02:00
Cohee
f730e2179b Merge branch 'staging' into claude-no-filler 2024-11-26 21:25:47 +02:00
Karl-Johan Alm
faf80d1b62 UI: move the derived context size flag into connection pane 2024-11-26 10:36:40 +09:00
Karl-Johan Alm
e02f57b7b0 respect context unlocked flag when auto-setting context size 2024-11-26 10:00:52 +09:00
Karl-Johan Alm
4988f22e94 feature: allow auto-use of max context size given by backend 2024-11-25 23:08:11 +09:00
Joe
e56faaaed5 Remove hash function 2024-11-24 14:55:22 -08:00
Joe
e1d6a47048 Remove key hashing and explicitly clear the cache after printing characters 2024-11-24 14:54:12 -08:00
Cohee
9382845dee Claude: remove user filler from prompt converter 2024-11-24 19:05:41 +02:00
Joe
d1cac298c6 Coment out log statements 2024-11-23 22:51:48 -08:00
Joe
e2c083ba31 Remove duplicated code 2024-11-23 22:34:56 -08:00
Joe
2661881bc4 Handle stale cache 2024-11-23 22:13:20 -08:00
Joe
eb29f03ab0 Sped up character search by 93% 2024-11-23 20:48:36 -08:00
29 changed files with 624 additions and 496 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

@@ -820,8 +820,8 @@
</div> </div>
</div> </div>
<div class="range-block m-t-1"> <div class="range-block m-t-1">
<div class="range-block-title openai_restorable" data-i18n="World Info Format Template"> <div class="range-block-title openai_restorable">
<span>World Info format template</span> <span data-i18n="World Info Format Template">World Info format template</span>
<div id="wi_format_restore" data-i18n="[title]Restore default format" title="Restore default format" class="right_menu_button"> <div id="wi_format_restore" data-i18n="[title]Restore default format" title="Restore default format" class="right_menu_button">
<div class="fa-solid fa-clock-rotate-left"></div> <div class="fa-solid fa-clock-rotate-left"></div>
</div> </div>
@@ -1524,15 +1524,15 @@
<div class=""> <div class="">
<label class="checkbox_label" for="early_stopping_textgenerationwebui"> <label class="checkbox_label" for="early_stopping_textgenerationwebui">
<input type="checkbox" id="early_stopping_textgenerationwebui" /> <input type="checkbox" id="early_stopping_textgenerationwebui" />
<small data-i18n="Early Stopping">Early Stopping <small data-i18n="Early Stopping">Early Stopping</small>
<div class="fa-solid fa-circle-info opacity50p" data-i18n="[title]Controls the stopping condition for beam search. If checked, the generation stops as soon as there are '# of Beams' sequences. If not checked, a heuristic is applied and the generation is stopped when it's very unlikely to find better candidates." title="Controls the stopping condition for beam search. If checked, the generation stops as soon as there are '# of Beams' sequences. If not checked, a heuristic is applied and the generation is stopped when it's very unlikely to find better candidates."></div> <div class="fa-solid fa-circle-info opacity50p" data-i18n="[title]Controls the stopping condition for beam search. If checked, the generation stops as soon as there are '# of Beams' sequences. If not checked, a heuristic is applied and the generation is stopped when it's very unlikely to find better candidates." title="Controls the stopping condition for beam search. If checked, the generation stops as soon as there are '# of Beams' sequences. If not checked, a heuristic is applied and the generation is stopped when it's very unlikely to find better candidates."></div>
</small>
</label> </label>
</div> </div>
</div> </div>
</div> </div>
<div data-tg-type="ooba" id="contrastiveSearchBlock" name="contrastiveSearchBlock" class="alignitemscenter flex-container flexFlowColumn flexBasis48p flexGrow flexShrink gap0"> <div data-tg-type="ooba" id="contrastiveSearchBlock" name="contrastiveSearchBlock" class="alignitemscenter flex-container flexFlowColumn flexBasis48p flexGrow flexShrink gap0">
<h4 class="textAlignCenter" data-i18n="Contrastive search">Contrastive Search <h4 class="textAlignCenter">
<span data-i18n="Contrastive search">Contrastive Search</span>
<div class=" fa-solid fa-circle-info opacity50p " data-i18n="Contrastive_search_txt" title="A sampler that encourages diversity while maintaining coherence, by exploiting the isotropicity of the representation space of most LLMs. For details, see the paper A Contrastive Framework for Neural Text Generation by Su et al. (2022)."></div> <div class=" fa-solid fa-circle-info opacity50p " data-i18n="Contrastive_search_txt" title="A sampler that encourages diversity while maintaining coherence, by exploiting the isotropicity of the representation space of most LLMs. For details, see the paper A Contrastive Framework for Neural Text Generation by Su et al. (2022)."></div>
</h4> </h4>
<div class="alignitemscenter flex-container flexFlowColumn wide100p gap0"> <div class="alignitemscenter flex-container flexFlowColumn wide100p gap0">
@@ -1566,9 +1566,8 @@
</label> </label>
<label data-tg-type="vllm, aphrodite, infermaticai" class="checkbox_label" for="ignore_eos_token_textgenerationwebui"> <label data-tg-type="vllm, aphrodite, infermaticai" class="checkbox_label" for="ignore_eos_token_textgenerationwebui">
<input type="checkbox" id="ignore_eos_token_textgenerationwebui" /> <input type="checkbox" id="ignore_eos_token_textgenerationwebui" />
<small data-i18n="Ignore EOS Token">Ignore EOS Token <small data-i18n="Ignore EOS Token">Ignore EOS Token</small>
<div class="fa-solid fa-circle-info opacity50p " data-i18n="[title]Ignore the EOS Token even if it generates." title="Ignore the EOS Token even if it generates."></div> <div class="fa-solid fa-circle-info opacity50p " data-i18n="[title]Ignore the EOS Token even if it generates." title="Ignore the EOS Token even if it generates."></div>
</small>
</label> </label>
<label class="checkbox_label flexGrow flexShrink" for="skip_special_tokens_textgenerationwebui"> <label class="checkbox_label flexGrow flexShrink" for="skip_special_tokens_textgenerationwebui">
<input type="checkbox" id="skip_special_tokens_textgenerationwebui" /> <input type="checkbox" id="skip_special_tokens_textgenerationwebui" />
@@ -1629,7 +1628,8 @@
</div> </div>
<div id="cfg_block_ooba" data-tg-type="ooba, tabby" class="wide100p"> <div id="cfg_block_ooba" data-tg-type="ooba, tabby" class="wide100p">
<hr class="width100p"> <hr class="width100p">
<h4 data-i18n="CFG" class="textAlignCenter">CFG <h4 class="textAlignCenter">
<span data-i18n="CFG">CFG</span>
<div class="margin5 fa-solid fa-circle-info opacity50p " data-i18n="[title]Classifier Free Guidance. More helpful tip coming soon" title="Classifier Free Guidance. More helpful tip coming soon."></div> <div class="margin5 fa-solid fa-circle-info opacity50p " data-i18n="[title]Classifier Free Guidance. More helpful tip coming soon" title="Classifier Free Guidance. More helpful tip coming soon."></div>
</h4> </h4>
<div class="alignitemscenter flex-container flexFlowColumn flexShrink gap0"> <div class="alignitemscenter flex-container flexFlowColumn flexShrink gap0">
@@ -1959,23 +1959,16 @@
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">
<div id="logit_bias_openai" class="range-block-title openai_restorable" data-i18n="Logit Bias"> <div id="logit_bias_openai" class="range-block-title openai_restorable" data-i18n="Logit Bias">
Logit Bias Logit Bias
</div> </div>
<div class="toggle-description justifyLeft" data-i18n="Helps to ban or reenforce the usage of certain words"> <div class="toggle-description justifyLeft">
Helps to ban or reinforce the usage of certain tokens. Confirm token parsing with <a target="_blank" href="https://platform.openai.com/tokenizer/">Tokenizer</a>. <span data-i18n="Helps to ban or reenforce the usage of certain words">Helps to ban or reinforce the usage of certain tokens.</span>
<span data-i18n="Confirm token parsing with">Confirm token parsing with</span>
<a target="_blank" href="https://platform.openai.com/tokenizer/" data-i18n="Tokenizer">Tokenizer</a>.
</div> </div>
<div class="openai_logit_bias_preset_form"> <div class="openai_logit_bias_preset_form">
<select id="openai_logit_bias_preset"> <select id="openai_logit_bias_preset">
@@ -2276,8 +2269,8 @@
<div data-tg-type="mancer" class="flex-container flexFlowColumn"> <div data-tg-type="mancer" class="flex-container flexFlowColumn">
<div class="flex-container flexFlowColumn"> <div class="flex-container flexFlowColumn">
</div> </div>
<h4 data-i18n="Mancer API key"> <h4>
Mancer API key <span data-i18n="Mancer API key">Mancer API key</span>
<a href="https://mancer.tech/" class="notes-link" target="_blank"> <a href="https://mancer.tech/" 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>
@@ -2573,15 +2566,21 @@
<input id="koboldcpp_api_url_text" class="text_pole wide100p" value="" autocomplete="off" data-server-history="koboldcpp"> <input id="koboldcpp_api_url_text" class="text_pole wide100p" value="" autocomplete="off" data-server-history="koboldcpp">
</div> </div>
</div> </div>
<div class="flex-container flexFlowColumn marginTopBot5">
<label data-tg-type="ooba" class="checkbox_label" for="bypass_status_check_textgenerationwebui">
<input type="checkbox" id="bypass_status_check_textgenerationwebui" />
<span data-i18n="Bypass status check">Bypass status check</span>
</label>
<label data-tg-type="koboldcpp, llamacpp" class="checkbox_label" for="context_size_derived">
<input type="checkbox" id="context_size_derived" />
<span data-i18n="Derive context size from backend">Derive context size from backend</span>
</label>
</div>
<div class="flex-container"> <div class="flex-container">
<div id="api_button_textgenerationwebui" class="api_button menu_button menu_button_icon" type="submit" data-i18n="Connect" data-server-connect="ooba_blocking,vllm,aphrodite,tabby,koboldcpp,ollama,llamacpp,huggingface">Connect</div> <div id="api_button_textgenerationwebui" class="api_button menu_button menu_button_icon" type="submit" data-i18n="Connect" data-server-connect="ooba_blocking,vllm,aphrodite,tabby,koboldcpp,ollama,llamacpp,huggingface">Connect</div>
<div data-tg-type="openrouter" class="menu_button menu_button_icon openrouter_authorize" title="Get your OpenRouter API token using OAuth flow. You will be redirected to openrouter.ai" data-i18n="Authorize;[title]Get your OpenRouter API token using OAuth flow. You will be redirected to openrouter.ai">Authorize</div> <div data-tg-type="openrouter" class="menu_button menu_button_icon openrouter_authorize" title="Get your OpenRouter API token using OAuth flow. You will be redirected to openrouter.ai" data-i18n="Authorize;[title]Get your OpenRouter API token using OAuth flow. You will be redirected to openrouter.ai">Authorize</div>
<div class="api_loading menu_button menu_button_icon" data-i18n="Cancel">Cancel</div> <div class="api_loading menu_button menu_button_icon" data-i18n="Cancel">Cancel</div>
</div> </div>
<label data-tg-type="ooba" class="checkbox_label margin-bot-10px" for="bypass_status_check_textgenerationwebui">
<input type="checkbox" id="bypass_status_check_textgenerationwebui" />
<span data-i18n="Bypass status check">Bypass status check</span>
</label>
</form> </form>
<div class="online_status"> <div class="online_status">
<div class="online_status_indicator"></div> <div class="online_status_indicator"></div>
@@ -2624,8 +2623,9 @@
</div> </div>
<div class="toggle-description justifyLeft"> <div class="toggle-description justifyLeft">
<span data-i18n="Saved addresses and passwords."> <span data-i18n="Saved addresses and passwords.">
Saved addresses and passwords.<br> Saved addresses and passwords.
</span> </span>
<br>
</div> </div>
<div class="openai_logit_bias_preset_form"> <div class="openai_logit_bias_preset_form">
<select id="openai_proxy_preset"> <select id="openai_proxy_preset">
@@ -2638,8 +2638,9 @@
</div> </div>
<div class="toggle-description justifyLeft"> <div class="toggle-description justifyLeft">
<span data-i18n="This will show up as your saved preset."> <span data-i18n="This will show up as your saved preset.">
This will show up as your saved preset.<br> This will show up as your saved preset.
</span> </span>
<br>
</div> </div>
<div class="wide100p"> <div class="wide100p">
<input id="openai_reverse_proxy_name" type="text" class="text_pole" placeholder="..." /> <input id="openai_reverse_proxy_name" type="text" class="text_pole" placeholder="..." />
@@ -2649,8 +2650,9 @@
</div> </div>
<div class="toggle-description justifyLeft wide100p"> <div class="toggle-description justifyLeft wide100p">
<span data-i18n="Alternative server URL (leave empty to use the default value)."> <span data-i18n="Alternative server URL (leave empty to use the default value).">
Alternative server URL (leave empty to use the default value).<br> Alternative server URL (leave empty to use the default value).
</span> </span>
<br>
</div> </div>
<div class="wide100p"> <div class="wide100p">
<input id="openai_reverse_proxy" type="text" class="text_pole" placeholder="https://api.openai.com/v1" /> <input id="openai_reverse_proxy" type="text" class="text_pole" placeholder="https://api.openai.com/v1" />
@@ -2663,8 +2665,9 @@
</div> </div>
<div class="toggle-description justifyLeft"> <div class="toggle-description justifyLeft">
<span data-i18n="Will be used as a password for the proxy instead of API key."> <span data-i18n="Will be used as a password for the proxy instead of API key.">
Will be used as a password for the proxy instead of API key.<br> Will be used as a password for the proxy instead of API key.
</span> </span>
<br>
</div> </div>
<div class="flex-container width100p"> <div class="flex-container width100p">
<input id="openai_proxy_password" type="password" class="text_pole flex1" placeholder="" form="openai_form" autocomplete="off" /> <input id="openai_proxy_password" type="password" class="text_pole flex1" placeholder="" form="openai_form" autocomplete="off" />
@@ -3281,7 +3284,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>
@@ -3743,7 +3746,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>
@@ -3752,7 +3755,11 @@
<div id="wiTopBlock" class="flex-container"> <div id="wiTopBlock" class="flex-container">
<div id="WIMultiSelector" class="flex1 flex alignSelfStart range-block"> <div id="WIMultiSelector" class="flex1 flex alignSelfStart range-block">
<div class="range-block-title justifyLeft"> <div class="range-block-title justifyLeft">
<span data-i18n="Active World(s) for all chats"><small>Active World(s) for all chats</small></span> <span>
<small data-i18n="Active World(s) for all chats">
Active World(s) for all chats
</small>
</span>
</div> </div>
<div class="range-block-range"> <div class="range-block-range">
<select id="world_info" class="select2_multi_sameline" multiple> <select id="world_info" class="select2_multi_sameline" multiple>
@@ -3937,7 +3944,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">
@@ -4071,7 +4084,10 @@
<div class="alignitemscenter flex-container flexFlowColumn flexBasis48p flexGrow flexShrink gap0"> <div class="alignitemscenter flex-container flexFlowColumn flexBasis48p flexGrow flexShrink gap0">
<small> <small>
<span data-i18n="Chat Width">Chat Width <i class="fa-solid fa-desktop"></i></span> <span>
<span data-i18n="Chat Width">Chat Width</span>
<i class="fa-solid fa-desktop"></i>
</span>
<div class="fa-solid fa-circle-info opacity50p" data-i18n="[title]Width of the main chat window in % of screen width" title="Width of the main chat window in % of screen width"></div> <div class="fa-solid fa-circle-info opacity50p" data-i18n="[title]Width of the main chat window in % of screen width" title="Width of the main chat window in % of screen width"></div>
</small> </small>
<input class="neo-range-slider" type="range" id="chat_width_slider" name="chat_width_slider" min="25" max="100" step="1"> <input class="neo-range-slider" type="range" id="chat_width_slider" name="chat_width_slider" min="25" max="100" step="1">
@@ -4243,7 +4259,7 @@
<div id="reload_chat" class="menu_button whitespacenowrap" data-i18n="[title]Reload and redraw the currently open chat" title="Reload and redraw the currently open chat."> <div id="reload_chat" class="menu_button whitespacenowrap" data-i18n="[title]Reload and redraw the currently open chat" title="Reload and redraw the currently open chat.">
<small data-i18n="Reload Chat">Reload Chat</small> <small data-i18n="Reload Chat">Reload Chat</small>
</div> </div>
<div id="debug_menu" class="menu_button whitespacenowrap" data-i18n="Debug Menu"> <div id="debug_menu" class="menu_button whitespacenowrap">
<small data-i18n="Debug Menu">Debug Menu</small> <small data-i18n="Debug Menu">Debug Menu</small>
</div> </div>
</div> </div>
@@ -4270,9 +4286,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">
@@ -4365,8 +4378,10 @@
</div> </div>
</div> </div>
<div id="examples-behavior-block"> <div id="examples-behavior-block">
<label data-i18n="Example Messages Behavior"> <label>
<small>Example Messages Behavior:</small> <small data-i18n="Example Messages Behavior">
Example Messages Behavior:
</small>
</label> </label>
<select id="example_messages_behavior"> <select id="example_messages_behavior">
<option value="normal" data-i18n="Gradual push-out">Gradual push-out</option> <option value="normal" data-i18n="Gradual push-out">Gradual push-out</option>
@@ -4530,7 +4545,9 @@
</label> </label>
<div class="flex-container"> <div class="flex-container">
<div class="flex1" title="Determines how entries are found for autocomplete." data-i18n="[title]Determines how entries are found for autocomplete."> <div class="flex1" title="Determines how entries are found for autocomplete." data-i18n="[title]Determines how entries are found for autocomplete.">
<label for="stscript_matching" data-i18n="Autocomplete Matching"><small>Matching</small></label> <label for="stscript_matching">
<small data-i18n="Autocomplete Matching">Matching</small>
</label>
<select id="stscript_matching"> <select id="stscript_matching">
<option data-i18n="Starts with" value="strict">Starts with</option> <option data-i18n="Starts with" value="strict">Starts with</option>
<option data-i18n="Includes" value="includes">Includes</option> <option data-i18n="Includes" value="includes">Includes</option>
@@ -4538,7 +4555,9 @@
</select> </select>
</div> </div>
<div class="flex1" title="Sets the style of the autocomplete." data-i18n="[title]Sets the style of the autocomplete."> <div class="flex1" title="Sets the style of the autocomplete." data-i18n="[title]Sets the style of the autocomplete.">
<label for="stscript_autocomplete_style" data-i18n="Autocomplete Style"><small>Style</small></label> <label for="stscript_autocomplete_style">
<small data-i18n="Autocomplete Style">Style</small>
</label>
<div class="flex-container flexFlowRow alignItemsBaseline"> <div class="flex-container flexFlowRow alignItemsBaseline">
<select id="stscript_autocomplete_style"> <select id="stscript_autocomplete_style">
<option data-i18n="Follow Theme" value="theme">Follow Theme</option> <option data-i18n="Follow Theme" value="theme">Follow Theme</option>
@@ -4550,8 +4569,8 @@
</div> </div>
</div> </div>
<div title="Determines which keys select an item from the AutoComplete suggestions"> <div title="Determines which keys select an item from the AutoComplete suggestions">
<label data-i18n="Keyboard"> <label>
<small>Keyboard:</small> <small data-i18n="Keyboard">Keyboard:</small>
</label> </label>
<select id="stscript_autocomplete_select"> <select id="stscript_autocomplete_select">
<option value="3" data-i18n="Select with Tab or Enter">Select with Tab or Enter</option> <option value="3" data-i18n="Select with Tab or Enter">Select with Tab or Enter</option>
@@ -4565,7 +4584,9 @@
<input class="neo-range-input" type="number" min="0.5" max="2" step="0.01" data-for="stscript_autocomplete_font_scale" id="stscript_autocomplete_font_scale_counter"> <input class="neo-range-input" type="number" min="0.5" max="2" step="0.01" data-for="stscript_autocomplete_font_scale" id="stscript_autocomplete_font_scale_counter">
</div> </div>
<div title="Sets the width of the autocomplete." data-i18n="[title]Sets the width of the autocomplete."> <div title="Sets the width of the autocomplete." data-i18n="[title]Sets the width of the autocomplete.">
<label for="stscript_autocomplete_width" data-i18n="Autocomplete Width"><small>Width</small></label> <label for="stscript_autocomplete_width">
<small data-i18n="Autocomplete Width">Width</small>
</label>
<div class="doubleRangeContainer"> <div class="doubleRangeContainer">
<div class="doubleRangeInputContainer"> <div class="doubleRangeInputContainer">
<input type="range" id="stscript_autocomplete_width_left" min="0" max="2" step="1"> <input type="range" id="stscript_autocomplete_width_left" min="0" max="2" step="1">
@@ -4593,14 +4614,18 @@
<label><small data-i18n="Parser Flags">Parser Flags</small></label> <label><small data-i18n="Parser Flags">Parser Flags</small></label>
<label class="checkbox_label" title="Switch to stricter escaping, allowing all delimiting characters to be escaped with a backslash, and backslashes to be escaped as well." data-i18n="[title]Switch to stricter escaping, allowing all delimiting characters to be escaped with a backslash, and backslashes to be escaped as well."> <label class="checkbox_label" title="Switch to stricter escaping, allowing all delimiting characters to be escaped with a backslash, and backslashes to be escaped as well." data-i18n="[title]Switch to stricter escaping, allowing all delimiting characters to be escaped with a backslash, and backslashes to be escaped as well.">
<input id="stscript_parser_flag_strict_escaping" type="checkbox" /> <input id="stscript_parser_flag_strict_escaping" type="checkbox" />
<span data-i18n="STRICT_ESCAPING"><small>STRICT_ESCAPING</small></span> <span>
<small data-i18n="STRICT_ESCAPING">STRICT_ESCAPING</small>
</span>
<a href="https://docs.sillytavern.app/usage/st-script/#strict-escaping" target="_blank" class="notes-link"> <a href="https://docs.sillytavern.app/usage/st-script/#strict-escaping" target="_blank" class="notes-link">
<span class="fa-solid fa-circle-question note-link-span"></span> <span class="fa-solid fa-circle-question note-link-span"></span>
</a> </a>
</label> </label>
<label class="checkbox_label" title="Prevents {{getvar::}} {{getglobalvar::}} macros from having literal macro-like values auto-evaluated.&NewLine;e.g. &quot;{{newline}}&quot; remains as literal string &quot;{{newline}}&quot;&NewLine;&NewLine;(This is done by internally replacing {{getvar::}} {{getglobalvar::}} macros with scoped variables.)" data-i18n="[title]stscript_parser_flag_replace_getvar_label"> <label class="checkbox_label" title="Prevents {{getvar::}} {{getglobalvar::}} macros from having literal macro-like values auto-evaluated.&NewLine;e.g. &quot;{{newline}}&quot; remains as literal string &quot;{{newline}}&quot;&NewLine;&NewLine;(This is done by internally replacing {{getvar::}} {{getglobalvar::}} macros with scoped variables.)" data-i18n="[title]stscript_parser_flag_replace_getvar_label">
<input id="stscript_parser_flag_replace_getvar" type="checkbox" /> <input id="stscript_parser_flag_replace_getvar" type="checkbox" />
<span data-i18n="REPLACE_GETVAR"><small>REPLACE_GETVAR</small></span> <span>
<small data-i18n="REPLACE_GETVAR">REPLACE_GETVAR</small>
</span>
<a href="https://docs.sillytavern.app/usage/st-script/#replace-variable-macros" target="_blank" class="notes-link"> <a href="https://docs.sillytavern.app/usage/st-script/#replace-variable-macros" target="_blank" class="notes-link">
<span class="fa-solid fa-circle-question note-link-span"></span> <span class="fa-solid fa-circle-question note-link-span"></span>
</a> </a>
@@ -4731,10 +4756,12 @@
<div class="flex-container wide100p alignitemscenter spaceBetween flexNoGap"> <div class="flex-container wide100p alignitemscenter spaceBetween flexNoGap">
<div class="flex-container alignItemsBaseline wide100p"> <div class="flex-container alignItemsBaseline wide100p">
<div class="flex1 flex-container alignItemsBaseline"> <div class="flex1 flex-container alignItemsBaseline">
<h3 class="margin0" data-i18n="Persona Management">Persona Management</h3> <h3 class="margin0" >
<a href="https://docs.sillytavern.app/usage/core-concepts/personas/" target="_blank" data-i18n="How do I use this?"> <span data-i18n="Persona Management">Persona Management</span>
<span class="fa-solid fa-circle-question note-link-span"></span> <a href="https://docs.sillytavern.app/usage/core-concepts/personas/" target="_blank">
</a> <span class="fa-solid fa-circle-question note-link-span"></span>
</a>
</h3>
</div> </div>
<div class="flex-container"> <div class="flex-container">
<div class="menu_button menu_button_icon user_stats_button" data-i18n="[title]Click for stats!" title="Click for stats!"> <div class="menu_button menu_button_icon user_stats_button" data-i18n="[title]Click for stats!" title="Click for stats!">
@@ -5455,7 +5482,12 @@
<span data-i18n="Examples of dialogue" class="mdhotkey_location">Examples of dialogue</span> <span data-i18n="Examples of dialogue" class="mdhotkey_location">Examples of dialogue</span>
<i class="editor_maximize fa-solid fa-maximize right_menu_button" data-for="mes_example_textarea" title="Expand the editor" data-i18n="[title]Expand the editor"></i> <i class="editor_maximize fa-solid fa-maximize right_menu_button" data-for="mes_example_textarea" title="Expand the editor" data-i18n="[title]Expand the editor"></i>
</h4> </h4>
<h5 data-i18n="Important to set the character's writing style.">Important to set the character's writing style. <a href="https://docs.sillytavern.app/usage/core-concepts/characterdesign/#examples-of-dialogue" class="notes-link" target="_blank"><span class="fa-solid fa-circle-question note-link-span"></span></a></h5> <h5>
<span data-i18n="Important to set the character's writing style.">Important to set the character's writing style.</span>
<a href="https://docs.sillytavern.app/usage/core-concepts/characterdesign/#examples-of-dialogue" class="notes-link" target="_blank">
<span class="fa-solid fa-circle-question note-link-span"></span>
</a>
</h5>
</div> </div>
<textarea id="mes_example_textarea" class="flexGrow mdHotkeys" name="mes_example" data-i18n="[placeholder](Examples of chat dialog. Begin each example with START on a new line.)" placeholder="(Examples of chat dialog. Begin each example with &lt;START&gt; on a new line.)" form="form_create" rows="6"></textarea> <textarea id="mes_example_textarea" class="flexGrow mdHotkeys" name="mes_example" data-i18n="[placeholder](Examples of chat dialog. Begin each example with START on a new line.)" placeholder="(Examples of chat dialog. Begin each example with &lt;START&gt; on a new line.)" form="form_create" rows="6"></textarea>
<div class="extension_token_counter"> <div class="extension_token_counter">
@@ -5531,7 +5563,8 @@
<div class="character_world range-block flexFlowColumn flex-container"> <div class="character_world range-block flexFlowColumn flex-container">
<div class="range-block-title"> <div class="range-block-title">
<h3> <h3>
<span data-i18n="Select a World Info file for"> Select a World Info file for <span class="character_name"></span></span>: <span data-i18n="Select a World Info file for">Select a World Info file for</span>
<span class="character_name"></span>:
</h3> </h3>
</div> </div>
<h4 data-i18n="Primary Lorebook">Primary Lorebook</h4> <h4 data-i18n="Primary Lorebook">Primary Lorebook</h4>
@@ -5832,7 +5865,7 @@
</div> </div>
<div class="flex2 flex-container flexFlowColumn flexNoGap" data-i18n="[title]Sticky entries will stay active for N messages after being triggered." title="Sticky entries will stay active for N messages after being triggered."> <div class="flex2 flex-container flexFlowColumn flexNoGap" data-i18n="[title]Sticky entries will stay active for N messages after being triggered." title="Sticky entries will stay active for N messages after being triggered.">
<div class="flex-container justifySpaceBetween marginBot5"> <div class="flex-container justifySpaceBetween marginBot5">
<small class="flex-container alignItemsBaseline" for="sticky" data-i18n="Sticky"> <small class="flex-container alignItemsBaseline" for="sticky">
<span data-i18n="Sticky"> <span data-i18n="Sticky">
Sticky Sticky
</span> </span>
@@ -5845,7 +5878,7 @@
</div> </div>
<div class="flex2 flex-container flexFlowColumn flexNoGap" data-i18n="[title]Entries with a cooldown can't be activated N messages after being triggered." title="Entries with a cooldown can't be activated N messages after being triggered."> <div class="flex2 flex-container flexFlowColumn flexNoGap" data-i18n="[title]Entries with a cooldown can't be activated N messages after being triggered." title="Entries with a cooldown can't be activated N messages after being triggered.">
<div class="flex-container justifySpaceBetween marginBot5"> <div class="flex-container justifySpaceBetween marginBot5">
<small class="flex-container alignItemsBaseline" for="cooldown" data-i18n="Cooldown"> <small class="flex-container alignItemsBaseline" for="cooldown">
<span data-i18n="Cooldown"> <span data-i18n="Cooldown">
Cooldown Cooldown
</span> </span>
@@ -5858,7 +5891,7 @@
</div> </div>
<div class="flex2 flex-container flexFlowColumn flexNoGap" data-i18n="[title]Entries with a delay can't be activated until there are N messages present in the chat." title="Entries with a delay can't be activated until there are N messages present in the chat."> <div class="flex2 flex-container flexFlowColumn flexNoGap" data-i18n="[title]Entries with a delay can't be activated until there are N messages present in the chat." title="Entries with a delay can't be activated until there are N messages present in the chat.">
<div class="flex-container justifySpaceBetween marginBot5"> <div class="flex-container justifySpaceBetween marginBot5">
<small class="flex-container alignItemsBaseline" for="delay" data-i18n="Delay"> <small class="flex-container alignItemsBaseline" for="delay">
<span data-i18n="Delay"> <span data-i18n="Delay">
Delay Delay
</span> </span>

View File

@@ -1238,8 +1238,9 @@ async function getStatusTextgen() {
const wantsInstructDerivation = (power_user.instruct.enabled && power_user.instruct.derived); const wantsInstructDerivation = (power_user.instruct.enabled && power_user.instruct.derived);
const wantsContextDerivation = power_user.context_derived; const wantsContextDerivation = power_user.context_derived;
const wantsContextSize = power_user.context_size_derived;
const supportsChatTemplate = [textgen_types.KOBOLDCPP, textgen_types.LLAMACPP].includes(textgen_settings.type); const supportsChatTemplate = [textgen_types.KOBOLDCPP, textgen_types.LLAMACPP].includes(textgen_settings.type);
if (supportsChatTemplate && (wantsInstructDerivation || wantsContextDerivation)) { if (supportsChatTemplate && (wantsInstructDerivation || wantsContextDerivation || wantsContextSize)) {
const response = await fetch('/api/backends/text-completions/props', { const response = await fetch('/api/backends/text-completions/props', {
method: 'POST', method: 'POST',
headers: getRequestHeaders(), headers: getRequestHeaders(),
@@ -1253,6 +1254,17 @@ async function getStatusTextgen() {
const data = await response.json(); const data = await response.json();
if (data) { if (data) {
const { chat_template, chat_template_hash } = data; const { chat_template, chat_template_hash } = data;
if (wantsContextSize && 'default_generation_settings' in data) {
const backend_max_context = data['default_generation_settings']['n_ctx'];
const old_value = max_context;
if (max_context !== backend_max_context) {
setGenerationParamsFromPreset({ max_length: backend_max_context });
}
if (old_value !== max_context) {
console.log(`Auto-switched max context from ${old_value} to ${max_context}`);
toastr.info(`${old_value}${max_context}`, 'Context Size Changed');
}
}
console.log(`We have chat template ${chat_template.split('\n')[0]}...`); console.log(`We have chat template ${chat_template.split('\n')[0]}...`);
const templates = await deriveTemplatesFromChatTemplate(chat_template, chat_template_hash); const templates = await deriveTemplatesFromChatTemplate(chat_template, chat_template_hash);
if (templates) { if (templates) {
@@ -1621,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);
@@ -1634,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 });
} }
} }
@@ -1654,6 +1666,7 @@ export function getEntitiesList({ doFilter = false, doSort = true } = {}) {
if (doSort) { if (doSort) {
sortEntitiesList(entities); sortEntitiesList(entities);
} }
entitiesFilter.clearFuzzySearchCaches();
return entities; return entities;
} }
@@ -6821,6 +6834,10 @@ export async function saveSettings(type) {
}); });
} }
/**
* Sets the generation parameters from a preset object.
* @param {{ genamt?: number, max_length?: number }} preset Preset object
*/
export function setGenerationParamsFromPreset(preset) { export function setGenerationParamsFromPreset(preset) {
const needsUnlock = (preset.max_length ?? max_context) > MAX_CONTEXT_DEFAULT || (preset.genamt ?? amount_gen) > MAX_RESPONSE_DEFAULT; const needsUnlock = (preset.max_length ?? max_context) > MAX_CONTEXT_DEFAULT || (preset.genamt ?? amount_gen) > MAX_RESPONSE_DEFAULT;
$('#max_context_unlocked').prop('checked', needsUnlock).trigger('change'); $('#max_context_unlocked').prop('checked', needsUnlock).trigger('change');

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

@@ -40,7 +40,7 @@
<div class="expression_fallback_block m-b-1 m-t-1"> <div class="expression_fallback_block m-b-1 m-t-1">
<label for="expression_fallback" data-i18n="Default / Fallback Expression">Default / Fallback Expression</label> <label for="expression_fallback" data-i18n="Default / Fallback Expression">Default / Fallback Expression</label>
<small data-i18n="Set the default and fallback expression being used when no matching expression is found.">Set the default and fallback expression being used when no matching expression is found.</small> <small data-i18n="Set the default and fallback expression being used when no matching expression is found.">Set the default and fallback expression being used when no matching expression is found.</small>
<select id="expression_fallback" class="flex1 margin0" data-i18n="Fallback Expression" placeholder="Fallback Expression"></select> <select id="expression_fallback" class="flex1 margin0"></select>
</div> </div>
<div class="expression_custom_block m-b-1 m-t-1"> <div class="expression_custom_block m-b-1 m-t-1">
<label for="expression_custom" data-i18n="Custom Expressions">Custom Expressions</label> <label for="expression_custom" data-i18n="Custom Expressions">Custom Expressions</label>

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

@@ -57,7 +57,7 @@
</div> </div>
<div id="playback_rate_block" class="range-block"> <div id="playback_rate_block" class="range-block">
<hr> <hr>
<div class="range-block-title justifyLeft" data-i18n="Audio Playback Speed"> <div class="range-block-title justifyLeft">
<small data-i18n="Audio Playback Speed">Audio Playback Speed</small> <small data-i18n="Audio Playback Speed">Audio Playback Speed</small>
</div> </div>
<div class="range-block-range-and-counter"> <div class="range-block-range-and-counter">
@@ -80,4 +80,4 @@
</div> </div>
</div> </div>
</div> </div>
</div> </div>

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,
@@ -245,6 +246,7 @@ let power_user = {
}, },
context_derived: false, context_derived: false,
context_size_derived: false,
sysprompt: { sysprompt: {
enabled: true, enabled: true,
@@ -1481,6 +1483,7 @@ async function loadPowerUserSettings(settings, data) {
$('#example_messages_behavior').val(getExampleMessagesBehavior()); $('#example_messages_behavior').val(getExampleMessagesBehavior());
$(`#example_messages_behavior option[value="${getExampleMessagesBehavior()}"]`).prop('selected', true); $(`#example_messages_behavior option[value="${getExampleMessagesBehavior()}"]`).prop('selected', true);
$('#context_derived').parent().find('i').toggleClass('toggleEnabled', !!power_user.context_derived); $('#context_derived').parent().find('i').toggleClass('toggleEnabled', !!power_user.context_derived);
$('#context_size_derived').prop('checked', !!power_user.context_size_derived);
$('#console_log_prompts').prop('checked', power_user.console_log_prompts); $('#console_log_prompts').prop('checked', power_user.console_log_prompts);
$('#request_token_probabilities').prop('checked', power_user.request_token_probabilities); $('#request_token_probabilities').prop('checked', power_user.request_token_probabilities);
@@ -1829,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,
@@ -1857,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;
} }
/** /**
@@ -3076,6 +3081,16 @@ $(document).ready(() => {
$('#context_derived').parent().find('i').toggleClass('toggleEnabled', !!power_user.context_derived); $('#context_derived').parent().find('i').toggleClass('toggleEnabled', !!power_user.context_derived);
}); });
$('#context_size_derived').on('input', function () {
const value = !!$(this).prop('checked');
power_user.context_size_derived = value;
saveSettingsDebounced();
});
$('#context_size_derived').on('change', function () {
$('#context_size_derived').prop('checked', !!power_user.context_size_derived);
});
$('#always-force-name2-checkbox').change(function () { $('#always-force-name2-checkbox').change(function () {
power_user.always_force_name2 = !!$(this).prop('checked'); power_user.always_force_name2 = !!$(this).prop('checked');
saveSettingsDebounced(); saveSettingsDebounced();

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

@@ -152,7 +152,7 @@ router.post('/status', jsonParser, async function (request, response) {
if (!modelsReply.ok) { if (!modelsReply.ok) {
console.log('Models endpoint is offline.'); console.log('Models endpoint is offline.');
return response.status(400); return response.sendStatus(400);
} }
/** @type {any} */ /** @type {any} */
@@ -173,7 +173,7 @@ router.post('/status', jsonParser, async function (request, response) {
if (!Array.isArray(data.data)) { if (!Array.isArray(data.data)) {
console.log('Models response is not an array.'); console.log('Models response is not an array.');
return response.status(400); return response.sendStatus(400);
} }
const modelIds = data.data.map(x => x.id); const modelIds = data.data.map(x => x.id);
@@ -224,7 +224,7 @@ router.post('/status', jsonParser, async function (request, response) {
return response.send({ result, data: data.data }); return response.send({ result, data: data.data });
} catch (error) { } catch (error) {
console.error(error); console.error(error);
return response.status(500); return response.sendStatus(500);
} }
}); });
@@ -244,7 +244,7 @@ router.post('/props', jsonParser, async function (request, response) {
const propsReply = await fetch(propsUrl, args); const propsReply = await fetch(propsUrl, args);
if (!propsReply.ok) { if (!propsReply.ok) {
return response.status(400); return response.sendStatus(400);
} }
/** @type {any} */ /** @type {any} */
@@ -258,7 +258,7 @@ router.post('/props', jsonParser, async function (request, response) {
return response.send(props); return response.send(props);
} catch (error) { } catch (error) {
console.error(error); console.error(error);
return response.status(500); return response.sendStatus(500);
} }
}); });
@@ -450,7 +450,7 @@ ollama.post('/download', jsonParser, async function (request, response) {
return response.send({ ok: true }); return response.send({ ok: true });
} catch (error) { } catch (error) {
console.error(error); console.error(error);
return response.status(500); return response.sendStatus(500);
} }
}); });
@@ -493,7 +493,7 @@ ollama.post('/caption-image', jsonParser, async function (request, response) {
return response.send({ caption }); return response.send({ caption });
} catch (error) { } catch (error) {
console.error(error); console.error(error);
return response.status(500); return response.sendStatus(500);
} }
}); });
@@ -540,7 +540,7 @@ llamacpp.post('/caption-image', jsonParser, async function (request, response) {
} catch (error) { } catch (error) {
console.error(error); console.error(error);
return response.status(500); return response.sendStatus(500);
} }
}); });
@@ -569,7 +569,7 @@ llamacpp.post('/props', jsonParser, async function (request, response) {
} catch (error) { } catch (error) {
console.error(error); console.error(error);
return response.status(500); return response.sendStatus(500);
} }
}); });
@@ -619,7 +619,7 @@ llamacpp.post('/slots', jsonParser, async function (request, response) {
} catch (error) { } catch (error) {
console.error(error); console.error(error);
return response.status(500); return response.sendStatus(500);
} }
}); });
@@ -665,7 +665,7 @@ tabby.post('/download', jsonParser, async function (request, response) {
return response.send({ ok: true }); return response.send({ ok: true });
} catch (error) { } catch (error) {
console.error(error); console.error(error);
return response.status(500); return response.sendStatus(500);
} }
}); });

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',
}, },