Merge branch 'staging' into persona-improvements

8bd4fd76ae604bed6c4960757e74d5a83de10e92

Wolfsblvt <wolfsblvt@gmail.com>

150 files changed, +3863 -1742Showing whitespace changes
.github/ISSUE_TEMPLATE/bug-report.yml+2 -0
@@ -80,6 +80,8 @@ body:
80 required: true80 required: true
81 - label: I have checked the [docs](https://docs.sillytavern.app/) ![important](https://img.shields.io/badge/Important!-F6094E)81 - label: I have checked the [docs](https://docs.sillytavern.app/) ![important](https://img.shields.io/badge/Important!-F6094E)
82 required: true82 required: true
83 - label: I confirm that my issue is not related to third-party content, unofficial extension or patch. If in doubt, check with a new [user account](https://docs.sillytavern.app/administration/multi-user/) and with extensions disabled
84 required: true
8385
84 - type: markdown86 - type: markdown
85 attributes:87 attributes:
default/!DO-NOT-EDIT-THESE-FILES.txt+13 -0
@@ -0,0 +1,13 @@
1These are master copies of the default content files and are managed by SillyTavern.
2
3Editing any of these files would not only have no effect, but will also cause merge conflicts during update pulls.
4
5You should edit their respective copies instead, for example:
6
71. /default/config.yaml => /config.yaml
82. /default/public/css/user.css => /public/css/user.css
9etc.
10
11Any questions? You're always welcome at our official documentation website:
12
13https://docs.sillytavern.app/
default/config.yaml+24 -0
@@ -6,7 +6,13 @@ cardsCacheCapacity: 100
6# -- SERVER CONFIGURATION --6# -- SERVER CONFIGURATION --
7# Listen for incoming connections7# Listen for incoming connections
8listen: false8listen: false
9# Listen on a specific address, supports IPv4 and IPv6
10listenAddress:
11 ipv4: 0.0.0.0
12 ipv6: '[::]'
9# Enables IPv6 and/or IPv4 protocols. Need to have at least one enabled!13# Enables IPv6 and/or IPv4 protocols. Need to have at least one enabled!
14# - Use option "auto" to automatically detect support
15# - Use true or false (no qoutes) to enable or disable each protocol
10protocol:16protocol:
11 ipv4: true17 ipv4: true
12 ipv6: false18 ipv6: false
@@ -77,6 +83,18 @@ cookieSecret: ''
77disableCsrfProtection: false83disableCsrfProtection: false
78# Disable startup security checks - NOT RECOMMENDED84# Disable startup security checks - NOT RECOMMENDED
79securityOverride: false85securityOverride: false
86# -- LOGGING CONFIGURATION --
87logging:
88 # Enable access logging to access.log file
89 # Records new connections with timestamp, IP address and user agent
90 enableAccessLog: true
91 # Minimum log level to display in the terminal (DEBUG = 0, INFO = 1, WARN = 2, ERROR = 3)
92 minLogLevel: 0
93# -- RATE LIMITING CONFIGURATION --
94rateLimiting:
95 # Use X-Real-IP header instead of socket IP for rate limiting
96 # Only enable this if you are using a properly configured reverse proxy (like Nginx/traefik/Caddy)
97 preferRealIpHeader: false
80# -- ADVANCED CONFIGURATION --98# -- ADVANCED CONFIGURATION --
81# Open the browser automatically99# Open the browser automatically
82autorun: true100autorun: true
@@ -179,6 +197,10 @@ ollama:
179 # * 0: Unload the model immediately after the request197 # * 0: Unload the model immediately after the request
180 # * N (any positive number): Keep the model loaded for N seconds after the request.198 # * N (any positive number): Keep the model loaded for N seconds after the request.
181 keepAlive: -1199 keepAlive: -1
200 # Controls the "num_batch" (batch size) parameter of the generation request
201 # * -1: Use the default value of the model
202 # * N (positive number): Use the specified value. Must be a power of 2, e.g. 128, 256, 512, etc.
203 batchSize: -1
182# -- ANTHROPIC CLAUDE API CONFIGURATION --204# -- ANTHROPIC CLAUDE API CONFIGURATION --
183claude:205claude:
184 # Enables caching of the system prompt (if supported).206 # Enables caching of the system prompt (if supported).
@@ -198,3 +220,5 @@ claude:
198 cachingAtDepth: -1220 cachingAtDepth: -1
199# -- SERVER PLUGIN CONFIGURATION --221# -- SERVER PLUGIN CONFIGURATION --
200enableServerPlugins: false222enableServerPlugins: false
223# Attempt to automatically update server plugins on startup
224enableServerPluginsAutoUpdate: true
default/content/index.json+0 -4
@@ -672,10 +672,6 @@
672 "type": "moving_ui"672 "type": "moving_ui"
673 },673 },
674 {674 {
675 "filename": "presets/moving-ui/Black Magic Time.json",
676 "type": "moving_ui"
677 },
678 {
679 "filename": "presets/quick-replies/Default.json",675 "filename": "presets/quick-replies/Default.json",
680 "type": "quick_replies"676 "type": "quick_replies"
681 },677 },
default/content/presets/moving-ui/Black Magic Time.json+0 -45
@@ -1,45 +0,0 @@
1{
2 "name": "Black Magic Time",
3 "movingUIState": {
4 "sheld": {
5 "top": 488,
6 "left": 1407,
7 "right": 1,
8 "bottom": 4,
9 "margin": "unset",
10 "width": 471,
11 "height": 439
12 },
13 "floatingPrompt": {
14 "width": 369,
15 "height": 441
16 },
17 "right-nav-panel": {
18 "top": 0,
19 "left": 1400,
20 "right": 111,
21 "bottom": 446,
22 "margin": "unset",
23 "width": 479,
24 "height": 487
25 },
26 "WorldInfo": {
27 "top": 41,
28 "left": 369,
29 "right": 642,
30 "bottom": 51,
31 "margin": "unset",
32 "width": 1034,
33 "height": 858
34 },
35 "left-nav-panel": {
36 "top": 442,
37 "left": 0,
38 "right": 1546,
39 "bottom": 25,
40 "margin": "unset",
41 "width": 368,
42 "height": 483
43 }
44 }
45}
45 \ No newline at end of file \ No newline at end of file
docker/build-lib.js+1 -1
@@ -1,4 +1,4 @@
1import getWebpackServeMiddleware from '../src/middleware/webpack-serve.js';1import getWebpackServeMiddleware from '../src/middleware/webpack-serve.js';
22
3const middleware = getWebpackServeMiddleware();3const middleware = getWebpackServeMiddleware();
4await middleware.runWebpackCompiler();4await middleware.runWebpackCompiler({ forceDist: true });
package-lock.json+65 -11
@@ -1,12 +1,12 @@
1{1{
2 "name": "sillytavern",2 "name": "sillytavern",
3 "version": "1.12.11",3 "version": "1.12.12",
4 "lockfileVersion": 3,4 "lockfileVersion": 3,
5 "requires": true,5 "requires": true,
6 "packages": {6 "packages": {
7 "": {7 "": {
8 "name": "sillytavern",8 "name": "sillytavern",
9 "version": "1.12.11",9 "version": "1.12.12",
10 "hasInstallScript": true,10 "hasInstallScript": true,
11 "license": "AGPL-3.0",11 "license": "AGPL-3.0",
12 "dependencies": {12 "dependencies": {
@@ -28,7 +28,7 @@
28 "cors": "^2.8.5",28 "cors": "^2.8.5",
29 "csrf-sync": "^4.0.3",29 "csrf-sync": "^4.0.3",
30 "diff-match-patch": "^1.0.5",30 "diff-match-patch": "^1.0.5",
31 "dompurify": "^3.1.7",31 "dompurify": "^3.2.4",
32 "droll": "^0.2.1",32 "droll": "^0.2.1",
33 "express": "^4.21.0",33 "express": "^4.21.0",
34 "form-data": "^4.0.0",34 "form-data": "^4.0.0",
@@ -41,7 +41,9 @@
41 "html-entities": "^2.5.2",41 "html-entities": "^2.5.2",
42 "iconv-lite": "^0.6.3",42 "iconv-lite": "^0.6.3",
43 "ip-matching": "^2.1.2",43 "ip-matching": "^2.1.2",
44 "ip-regex": "^5.0.0",
44 "ipaddr.js": "^2.0.1",45 "ipaddr.js": "^2.0.1",
46 "is-docker": "^3.0.0",
45 "jimp": "^0.22.10",47 "jimp": "^0.22.10",
46 "localforage": "^1.10.0",48 "localforage": "^1.10.0",
47 "lodash": "^4.17.21",49 "lodash": "^4.17.21",
@@ -1462,6 +1464,13 @@
1462 "@types/jquery": "*"1464 "@types/jquery": "*"
1463 }1465 }
1464 },1466 },
1467 "node_modules/@types/trusted-types": {
1468 "version": "2.0.7",
1469 "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
1470 "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
1471 "license": "MIT",
1472 "optional": true
1473 },
1465 "node_modules/@types/write-file-atomic": {1474 "node_modules/@types/write-file-atomic": {
1466 "version": "4.0.3",1475 "version": "4.0.3",
1467 "resolved": "https://registry.npmjs.org/@types/write-file-atomic/-/write-file-atomic-4.0.3.tgz",1476 "resolved": "https://registry.npmjs.org/@types/write-file-atomic/-/write-file-atomic-4.0.3.tgz",
@@ -3217,10 +3226,13 @@
3217 }3226 }
3218 },3227 },
3219 "node_modules/dompurify": {3228 "node_modules/dompurify": {
3220 "version": "3.1.7",3229 "version": "3.2.4",
3221 "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.1.7.tgz",3230 "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.4.tgz",
3222 "integrity": "sha512-VaTstWtsneJY8xzy7DekmYWEOZcmzIe3Qb3zPd4STve1OBTa+e+WmS1ITQec1fZYXI3HCsOZZiSMpG6oxoWMWQ==",3231 "integrity": "sha512-ysFSFEDVduQpyhzAob/kkuJjf5zWkZD8/A9ywSp1byueyuCfHamrCBa14/Oc2iiB0e51B+NpxSl5gmzn+Ms/mg==",
3223 "license": "(MPL-2.0 OR Apache-2.0)"3232 "license": "(MPL-2.0 OR Apache-2.0)",
3233 "optionalDependencies": {
3234 "@types/trusted-types": "^2.0.7"
3235 }
3224 },3236 },
3225 "node_modules/domutils": {3237 "node_modules/domutils": {
3226 "version": "3.1.0",3238 "version": "3.1.0",
@@ -4610,6 +4622,18 @@
4610 "integrity": "sha512-/ok+VhKMasgR5gvTRViwRFQfc0qYt9Vdowg6TO4/pFlDCob5ZjGPkwuOoQVCd5OrMm20zqh+1vA8KLJZTeWudg==",4622 "integrity": "sha512-/ok+VhKMasgR5gvTRViwRFQfc0qYt9Vdowg6TO4/pFlDCob5ZjGPkwuOoQVCd5OrMm20zqh+1vA8KLJZTeWudg==",
4611 "license": "LGPL-3.0-only"4623 "license": "LGPL-3.0-only"
4612 },4624 },
4625 "node_modules/ip-regex": {
4626 "version": "5.0.0",
4627 "resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-5.0.0.tgz",
4628 "integrity": "sha512-fOCG6lhoKKakwv+C6KdsOnGvgXnmgfmp0myi3bcNwj3qfwPAxRKWEuFhvEFF7ceYIz6+1jRZ+yguLFAmUNPEfw==",
4629 "license": "MIT",
4630 "engines": {
4631 "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
4632 },
4633 "funding": {
4634 "url": "https://github.com/sponsors/sindresorhus"
4635 }
4636 },
4613 "node_modules/ipaddr.js": {4637 "node_modules/ipaddr.js": {
4614 "version": "2.1.0",4638 "version": "2.1.0",
4615 "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.1.0.tgz",4639 "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.1.0.tgz",
@@ -4626,15 +4650,15 @@
4626 "license": "MIT"4650 "license": "MIT"
4627 },4651 },
4628 "node_modules/is-docker": {4652 "node_modules/is-docker": {
4629 "version": "2.2.1",4653 "version": "3.0.0",
4630 "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz",4654 "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz",
4631 "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==",4655 "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==",
4632 "license": "MIT",4656 "license": "MIT",
4633 "bin": {4657 "bin": {
4634 "is-docker": "cli.js"4658 "is-docker": "cli.js"
4635 },4659 },
4636 "engines": {4660 "engines": {
4637 "node": ">=8"4661 "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
4638 },4662 },
4639 "funding": {4663 "funding": {
4640 "url": "https://github.com/sponsors/sindresorhus"4664 "url": "https://github.com/sponsors/sindresorhus"
@@ -4711,6 +4735,21 @@
4711 "node": ">=8"4735 "node": ">=8"
4712 }4736 }
4713 },4737 },
4738 "node_modules/is-wsl/node_modules/is-docker": {
4739 "version": "2.2.1",
4740 "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz",
4741 "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==",
4742 "license": "MIT",
4743 "bin": {
4744 "is-docker": "cli.js"
4745 },
4746 "engines": {
4747 "node": ">=8"
4748 },
4749 "funding": {
4750 "url": "https://github.com/sponsors/sindresorhus"
4751 }
4752 },
4714 "node_modules/isarray": {4753 "node_modules/isarray": {
4715 "version": "1.0.0",4754 "version": "1.0.0",
4716 "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",4755 "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
@@ -5495,6 +5534,21 @@
5495 "url": "https://github.com/sponsors/sindresorhus"5534 "url": "https://github.com/sponsors/sindresorhus"
5496 }5535 }
5497 },5536 },
5537 "node_modules/open/node_modules/is-docker": {
5538 "version": "2.2.1",
5539 "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz",
5540 "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==",
5541 "license": "MIT",
5542 "bin": {
5543 "is-docker": "cli.js"
5544 },
5545 "engines": {
5546 "node": ">=8"
5547 },
5548 "funding": {
5549 "url": "https://github.com/sponsors/sindresorhus"
5550 }
5551 },
5498 "node_modules/openai": {5552 "node_modules/openai": {
5499 "version": "4.17.4",5553 "version": "4.17.4",
5500 "resolved": "https://registry.npmjs.org/openai/-/openai-4.17.4.tgz",5554 "resolved": "https://registry.npmjs.org/openai/-/openai-4.17.4.tgz",
package.json+5 -2
@@ -18,7 +18,7 @@
18 "cors": "^2.8.5",18 "cors": "^2.8.5",
19 "csrf-sync": "^4.0.3",19 "csrf-sync": "^4.0.3",
20 "diff-match-patch": "^1.0.5",20 "diff-match-patch": "^1.0.5",
21 "dompurify": "^3.1.7",21 "dompurify": "^3.2.4",
22 "droll": "^0.2.1",22 "droll": "^0.2.1",
23 "express": "^4.21.0",23 "express": "^4.21.0",
24 "form-data": "^4.0.0",24 "form-data": "^4.0.0",
@@ -31,7 +31,9 @@
31 "html-entities": "^2.5.2",31 "html-entities": "^2.5.2",
32 "iconv-lite": "^0.6.3",32 "iconv-lite": "^0.6.3",
33 "ip-matching": "^2.1.2",33 "ip-matching": "^2.1.2",
34 "ip-regex": "^5.0.0",
34 "ipaddr.js": "^2.0.1",35 "ipaddr.js": "^2.0.1",
36 "is-docker": "^3.0.0",
35 "jimp": "^0.22.10",37 "jimp": "^0.22.10",
36 "localforage": "^1.10.0",38 "localforage": "^1.10.0",
37 "lodash": "^4.17.21",39 "lodash": "^4.17.21",
@@ -86,9 +88,10 @@
86 "type": "git",88 "type": "git",
87 "url": "https://github.com/SillyTavern/SillyTavern.git"89 "url": "https://github.com/SillyTavern/SillyTavern.git"
88 },90 },
89 "version": "1.12.11",91 "version": "1.12.12",
90 "scripts": {92 "scripts": {
91 "start": "node server.js",93 "start": "node server.js",
94 "debug": "node server.js --inspect",
92 "start:deno": "deno run --allow-run --allow-net --allow-read --allow-write --allow-sys --allow-env server.js",95 "start:deno": "deno run --allow-run --allow-net --allow-read --allow-write --allow-sys --allow-env server.js",
93 "start:bun": "bun server.js",96 "start:bun": "bun server.js",
94 "start:no-csrf": "node server.js --disableCsrf",97 "start:no-csrf": "node server.js --disableCsrf",
plugins.js+8 -1
@@ -8,7 +8,7 @@ import path from 'node:path';
8import process from 'node:process';8import process from 'node:process';
9import { fileURLToPath } from 'node:url';9import { fileURLToPath } from 'node:url';
1010
11import { default as git } from 'simple-git';11import { default as git, CheckRepoActions } from 'simple-git';
12import { color } from './src/util.js';12import { color } from './src/util.js';
1313
14const __dirname = import.meta.dirname ?? path.dirname(fileURLToPath(import.meta.url));14const __dirname = import.meta.dirname ?? path.dirname(fileURLToPath(import.meta.url));
@@ -48,6 +48,13 @@ async function updatePlugins() {
48 console.log(`Updating plugin ${color.green(directory)}...`);48 console.log(`Updating plugin ${color.green(directory)}...`);
49 const pluginPath = path.join(pluginsPath, directory);49 const pluginPath = path.join(pluginsPath, directory);
50 const pluginRepo = git(pluginPath);50 const pluginRepo = git(pluginPath);
51
52 const isRepo = await pluginRepo.checkIsRepo(CheckRepoActions.IS_REPO_ROOT);
53 if (!isRepo) {
54 console.log(`Directory ${color.yellow(directory)} is not a Git repository`);
55 continue;
56 }
57
51 await pluginRepo.fetch();58 await pluginRepo.fetch();
52 const commitHash = await pluginRepo.revparse(['HEAD']);59 const commitHash = await pluginRepo.revparse(['HEAD']);
53 const trackingBranch = await pluginRepo.revparse(['--abbrev-ref', '@{u}']);60 const trackingBranch = await pluginRepo.revparse(['--abbrev-ref', '@{u}']);
post-install.js+5 -0
@@ -104,6 +104,11 @@ const keyMigrationMap = [
104 newKey: 'extensions.models.textToSpeech',104 newKey: 'extensions.models.textToSpeech',
105 migrate: (value) => value,105 migrate: (value) => value,
106 },106 },
107 {
108 oldKey: 'minLogLevel',
109 newKey: 'logging.minLogLevel',
110 migrate: (value) => value,
111 },
107];112];
108113
109/**114/**
public/css/mobile-styles.css+0 -2
@@ -216,8 +216,6 @@
216216
217 }217 }
218218
219 #showRawPrompt,
220 #copyPromptToClipboard,
221 #groupCurrentMemberPopoutButton,219 #groupCurrentMemberPopoutButton,
222 #summaryExtensionPopoutButton {220 #summaryExtensionPopoutButton {
223 display: none;221 display: none;
public/css/popup.css+4 -0
@@ -72,6 +72,10 @@ dialog {
72 overflow-x: auto;72 overflow-x: auto;
73}73}
7474
75.popup.left_aligned_dialogue_popup .popup-content {
76 text-align: start;
77}
78
75/* Opening animation */79/* Opening animation */
76.popup[opening] {80.popup[opening] {
77 animation: pop-in var(--popup-animation-speed) ease-in-out;81 animation: pop-in var(--popup-animation-speed) ease-in-out;
public/css/select2-overrides.css+7 -0
@@ -100,6 +100,13 @@
100 border: 1px solid var(--SmartThemeBorderColor);100 border: 1px solid var(--SmartThemeBorderColor);
101}101}
102102
103.select2-container .select2-results .select2-results__option--disabled {
104 color: inherit;
105 background-color: inherit;
106 cursor: not-allowed;
107 filter: brightness(0.5);
108}
109
103.select2-container .select2-selection--multiple .select2-selection__choice,110.select2-container .select2-selection--multiple .select2-selection__choice,
104.select2-container .select2-selection--single .select2-selection__choice {111.select2-container .select2-selection--single .select2-selection__choice {
105 border-radius: 5px;112 border-radius: 5px;
public/css/toggle-dependent.css+9 -0
@@ -473,6 +473,11 @@ label[for="trim_spaces"]:has(input:checked) i.warning {
473 display: none;473 display: none;
474}474}
475475
476label[for="trim_spaces"]:not(:has(input:checked)) small {
477 color: var(--warning);
478 opacity: 1;
479}
480
476#claude_function_prefill_warning {481#claude_function_prefill_warning {
477 display: none;482 display: none;
478 color: red;483 color: red;
@@ -489,3 +494,7 @@ label[for="trim_spaces"]:has(input:checked) i.warning {
489#mistralai_other_models:empty {494#mistralai_other_models:empty {
490 display: none;495 display: none;
491}496}
497
498#banned_tokens_block_ooba:not(:has(#send_banned_tokens_textgenerationwebui:checked)) #banned_tokens_controls_ooba {
499 filter: brightness(0.5);
500}
public/global.d.ts+8 -0
@@ -40,4 +40,12 @@ declare global {
40 searchInputCssClass?: string;40 searchInputCssClass?: string;
41 }41 }
42 }42 }
43
44 /**
45 * Translates a text to a target language using a translation provider.
46 * @param text Text to translate
47 * @param lang Target language
48 * @param provider Translation provider
49 */
50 async function translate(text: string, lang: string, provider: string = null): Promise<string>;
43}51}
public/index.html+167 -111
@@ -730,7 +730,7 @@
730 <input type="range" id="top_k_openai" name="volume" min="0" max="500" step="1">730 <input type="range" id="top_k_openai" name="volume" min="0" max="500" step="1">
731 </div>731 </div>
732 <div class="range-block-counter">732 <div class="range-block-counter">
733 <input type="number" min="0" max="200" step="1" data-for="top_k_openai" id="top_k_counter_openai">733 <input type="number" min="0" max="500" step="1" data-for="top_k_openai" id="top_k_counter_openai">
734 </div>734 </div>
735 </div>735 </div>
736 </div>736 </div>
@@ -1587,6 +1587,10 @@
1587 <input type="checkbox" id="skip_special_tokens_textgenerationwebui" />1587 <input type="checkbox" id="skip_special_tokens_textgenerationwebui" />
1588 <small data-i18n="Skip Special Tokens">Skip Special Tokens</small>1588 <small data-i18n="Skip Special Tokens">Skip Special Tokens</small>
1589 </label>1589 </label>
1590 <label data-tg-type="openrouter" class="checkbox_label flexGrow flexShrink" for="include_reasoning_textgenerationwebui">
1591 <input type="checkbox" id="include_reasoning_textgenerationwebui" />
1592 <small data-i18n="Request Model Reasoning">Request Model Reasoning</small>
1593 </label>
1590 <label data-tg-type="ooba, aphrodite, tabby" class="checkbox_label flexGrow flexShrink" for="temperature_last_textgenerationwebui">1594 <label data-tg-type="ooba, aphrodite, tabby" class="checkbox_label flexGrow flexShrink" for="temperature_last_textgenerationwebui">
1591 <input type="checkbox" id="temperature_last_textgenerationwebui" />1595 <input type="checkbox" id="temperature_last_textgenerationwebui" />
1592 <label>1596 <label>
@@ -1617,17 +1621,34 @@
1617 </div>1621 </div>
1618 <div data-tg-type-mode="except" data-tg-type="generic" id="banned_tokens_block_ooba" class="wide100p">1622 <div data-tg-type-mode="except" data-tg-type="generic" id="banned_tokens_block_ooba" class="wide100p">
1619 <hr class="width100p">1623 <hr class="width100p">
1620 <h4 class="range-block-title justifyCenter">1624 <div class="range-block-title title_restorable">
1621 <span data-i18n="Banned Tokens">Banned Tokens/Strings</span>1625 <div>
1626 <strong data-i18n="Banned Tokens">Banned Tokens/Strings</strong>
1622 <div class="margin5 fa-solid fa-circle-info opacity50p " data-i18n="[title]LLaMA / Mistral / Yi models only" title="Enter sequences you don't want to appear in the output.&#13;Unquoted text will be tokenized in the back end and banned as tokens.&#13;[token ids] will be banned as-is.&#13;Most tokens have a leading space. Use token counter (with the correct tokenizer selected first!) if you are unsure.&#13;Enclose text in double quotes to ban the entire string as a set.&#13;Quoted Strings and [Token ids] must be on their own line."></div>1627 <div class="margin5 fa-solid fa-circle-info opacity50p " data-i18n="[title]LLaMA / Mistral / Yi models only" title="Enter sequences you don't want to appear in the output.&#13;Unquoted text will be tokenized in the back end and banned as tokens.&#13;[token ids] will be banned as-is.&#13;Most tokens have a leading space. Use token counter (with the correct tokenizer selected first!) if you are unsure.&#13;Enclose text in double quotes to ban the entire string as a set.&#13;Quoted Strings and [Token ids] must be on their own line."></div>
1623 </h4>1628 </div>
1629 <label id="send_banned_tokens_label" for="send_banned_tokens_textgenerationwebui" class="checkbox_label">
1630 <input id="send_banned_tokens_textgenerationwebui" type="checkbox" style="display:none;" />
1631 <small><i class="fa-solid fa-power-off menu_button togglable margin0"></i></small>
1632 </label>
1633 </div>
1634 <div id="banned_tokens_controls_ooba">
1635 <div class="textAlignCenter">
1636 <small data-i18n="Global list">Global list</small>
1637 </div>
1638 <div class="wide100p marginBot10">
1639 <textarea id="global_banned_tokens_textgenerationwebui" class="text_pole textarea_compact" name="global_banned_tokens_textgenerationwebui" rows="3" data-i18n="[placeholder]Example: some text [42, 69, 1337]" placeholder='some text as tokens&#10;[420, 69, 1337]&#10;"Some verbatim string"'></textarea>
1640 </div>
1641 <div class="textAlignCenter">
1642 <small data-i18n="Preset-specific list">Preset-specific list</small>
1643 </div>
1624 <div class="wide100p">1644 <div class="wide100p">
1625 <textarea id="banned_tokens_textgenerationwebui" class="text_pole textarea_compact" name="banned_tokens_textgenerationwebui" rows="3" data-i18n="[placeholder]Example: some text [42, 69, 1337]" placeholder='some text as tokens&#10;[420, 69, 1337]&#10;"Some verbatim string"'></textarea>1645 <textarea id="banned_tokens_textgenerationwebui" class="text_pole textarea_compact" name="banned_tokens_textgenerationwebui" rows="3" data-i18n="[placeholder]Example: some text [42, 69, 1337]" placeholder='some text as tokens&#10;[420, 69, 1337]&#10;"Some verbatim string"'></textarea>
1626 </div>1646 </div>
1627 </div>1647 </div>
1648 </div>
1628 <div class="range-block wide100p">1649 <div class="range-block wide100p">
1629 <div id="logit_bias_textgenerationwebui" class="range-block-title title_restorable">1650 <div id="logit_bias_textgenerationwebui" class="range-block-title title_restorable">
1630 <span data-i18n="Logit Bias">Logit Bias</span>1651 <strong data-i18n="Logit Bias">Logit Bias</strong>
1631 <div id="textgen_logit_bias_new_entry" class="menu_button menu_button_icon">1652 <div id="textgen_logit_bias_new_entry" class="menu_button menu_button_icon">
1632 <i class="fa-xs fa-solid fa-plus"></i>1653 <i class="fa-xs fa-solid fa-plus"></i>
1633 <small data-i18n="Add">Add</small>1654 <small data-i18n="Add">Add</small>
@@ -1930,7 +1951,7 @@
1930 </span>1951 </span>
1931 </div>1952 </div>
1932 </div>1953 </div>
1933 <div class="range-block" data-source="openai,cohere,mistralai,custom,claude,openrouter,groq">1954 <div class="range-block" data-source="openai,cohere,mistralai,custom,claude,openrouter,groq,deepseek">
1934 <label for="openai_function_calling" class="checkbox_label flexWrap widthFreeExpand">1955 <label for="openai_function_calling" class="checkbox_label flexWrap widthFreeExpand">
1935 <input id="openai_function_calling" type="checkbox" />1956 <input id="openai_function_calling" type="checkbox" />
1936 <span data-i18n="Enable function calling">Enable function calling</span>1957 <span data-i18n="Enable function calling">Enable function calling</span>
@@ -1953,6 +1974,7 @@
1953 <span data-i18n="image_inlining_hint_3">menu to attach an image file to the chat.</span>1974 <span data-i18n="image_inlining_hint_3">menu to attach an image file to the chat.</span>
1954 </div>1975 </div>
1955 <div class="flex-container flexFlowColumn wide100p textAlignCenter marginTop10" data-source="openai,custom">1976 <div class="flex-container flexFlowColumn wide100p textAlignCenter marginTop10" data-source="openai,custom">
1977 <div class="flex-container oneline-dropdown">
1956 <label for="openai_inline_image_quality" data-i18n="Inline Image Quality">1978 <label for="openai_inline_image_quality" data-i18n="Inline Image Quality">
1957 Inline Image Quality1979 Inline Image Quality
1958 </label>1980 </label>
@@ -1963,6 +1985,7 @@
1963 </select>1985 </select>
1964 </div>1986 </div>
1965 </div>1987 </div>
1988 </div>
1966 <div class="range-block" data-source="makersuite">1989 <div class="range-block" data-source="makersuite">
1967 <label for="use_makersuite_sysprompt" class="checkbox_label widthFreeExpand">1990 <label for="use_makersuite_sysprompt" class="checkbox_label widthFreeExpand">
1968 <input id="use_makersuite_sysprompt" type="checkbox" />1991 <input id="use_makersuite_sysprompt" type="checkbox" />
@@ -1977,20 +2000,32 @@
1977 </span>2000 </span>
1978 </div>2001 </div>
1979 </div>2002 </div>
1980 <div class="range-block" data-source="makersuite,deepseek,openrouter">2003 <div class="range-block" data-source="deepseek,openrouter,custom">
1981 <label for="openai_show_thoughts" class="checkbox_label widthFreeExpand">2004 <label for="openai_show_thoughts" class="checkbox_label widthFreeExpand">
1982 <input id="openai_show_thoughts" type="checkbox" />2005 <input id="openai_show_thoughts" type="checkbox" />
1983 <span>2006 <span>
1984 <span data-i18n="Show model reasoning">Show model reasoning</span>2007 <span data-i18n="Request model reasoning">Request model reasoning</span>
1985 <i class="opacity50p fa-solid fa-circle-info" title="Gemini 2.0 Thinking / DeepSeek Reasoner"></i>2008 <i class="opacity50p fa-solid fa-circle-info" title="DeepSeek Reasoner"></i>
1986 </span>2009 </span>
1987 </label>2010 </label>
1988 <div class="toggle-description justifyLeft marginBot5">2011 <div class="toggle-description justifyLeft marginBot5">
1989 <span data-i18n="Display the model's internal thoughts in the response.">2012 <span data-i18n="Allows the model to return its thinking process.">
1990 Display the model's internal thoughts in the response.2013 Allows the model to return its thinking process.
1991 </span>2014 </span>
1992 </div>2015 </div>
1993 </div>2016 </div>
2017 <div class="flex-container flexFlowColumn wide100p textAlignCenter marginTop10" data-source="openai,custom">
2018 <div class="flex-container oneline-dropdown" title="Constrains effort on reasoning for reasoning models.&#10;Currently supported values are low, medium, and high.&#10;Reducing reasoning effort can result in faster responses and fewer tokens used on reasoning in a response." data-i18n="[title]Constrains effort on reasoning for reasoning models.">
2019 <label for="openai_reasoning_effort" data-i18n="Reasoning Effort">
2020 Reasoning Effort
2021 </label>
2022 <select id="openai_reasoning_effort">
2023 <option data-i18n="openai_reasoning_effort_low" value="low">Low</option>
2024 <option data-i18n="openai_reasoning_effort_medium" value="medium">Medium</option>
2025 <option data-i18n="openai_reasoning_effort_high" value="high">High</option>
2026 </select>
2027 </div>
2028 </div>
1994 <div class="range-block" data-source="claude">2029 <div class="range-block" data-source="claude">
1995 <div class="wide100p">2030 <div class="wide100p">
1996 <div class="flex-container alignItemsCenter">2031 <div class="flex-container alignItemsCenter">
@@ -2805,27 +2840,6 @@
2805 <div>2840 <div>
2806 <h4 data-i18n="OpenAI Model">OpenAI Model</h4>2841 <h4 data-i18n="OpenAI Model">OpenAI Model</h4>
2807 <select id="model_openai_select">2842 <select id="model_openai_select">
2808 <optgroup label="GPT-3.5 Turbo">
2809 <option value="gpt-3.5-turbo">gpt-3.5-turbo</option>
2810 <option value="gpt-3.5-turbo-0125">gpt-3.5-turbo-0125 (2024)</option>
2811 <option value="gpt-3.5-turbo-1106">gpt-3.5-turbo-1106 (2023)</option>
2812 <option value="gpt-3.5-turbo-0613">gpt-3.5-turbo-0613 (2023)</option>
2813 <option value="gpt-3.5-turbo-0301">gpt-3.5-turbo-0301 (2023)</option>
2814 <option value="gpt-3.5-turbo-16k">gpt-3.5-turbo-16k</option>
2815 <option value="gpt-3.5-turbo-16k-0613">gpt-3.5-turbo-16k-0613 (2023)</option>
2816 </optgroup>
2817 <optgroup label="GPT-3.5 Turbo Instruct">
2818 <option value="gpt-3.5-turbo-instruct">gpt-3.5-turbo-instruct</option>
2819 <option value="gpt-3.5-turbo-instruct-0914">gpt-3.5-turbo-instruct-0914</option>
2820 </optgroup>
2821 <optgroup label="GPT-4">
2822 <option value="gpt-4">gpt-4</option>
2823 <option value="gpt-4-0613">gpt-4-0613 (2023)</option>
2824 <option value="gpt-4-0314">gpt-4-0314 (2023)</option>
2825 <option value="gpt-4-32k">gpt-4-32k</option>
2826 <option value="gpt-4-32k-0613">gpt-4-32k-0613 (2023)</option>
2827 <option value="gpt-4-32k-0314">gpt-4-32k-0314 (2023)</option>
2828 </optgroup>
2829 <optgroup label="GPT-4o">2843 <optgroup label="GPT-4o">
2830 <option value="gpt-4o">gpt-4o</option>2844 <option value="gpt-4o">gpt-4o</option>
2831 <option value="gpt-4o-2024-11-20">gpt-4o-2024-11-20</option>2845 <option value="gpt-4o-2024-11-20">gpt-4o-2024-11-20</option>
@@ -2833,29 +2847,44 @@
2833 <option value="gpt-4o-2024-05-13">gpt-4o-2024-05-13</option>2847 <option value="gpt-4o-2024-05-13">gpt-4o-2024-05-13</option>
2834 <option value="chatgpt-4o-latest">chatgpt-4o-latest</option>2848 <option value="chatgpt-4o-latest">chatgpt-4o-latest</option>
2835 </optgroup>2849 </optgroup>
2836 <optgroup label="gpt-4o-mini">2850 <optgroup label="GPT-4o mini">
2837 <option value="gpt-4o-mini">gpt-4o-mini</option>2851 <option value="gpt-4o-mini">gpt-4o-mini</option>
2838 <option value="gpt-4o-mini-2024-07-18">gpt-4o-mini-2024-07-18</option>2852 <option value="gpt-4o-2024-11-20">gpt-4o-2024-11-20</option>
2853 <option value="gpt-4o-2024-08-06">gpt-4o-2024-08-06</option>
2854 <option value="gpt-4o-2024-05-13">gpt-4o-2024-05-13</option>
2855 <option value="chatgpt-4o-latest">chatgpt-4o-latest</option>
2839 </optgroup>2856 </optgroup>
2840 <optgroup label="GPT-4 Turbo">2857 <optgroup label="o1 and o1-mini">
2858 <option value="o1">o1</option>
2859 <option value="o1-2024-12-17">o1-2024-12-17</option>
2860 <option value="o1-mini">o1-mini</option>
2861 <option value="o1-mini-2024-09-12">o1-mini-2024-09-12</option>
2862 <option value="o1-preview">o1-preview</option>
2863 <option value="o1-preview-2024-09-12">o1-preview-2024-09-12</option>
2864 </optgroup>
2865 <optgroup label="o3">
2866 <option value="o3-mini">o3-mini</option>
2867 <option value="o3-mini-2025-01-31">o3-mini-2025-01-31</option>
2868 </optgroup>
2869 <optgroup label="GPT-4 Turbo and GPT-4">
2841 <option value="gpt-4-turbo">gpt-4-turbo</option>2870 <option value="gpt-4-turbo">gpt-4-turbo</option>
2842 <option value="gpt-4-turbo-2024-04-09">gpt-4-turbo-2024-04-09</option>2871 <option value="gpt-4-turbo-2024-04-09">gpt-4-turbo-2024-04-09</option>
2843 <option value="gpt-4-turbo-preview">gpt-4-turbo-preview</option>2872 <option value="gpt-4-turbo-preview">gpt-4-turbo-preview</option>
2844 <option value="gpt-4-vision-preview">gpt-4-vision-preview</option>
2845 <option value="gpt-4-0125-preview">gpt-4-0125-preview (2024)</option>2873 <option value="gpt-4-0125-preview">gpt-4-0125-preview (2024)</option>
2846 <option value="gpt-4-1106-preview">gpt-4-1106-preview (2023)</option>2874 <option value="gpt-4-1106-preview">gpt-4-1106-preview (2023)</option>
2875 <option value="gpt-4">gpt-4</option>
2876 <option value="gpt-4-0613">gpt-4-0613 (2023)</option>
2877 <option value="gpt-4-0314">gpt-4-0314 (2023)</option>
2847 </optgroup>2878 </optgroup>
2848 <optgroup label="o1">2879 <optgroup label="GPT-3.5 Turbo">
2849 <option value="o1-preview">o1-preview</option>2880 <option value="gpt-3.5-turbo">gpt-3.5-turbo</option>
2850 <option value="o1-mini">o1-mini</option>2881 <option value="gpt-3.5-turbo-0125">gpt-3.5-turbo-0125 (2024)</option>
2882 <option value="gpt-3.5-turbo-1106">gpt-3.5-turbo-1106 (2023)</option>
2883 <option value="gpt-3.5-turbo-instruct">gpt-3.5-turbo-instruct</option>
2851 </optgroup>2884 </optgroup>
2852 <optgroup label="Other">2885 <optgroup label="Other">
2853 <option value="text-davinci-003">text-davinci-003</option>2886 <option value="babbage-002">babbage-002</option>
2854 <option value="text-davinci-002">text-davinci-002</option>2887 <option value="davinci-002">davinci-002</option>
2855 <option value="text-curie-001">text-curie-001</option>
2856 <option value="text-babbage-001">text-babbage-001</option>
2857 <option value="text-ada-001">text-ada-001</option>
2858 <option value="code-davinci-002">code-davinci-002</option>
2859 </optgroup>2888 </optgroup>
2860 <optgroup id="openai_external_category" label="External">2889 <optgroup id="openai_external_category" label="External">
2861 </optgroup>2890 </optgroup>
@@ -3054,6 +3083,7 @@
3054 <h4 data-i18n="Google Model">Google Model</h4>3083 <h4 data-i18n="Google Model">Google Model</h4>
3055 <select id="model_google_select">3084 <select id="model_google_select">
3056 <optgroup label="Primary">3085 <optgroup label="Primary">
3086 <option value="gemini-2.0-flash">Gemini 2.0 Flash</option>
3057 <option value="gemini-1.5-pro">Gemini 1.5 Pro</option>3087 <option value="gemini-1.5-pro">Gemini 1.5 Pro</option>
3058 <option value="gemini-1.5-flash">Gemini 1.5 Flash</option>3088 <option value="gemini-1.5-flash">Gemini 1.5 Flash</option>
3059 <option value="gemini-1.0-pro">Gemini 1.0 Pro (Deprecated)</option>3089 <option value="gemini-1.0-pro">Gemini 1.0 Pro (Deprecated)</option>
@@ -3062,6 +3092,11 @@
3062 <option value="gemini-1.0-ultra-latest">Gemini 1.0 Ultra</option>3092 <option value="gemini-1.0-ultra-latest">Gemini 1.0 Ultra</option>
3063 </optgroup>3093 </optgroup>
3064 <optgroup label="Subversions">3094 <optgroup label="Subversions">
3095 <option value="gemini-2.0-pro-exp">Gemini 2.0 Pro Experimental</option>
3096 <option value="gemini-2.0-pro-exp-02-05">Gemini 2.0 Pro Experimental 2025-02-05</option>
3097 <option value="gemini-2.0-flash-lite-preview">Gemini 2.0 Flash-Lite Preview</option>
3098 <option value="gemini-2.0-flash-lite-preview-02-05">Gemini 2.0 Flash-Lite Preview 2025-02-05</option>
3099 <option value="gemini-2.0-flash-001">Gemini 2.0 Flash [001]</option>
3065 <option value="gemini-2.0-flash-thinking-exp">Gemini 2.0 Flash Thinking Experimental</option>3100 <option value="gemini-2.0-flash-thinking-exp">Gemini 2.0 Flash Thinking Experimental</option>
3066 <option value="gemini-2.0-flash-thinking-exp-01-21">Gemini 2.0 Flash Thinking Experimental 2025-01-21</option>3101 <option value="gemini-2.0-flash-thinking-exp-01-21">Gemini 2.0 Flash Thinking Experimental 2025-01-21</option>
3067 <option value="gemini-2.0-flash-thinking-exp-1219">Gemini 2.0 Flash Thinking Experimental 2024-12-19</option>3102 <option value="gemini-2.0-flash-thinking-exp-1219">Gemini 2.0 Flash Thinking Experimental 2024-12-19</option>
@@ -3151,33 +3186,33 @@
3151 </div>3186 </div>
3152 <h4 data-i18n="Groq Model">Groq Model</h4>3187 <h4 data-i18n="Groq Model">Groq Model</h4>
3153 <select id="model_groq_select">3188 <select id="model_groq_select">
3154 <optgroup label="Llama 3.3">3189 <optgroup label="Alibaba Cloud">
3155 <option value="llama-3.3-70b-versatile">llama-3.3-70b-versatile</option>3190 <option value="qwen-2.5-32b">qwen-2.5-32b</option>
3191 <option value="qwen-2.5-coder-32b">qwen-2.5-coder-32b</option>
3192 </optgroup>
3193 <optgroup label="DeepSeek / Alibaba Cloud">
3194 <option value="deepseek-r1-distill-qwen-32b">deepseek-r1-distill-qwen-32b</option>
3195 </optgroup>
3196 <optgroup label="DeepSeek / Meta">
3197 <option value="deepseek-r1-distill-llama-70b">deepseek-r1-distill-llama-70b</option>
3198 </optgroup>
3199 <optgroup label="Google">
3200 <option value="gemma2-9b-it">gemma2-9b-it</option>
3156 </optgroup>3201 </optgroup>
3157 <optgroup label="Llama 3.2">3202 <optgroup label="Meta">
3203 <option value="llama-3.1-8b-instant">llama-3.1-8b-instant </option>
3204 <option value="llama-3.2-11b-vision-preview">llama-3.2-11b-vision-preview </option>
3158 <option value="llama-3.2-1b-preview">llama-3.2-1b-preview </option>3205 <option value="llama-3.2-1b-preview">llama-3.2-1b-preview </option>
3159 <option value="llama-3.2-3b-preview">llama-3.2-3b-preview </option>3206 <option value="llama-3.2-3b-preview">llama-3.2-3b-preview </option>
3160 <option value="llama-3.2-11b-vision-preview">llama-3.2-11b-vision-preview</option>
3161 <option value="llama-3.2-90b-vision-preview">llama-3.2-90b-vision-preview </option>3207 <option value="llama-3.2-90b-vision-preview">llama-3.2-90b-vision-preview </option>
3162 </optgroup>3208 <option value="llama-3.3-70b-specdec">llama-3.3-70b-specdec </option>
3163 <optgroup label="Llama 3.1">3209 <option value="llama-3.3-70b-versatile">llama-3.3-70b-versatile </option>
3164 <option value="llama-3.1-8b-instant">llama-3.1-8b-instant</option>3210 <option value="llama-guard-3-8b">llama-guard-3-8b </option>
3165 <option value="llama-3.1-70b-versatile">llama-3.1-70b-versatile</option>
3166 <option value="llama-3.1-405b-reasoning">llama-3.1-405b-reasoning</option>
3167 </optgroup>
3168 <optgroup label="Llama 3">
3169 <option value="llama3-groq-8b-8192-tool-use-preview">llama3-groq-8b-8192-tool-use-preview</option>
3170 <option value="llama3-groq-70b-8192-tool-use-preview">llama3-groq-70b-8192-tool-use-preview</option>
3171 <option value="llama3-8b-8192">llama3-8b-8192</option>
3172 <option value="llama3-70b-8192">llama3-70b-8192 </option>3211 <option value="llama3-70b-8192">llama3-70b-8192 </option>
3212 <option value="llama3-8b-8192">llama3-8b-8192 </option>
3173 </optgroup>3213 </optgroup>
3174 <optgroup label="Gemma">3214 <optgroup label="Mistral AI">
3175 <option value="gemma-7b-it">gemma-7b-it</option>
3176 <option value="gemma2-9b-it">gemma2-9b-it</option>
3177 </optgroup>
3178 <optgroup label="Other">
3179 <option value="mixtral-8x7b-32768">mixtral-8x7b-32768</option>3215 <option value="mixtral-8x7b-32768">mixtral-8x7b-32768</option>
3180 <option value="llava-v1.5-7b-4096-preview">llava-v1.5-7b-4096-preview</option>
3181 </optgroup>3216 </optgroup>
3182 </select>3217 </select>
3183 </div>3218 </div>
@@ -3227,32 +3262,23 @@
3227 <h4 data-i18n="Perplexity Model">Perplexity Model</h4>3262 <h4 data-i18n="Perplexity Model">Perplexity Model</h4>
3228 <select id="model_perplexity_select">3263 <select id="model_perplexity_select">
3229 <optgroup label="Perplexity Sonar Models">3264 <optgroup label="Perplexity Sonar Models">
3265 <option value="sonar">sonar</option>
3266 <option value="sonar-pro">sonar-pro</option>
3267 <option value="sonar-reasoning">sonar-reasoning</option>
3268 <option value="sonar-reasoning-pro">sonar-reasoning-pro</option>
3269 </optgroup>
3270 <optgroup label="Offline Models">
3271 <option value="r1-1776">r1-1776</option>
3272 </optgroup>
3273 <optgroup label="Deprecated Models">
3274 <!-- These are scheduled for deprecation after 2/22/2025 -->
3230 <option value="llama-3.1-sonar-small-128k-online">llama-3.1-sonar-small-128k-online</option>3275 <option value="llama-3.1-sonar-small-128k-online">llama-3.1-sonar-small-128k-online</option>
3231 <option value="llama-3.1-sonar-large-128k-online">llama-3.1-sonar-large-128k-online</option>3276 <option value="llama-3.1-sonar-large-128k-online">llama-3.1-sonar-large-128k-online</option>
3232 <option value="llama-3.1-sonar-huge-128k-online">llama-3.1-sonar-huge-128k-online</option>3277 <option value="llama-3.1-sonar-huge-128k-online">llama-3.1-sonar-huge-128k-online</option>
3233 </optgroup>3278 <!-- These are not listed on the site anymore -->
3234 <optgroup label="Perplexity Chat Models">
3235 <option value="llama-3.1-sonar-small-128k-chat">llama-3.1-sonar-small-128k-chat</option>3279 <option value="llama-3.1-sonar-small-128k-chat">llama-3.1-sonar-small-128k-chat</option>
3236 <option value="llama-3.1-sonar-large-128k-chat">llama-3.1-sonar-large-128k-chat</option>3280 <option value="llama-3.1-sonar-large-128k-chat">llama-3.1-sonar-large-128k-chat</option>
3237 </optgroup>3281 </optgroup>
3238 <optgroup label="Open-Source Models">
3239 <option value="llama-3.1-8b-instruct">llama-3.1-8b-instruct</option>
3240 <option value="llama-3.1-70b-instruct">llama-3.1-70b-instruct</option>
3241 </optgroup>
3242 <optgroup label="Deprecated Models">
3243 <option value="llama-3-sonar-small-32k-chat">llama-3-sonar-small-32k-chat</option>
3244 <option value="llama-3-sonar-small-32k-online">llama-3-sonar-small-32k-online</option>
3245 <option value="llama-3-sonar-large-32k-chat">llama-3-sonar-large-32k-chat</option>
3246 <option value="llama-3-sonar-large-32k-online">llama-3-sonar-large-32k-online</option>
3247 <option value="sonar-small-chat">sonar-small-chat</option>
3248 <option value="sonar-small-online">sonar-small-online</option>
3249 <option value="sonar-medium-chat">sonar-medium-chat</option>
3250 <option value="sonar-medium-online">sonar-medium-online</option>
3251 <option value="llama-3-8b-instruct">llama-3-8b-instruct</option>
3252 <option value="llama-3-70b-instruct">llama-3-70b-instruct</option>
3253 <option value="mistral-7b-instruct">mistral-7b-instruct (v0.2)</option>
3254 <option value="mixtral-8x7b-instruct">mixtral-8x7b-instruct</option>
3255 </optgroup>
3256 </select>3282 </select>
3257 </div>3283 </div>
3258 <form id="cohere_form" data-source="cohere" action="javascript:void(null);" method="post" enctype="multipart/form-data">3284 <form id="cohere_form" data-source="cohere" action="javascript:void(null);" method="post" enctype="multipart/form-data">
@@ -3524,7 +3550,7 @@
3524 </label>3550 </label>
3525 <label id="instruct_enabled_label"for="instruct_enabled" class="checkbox_label flex1" title="Enable Instruct Mode" data-i18n="[title]instruct_enabled">3551 <label id="instruct_enabled_label"for="instruct_enabled" class="checkbox_label flex1" title="Enable Instruct Mode" data-i18n="[title]instruct_enabled">
3526 <input id="instruct_enabled" type="checkbox" style="display:none;" />3552 <input id="instruct_enabled" type="checkbox" style="display:none;" />
3527 <small><i class="fa-solid fa-power-off menu_button margin0"></i></small>3553 <small><i class="fa-solid fa-power-off menu_button togglable margin0"></i></small>
3528 </label>3554 </label>
3529 </div>3555 </div>
3530 </h4>3556 </h4>
@@ -3702,7 +3728,7 @@
3702 <div class="flex-container">3728 <div class="flex-container">
3703 <label id="sysprompt_enabled_label" for="sysprompt_enabled" class="checkbox_label flex1" title="Enable System Prompt" data-i18n="[title]sysprompt_enabled">3729 <label id="sysprompt_enabled_label" for="sysprompt_enabled" class="checkbox_label flex1" title="Enable System Prompt" data-i18n="[title]sysprompt_enabled">
3704 <input id="sysprompt_enabled" type="checkbox" style="display:none;" />3730 <input id="sysprompt_enabled" type="checkbox" style="display:none;" />
3705 <small><i class="fa-solid fa-power-off menu_button margin0"></i></small>3731 <small><i class="fa-solid fa-power-off menu_button togglable margin0"></i></small>
3706 </label>3732 </label>
3707 </div>3733 </div>
3708 </h4>3734 </h4>
@@ -3756,8 +3782,8 @@
3756 </div>3782 </div>
3757 <label class="checkbox_label" for="custom_stopping_strings_macro">3783 <label class="checkbox_label" for="custom_stopping_strings_macro">
3758 <input id="custom_stopping_strings_macro" type="checkbox" checked>3784 <input id="custom_stopping_strings_macro" type="checkbox" checked>
3759 <small data-i18n="Replace Macro in Custom Stopping Strings">3785 <small data-i18n="Replace Macro in Stop Strings">
3760 Replace Macro in Custom Stopping Strings3786 Replace Macro in Stop Strings
3761 </small>3787 </small>
3762 </label>3788 </label>
3763 </div>3789 </div>
@@ -3804,12 +3830,42 @@
3804 <span data-i18n="Reasoning">Reasoning</span>3830 <span data-i18n="Reasoning">Reasoning</span>
3805 </h4>3831 </h4>
3806 <div>3832 <div>
3807 <label class="checkbox_label" for="reasoning_add_to_prompts" title="Add existing reasoning blocks to prompts. To add a new reasoning block, use the message edit menu." data-i18n="[title]reasoning_add_to_prompts">3833 <div class="flex-container alignItemsBaseline">
3834 <label class="checkbox_label flex1" for="reasoning_auto_parse" title="Automatically parse reasoning blocks from main content between the reasoning prefix/suffix. Both fields must be defined and non-empty." data-i18n="[title]reasoning_auto_parse">
3835 <input id="reasoning_auto_parse" type="checkbox" />
3836 <small data-i18n="Auto-Parse">
3837 Auto-Parse
3838 </small>
3839 </label>
3840 <label class="checkbox_label flex1" for="reasoning_auto_expand" title="Automatically expand reasoning blocks." data-i18n="[title]reasoning_auto_expand">
3841 <input id="reasoning_auto_expand" type="checkbox" />
3842 <small data-i18n="Auto-Expand">
3843 Auto-Expand
3844 </small>
3845 </label>
3846 <label class="checkbox_label flex1" for="reasoning_show_hidden" title="Show reasoning time for models with hidden reasoning." data-i18n="[title]reasoning_show_hidden">
3847 <input id="reasoning_show_hidden" type="checkbox" />
3848 <small data-i18n="Show Hidden">
3849 Show Hidden
3850 </small>
3851 </label>
3852 </div>
3853 <div class="flex-container alignItemsBaseline">
3854 <label class="checkbox_label flex1" for="reasoning_add_to_prompts" title="Add existing reasoning blocks to prompts. To add a new reasoning block, use the message edit menu." data-i18n="[title]reasoning_add_to_prompts">
3808 <input id="reasoning_add_to_prompts" type="checkbox" />3855 <input id="reasoning_add_to_prompts" type="checkbox" />
3809 <small data-i18n="Add Reasoning to Prompts">3856 <small data-i18n="Add to Prompts">
3810 Add Reasoning to Prompts3857 Add to Prompts
3811 </small>3858 </small>
3812 </label>3859 </label>
3860 <div class="flex1 flex-container alignItemsBaseline" title="Maximum number of reasoning blocks to be added per prompt, counting from the last message." data-i18n="[title]reasoning_max_additions">
3861 <input id="reasoning_max_additions" class="text_pole textarea_compact widthUnset" type="number" min="0" max="999"></textarea>
3862 <small data-i18n="Max">Max</small>
3863 </div>
3864 </div>
3865 <details>
3866 <summary data-i18n="Reasoning Formatting">
3867 Reasoning Formatting
3868 </summary>
3813 <div class="flex-container">3869 <div class="flex-container">
3814 <div class="flex1" title="Inserted before the reasoning content." data-i18n="[title]reasoning_prefix">3870 <div class="flex1" title="Inserted before the reasoning content." data-i18n="[title]reasoning_prefix">
3815 <small data-i18n="Prefix">Prefix</small>3871 <small data-i18n="Prefix">Prefix</small>
@@ -3825,11 +3881,8 @@
3825 <small data-i18n="Separator">Separator</small>3881 <small data-i18n="Separator">Separator</small>
3826 <textarea id="reasoning_separator" class="text_pole textarea_compact autoSetHeight"></textarea>3882 <textarea id="reasoning_separator" class="text_pole textarea_compact autoSetHeight"></textarea>
3827 </div>3883 </div>
3828 <div class="flex1" title="Maximum number of reasoning blocks to be added per prompt, counting from the last message." data-i18n="[title]reasoning_max_additions">
3829 <small data-i18n="Max Additions">Max Additions</small>
3830 <input id="reasoning_max_additions" class="text_pole textarea_compact" type="number" min="0" max="999"></textarea>
3831 </div>
3832 </div>3884 </div>
3885 </details>
3833 </div>3886 </div>
3834 </div>3887 </div>
3835 <div>3888 <div>
@@ -3964,7 +4017,7 @@
3964 <div class="alignitemscenter flex-container flexFlowColumn flexGrow flexShrink gap0 flexBasis48p" title="Cap the number of entry activation recursions" data-i18n="[title]Cap the number of entry activation recursions">4017 <div class="alignitemscenter flex-container flexFlowColumn flexGrow flexShrink gap0 flexBasis48p" title="Cap the number of entry activation recursions" data-i18n="[title]Cap the number of entry activation recursions">
3965 <small>4018 <small>
3966 <span data-i18n="Max Recursion Steps">Max Recursion Steps</span>4019 <span data-i18n="Max Recursion Steps">Max Recursion Steps</span>
3967 <div class="fa-solid fa-triangle-exclamation opacity50p" data-i18n="[title]0 = unlimited, 1 = scans once and doesn't recurse, 2 = scans once and recurses once, etc\n(disabled when min activations are used)" title="0 = unlimited, 1 = scans once and doesn't recurse, 2 = scans once and recurses once, etc&#10;(disabled when min activations are used)"></div>4020 <div class="fa-solid fa-triangle-exclamation opacity50p" data-i18n="[title]0 = unlimited, 1 = scans once and doesn't recurse, 2 = scans once and recurses once, etc" title="0 = unlimited, 1 = scans once and doesn't recurse, 2 = scans once and recurses once, etc&#10;(disabled when min activations are used)"></div>
3968 </small>4021 </small>
3969 <input class="neo-range-slider" type="range" id="world_info_max_recursion_steps" name="world_info_max_recursion_steps" min="0" max="10" step="1">4022 <input class="neo-range-slider" type="range" id="world_info_max_recursion_steps" name="world_info_max_recursion_steps" min="0" max="10" step="1">
3970 <input class="neo-range-input" type="number" min="0" max="10" step="1" data-for="world_info_max_recursion_steps" id="world_info_max_recursion_steps_counter">4023 <input class="neo-range-input" type="number" min="0" max="10" step="1" data-for="world_info_max_recursion_steps" id="world_info_max_recursion_steps_counter">
@@ -4629,7 +4682,7 @@
4629 <small data-i18n="Enabled">Enabled</small>4682 <small data-i18n="Enabled">Enabled</small>
4630 </label>4683 </label>
4631 <small data-i18n="Minimum generated message length">Minimum generated message length</small>4684 <small data-i18n="Minimum generated message length">Minimum generated message length</small>
4632 <input id="auto_swipe_minimum_length" name="auto_swipe_minimum_length" type="number" min="0" step="1" value="0" class="text_pole" title="If the generated message is shorter than this, trigger an auto-swipe." data-i18n="[title]If the generated message is shorter than this, trigger an auto-swipe">4685 <input id="auto_swipe_minimum_length" name="auto_swipe_minimum_length" type="number" min="0" step="1" value="0" class="text_pole" title="If the generated message is shorter than these many characters, trigger an auto-swipe." data-i18n="[title]If the generated message is shorter than these many characters, trigger an auto-swipe">
4633 <small data-i18n="Blacklisted words">Blacklisted words</small>4686 <small data-i18n="Blacklisted words">Blacklisted words</small>
4634 <div class="auto_swipe">4687 <div class="auto_swipe">
4635 <textarea id="auto_swipe_blacklist" name="auto_swipe_blacklist" data-i18n="[placeholder]words you dont want generated separated by comma ','" placeholder="words you don't want generated separated by comma ','" class="text_pole textarea_compact" value="" autocomplete="off" rows="3"></textarea>4688 <textarea id="auto_swipe_blacklist" name="auto_swipe_blacklist" data-i18n="[placeholder]words you dont want generated separated by comma ','" placeholder="words you don't want generated separated by comma ','" class="text_pole textarea_compact" value="" autocomplete="off" rows="3"></textarea>
@@ -4845,6 +4898,7 @@
4845 </div>4898 </div>
4846 <div id="extensions_settings" class="flex1 wide50p">4899 <div id="extensions_settings" class="flex1 wide50p">
4847 <div id="assets_container" class="extension_container"></div>4900 <div id="assets_container" class="extension_container"></div>
4901 <div id="typing_indicator_container" class="extension_container"></div>
4848 <div id="expressions_container" class="extension_container"></div>4902 <div id="expressions_container" class="extension_container"></div>
4849 <div id="sd_container" class="extension_container"></div>4903 <div id="sd_container" class="extension_container"></div>
4850 <div id="tts_container" class="extension_container"></div>4904 <div id="tts_container" class="extension_container"></div>
@@ -5880,7 +5934,7 @@
5880 <div class="inline-drawer-content flex-container paddingBottom5px wide100p">5934 <div class="inline-drawer-content flex-container paddingBottom5px wide100p">
5881 <div class="flex-container wide100p alignitemscenter">5935 <div class="flex-container wide100p alignitemscenter">
5882 <div name="keywordsAndLogicBlock" class="flex-container wide100p alignitemscenter">5936 <div name="keywordsAndLogicBlock" class="flex-container wide100p alignitemscenter">
5883 <div class="world_entry_form_control flex1">5937 <div class="world_entry_form_control keyprimary flex1">
5884 <small class="displayNone">5938 <small class="displayNone">
5885 <span data-i18n="Comma separated (required)">5939 <span data-i18n="Comma separated (required)">
5886 Comma separated (required)5940 Comma separated (required)
@@ -6302,14 +6356,19 @@
6302 </div>6356 </div>
6303 </div>6357 </div>
6304 <details class="mes_reasoning_details">6358 <details class="mes_reasoning_details">
6305 <summary class="mes_reasoning_summary">6359 <summary class="mes_reasoning_summary flex-container">
6306 <span data-i18n="Reasoning">Reasoning</span>6360 <div class="mes_reasoning_header_block flex-container">
6307 <div class="mes_reasoning_actions">6361 <div class="mes_reasoning_header flex-container">
6308 <div class="mes_reasoning_edit_done mes_button fa-solid fa-check" title="Confirm" data-i18n="[title]Confirmedit"></div>6362 <span class="mes_reasoning_header_title" data-i18n="Thought for some time">Thought for some time</span>
6309 <div class="mes_reasoning_edit_cancel mes_button fa-solid fa-xmark" title="Cancel edit" data-i18n="[title]Cancel edit"></div>6363 <div class="mes_reasoning_arrow fa-solid fa-chevron-up"></div>
6310 <div class="mes_reasoning_edit mes_button fa-solid fa-pencil" title="Edit reasoning" data-i18n="[title]Edit reasoning"></div>6364 </div>
6365 </div>
6366 <div class="mes_reasoning_actions flex-container">
6367 <div class="mes_reasoning_edit_done menu_button edit_button fa-solid fa-check" title="Confirm" data-i18n="[title]Confirmedit"></div>
6368 <div class="mes_reasoning_delete menu_button edit_button fa-solid fa-trash-can" title="Remove reasoning" data-i18n="[title]Remove reasoning"></div>
6369 <div class="mes_reasoning_edit_cancel menu_button edit_button fa-solid fa-xmark" title="Cancel edit" data-i18n="[title]Cancel edit"></div>
6311 <div class="mes_reasoning_copy mes_button fa-solid fa-copy" title="Copy reasoning" data-i18n="[title]Copy reasoning"></div>6370 <div class="mes_reasoning_copy mes_button fa-solid fa-copy" title="Copy reasoning" data-i18n="[title]Copy reasoning"></div>
6312 <div class="mes_reasoning_delete mes_button fa-solid fa-trash-can" title="Remove reasoning" data-i18n="[title]Remove reasoning"></div>6371 <div class="mes_reasoning_edit mes_button fa-solid fa-pencil" title="Edit reasoning" data-i18n="[title]Edit reasoning"></div>
6313 </div>6372 </div>
6314 </summary>6373 </summary>
6315 <div class="mes_reasoning"></div>6374 <div class="mes_reasoning"></div>
@@ -6528,9 +6587,6 @@
6528 </div>6587 </div>
65296588
6530 <!-- chat and input bar -->6589 <!-- chat and input bar -->
6531 <div id="typing_indicator_template" class="template_element">
6532 <div class="typing_indicator"><span class="typing_indicator_name">CHAR</span> is typing</div>
6533 </div>
6534 <div id="message_file_template" class="template_element">6590 <div id="message_file_template" class="template_element">
6535 <div class="mes_file_container">6591 <div class="mes_file_container">
6536 <div class="fa-lg fa-solid fa-file-alt mes_file_icon"></div>6592 <div class="fa-lg fa-solid fa-file-alt mes_file_icon"></div>
@@ -6890,8 +6946,8 @@
6890 </div>6946 </div>
6891 <div id="form_sheld">6947 <div id="form_sheld">
6892 <div id="dialogue_del_mes">6948 <div id="dialogue_del_mes">
6893 <div id="dialogue_del_mes_ok" class="menu_button">Delete</div>6949 <div id="dialogue_del_mes_ok" data-i18n="Delete" class="menu_button">Delete</div>
6894 <div id="dialogue_del_mes_cancel" class="menu_button">Cancel</div>6950 <div id="dialogue_del_mes_cancel" data-i18n="Cancel" class="menu_button">Cancel</div>
6895 </div>6951 </div>
6896 <div id="send_form" class="no-connection">6952 <div id="send_form" class="no-connection">
6897 <form id="file_form" class="wide100p displayNone">6953 <form id="file_form" class="wide100p displayNone">
public/lib/eventemitter.js+42 -1
@@ -24,10 +24,22 @@ if (typeof Array.prototype.indexOf === 'function') {
2424
2525
26/* Polyfill EventEmitter. */26/* Polyfill EventEmitter. */
27var EventEmitter = function () {27/**
28 * Creates an event emitter.
29 * @param {string[]} autoFireAfterEmit Auto-fire event names
30 */
31var EventEmitter = function (autoFireAfterEmit = []) {
28 this.events = {};32 this.events = {};
33 this.autoFireLastArgs = new Map();
34 this.autoFireAfterEmit = new Set(autoFireAfterEmit);
29};35};
3036
37/**
38 * Adds a listener to an event.
39 * @param {string} event Event name
40 * @param {function} listener Event listener
41 * @returns
42 */
31EventEmitter.prototype.on = function (event, listener) {43EventEmitter.prototype.on = function (event, listener) {
32 // Unknown event used by external libraries?44 // Unknown event used by external libraries?
33 if (event === undefined) {45 if (event === undefined) {
@@ -40,6 +52,10 @@ EventEmitter.prototype.on = function (event, listener) {
40 }52 }
4153
42 this.events[event].push(listener);54 this.events[event].push(listener);
55
56 if (this.autoFireAfterEmit.has(event) && this.autoFireLastArgs.has(event)) {
57 listener.apply(this, this.autoFireLastArgs.get(event));
58 }
43};59};
4460
45/**61/**
@@ -60,6 +76,10 @@ EventEmitter.prototype.makeLast = function (event, listener) {
60 }76 }
6177
62 events.push(listener);78 events.push(listener);
79
80 if (this.autoFireAfterEmit.has(event) && this.autoFireLastArgs.has(event)) {
81 listener.apply(this, this.autoFireLastArgs.get(event));
82 }
63}83}
6484
65/**85/**
@@ -80,8 +100,17 @@ EventEmitter.prototype.makeFirst = function (event, listener) {
80 }100 }
81101
82 events.unshift(listener);102 events.unshift(listener);
103
104 if (this.autoFireAfterEmit.has(event) && this.autoFireLastArgs.has(event)) {
105 listener.apply(this, this.autoFireLastArgs.get(event));
106 }
83}107}
84108
109/**
110 * Removes a listener from an event.
111 * @param {string} event Event name
112 * @param {function} listener Event listener
113 */
85EventEmitter.prototype.removeListener = function (event, listener) {114EventEmitter.prototype.removeListener = function (event, listener) {
86 var idx;115 var idx;
87116
@@ -94,6 +123,10 @@ EventEmitter.prototype.removeListener = function (event, listener) {
94 }123 }
95};124};
96125
126/**
127 * Emits an event with optional arguments.
128 * @param {string} event Event name
129 */
97EventEmitter.prototype.emit = async function (event) {130EventEmitter.prototype.emit = async function (event) {
98 let args = [].slice.call(arguments, 1);131 let args = [].slice.call(arguments, 1);
99 if (localStorage.getItem('eventTracing') === 'true') {132 if (localStorage.getItem('eventTracing') === 'true') {
@@ -118,6 +151,10 @@ EventEmitter.prototype.emit = async function (event) {
118 }151 }
119 }152 }
120 }153 }
154
155 if (this.autoFireAfterEmit.has(event)) {
156 this.autoFireLastArgs.set(event, args);
157 }
121};158};
122159
123EventEmitter.prototype.emitAndWait = function (event) {160EventEmitter.prototype.emitAndWait = function (event) {
@@ -144,6 +181,10 @@ EventEmitter.prototype.emitAndWait = function (event) {
144 }181 }
145 }182 }
146 }183 }
184
185 if (this.autoFireAfterEmit.has(event)) {
186 this.autoFireLastArgs.set(event, args);
187 }
147};188};
148189
149EventEmitter.prototype.once = function (event, listener) {190EventEmitter.prototype.once = function (event, listener) {
public/locales/ar-sa.json+2 -2
@@ -482,7 +482,7 @@
482 "separate with commas w/o space between": "فصل بفواصل دون مسافة بينها",482 "separate with commas w/o space between": "فصل بفواصل دون مسافة بينها",
483 "Custom Stopping Strings": "سلاسل توقف مخصصة",483 "Custom Stopping Strings": "سلاسل توقف مخصصة",
484 "JSON serialized array of strings": "مصفوفة سلسلة JSON متسلسلة",484 "JSON serialized array of strings": "مصفوفة سلسلة JSON متسلسلة",
485 "Replace Macro in Custom Stopping Strings": "استبدال الماكرو في سلاسل التوقف المخصصة",485 "Replace Macro in Stop Strings": "استبدال الماكرو في سلاسل التوقف المخصصة",
486 "Auto-Continue": "المتابعة التلقائية",486 "Auto-Continue": "المتابعة التلقائية",
487 "Allow for Chat Completion APIs": "السماح بواجهات برمجة التطبيقات لإكمال الدردشة",487 "Allow for Chat Completion APIs": "السماح بواجهات برمجة التطبيقات لإكمال الدردشة",
488 "Target length (tokens)": "الطول المستهدف (رموز)",488 "Target length (tokens)": "الطول المستهدف (رموز)",
@@ -709,7 +709,7 @@
709 "Auto-swipe": "السحب التلقائي",709 "Auto-swipe": "السحب التلقائي",
710 "Enable the auto-swipe function. Settings in this section only have an effect when auto-swipe is enabled": "تمكين وظيفة السحب التلقائي. الإعدادات في هذا القسم تؤثر فقط عند تمكين السحب التلقائي",710 "Enable the auto-swipe function. Settings in this section only have an effect when auto-swipe is enabled": "تمكين وظيفة السحب التلقائي. الإعدادات في هذا القسم تؤثر فقط عند تمكين السحب التلقائي",
711 "Minimum generated message length": "الحد الأدنى لطول الرسالة المولدة",711 "Minimum generated message length": "الحد الأدنى لطول الرسالة المولدة",
712 "If the generated message is shorter than this, trigger an auto-swipe": "إذا كانت الرسالة المولدة أقصر من هذا، فتحريض السحب التلقائي",712 "If the generated message is shorter than these many characters, trigger an auto-swipe": "إذا كانت الرسالة المولدة أقصر من هذا، فتحريض السحب التلقائي",
713 "Blacklisted words": "الكلمات الممنوعة",713 "Blacklisted words": "الكلمات الممنوعة",
714 "words you dont want generated separated by comma ','": "الكلمات التي لا تريد توليدها مفصولة بفاصلة ','",714 "words you dont want generated separated by comma ','": "الكلمات التي لا تريد توليدها مفصولة بفاصلة ','",
715 "Blacklisted word count to swipe": "عدد الكلمات الممنوعة للسحب",715 "Blacklisted word count to swipe": "عدد الكلمات الممنوعة للسحب",
public/locales/de-de.json+2 -2
@@ -482,7 +482,7 @@
482 "separate with commas w/o space between": "getrennt durch Kommas ohne Leerzeichen dazwischen",482 "separate with commas w/o space between": "getrennt durch Kommas ohne Leerzeichen dazwischen",
483 "Custom Stopping Strings": "Benutzerdefinierte Stoppzeichenfolgen",483 "Custom Stopping Strings": "Benutzerdefinierte Stoppzeichenfolgen",
484 "JSON serialized array of strings": "JSON serialisierte Reihe von Zeichenfolgen",484 "JSON serialized array of strings": "JSON serialisierte Reihe von Zeichenfolgen",
485 "Replace Macro in Custom Stopping Strings": "Makro in benutzerdefinierten Stoppzeichenfolgen ersetzen",485 "Replace Macro in Stop Strings": "Makro in benutzerdefinierten Stoppzeichenfolgen ersetzen",
486 "Auto-Continue": "Automatisch fortsetzen",486 "Auto-Continue": "Automatisch fortsetzen",
487 "Allow for Chat Completion APIs": "Erlaube Chat-Vervollständigungs-APIs",487 "Allow for Chat Completion APIs": "Erlaube Chat-Vervollständigungs-APIs",
488 "Target length (tokens)": "Ziel-Länge (Tokens)",488 "Target length (tokens)": "Ziel-Länge (Tokens)",
@@ -709,7 +709,7 @@
709 "Auto-swipe": "Automatisches Wischen",709 "Auto-swipe": "Automatisches Wischen",
710 "Enable the auto-swipe function. Settings in this section only have an effect when auto-swipe is enabled": "Aktiviere die Auto-Wisch-Funktion. Einstellungen in diesem Abschnitt haben nur dann Auswirkungen, wenn das automatische Wischen aktiviert ist",710 "Enable the auto-swipe function. Settings in this section only have an effect when auto-swipe is enabled": "Aktiviere die Auto-Wisch-Funktion. Einstellungen in diesem Abschnitt haben nur dann Auswirkungen, wenn das automatische Wischen aktiviert ist",
711 "Minimum generated message length": "Minimale generierte Nachrichtenlänge",711 "Minimum generated message length": "Minimale generierte Nachrichtenlänge",
712 "If the generated message is shorter than this, trigger an auto-swipe": "Wenn die generierte Nachricht kürzer ist als diese, löse automatisches Wischen aus",712 "If the generated message is shorter than these many characters, trigger an auto-swipe": "Wenn die generierte Nachricht kürzer ist als diese, löse automatisches Wischen aus",
713 "Blacklisted words": "Verbotene Wörter",713 "Blacklisted words": "Verbotene Wörter",
714 "words you dont want generated separated by comma ','": "Wörter, die du nicht generiert haben möchtest, durch Komma ',' getrennt",714 "words you dont want generated separated by comma ','": "Wörter, die du nicht generiert haben möchtest, durch Komma ',' getrennt",
715 "Blacklisted word count to swipe": "Anzahl der verbotenen Wörter, um zu wischen",715 "Blacklisted word count to swipe": "Anzahl der verbotenen Wörter, um zu wischen",
public/locales/es-es.json+2 -2
@@ -482,7 +482,7 @@
482 "separate with commas w/o space between": "separe con comas sin espacio entre ellas",482 "separate with commas w/o space between": "separe con comas sin espacio entre ellas",
483 "Custom Stopping Strings": "Cadenas de Detención Personalizadas",483 "Custom Stopping Strings": "Cadenas de Detención Personalizadas",
484 "JSON serialized array of strings": "Arreglo de cadenas serializado en JSON",484 "JSON serialized array of strings": "Arreglo de cadenas serializado en JSON",
485 "Replace Macro in Custom Stopping Strings": "Reemplazar macro en Cadenas de Detención Personalizadas",485 "Replace Macro in Stop Strings": "Reemplazar macro en Cadenas de Detención Personalizadas",
486 "Auto-Continue": "Autocontinuar",486 "Auto-Continue": "Autocontinuar",
487 "Allow for Chat Completion APIs": "Permitir para APIs de Completado de Chat",487 "Allow for Chat Completion APIs": "Permitir para APIs de Completado de Chat",
488 "Target length (tokens)": "Longitud objetivo (tokens)",488 "Target length (tokens)": "Longitud objetivo (tokens)",
@@ -709,7 +709,7 @@
709 "Auto-swipe": "Deslizamiento automático",709 "Auto-swipe": "Deslizamiento automático",
710 "Enable the auto-swipe function. Settings in this section only have an effect when auto-swipe is enabled": "Habilitar la función de deslizamiento automático. La configuración en esta sección solo tiene efecto cuando el deslizamiento automático está habilitado",710 "Enable the auto-swipe function. Settings in this section only have an effect when auto-swipe is enabled": "Habilitar la función de deslizamiento automático. La configuración en esta sección solo tiene efecto cuando el deslizamiento automático está habilitado",
711 "Minimum generated message length": "Longitud mínima del mensaje generado",711 "Minimum generated message length": "Longitud mínima del mensaje generado",
712 "If the generated message is shorter than this, trigger an auto-swipe": "Si el mensaje generado es más corto que esto, activar un deslizamiento automático",712 "If the generated message is shorter than these many characters, trigger an auto-swipe": "Si el mensaje generado es más corto que esto, activar un deslizamiento automático",
713 "Blacklisted words": "Palabras prohibidas",713 "Blacklisted words": "Palabras prohibidas",
714 "words you dont want generated separated by comma ','": "palabras que no desea generar separadas por coma ','",714 "words you dont want generated separated by comma ','": "palabras que no desea generar separadas por coma ','",
715 "Blacklisted word count to swipe": "Número de palabras prohibidas para deslizar",715 "Blacklisted word count to swipe": "Número de palabras prohibidas para deslizar",
public/locales/fr-fr.json+5 -6
@@ -434,7 +434,7 @@
434 "Non-markdown strings": "Chaînes non Markdown",434 "Non-markdown strings": "Chaînes non Markdown",
435 "Custom Stopping Strings": "Chaînes d'arrêt personnalisées",435 "Custom Stopping Strings": "Chaînes d'arrêt personnalisées",
436 "JSON serialized array of strings": "Tableau de chaînes sérialisé JSON",436 "JSON serialized array of strings": "Tableau de chaînes sérialisé JSON",
437 "Replace Macro in Custom Stopping Strings": "Remplacer les macro dans les chaînes d'arrêt personnalisées",437 "Replace Macro in Stop Strings": "Remplacer les macro dans les chaînes d'arrêt personnalisées",
438 "Auto-Continue": "Auto-Continue",438 "Auto-Continue": "Auto-Continue",
439 "Allow for Chat Completion APIs": "Autoriser les APIs de complétion de chat",439 "Allow for Chat Completion APIs": "Autoriser les APIs de complétion de chat",
440 "Target length (tokens)": "Longueur cible (tokens)",440 "Target length (tokens)": "Longueur cible (tokens)",
@@ -656,7 +656,7 @@
656 "Auto-swipe": "Balayage automatique",656 "Auto-swipe": "Balayage automatique",
657 "Enable the auto-swipe function. Settings in this section only have an effect when auto-swipe is enabled": "Activer la fonction de balayage automatique. Les paramètres de cette section n'ont d'effet que lorsque le balayage automatique est activé",657 "Enable the auto-swipe function. Settings in this section only have an effect when auto-swipe is enabled": "Activer la fonction de balayage automatique. Les paramètres de cette section n'ont d'effet que lorsque le balayage automatique est activé",
658 "Minimum generated message length": "Longueur minimale du message généré",658 "Minimum generated message length": "Longueur minimale du message généré",
659 "If the generated message is shorter than this, trigger an auto-swipe": "Si le message généré est plus court que cela, déclenchez un balayage automatique",659 "If the generated message is shorter than these many characters, trigger an auto-swipe": "Si le message généré est plus court que cela, déclenchez un balayage automatique",
660 "Blacklisted words": "Mots en liste noire",660 "Blacklisted words": "Mots en liste noire",
661 "words you dont want generated separated by comma ','": "mots que vous ne voulez pas générer séparés par des virgules ','",661 "words you dont want generated separated by comma ','": "mots que vous ne voulez pas générer séparés par des virgules ','",
662 "Blacklisted word count to swipe": "Nombre de mots en liste noire pour balayer",662 "Blacklisted word count to swipe": "Nombre de mots en liste noire pour balayer",
@@ -1385,8 +1385,8 @@
1385 "enable_functions_desc_1": "Autorise l'utilisation",1385 "enable_functions_desc_1": "Autorise l'utilisation",
1386 "enable_functions_desc_2": "outils de fonction",1386 "enable_functions_desc_2": "outils de fonction",
1387 "enable_functions_desc_3": "Peut être utilisé par diverses extensions pour fournir des fonctionnalités supplémentaires.",1387 "enable_functions_desc_3": "Peut être utilisé par diverses extensions pour fournir des fonctionnalités supplémentaires.",
1388 "Show model reasoning": "Afficher les pensées du modèle",1388 "Request model reasoning": "Demander les pensées du modèle",
1389 "Display the model's internal thoughts in the response.": "Afficher les pensées internes du modèle dans la réponse.",1389 "Allows the model to return its thinking process.": "Permet au modèle de retourner son processus de réflexion.",
1390 "Confirm token parsing with": "Confirmer l'analyse des tokens avec",1390 "Confirm token parsing with": "Confirmer l'analyse des tokens avec",
1391 "openai_logit_bias_no_items": "Aucun élément",1391 "openai_logit_bias_no_items": "Aucun élément",
1392 "api_no_connection": "Pas de connection...",1392 "api_no_connection": "Pas de connection...",
@@ -1485,7 +1485,7 @@
1485 "(disabled when max recursion steps are used)": "(désactivé lorsque le nombre maximum de pas de récursivité est utilisé)",1485 "(disabled when max recursion steps are used)": "(désactivé lorsque le nombre maximum de pas de récursivité est utilisé)",
1486 "Cap the number of entry activation recursions": "Plafonner le nombre de récursions d'activation d'entrée",1486 "Cap the number of entry activation recursions": "Plafonner le nombre de récursions d'activation d'entrée",
1487 "Max Recursion Steps": "Nombre maximal d'étapes de récursivité",1487 "Max Recursion Steps": "Nombre maximal d'étapes de récursivité",
1488 "0 = unlimited, 1 = scans once and doesn't recurse, 2 = scans once and recurses once, etc\\n(disabled when min activations are used)": "0 = illimité, 1 = scanne une fois et ne récure pas, 2 = scanne une fois et récure une fois, etc.\n(désactivé lorsque des activations minimales sont utilisées)",1488 "0 = unlimited, 1 = scans once and doesn't recurse, 2 = scans once and recurses once, etc": "0 = illimité, 1 = scanne une fois et ne récure pas, 2 = scanne une fois et récure une fois, etc.\n(désactivé lorsque des activations minimales sont utilisées)",
1489 "Include names with each message into the context for scanning": "Inclure les noms dans chaque message dans le contexte pour l'analyse.",1489 "Include names with each message into the context for scanning": "Inclure les noms dans chaque message dans le contexte pour l'analyse.",
1490 "Apply current sorting as Order": "Appliquer le tri actuel comme ordre",1490 "Apply current sorting as Order": "Appliquer le tri actuel comme ordre",
1491 "Display swipe numbers for all messages, not just the last.": "Afficher le nombre de balayage sur tous les messages, et pas seulement le dernier.",1491 "Display swipe numbers for all messages, not just the last.": "Afficher le nombre de balayage sur tous les messages, et pas seulement le dernier.",
@@ -1602,7 +1602,6 @@
1602 "Character Expressions": "Expressions de personnages",1602 "Character Expressions": "Expressions de personnages",
1603 "Translate text to English before classification": "Traduire le texte en anglais avant de le classer",1603 "Translate text to English before classification": "Traduire le texte en anglais avant de le classer",
1604 "Show default images (emojis) if sprite missing": "Afficher les images par défaut (emojis) si le sprite est manquant",1604 "Show default images (emojis) if sprite missing": "Afficher les images par défaut (emojis) si le sprite est manquant",
1605 "Image Type - talkinghead (extras)": "Type d'image - talkinghead (extras)",
1606 "Classifier API": "API de classification",1605 "Classifier API": "API de classification",
1607 "Select the API for classifying expressions.": "Sélectionnez l'API pour classer les expressions.",1606 "Select the API for classifying expressions.": "Sélectionnez l'API pour classer les expressions.",
1608 "Main API": "API principale",1607 "Main API": "API principale",
public/locales/is-is.json+2 -2
@@ -482,7 +482,7 @@
482 "separate with commas w/o space between": "aðskilið með kommum án bila milli",482 "separate with commas w/o space between": "aðskilið með kommum án bila milli",
483 "Custom Stopping Strings": "Eigin stopp-strengir",483 "Custom Stopping Strings": "Eigin stopp-strengir",
484 "JSON serialized array of strings": "JSON raðað fylki af strengjum",484 "JSON serialized array of strings": "JSON raðað fylki af strengjum",
485 "Replace Macro in Custom Stopping Strings": "Skiptu út í macro í sérsniðnum stoppa strengjum",485 "Replace Macro in Stop Strings": "Skiptu út í macro í sérsniðnum stoppa strengjum",
486 "Auto-Continue": "Sjálfvirk Forná",486 "Auto-Continue": "Sjálfvirk Forná",
487 "Allow for Chat Completion APIs": "Leyfa fyrir spjall Loka APIs",487 "Allow for Chat Completion APIs": "Leyfa fyrir spjall Loka APIs",
488 "Target length (tokens)": "Markaðarlengd (texti)",488 "Target length (tokens)": "Markaðarlengd (texti)",
@@ -709,7 +709,7 @@
709 "Auto-swipe": "Sjálfvirkur sveip",709 "Auto-swipe": "Sjálfvirkur sveip",
710 "Enable the auto-swipe function. Settings in this section only have an effect when auto-swipe is enabled": "Virkjaðu sjálfvirka sveiflugerð. Stillingar í þessum hluta hafa aðeins áhrif þegar sjálfvirkur sveiflugerð er virk",710 "Enable the auto-swipe function. Settings in this section only have an effect when auto-swipe is enabled": "Virkjaðu sjálfvirka sveiflugerð. Stillingar í þessum hluta hafa aðeins áhrif þegar sjálfvirkur sveiflugerð er virk",
711 "Minimum generated message length": "Lágmarks lengd á mynduðum skilaboðum",711 "Minimum generated message length": "Lágmarks lengd á mynduðum skilaboðum",
712 "If the generated message is shorter than this, trigger an auto-swipe": "Ef mynduðu skilaboðin eru styttri en þessi, kallaðu fram sjálfvirkar sveiflugerðar",712 "If the generated message is shorter than these many characters, trigger an auto-swipe": "Ef mynduðu skilaboðin eru styttri en þessi, kallaðu fram sjálfvirkar sveiflugerðar",
713 "Blacklisted words": "Svört orð",713 "Blacklisted words": "Svört orð",
714 "words you dont want generated separated by comma ','": "orð sem þú vilt ekki að framleiða aðskilin með kommu ','",714 "words you dont want generated separated by comma ','": "orð sem þú vilt ekki að framleiða aðskilin með kommu ','",
715 "Blacklisted word count to swipe": "Fjöldi svörtra orða til að sveipa",715 "Blacklisted word count to swipe": "Fjöldi svörtra orða til að sveipa",
public/locales/it-it.json+2 -2
@@ -482,7 +482,7 @@
482 "separate with commas w/o space between": "separati con virgole senza spazio tra loro",482 "separate with commas w/o space between": "separati con virgole senza spazio tra loro",
483 "Custom Stopping Strings": "Stringhe di Stop Personalizzate",483 "Custom Stopping Strings": "Stringhe di Stop Personalizzate",
484 "JSON serialized array of strings": "Matrice serializzata JSON di stringhe",484 "JSON serialized array of strings": "Matrice serializzata JSON di stringhe",
485 "Replace Macro in Custom Stopping Strings": "Sostituisci Macro in Stringhe di Arresto Personalizzate",485 "Replace Macro in Stop Strings": "Sostituisci Macro in Stringhe di Arresto Personalizzate",
486 "Auto-Continue": "Auto-continua",486 "Auto-Continue": "Auto-continua",
487 "Allow for Chat Completion APIs": "Consenti per API di completamento chat",487 "Allow for Chat Completion APIs": "Consenti per API di completamento chat",
488 "Target length (tokens)": "Lunghezza obiettivo (token)",488 "Target length (tokens)": "Lunghezza obiettivo (token)",
@@ -709,7 +709,7 @@
709 "Auto-swipe": "Auto-swipe",709 "Auto-swipe": "Auto-swipe",
710 "Enable the auto-swipe function. Settings in this section only have an effect when auto-swipe is enabled": "Abilita la funzione di auto-swipe. Le impostazioni in questa sezione hanno effetto solo quando l'auto-swipe è abilitato",710 "Enable the auto-swipe function. Settings in this section only have an effect when auto-swipe is enabled": "Abilita la funzione di auto-swipe. Le impostazioni in questa sezione hanno effetto solo quando l'auto-swipe è abilitato",
711 "Minimum generated message length": "Lunghezza minima del messaggio generato",711 "Minimum generated message length": "Lunghezza minima del messaggio generato",
712 "If the generated message is shorter than this, trigger an auto-swipe": "Se il messaggio generato è più breve di questo, attiva un'automatica rimozione",712 "If the generated message is shorter than these many characters, trigger an auto-swipe": "Se il messaggio generato è più breve di questo, attiva un'automatica rimozione",
713 "Blacklisted words": "Parole in blacklist",713 "Blacklisted words": "Parole in blacklist",
714 "words you dont want generated separated by comma ','": "parole che non vuoi generate separate da virgola ','",714 "words you dont want generated separated by comma ','": "parole che non vuoi generate separate da virgola ','",
715 "Blacklisted word count to swipe": "Numero di parole in blacklist per attivare un'automatica rimozione",715 "Blacklisted word count to swipe": "Numero di parole in blacklist per attivare un'automatica rimozione",
public/locales/ja-jp.json+2 -2
@@ -482,7 +482,7 @@
482 "separate with commas w/o space between": "間にスペースのないカンマで区切ります",482 "separate with commas w/o space between": "間にスペースのないカンマで区切ります",
483 "Custom Stopping Strings": "カスタム停止文字列",483 "Custom Stopping Strings": "カスタム停止文字列",
484 "JSON serialized array of strings": "文字列のJSONシリアル化配列",484 "JSON serialized array of strings": "文字列のJSONシリアル化配列",
485 "Replace Macro in Custom Stopping Strings": "カスタム停止文字列内のマクロを置換する",485 "Replace Macro in Stop Strings": "カスタム停止文字列内のマクロを置換する",
486 "Auto-Continue": "自動継続",486 "Auto-Continue": "自動継続",
487 "Allow for Chat Completion APIs": "チャット補完APIを許可",487 "Allow for Chat Completion APIs": "チャット補完APIを許可",
488 "Target length (tokens)": "ターゲット長さ(トークン)",488 "Target length (tokens)": "ターゲット長さ(トークン)",
@@ -709,7 +709,7 @@
709 "Auto-swipe": "オートスワイプ",709 "Auto-swipe": "オートスワイプ",
710 "Enable the auto-swipe function. Settings in this section only have an effect when auto-swipe is enabled": "自動スワイプ機能を有効にします。このセクションの設定は、自動スワイプが有効になっている場合にのみ効果があります",710 "Enable the auto-swipe function. Settings in this section only have an effect when auto-swipe is enabled": "自動スワイプ機能を有効にします。このセクションの設定は、自動スワイプが有効になっている場合にのみ効果があります",
711 "Minimum generated message length": "生成されたメッセージの最小長",711 "Minimum generated message length": "生成されたメッセージの最小長",
712 "If the generated message is shorter than this, trigger an auto-swipe": "生成されたメッセージがこれよりも短い場合、自動スワイプをトリガーします",712 "If the generated message is shorter than these many characters, trigger an auto-swipe": "生成されたメッセージがこれよりも短い場合、自動スワイプをトリガーします",
713 "Blacklisted words": "ブラックリストされた単語",713 "Blacklisted words": "ブラックリストされた単語",
714 "words you dont want generated separated by comma ','": "コンマ ',' で区切られた生成したくない単語",714 "words you dont want generated separated by comma ','": "コンマ ',' で区切られた生成したくない単語",
715 "Blacklisted word count to swipe": "スワイプするブラックリストされた単語の数",715 "Blacklisted word count to swipe": "スワイプするブラックリストされた単語の数",
public/locales/ko-kr.json+11 -12
@@ -211,7 +211,7 @@
211 "Sampler Priority": "샘플러 우선 순위",211 "Sampler Priority": "샘플러 우선 순위",
212 "Ooba only. Determines the order of samplers.": "Ooba 전용. 샘플러의 순서를 결정합니다.",212 "Ooba only. Determines the order of samplers.": "Ooba 전용. 샘플러의 순서를 결정합니다.",
213 "Character Names Behavior": "캐릭터 이름 동작",213 "Character Names Behavior": "캐릭터 이름 동작",
214 "[title]character_names_none": "캐릭터 이름 접두사를 추가하지 않습니다. 그룹 채팅에서는 좋지 않을 수 있으므로, 이 설정을 선택할 때는 주의해야 합니다.",214 "character_names_none": "캐릭터 이름 접두사를 추가하지 않습니다. 그룹 채팅에서는 좋지 않을 수 있으므로, 이 설정을 선택할 때는 주의해야 합니다.",
215 "Helps the model to associate messages with characters.": "모델이 메시지를 캐릭터와 연관시키는 데 도움이 됩니다.",215 "Helps the model to associate messages with characters.": "모델이 메시지를 캐릭터와 연관시키는 데 도움이 됩니다.",
216 "None": "없음",216 "None": "없음",
217 "None (not injected)": "없음 (삽입되지 않음)",217 "None (not injected)": "없음 (삽입되지 않음)",
@@ -404,7 +404,7 @@
404 "Custom API Key": "커스텀 API 키",404 "Custom API Key": "커스텀 API 키",
405 "Available Models": "사용 가능한 모델",405 "Available Models": "사용 가능한 모델",
406 "Prompt Post-Processing": "신속한 후처리",406 "Prompt Post-Processing": "신속한 후처리",
407 "[title]API Connections;[no_connection_text]api_no_connection": "연결이 되지 않았습니다...",407 "api_no_connection": "연결이 되지 않았습니다...",
408 "Applies additional processing to the prompt before sending it to the API.": "API로 보내기 전에 프롬프트에 추가 처리를 적용합니다.",408 "Applies additional processing to the prompt before sending it to the API.": "API로 보내기 전에 프롬프트에 추가 처리를 적용합니다.",
409 "Verifies your API connection by sending a short test message. Be aware that you'll be credited for it!": "짧은 테스트 메시지를 보내어 API 연결을 확인합니다. 이에 대해 유료 크레딧이 지불될 수 있음을 인식하세요!",409 "Verifies your API connection by sending a short test message. Be aware that you'll be credited for it!": "짧은 테스트 메시지를 보내어 API 연결을 확인합니다. 이에 대해 유료 크레딧이 지불될 수 있음을 인식하세요!",
410 "Test Message": "테스트 메시지",410 "Test Message": "테스트 메시지",
@@ -492,7 +492,7 @@
492 "separate with commas w/o space between": "쉼표로 구분 (공백 없이)",492 "separate with commas w/o space between": "쉼표로 구분 (공백 없이)",
493 "Custom Stopping Strings": "사용자 정의 중지 문자열",493 "Custom Stopping Strings": "사용자 정의 중지 문자열",
494 "JSON serialized array of strings": "문자열의 JSON 직렬화된 배열",494 "JSON serialized array of strings": "문자열의 JSON 직렬화된 배열",
495 "Replace Macro in Custom Stopping Strings": "사용자 정의 중단 문자열에서 매크로 교체",495 "Replace Macro in Stop Strings": "사용자 정의 중단 문자열에서 매크로 교체",
496 "Auto-Continue": "자동 계속하기",496 "Auto-Continue": "자동 계속하기",
497 "Allow for Chat Completion APIs": "채팅 완성 API 허용",497 "Allow for Chat Completion APIs": "채팅 완성 API 허용",
498 "Target length (tokens)": "대상 길이 (토큰)",498 "Target length (tokens)": "대상 길이 (토큰)",
@@ -625,7 +625,7 @@
625 "Single-row message input area. Mobile only, no effect on PC": "한 줄짜리 메시지 입력 영역. 모바일 전용, PC에는 영향 없음",625 "Single-row message input area. Mobile only, no effect on PC": "한 줄짜리 메시지 입력 영역. 모바일 전용, PC에는 영향 없음",
626 "Compact Input Area (Mobile)": "조그마한 입력 영역 (모바일)",626 "Compact Input Area (Mobile)": "조그마한 입력 영역 (모바일)",
627 "Swipe # for All Messages": "모든 스와이프 메시지에 대해 번호 매기기",627 "Swipe # for All Messages": "모든 스와이프 메시지에 대해 번호 매기기",
628 "[title]Display swipe numbers for all messages, not just the last.": "마지막 메시지만이 아니라 모든 메시지에 대한 스와이프 번호를 표시합니다.",628 "Display swipe numbers for all messages, not just the last.": "마지막 메시지만이 아니라 모든 메시지에 대한 스와이프 번호를 표시합니다.",
629 "In the Character Management panel, show quick selection buttons for favorited characters": "캐릭터 관리 패널에서 즐겨찾는 캐릭터에 대한 빠른 선택 버튼을 표시합니다",629 "In the Character Management panel, show quick selection buttons for favorited characters": "캐릭터 관리 패널에서 즐겨찾는 캐릭터에 대한 빠른 선택 버튼을 표시합니다",
630 "Characters Hotswap": "캐릭터 핫스왑",630 "Characters Hotswap": "캐릭터 핫스왑",
631 "Enable magnification for zoomed avatar display.": "마우스 포인터를 아바타 위에 올려두면 아바타가 확대 됩니다.",631 "Enable magnification for zoomed avatar display.": "마우스 포인터를 아바타 위에 올려두면 아바타가 확대 됩니다.",
@@ -724,7 +724,7 @@
724 "Auto-swipe": "자동 스와이프",724 "Auto-swipe": "자동 스와이프",
725 "Enable the auto-swipe function. Settings in this section only have an effect when auto-swipe is enabled": "자동 스와이프 기능을 활성화합니다. 이 섹션의 설정은 자동 스와이프가 활성화되었을 때만 영향을 미칩니다",725 "Enable the auto-swipe function. Settings in this section only have an effect when auto-swipe is enabled": "자동 스와이프 기능을 활성화합니다. 이 섹션의 설정은 자동 스와이프가 활성화되었을 때만 영향을 미칩니다",
726 "Minimum generated message length": "생성된 메시지 최소 길이",726 "Minimum generated message length": "생성된 메시지 최소 길이",
727 "If the generated message is shorter than this, trigger an auto-swipe": "생성된 메시지가이보다 짧으면 자동 스와이프를 트리거합니다",727 "If the generated message is shorter than these many characters, trigger an auto-swipe": "생성된 메시지가이보다 짧으면 자동 스와이프를 트리거합니다",
728 "Blacklisted words": "금지어",728 "Blacklisted words": "금지어",
729 "words you dont want generated separated by comma ','": "쉼표로 구분된 생성하지 않으려는 단어",729 "words you dont want generated separated by comma ','": "쉼표로 구분된 생성하지 않으려는 단어",
730 "Blacklisted word count to swipe": "스와이프할 금지어 개수",730 "Blacklisted word count to swipe": "스와이프할 금지어 개수",
@@ -1467,7 +1467,6 @@
1467 "menu within": "내의 메뉴",1467 "menu within": "내의 메뉴",
1468 "Translate text to English before classification": "분류 전에 텍스트를 영어로 번역합니다.",1468 "Translate text to English before classification": "분류 전에 텍스트를 영어로 번역합니다.",
1469 "Show default images (emojis) if sprite missing": "해당하는 스프라이트가 없으면 기본 이미지 (이모지들)을 표시합니다.",1469 "Show default images (emojis) if sprite missing": "해당하는 스프라이트가 없으면 기본 이미지 (이모지들)을 표시합니다.",
1470 "Image Type - talkinghead (extras)": "이미지 유형 - 토킹 헤드 (부가 사항)",
1471 "Classifier API": "분류를 위한 API",1470 "Classifier API": "분류를 위한 API",
1472 "Select the API for classifying expressions.": "감정 이미지들을 분류할 API를 선택하세요.",1471 "Select the API for classifying expressions.": "감정 이미지들을 분류할 API를 선택하세요.",
1473 "Local": "로컬",1472 "Local": "로컬",
@@ -1538,7 +1537,7 @@
1538 "Only apply color as accent": "색상은 오직 강조로써만 적용됩니다",1537 "Only apply color as accent": "색상은 오직 강조로써만 적용됩니다",
1539 "qr--colorClear": "색상 지우기",1538 "qr--colorClear": "색상 지우기",
1540 "Color": "색상",1539 "Color": "색상",
1541 "[title]world_button_title": "캐릭터 로어. 클릭하여 로드하세요. Shift를 클릭하면 '월드 인포 링크' 팝업이 열립니다.",1540 "world_button_title": "캐릭터 로어. 클릭하여 로드하세요. Shift를 클릭하면 '월드 인포 링크' 팝업이 열립니다.",
1542 "Select TTS Provider": "TTS 공급자 선택",1541 "Select TTS Provider": "TTS 공급자 선택",
1543 "tts_enabled": "활성화",1542 "tts_enabled": "활성화",
1544 "Narrate user messages": "사용자 메시지 나레이션",1543 "Narrate user messages": "사용자 메시지 나레이션",
@@ -1583,15 +1582,15 @@
1583 "Prompt Content": "프롬프트 내용",1582 "Prompt Content": "프롬프트 내용",
1584 "Instruct Sequences": "지시 시퀀스",1583 "Instruct Sequences": "지시 시퀀스",
1585 "Prefer Character Card Instructions": "캐릭터 카드의 지시사항을 선호",1584 "Prefer Character Card Instructions": "캐릭터 카드의 지시사항을 선호",
1586 "[title]If checked and the character card contains a Post-History Instructions override, use that instead": "활성화 된 경우, 캐릭터 카드에 Post-History 지시 무시 항목이 포함되어 있으면, 카드 지시사항의 내용으로 대신 사용합니다.",1585 "If checked and the character card contains a Post-History Instructions override, use that instead": "활성화 된 경우, 캐릭터 카드에 Post-History 지시 무시 항목이 포함되어 있으면, 카드 지시사항의 내용으로 대신 사용합니다.",
1587 "Auto-select Input Text": "입력 텍스트 자동 선택",1586 "Auto-select Input Text": "입력 텍스트 자동 선택",
1588 "[title]Enable auto-select of input text in some text fields when clicking/selecting them. Applies to popup input textboxes, and possible other custom input fields.": "일부 텍스트 필드를 클릭하거나 선택할 때 자동으로 입력된 텍스트가 선택되도록 설정합니다. 팝업 입력창과 기타 커스텀 입력 필드에 적용됩니다.",1587 "Enable auto-select of input text in some text fields when clicking/selecting them. Applies to popup input textboxes, and possible other custom input fields.": "일부 텍스트 필드를 클릭하거나 선택할 때 자동으로 입력된 텍스트가 선택되도록 설정합니다. 팝업 입력창과 기타 커스텀 입력 필드에 적용됩니다.",
1589 "Markdown Hotkeys": "마크다운 입력 단축키",1588 "Markdown Hotkeys": "마크다운 입력 단축키",
1590 "[title]markdown_hotkeys_desc": "특정 텍스트 입력창에서 마크다운 형식 문자를 입력하기 위한 단축키를 활성화합니다. '/help hotkeys'를 참고하세요.",1589 "markdown_hotkeys_desc": "특정 텍스트 입력창에서 마크다운 형식 문자를 입력하기 위한 단축키를 활성화합니다. '/help hotkeys'를 참고하세요.",
1591 "Show group chat queue": "그룹 채팅 대기열 표시",1590 "Show group chat queue": "그룹 채팅 대기열 표시",
1592 "[title]In group chat, highlight the character(s) that are currently queued to generate responses and the order in which they will respond.": "그룹 채팅에서 응답을 생성하기 위해 현재 대기 중인 캐릭터와 응답할 순서를 강조 표시합니다.",1591 "In group chat, highlight the character(s) that are currently queued to generate responses and the order in which they will respond.": "그룹 채팅에서 응답을 생성하기 위해 현재 대기 중인 캐릭터와 응답할 순서를 강조 표시합니다.",
1593 "Quick 'Impersonate' button": "빠른 '사칭' 버튼",1592 "Quick 'Impersonate' button": "빠른 '사칭' 버튼",
1594 "[title]Show a button in the input area to ask the AI to impersonate your character for a single message": "입력 영역에 AI에게 한 메시지 동안 당신의 캐릭터 연기를 사칭하도록 요청하는 버튼을 표시합니다.",1593 "Show a button in the input area to ask the AI to impersonate your character for a single message": "입력 영역에 AI에게 한 메시지 동안 당신의 캐릭터 연기를 사칭하도록 요청하는 버튼을 표시합니다.",
1595 "Injection Template": "삽입 템플릿",1594 "Injection Template": "삽입 템플릿",
1596 "Query messages": "쿼리 메시지 수",1595 "Query messages": "쿼리 메시지 수",
1597 "Score threshold": "점수 임계값",1596 "Score threshold": "점수 임계값",
public/locales/nl-nl.json+2 -2
@@ -482,7 +482,7 @@
482 "separate with commas w/o space between": "gescheiden met komma's zonder spatie ertussen",482 "separate with commas w/o space between": "gescheiden met komma's zonder spatie ertussen",
483 "Custom Stopping Strings": "Aangepaste Stopreeksen",483 "Custom Stopping Strings": "Aangepaste Stopreeksen",
484 "JSON serialized array of strings": "JSON geserialiseerde reeks van strings",484 "JSON serialized array of strings": "JSON geserialiseerde reeks van strings",
485 "Replace Macro in Custom Stopping Strings": "Macro vervangen in aangepaste stopreeksen",485 "Replace Macro in Stop Strings": "Macro vervangen in aangepaste stopreeksen",
486 "Auto-Continue": "Automatisch doorgaan",486 "Auto-Continue": "Automatisch doorgaan",
487 "Allow for Chat Completion APIs": "Chatvervolledigings-API's toestaan",487 "Allow for Chat Completion APIs": "Chatvervolledigings-API's toestaan",
488 "Target length (tokens)": "Doellengte (tokens)",488 "Target length (tokens)": "Doellengte (tokens)",
@@ -709,7 +709,7 @@
709 "Auto-swipe": "Automatisch vegen",709 "Auto-swipe": "Automatisch vegen",
710 "Enable the auto-swipe function. Settings in this section only have an effect when auto-swipe is enabled": "Schakel de automatische-vegen functie in. Instellingen in dit gedeelte hebben alleen effect wanneer automatisch vegen is ingeschakeld",710 "Enable the auto-swipe function. Settings in this section only have an effect when auto-swipe is enabled": "Schakel de automatische-vegen functie in. Instellingen in dit gedeelte hebben alleen effect wanneer automatisch vegen is ingeschakeld",
711 "Minimum generated message length": "Minimale gegenereerde berichtlengte",711 "Minimum generated message length": "Minimale gegenereerde berichtlengte",
712 "If the generated message is shorter than this, trigger an auto-swipe": "Als het gegenereerde bericht korter is dan dit, activeer dan een automatische veeg",712 "If the generated message is shorter than these many characters, trigger an auto-swipe": "Als het gegenereerde bericht korter is dan dit, activeer dan een automatische veeg",
713 "Blacklisted words": "Verboden woorden",713 "Blacklisted words": "Verboden woorden",
714 "words you dont want generated separated by comma ','": "woorden die je niet gegenereerd wilt hebben gescheiden door komma ','",714 "words you dont want generated separated by comma ','": "woorden die je niet gegenereerd wilt hebben gescheiden door komma ','",
715 "Blacklisted word count to swipe": "Aantal verboden woorden om te vegen",715 "Blacklisted word count to swipe": "Aantal verboden woorden om te vegen",
public/locales/pt-pt.json+2 -2
@@ -482,7 +482,7 @@
482 "separate with commas w/o space between": "separe com vírgulas sem espaço entre",482 "separate with commas w/o space between": "separe com vírgulas sem espaço entre",
483 "Custom Stopping Strings": "Cadeias de parada personalizadas",483 "Custom Stopping Strings": "Cadeias de parada personalizadas",
484 "JSON serialized array of strings": "Matriz de strings serializada em JSON",484 "JSON serialized array of strings": "Matriz de strings serializada em JSON",
485 "Replace Macro in Custom Stopping Strings": "Substituir Macro em Strings de Parada Personalizadas",485 "Replace Macro in Stop Strings": "Substituir Macro em Strings de Parada Personalizadas",
486 "Auto-Continue": "Auto-Continuar",486 "Auto-Continue": "Auto-Continuar",
487 "Allow for Chat Completion APIs": "Permitir APIs de Completar Chat",487 "Allow for Chat Completion APIs": "Permitir APIs de Completar Chat",
488 "Target length (tokens)": "Comprimento alvo (tokens)",488 "Target length (tokens)": "Comprimento alvo (tokens)",
@@ -709,7 +709,7 @@
709 "Auto-swipe": "Auto-swipe",709 "Auto-swipe": "Auto-swipe",
710 "Enable the auto-swipe function. Settings in this section only have an effect when auto-swipe is enabled": "Ativar a função de auto-swipe. As configurações nesta seção só têm efeito quando o auto-swipe está ativado",710 "Enable the auto-swipe function. Settings in this section only have an effect when auto-swipe is enabled": "Ativar a função de auto-swipe. As configurações nesta seção só têm efeito quando o auto-swipe está ativado",
711 "Minimum generated message length": "Comprimento mínimo da mensagem gerada",711 "Minimum generated message length": "Comprimento mínimo da mensagem gerada",
712 "If the generated message is shorter than this, trigger an auto-swipe": "Se a mensagem gerada for mais curta que isso, acione um auto-swipe",712 "If the generated message is shorter than these many characters, trigger an auto-swipe": "Se a mensagem gerada for mais curta que isso, acione um auto-swipe",
713 "Blacklisted words": "Palavras proibidas",713 "Blacklisted words": "Palavras proibidas",
714 "words you dont want generated separated by comma ','": "palavras que você não quer geradas separadas por vírgula ','",714 "words you dont want generated separated by comma ','": "palavras que você não quer geradas separadas por vírgula ','",
715 "Blacklisted word count to swipe": "Contagem de palavras proibidas para swipe",715 "Blacklisted word count to swipe": "Contagem de palavras proibidas para swipe",
public/locales/ru-ru.json+74 -14
@@ -161,7 +161,7 @@
161 "View hidden API keys": "Посмотреть скрытые API-ключи",161 "View hidden API keys": "Посмотреть скрытые API-ключи",
162 "Advanced Formatting": "Расширенное форматирование",162 "Advanced Formatting": "Расширенное форматирование",
163 "Context Template": "Шаблон контекста",163 "Context Template": "Шаблон контекста",
164 "Replace Macro in Custom Stopping Strings": "Заменять макросы в пользовательских стоп-строках",164 "Replace Macro in Stop Strings": "Заменять макросы в пользовательских стоп-строках",
165 "Story String": "Строка истории",165 "Story String": "Строка истории",
166 "Example Separator": "Разделитель примеров сообщений",166 "Example Separator": "Разделитель примеров сообщений",
167 "Chat Start": "Начало чата",167 "Chat Start": "Начало чата",
@@ -195,7 +195,7 @@
195 "Yes": "Да",195 "Yes": "Да",
196 "No": "Нет",196 "No": "Нет",
197 "Context %": "Процент контекста",197 "Context %": "Процент контекста",
198 "Budget Cap": "Бюджетный лимит",198 "Budget Cap": "Лимит бюджета",
199 "(0 = disabled)": "(0 = отключено)",199 "(0 = disabled)": "(0 = отключено)",
200 "None": "Отсутствует",200 "None": "Отсутствует",
201 "User Settings": "Настройки пользователя",201 "User Settings": "Настройки пользователя",
@@ -426,7 +426,7 @@
426 "Requests logprobs from the API for the Token Probabilities feature": "Запросить логпробы из API для функции Token Probabilities.",426 "Requests logprobs from the API for the Token Probabilities feature": "Запросить логпробы из API для функции Token Probabilities.",
427 "Automatically reject and re-generate AI message based on configurable criteria": "Автоматическое отклонение и повторная генерация сообщений AI на основе настраиваемых критериев.",427 "Automatically reject and re-generate AI message based on configurable criteria": "Автоматическое отклонение и повторная генерация сообщений AI на основе настраиваемых критериев.",
428 "Enable the auto-swipe function. Settings in this section only have an effect when auto-swipe is enabled": "Включить авто-свайп. Настройки в этом разделе действуют только при включенном авто-свайпе.",428 "Enable the auto-swipe function. Settings in this section only have an effect when auto-swipe is enabled": "Включить авто-свайп. Настройки в этом разделе действуют только при включенном авто-свайпе.",
429 "If the generated message is shorter than this, trigger an auto-swipe": "Если сгенерированное сообщение короче этого значения, срабатывает авто-свайп.",429 "If the generated message is shorter than these many characters, trigger an auto-swipe": "Если сгенерированное сообщение короче этого значения, срабатывает авто-свайп.",
430 "Reload and redraw the currently open chat": "Перезагрузить и перерисовать открытый в данный момент чат.",430 "Reload and redraw the currently open chat": "Перезагрузить и перерисовать открытый в данный момент чат.",
431 "Auto-Expand Message Actions": "Развернуть действия",431 "Auto-Expand Message Actions": "Развернуть действия",
432 "Persona Management": "Управление персоной",432 "Persona Management": "Управление персоной",
@@ -575,10 +575,10 @@
575 "Characters sorting order": "Порядок сортировки персонажей",575 "Characters sorting order": "Порядок сортировки персонажей",
576 "Remove": "Убрать",576 "Remove": "Убрать",
577 "Select a World Info file for": "Выбрать файл с миром для",577 "Select a World Info file for": "Выбрать файл с миром для",
578 "Primary Lorebook": "Основного лорбука",578 "Primary Lorebook": "Основной лорбук",
579 "A selected World Info will be bound to this character as its own Lorebook.": "Информация о мире будет привязана к персонажу как его собственный лорбук",579 "A selected World Info will be bound to this character as its own Lorebook.": "Информация о мире будет привязана к персонажу как его собственный лорбук.",
580 "When generating an AI reply, it will be combined with the entries from a global World Info selector.": "Когда ИИ генерирует ответ, он будет совмещён с записями из глобально выбранного мира",580 "When generating an AI reply, it will be combined with the entries from a global World Info selector.": "Когда ИИ генерирует ответ, он будет совмещён с записями из глобально выбранного мира.",
581 "Exporting a character would also export the selected Lorebook file embedded in the JSON data.": "При экспорте персонажа вместе с ним также выгрузится выбранный лорбук в виде JSON",581 "Exporting a character would also export the selected Lorebook file embedded in the JSON data.": "При экспорте персонажа вместе с ним также выгрузится выбранный лорбук в виде JSON.",
582 "Additional Lorebooks": "Вспомогательные лорбуки",582 "Additional Lorebooks": "Вспомогательные лорбуки",
583 "Associate one or more auxillary Lorebooks with this character.": "Привязать к этому персонажу один или больше вспомогательных лорбуков",583 "Associate one or more auxillary Lorebooks with this character.": "Привязать к этому персонажу один или больше вспомогательных лорбуков",
584 "NOTE: These choices are optional and won't be preserved on character export!": "ВНИМАНИЕ: эти выборы необязательные и не будут сохранены при экспорте персонажа!",584 "NOTE: These choices are optional and won't be preserved on character export!": "ВНИМАНИЕ: эти выборы необязательные и не будут сохранены при экспорте персонажа!",
@@ -593,7 +593,7 @@
593 "Prompt": "Промпт",593 "Prompt": "Промпт",
594 "Copy": "Скопировать",594 "Copy": "Скопировать",
595 "Confirm": "Подтвердить",595 "Confirm": "Подтвердить",
596 "Copy this message": "Скопировать сообщение",596 "Copy this message": "Продублировать сообщение",
597 "Delete this message": "Удалить сообщение",597 "Delete this message": "Удалить сообщение",
598 "Move message up": "Переместить сообщение вверх",598 "Move message up": "Переместить сообщение вверх",
599 "Move message down": "Переместить сообщение вниз",599 "Move message down": "Переместить сообщение вниз",
@@ -612,7 +612,7 @@
612 "Ask AI to write your message for you": "Попросить ИИ написать сообщение за вас",612 "Ask AI to write your message for you": "Попросить ИИ написать сообщение за вас",
613 "Continue the last message": "Продолжить текущее сообщение",613 "Continue the last message": "Продолжить текущее сообщение",
614 "Bind user name to that avatar": "Закрепить имя за этим аватаром",614 "Bind user name to that avatar": "Закрепить имя за этим аватаром",
615 "Select this as default persona for the new chats.": "Выберать эту Персону в качестве персоны по умолчанию для новых чатов.",615 "Select this as default persona for the new chats.": "Выбирать эту персону по умолчанию для всех новых чатов.",
616 "Change persona image": "Сменить аватар персоны",616 "Change persona image": "Сменить аватар персоны",
617 "Delete persona": "Удалить персону",617 "Delete persona": "Удалить персону",
618 "Reduced Motion": "Сокращение анимаций",618 "Reduced Motion": "Сокращение анимаций",
@@ -640,7 +640,7 @@
640 "Token Probabilities": "Вероятности токенов",640 "Token Probabilities": "Вероятности токенов",
641 "Close chat": "Закрыть чат",641 "Close chat": "Закрыть чат",
642 "Manage chat files": "Все чаты",642 "Manage chat files": "Все чаты",
643 "Import Extension From Git Repo": "Импортировать расширение из Git Repository",643 "Import Extension From Git Repo": "Импортировать расширение из Git-репозитория.",
644 "Install extension": "Установить расширение",644 "Install extension": "Установить расширение",
645 "Manage extensions": "Управление расширениями",645 "Manage extensions": "Управление расширениями",
646 "Tokens persona description": "Токенов",646 "Tokens persona description": "Токенов",
@@ -1122,7 +1122,7 @@
1122 "help_hotkeys_0": "Горячие клавиши",1122 "help_hotkeys_0": "Горячие клавиши",
1123 "You can browse a list of bundled characters in the": "Комплектных персонажей можно найти в меню",1123 "You can browse a list of bundled characters in the": "Комплектных персонажей можно найти в меню",
1124 "Download Extensions & Assets": "Загрузить расширения и ресурсы",1124 "Download Extensions & Assets": "Загрузить расширения и ресурсы",
1125 "menu within": "внутри этих кубиков",1125 "menu within": "в меню",
1126 "Assets URL": "URL с описанием ресурсов",1126 "Assets URL": "URL с описанием ресурсов",
1127 "Custom (OpenAI-compatible)": "Кастомный (совместимый с OpenAI)",1127 "Custom (OpenAI-compatible)": "Кастомный (совместимый с OpenAI)",
1128 "Custom Endpoint (Base URL)": "Кастомный эндпоинт (базовый URL)",1128 "Custom Endpoint (Base URL)": "Кастомный эндпоинт (базовый URL)",
@@ -1943,7 +1943,7 @@
1943 "and connect to an": "и подключитесь к",1943 "and connect to an": "и подключитесь к",
1944 "You can add more": "Можете добавить больше",1944 "You can add more": "Можете добавить больше",
1945 "from other websites": "с других сайтов.",1945 "from other websites": "с других сайтов.",
1946 "Go to the": "Загляните в",1946 "Go to the": "Заходите в",
1947 "to install additional features.": ", чтобы установить разные дополнительные ресурсы.",1947 "to install additional features.": ", чтобы установить разные дополнительные ресурсы.",
1948 "or_welcome": "; также доступен",1948 "or_welcome": "; также доступен",
1949 "Claude API Key": "Ключ от API Claude",1949 "Claude API Key": "Ключ от API Claude",
@@ -1958,7 +1958,7 @@
1958 "Save": "Сохранить",1958 "Save": "Сохранить",
1959 "Chat Lorebook": "Лорбук для чата",1959 "Chat Lorebook": "Лорбук для чата",
1960 "chat_world_template_txt": "Выбранный мир будет привязан к этому чату. Будет добавляться в промпт наряду с глобальным лорбуком и лором персонажа.",1960 "chat_world_template_txt": "Выбранный мир будет привязан к этому чату. Будет добавляться в промпт наряду с глобальным лорбуком и лором персонажа.",
1961 "world_button_title": "Лор персонажа\n\nНажмите, чтобы загрузить\nShift + клик, чтобы открыть диалог привязки мира",1961 "world_button_title": "Лор персонажа\n\nНажмите, чтобы загрузить\nShift + ЛКМ, чтобы открыть диалог привязки мира",
1962 "No auxillary Lorebooks set. Click here to select.": "Вспомогательный лорбук не выбран. Нажмите, чтобы выбрать.",1962 "No auxillary Lorebooks set. Click here to select.": "Вспомогательный лорбук не выбран. Нажмите, чтобы выбрать.",
1963 "ext_regex_user_input_desc": "Отправленные вами сообщения.",1963 "ext_regex_user_input_desc": "Отправленные вами сообщения.",
1964 "ext_regex_ai_input_desc": "Полученные от API ответы.",1964 "ext_regex_ai_input_desc": "Полученные от API ответы.",
@@ -2144,5 +2144,65 @@
2144 "Not connected to the API!": "Нет соединения с API!",2144 "Not connected to the API!": "Нет соединения с API!",
2145 "ext_type_system": "Это комплектное расширение. Его нельзя удалить, а обновляется оно вместе со всей системой.",2145 "ext_type_system": "Это комплектное расширение. Его нельзя удалить, а обновляется оно вместе со всей системой.",
2146 "Update all": "Обновить все",2146 "Update all": "Обновить все",
2147 "Close": "Закрыть"2147 "Close": "Закрыть",
2148 "Optional modules:": "Необязательные модули:",
2149 "Sort: Display Name": "Сортировать: по названию",
2150 "Sort: Loading Order": "Сортировать: в порядке загрузки",
2151 "Click to toggle": "Нажмите, чтобы включить или выключить",
2152 "Loading Asset List": "Загрузить список ресурсов",
2153 "Don't ask again for this URL": "Запомнить выбор для этого адреса",
2154 "Are you sure you want to connect to the following url?": "Вы точно хотите подключиться к этому адресу?",
2155 "All": "Всё",
2156 "Characters": "Персонажи",
2157 "Ambient sounds": "Звуковой эмбиент",
2158 "Blip sounds": "Звуки уведомлений",
2159 "Background music": "Фоновая музыка",
2160 "Search": "Поиск",
2161 "extension_install_1": "Чтобы загружать расширения из этого списка, у вас должен быть установлен ",
2162 "extension_install_2": ".",
2163 "extension_install_3": "Нажмите на иконку ",
2164 "extension_install_4": ", чтобы перейти в репозиторий расширения и получить более подробную информацию о нём.",
2165 "Extension repo/guide:": "Репозиторий расширения:",
2166 "Preview in browser": "Предпросмотр",
2167 "Adds a function tool": "Частично или полностью работает через вызов функций",
2168 "Tool": "Функции",
2169 "Move extension": "Переместить расширение",
2170 "ext_type_local": "Это локальное расширение, доступно только вам",
2171 "ext_type_global": "Это глобальное расширение, доступно всем пользователям",
2172 "Move": "Переместить",
2173 "Enter the Git URL of the extension to install": "Введите Git-адрес расширения",
2174 "Please be aware that using external extensions can have unintended side effects and may pose security risks. Always make sure you trust the source before importing an extension. We are not responsible for any damage caused by third-party extensions.": "помните, что используя расширения от сторонних авторов, вы можете подвергать систему опасности. Устанавливайте расширения только от проверенных разработчиков. Мы не несём ответственности за любой ущерб, причинённый сторонними расширениями.",
2175 "Disclaimer:": "Внимание:",
2176 "Example:": "Пример:",
2177 "context_derived": "Считывать из метаданных модели (по возможности)",
2178 "instruct_derived": "Считывать из метаданных модели (по возможности)",
2179 "Confirm token parsing with": "Чтобы убедиться в правильности выделения токенов, используйте",
2180 "Reasoning Effort": "Рассуждения",
2181 "Constrains effort on reasoning for reasoning models.": "Регулирует объём внутренних рассуждений модели (reasoning), для моделей которые поддерживают эту возможность.\nНа данный момент поддерживаются три значения: Подробные, Обычные, Поверхностные.\nПри менее подробном рассуждении ответ получается быстрее, а также экономятся токены, уходящие на рассуждения.",
2182 "openai_reasoning_effort_low": "Поверхностные",
2183 "openai_reasoning_effort_medium": "Обычные",
2184 "openai_reasoning_effort_high": "Подробные",
2185 "Persona Lore Alt+Click to open the lorebook": "Лорбук данной персоны\nAlt + ЛКМ чтобы открыть лорбук",
2186 "Persona Lorebook for": "Лорбук для персоны",
2187 "persona_world_template_txt": "Выбранная Информация о мире будет привязана к этой персоне. Информация будет добавляться в каждом промпте вместе с глобальным лорбуком и лорбуками персонажа и чата.",
2188 "Global list": "Глобальный список",
2189 "Preset-specific list": "Список для данного пресета",
2190 "Banned tokens/strings are being sent in the request.": "Запрещённые токены и строки отсылаются в запросе.",
2191 "Banned tokens/strings are NOT being sent in the request.": "Запрещённые токены и строки НЕ отсылаются в запросе.",
2192 "Add a reasoning block": "Добавить блок рассуждений",
2193 "Create a copy of this message?": "Продублировать это сообщение?",
2194 "Max Recursion Steps": "Макс. глубина рекурсии",
2195 "0 = unlimited, 1 = scans once and doesn't recurse, 2 = scans once and recurses once, etc": "0 = неограничено, 1 = сканировать единожды, 2 = сканировать единожды и сделать один повторный проход, и т.д.\n(неактивно при указанном мин. числе активаций)",
2196 "(disabled when max recursion steps are used)": "(неактивно при указанной макс. глубине рекурсии)",
2197 "Enter a valid API URL": "Введите корректный адрес API",
2198 "No Ollama model selected.": "Не выбрана модель Ollama",
2199 "Background Fitting": "Способ подгонки фона под разрешение",
2200 "Chat Lore Alt+Click to open the lorebook": "Лорбук данного чата\nAlt + ЛКМ чтобы открыть лорбук",
2201 "Token Counter": "Подсчитать токены",
2202 "Type / paste in the box below to see the number of tokens in the text.": "Введите или вставьте текст в окошко ниже, чтобы подсчитать количество токенов в нём.",
2203 "Selected tokenizer:": "Выбранный токенайзер:",
2204 "Input:": "Входные данные:",
2205 "Tokenized text:": "Токенизированный текст:",
2206 "Token IDs:": "Идентификаторы токенов:",
2207 "Tokens:": "Токенов:"
2148}2208}
public/locales/uk-ua.json+2 -2
@@ -482,7 +482,7 @@
482 "separate with commas w/o space between": "розділяйте комами без пропусків між ними",482 "separate with commas w/o space between": "розділяйте комами без пропусків між ними",
483 "Custom Stopping Strings": "Власні рядки зупинки",483 "Custom Stopping Strings": "Власні рядки зупинки",
484 "JSON serialized array of strings": "JSON-серіалізований масив рядків",484 "JSON serialized array of strings": "JSON-серіалізований масив рядків",
485 "Replace Macro in Custom Stopping Strings": "Замінювати макроси у власних рядках зупинки",485 "Replace Macro in Stop Strings": "Замінювати макроси у власних рядках зупинки",
486 "Auto-Continue": "Автоматичне продовження",486 "Auto-Continue": "Автоматичне продовження",
487 "Allow for Chat Completion APIs": "Дозволити для Chat Completion API",487 "Allow for Chat Completion APIs": "Дозволити для Chat Completion API",
488 "Target length (tokens)": "Цільова довжина (токени)",488 "Target length (tokens)": "Цільова довжина (токени)",
@@ -709,7 +709,7 @@
709 "Auto-swipe": "Автоматичний змах",709 "Auto-swipe": "Автоматичний змах",
710 "Enable the auto-swipe function. Settings in this section only have an effect when auto-swipe is enabled": "Вмикає функцію автоматичного змаху. Налаштування в цьому розділі діють лише тоді, коли увімкнено автоматичний змах",710 "Enable the auto-swipe function. Settings in this section only have an effect when auto-swipe is enabled": "Вмикає функцію автоматичного змаху. Налаштування в цьому розділі діють лише тоді, коли увімкнено автоматичний змах",
711 "Minimum generated message length": "Мінімальна довжина згенерованого повідомлення",711 "Minimum generated message length": "Мінімальна довжина згенерованого повідомлення",
712 "If the generated message is shorter than this, trigger an auto-swipe": "Якщо згенероване повідомлення коротше за це, викликайте автоматичний змаху",712 "If the generated message is shorter than these many characters, trigger an auto-swipe": "Якщо згенероване повідомлення коротше за це, викликайте автоматичний змаху",
713 "Blacklisted words": "Список заборонених слів",713 "Blacklisted words": "Список заборонених слів",
714 "words you dont want generated separated by comma ','": "слова, які ви не хочете генерувати, розділені комою ','",714 "words you dont want generated separated by comma ','": "слова, які ви не хочете генерувати, розділені комою ','",
715 "Blacklisted word count to swipe": "Кількість заборонених слів для змаху",715 "Blacklisted word count to swipe": "Кількість заборонених слів для змаху",
public/locales/vi-vn.json+2 -2
@@ -482,7 +482,7 @@
482 "separate with commas w/o space between": "phân tách bằng dấu phẩy không có khoảng trắng giữa",482 "separate with commas w/o space between": "phân tách bằng dấu phẩy không có khoảng trắng giữa",
483 "Custom Stopping Strings": "Chuỗi dừng tùy chỉnh",483 "Custom Stopping Strings": "Chuỗi dừng tùy chỉnh",
484 "JSON serialized array of strings": "Mảng chuỗi được tuần tự hóa JSON",484 "JSON serialized array of strings": "Mảng chuỗi được tuần tự hóa JSON",
485 "Replace Macro in Custom Stopping Strings": "Thay thế Macro trong Chuỗi Dừng Tùy chỉnh",485 "Replace Macro in Stop Strings": "Thay thế Macro trong Chuỗi Dừng Tùy chỉnh",
486 "Auto-Continue": "Tự động Tiếp tục",486 "Auto-Continue": "Tự động Tiếp tục",
487 "Allow for Chat Completion APIs": "Cho phép các API hoàn thành Trò chuyện",487 "Allow for Chat Completion APIs": "Cho phép các API hoàn thành Trò chuyện",
488 "Target length (tokens)": "Độ dài mục tiêu (token)",488 "Target length (tokens)": "Độ dài mục tiêu (token)",
@@ -709,7 +709,7 @@
709 "Auto-swipe": "Tự động vuốt",709 "Auto-swipe": "Tự động vuốt",
710 "Enable the auto-swipe function. Settings in this section only have an effect when auto-swipe is enabled": "Bật chức năng tự động vuốt. Các cài đặt trong phần này chỉ có tác dụng khi tự động vuốt được bật",710 "Enable the auto-swipe function. Settings in this section only have an effect when auto-swipe is enabled": "Bật chức năng tự động vuốt. Các cài đặt trong phần này chỉ có tác dụng khi tự động vuốt được bật",
711 "Minimum generated message length": "Độ dài tối thiểu của tin nhắn được tạo",711 "Minimum generated message length": "Độ dài tối thiểu của tin nhắn được tạo",
712 "If the generated message is shorter than this, trigger an auto-swipe": "Nếu tin nhắn được tạo ra ngắn hơn điều này, kích hoạt tự động vuốt",712 "If the generated message is shorter than these many characters, trigger an auto-swipe": "Nếu tin nhắn được tạo ra ngắn hơn điều này, kích hoạt tự động vuốt",
713 "Blacklisted words": "Từ trong danh sách đen",713 "Blacklisted words": "Từ trong danh sách đen",
714 "words you dont want generated separated by comma ','": "các từ bạn không muốn được tạo ra được phân tách bằng dấu phẩy ','",714 "words you dont want generated separated by comma ','": "các từ bạn không muốn được tạo ra được phân tách bằng dấu phẩy ','",
715 "Blacklisted word count to swipe": "Số từ trong danh sách đen để vuốt",715 "Blacklisted word count to swipe": "Số từ trong danh sách đen để vuốt",
public/locales/zh-cn.json+15 -16
@@ -215,7 +215,7 @@
215 "Classifier Free Guidance. More helpful tip coming soon": "无分类器指导(CFG)。更多有用的提示敬请期待。",215 "Classifier Free Guidance. More helpful tip coming soon": "无分类器指导(CFG)。更多有用的提示敬请期待。",
216 "Scale": "缩放比例",216 "Scale": "缩放比例",
217 "Negative Prompt": "负面提示词",217 "Negative Prompt": "负面提示词",
218 "Used if CFG Scale is unset globally, per chat or character": "如果无分类器指导(CFG)缩放比例未在全局设置,它将作用于每个聊天或每个角色",218 "Used if CFG Scale is unset globally, per chat or character": "如果CFG缩放比例未被全局设置,它将作用于所有聊天或角色",
219 "Add text here that would make the AI generate things you don't want in your outputs.": "请在此处添加文本,以避免生成您不希望出现在输出中的内容。",219 "Add text here that would make the AI generate things you don't want in your outputs.": "请在此处添加文本,以避免生成您不希望出现在输出中的内容。",
220 "Grammar String": "语法字符串",220 "Grammar String": "语法字符串",
221 "GBNF or EBNF, depends on the backend in use. If you're using this you should know which.": "GBNF 或 EBNF,取决于使用的后端。如果您使用这个,您应该知道该用哪一个。",221 "GBNF or EBNF, depends on the backend in use. If you're using this you should know which.": "GBNF 或 EBNF,取决于使用的后端。如果您使用这个,您应该知道该用哪一个。",
@@ -266,8 +266,8 @@
266 "Use system prompt": "使用系统提示词",266 "Use system prompt": "使用系统提示词",
267 "Merges_all_system_messages_desc_1": "合并所有系统消息,直到第一条具有非系统角色的消息,然后通过",267 "Merges_all_system_messages_desc_1": "合并所有系统消息,直到第一条具有非系统角色的消息,然后通过",
268 "Merges_all_system_messages_desc_2": "字段发送。",268 "Merges_all_system_messages_desc_2": "字段发送。",
269 "Show model reasoning": "展示思维链",269 "Request model reasoning": "请求思维链",
270 "Display the model's internal thoughts in the response.": "展示模型在回复时的内部思维链。",270 "Allows the model to return its thinking process.": "允许模型返回其思维过程。",
271 "Assistant Prefill": "AI预填",271 "Assistant Prefill": "AI预填",
272 "Expand the editor": "展开编辑器",272 "Expand the editor": "展开编辑器",
273 "Start Claude's answer with...": "以如下内容开始Claude的回答...",273 "Start Claude's answer with...": "以如下内容开始Claude的回答...",
@@ -559,7 +559,7 @@
559 "Prompt Content": "提示词内容",559 "Prompt Content": "提示词内容",
560 "Custom Stopping Strings": "自定义停止字符串",560 "Custom Stopping Strings": "自定义停止字符串",
561 "JSON serialized array of strings": "JSON序列化的字符串数组",561 "JSON serialized array of strings": "JSON序列化的字符串数组",
562 "Replace Macro in Custom Stopping Strings": "替换自定义停止字符串中的宏",562 "Replace Macro in Stop Strings": "替换自定义停止字符串中的宏",
563 "Token Padding": "词符填充",563 "Token Padding": "词符填充",
564 "Miscellaneous": "杂项",564 "Miscellaneous": "杂项",
565 "Non-markdown strings": "非 Markdown 字符串",565 "Non-markdown strings": "非 Markdown 字符串",
@@ -584,7 +584,7 @@
584 "(0 = unlimited, use budget)": "(“0”为无限制,使用预算)",584 "(0 = unlimited, use budget)": "(“0”为无限制,使用预算)",
585 "Cap the number of entry activation recursions": "限制条目激活递归的次数",585 "Cap the number of entry activation recursions": "限制条目激活递归的次数",
586 "Max Recursion Steps": "最大递归深度",586 "Max Recursion Steps": "最大递归深度",
587 "0 = unlimited, 1 = scans once and doesn't recurse, 2 = scans once and recurses once, etc\\n(disabled when min activations are used)": "“0”为无限制,“1”为扫描一次且不递归,“2”为扫描一次且递归一次,依此类推\n(当使用最小激活次数时,此功能被禁用)",587 "0 = unlimited, 1 = scans once and doesn't recurse, 2 = scans once and recurses once, etc": "“0”为无限制,“1”为扫描一次且不递归,“2”为扫描一次且递归一次,依此类推\n(当使用最小激活次数时,此功能被禁用)",
588 "Insertion Strategy": "插入策略",588 "Insertion Strategy": "插入策略",
589 "Sorted Evenly": "均匀排序",589 "Sorted Evenly": "均匀排序",
590 "Character Lore First": "角色世界书优先",590 "Character Lore First": "角色世界书优先",
@@ -804,7 +804,7 @@
804 "Auto-swipe": "自动滑动",804 "Auto-swipe": "自动滑动",
805 "Enable the auto-swipe function. Settings in this section only have an effect when auto-swipe is enabled": "启用自动滑动功能。仅当启用自动滑动时,本节中的设置才会生效",805 "Enable the auto-swipe function. Settings in this section only have an effect when auto-swipe is enabled": "启用自动滑动功能。仅当启用自动滑动时,本节中的设置才会生效",
806 "Minimum generated message length": "生成的消息的最小长度",806 "Minimum generated message length": "生成的消息的最小长度",
807 "If the generated message is shorter than this, trigger an auto-swipe": "如果生成的消息短于此长度,则触发自动滑动",807 "If the generated message is shorter than these many characters, trigger an auto-swipe": "如果生成的消息短于此长度,则触发自动滑动",
808 "Blacklisted words": "屏蔽词",808 "Blacklisted words": "屏蔽词",
809 "words you dont want generated separated by comma ','": "不想生成的词语,用半角逗号“,”分隔",809 "words you dont want generated separated by comma ','": "不想生成的词语,用半角逗号“,”分隔",
810 "Blacklisted word count to swipe": "触发滑动的黑名单词语数量",810 "Blacklisted word count to swipe": "触发滑动的黑名单词语数量",
@@ -1208,7 +1208,7 @@
1208 "View contents": "查看内容",1208 "View contents": "查看内容",
1209 "Remove the file": "删除文件",1209 "Remove the file": "删除文件",
1210 "Author's Note": "作者注释",1210 "Author's Note": "作者注释",
1211 "Unique to this chat": "此聊天独有",1211 "Unique to this chat": "仅对此聊天生效",
1212 "Checkpoints inherit the Note from their parent, and can be changed individually after that.": "检查点从其父级继承注释,之后可以单独更改。",1212 "Checkpoints inherit the Note from their parent, and can be changed individually after that.": "检查点从其父级继承注释,之后可以单独更改。",
1213 "Include in World Info Scanning": "纳入世界信息扫描",1213 "Include in World Info Scanning": "纳入世界信息扫描",
1214 "Before Main Prompt / Story String": "主提示词/故事线之前",1214 "Before Main Prompt / Story String": "主提示词/故事线之前",
@@ -1224,13 +1224,13 @@
1224 "Replace Author's Note": "替换作者注",1224 "Replace Author's Note": "替换作者注",
1225 "Default Author's Note": "默认作者注",1225 "Default Author's Note": "默认作者注",
1226 "Will be automatically added as the Author's Note for all new chats.": "将自动添加为所有新聊天的作者注释。",1226 "Will be automatically added as the Author's Note for all new chats.": "将自动添加为所有新聊天的作者注释。",
1227 "Chat CFG": "聊天CFG",1227 "Chat CFG": "本聊天的CFG缩放",
1228 "1 = disabled": "“1”为已禁用",1228 "1 = disabled": "“1”为禁用",
1229 "write short replies, write replies using past tense": "写简短的回复,用过去时写回复",1229 "write short replies, write replies using past tense": "写简短的回复,用过去时写回复",
1230 "Positive Prompt": "正面提示词",1230 "Positive Prompt": "正面提示词",
1231 "Use character CFG scales": "单独为各个角色设置CFG缩放",1231 "Use character CFG scales": "单独为各个角色设置CFG缩放",
1232 "Character CFG": "角色CFG配置",1232 "Character CFG": "角色CFG配置",
1233 "Will be automatically added as the CFG for this character.": "将自动添加为该角色的 CFG。",1233 "Will be automatically added as the CFG for this character.": "将自动添加到该角色的CFG设置中。",
1234 "Global CFG": "全局CFG",1234 "Global CFG": "全局CFG",
1235 "Will be used as the default CFG options for every chat unless overridden.": "除非被覆盖,否则将用作每次聊天的默认 CFG 选项。",1235 "Will be used as the default CFG options for every chat unless overridden.": "除非被覆盖,否则将用作每次聊天的默认 CFG 选项。",
1236 "CFG Prompt Cascading": "CFG 提示词级联",1236 "CFG Prompt Cascading": "CFG 提示词级联",
@@ -1349,7 +1349,6 @@
1349 "Character Expressions": "角色表情",1349 "Character Expressions": "角色表情",
1350 "Translate text to English before classification": "分类之前将文本翻译成英文",1350 "Translate text to English before classification": "分类之前将文本翻译成英文",
1351 "Show default images (emojis) if sprite missing": "如果表情包缺失,则显示默认图像(表情符号)",1351 "Show default images (emojis) if sprite missing": "如果表情包缺失,则显示默认图像(表情符号)",
1352 "Image Type - talkinghead (extras)": "图像类型 - 说话头像(附加内容)",
1353 "Classifier API": "分类器 API",1352 "Classifier API": "分类器 API",
1354 "Select the API for classifying expressions.": "选择用于对表达式进行分类的API。",1353 "Select the API for classifying expressions.": "选择用于对表达式进行分类的API。",
1355 "Main API": "主要 API",1354 "Main API": "主要 API",
@@ -1486,7 +1485,7 @@
1486 "ext_regex_replace_string_placeholder": "使用 {{match}} 包含来自“查找正则表达式”或“$1”、“$2”等的匹配文本作为捕获组。",1485 "ext_regex_replace_string_placeholder": "使用 {{match}} 包含来自“查找正则表达式”或“$1”、“$2”等的匹配文本作为捕获组。",
1487 "Trim Out": "修剪掉",1486 "Trim Out": "修剪掉",
1488 "ext_regex_trim_placeholder": "在替换之前全局修剪正则表达式匹配中任何不需要的部分。用回车键分隔每个元素。",1487 "ext_regex_trim_placeholder": "在替换之前全局修剪正则表达式匹配中任何不需要的部分。用回车键分隔每个元素。",
1489 "ext_regex_affects": "影响",1488 "ext_regex_affects": "作用范围",
1490 "ext_regex_user_input_desc": "用户发送的消息",1489 "ext_regex_user_input_desc": "用户发送的消息",
1491 "ext_regex_user_input": "用户输入",1490 "ext_regex_user_input": "用户输入",
1492 "ext_regex_ai_input_desc": "从生成式API中获取的信息。",1491 "ext_regex_ai_input_desc": "从生成式API中获取的信息。",
@@ -1720,9 +1719,9 @@
1720 "Chat Lorebook for": "聊天知识书",1719 "Chat Lorebook for": "聊天知识书",
1721 "chat_world_template_txt": "选定的世界信息将绑定到此聊天。生成 AI 回复时,\n它将与全球和角色传说书中的条目相结合。",1720 "chat_world_template_txt": "选定的世界信息将绑定到此聊天。生成 AI 回复时,\n它将与全球和角色传说书中的条目相结合。",
1722 "chat_rename_1": "输入聊天的新名称:",1721 "chat_rename_1": "输入聊天的新名称:",
1723 "chat_rename_2": "注意!!使用已有文件名会导致错误!!",1722 "chat_rename_2": "注意!!与其他文件重名会导致错误!!",
1724 "chat_rename_3": "此举会将次聊天与标记为“检查点”的聊天解绑。",1723 "chat_rename_3": "此举会将此聊天与标记为“检查点”的聊天解绑。",
1725 "chat_rename_4": "不需要在结尾添加 '.JSONL'",1724 "chat_rename_4": "(不需要在结尾添加 '.JSONL' 后缀)",
1726 "Enter Checkpoint Name:": "输入检查点名称:",1725 "Enter Checkpoint Name:": "输入检查点名称:",
1727 "(Leave empty to auto-generate)": "(留空以自动生成)",1726 "(Leave empty to auto-generate)": "(留空以自动生成)",
1728 "The currently existing checkpoint will be unlinked and replaced with the new checkpoint, but can still be found in the Chat Management.": "当前检查点将会被解绑并替换为新的检查点,但仍可在聊天管理中找到。",1727 "The currently existing checkpoint will be unlinked and replaced with the new checkpoint, but can still be found in the Chat Management.": "当前检查点将会被解绑并替换为新的检查点,但仍可在聊天管理中找到。",
@@ -1975,7 +1974,7 @@
1975 "Enter your password below to confirm:": "输入您的密码以确认:",1974 "Enter your password below to confirm:": "输入您的密码以确认:",
1976 "Chat Scenario Override": "聊天场景覆盖",1975 "Chat Scenario Override": "聊天场景覆盖",
1977 "Remove": "移除",1976 "Remove": "移除",
1978 "Unique to this chat.": "Unique to this chat.",1977 "Unique to this chat.": "仅对此聊天生效。",
1979 "All group members will use the following scenario text instead of what is specified in their character cards.": "All group members will use the following scenario text instead of what is specified in their character cards.",1978 "All group members will use the following scenario text instead of what is specified in their character cards.": "All group members will use the following scenario text instead of what is specified in their character cards.",
1980 "The following scenario text will be used instead of the value set in the character card.": "The following scenario text will be used instead of the value set in the character card.",1979 "The following scenario text will be used instead of the value set in the character card.": "The following scenario text will be used instead of the value set in the character card.",
1981 "Checkpoints inherit the scenario override from their parent, and can be changed individually after that.": "Checkpoints inherit the scenario override from their parent, and can be changed individually after that.",1980 "Checkpoints inherit the scenario override from their parent, and can be changed individually after that.": "Checkpoints inherit the scenario override from their parent, and can be changed individually after that.",
public/locales/zh-tw.json+6 -7
@@ -483,7 +483,7 @@
483 "separate with commas w/o space between": "用逗號分隔,之間無空格",483 "separate with commas w/o space between": "用逗號分隔,之間無空格",
484 "Custom Stopping Strings": "自訂停止字串",484 "Custom Stopping Strings": "自訂停止字串",
485 "JSON serialized array of strings": "JSON 序列化字串數組",485 "JSON serialized array of strings": "JSON 序列化字串數組",
486 "Replace Macro in Custom Stopping Strings": "取代自訂停止字串中的巨集",486 "Replace Macro in Stop Strings": "取代自訂停止字串中的巨集",
487 "Auto-Continue": "自動繼續",487 "Auto-Continue": "自動繼續",
488 "Allow for Chat Completion APIs": "允許聊天補全 API",488 "Allow for Chat Completion APIs": "允許聊天補全 API",
489 "Target length (tokens)": "目標長度(符元)",489 "Target length (tokens)": "目標長度(符元)",
@@ -710,7 +710,7 @@
710 "Auto-swipe": "自動滑動",710 "Auto-swipe": "自動滑動",
711 "Enable the auto-swipe function. Settings in this section only have an effect when auto-swipe is enabled": "啟用自動滑動功能。此部分的設定僅在啟用自動滑動時有效。",711 "Enable the auto-swipe function. Settings in this section only have an effect when auto-swipe is enabled": "啟用自動滑動功能。此部分的設定僅在啟用自動滑動時有效。",
712 "Minimum generated message length": "生成訊息的最小長度",712 "Minimum generated message length": "生成訊息的最小長度",
713 "If the generated message is shorter than this, trigger an auto-swipe": "如果生成的訊息比這個短,將觸發自動滑動。",713 "If the generated message is shorter than these many characters, trigger an auto-swipe": "如果生成的訊息比這個短,將觸發自動滑動。",
714 "Blacklisted words": "黑名單詞語",714 "Blacklisted words": "黑名單詞語",
715 "words you dont want generated separated by comma ','": "您不想生成的文字,使用逗號分隔",715 "words you dont want generated separated by comma ','": "您不想生成的文字,使用逗號分隔",
716 "Blacklisted word count to swipe": "滑動的黑名單詞語數量",716 "Blacklisted word count to swipe": "滑動的黑名單詞語數量",
@@ -1458,7 +1458,7 @@
1458 "Example: http://localhost:1234/v1": "例如:http://localhost:1234/v1",1458 "Example: http://localhost:1234/v1": "例如:http://localhost:1234/v1",
1459 "popup-button-crop": "裁剪",1459 "popup-button-crop": "裁剪",
1460 "(disabled when max recursion steps are used)": "(當最大遞歸步驟數使用時將停用)",1460 "(disabled when max recursion steps are used)": "(當最大遞歸步驟數使用時將停用)",
1461 "0 = unlimited, 1 = scans once and doesn't recurse, 2 = scans once and recurses once, etc\n(disabled when min activations are used)": "0 = 無限制,1 = 掃描一次且不遞歸,2 = 掃描一次並遞歸一次,以此類推\n(使用最小啟動設定時將停用)",1461 "0 = unlimited, 1 = scans once and doesn't recurse, 2 = scans once and recurses once, etc": "0 = 無限制,1 = 掃描一次且不遞歸,2 = 掃描一次並遞歸一次,以此類推\n(使用最小啟動設定時將停用)",
1462 "A greedy, brute-force algorithm used in LLM sampling to find the most likely sequence of words or tokens. It expands multiple candidate sequences at once, maintaining a fixed number (beam width) of top sequences at each step.": "一種用於 LLM 抽樣的貪婪演算法,用於尋找最可能的單詞或標記序列。該方法會同時展開多個候選序列,並在每一步中保持固定數量的頂級序列(beam width)。",1462 "A greedy, brute-force algorithm used in LLM sampling to find the most likely sequence of words or tokens. It expands multiple candidate sequences at once, maintaining a fixed number (beam width) of top sequences at each step.": "一種用於 LLM 抽樣的貪婪演算法,用於尋找最可能的單詞或標記序列。該方法會同時展開多個候選序列,並在每一步中保持固定數量的頂級序列(beam width)。",
1463 "A multiplicative factor to expand the overall area that the nodes take up.": "節點佔用該擴充功能區域的倍數。",1463 "A multiplicative factor to expand the overall area that the nodes take up.": "節點佔用該擴充功能區域的倍數。",
1464 "Abort current image generation task": "終止目前的圖片生成任務",1464 "Abort current image generation task": "終止目前的圖片生成任務",
@@ -1653,7 +1653,6 @@
1653 "HuggingFace Token": "HuggingFace 符元",1653 "HuggingFace Token": "HuggingFace 符元",
1654 "Image Captioning": "圖片註解",1654 "Image Captioning": "圖片註解",
1655 "Generate Caption": "產生圖片註解",1655 "Generate Caption": "產生圖片註解",
1656 "Image Type - talkinghead (extras)": "圖片類型 - talkinghead(額外選項)",
1657 "Injection Position": "插入位置",1656 "Injection Position": "插入位置",
1658 "Injection position. Relative (to other prompts in prompt manager) or In-chat @ Depth.": "插入位置(與提示詞管理器中的其他提示相比)或聊天中的深度位置。",1657 "Injection position. Relative (to other prompts in prompt manager) or In-chat @ Depth.": "插入位置(與提示詞管理器中的其他提示相比)或聊天中的深度位置。",
1659 "Injection Template": "插入範本",1658 "Injection Template": "插入範本",
@@ -1806,7 +1805,7 @@
1806 "context_derived": "若可能,根據模型元數據推導。",1805 "context_derived": "若可能,根據模型元數據推導。",
1807 "instruct_derived": "若可能,根據模型元數據推導。",1806 "instruct_derived": "若可能,根據模型元數據推導。",
1808 "Inserted before the first User's message.": "插入於第一則使用者訊息之前。",1807 "Inserted before the first User's message.": "插入於第一則使用者訊息之前。",
1809 "0 = unlimited, 1 = scans once and doesn't recurse, 2 = scans once and recurses once, etc\\n(disabled when min activations are used)": "0 = 無限制,1 = 掃描一次不遞歸,2 = 掃描一次後遞歸一次 ⋯以此類推\n(啟用最小啟動次數時無效)",1808 "0 = unlimited, 1 = scans once and doesn't recurse, 2 = scans once and recurses once, etc": "0 = 無限制,1 = 掃描一次不遞歸,2 = 掃描一次後遞歸一次 ⋯以此類推\n(啟用最小啟動次數時無效)",
1810 "Quick 'Impersonate' button": "快速「AI 扮演使用者」按鈕",1809 "Quick 'Impersonate' button": "快速「AI 扮演使用者」按鈕",
1811 "Manual": "手動",1810 "Manual": "手動",
1812 "Any contents here will replace the default Post-History Instructions used for this character. (v2 spec: post_history_instructions)": "此處填入的內容將取代該角色的默認聊天歷史後指示(Post-History Instructions)。\n(v2 格式:specpost_history_instructions)",1811 "Any contents here will replace the default Post-History Instructions used for this character. (v2 spec: post_history_instructions)": "此處填入的內容將取代該角色的默認聊天歷史後指示(Post-History Instructions)。\n(v2 格式:specpost_history_instructions)",
@@ -2357,8 +2356,8 @@
2357 "Forbid": "禁止",2356 "Forbid": "禁止",
2358 "Aphrodite only. Determines the order of samplers. Skew is always applied post-softmax, so it's not included here.": "僅限 Aphrodite 使用。決定採樣器的順序。偏移總是在 softmax 後應用,因此不包括在此。",2357 "Aphrodite only. Determines the order of samplers. Skew is always applied post-softmax, so it's not included here.": "僅限 Aphrodite 使用。決定採樣器的順序。偏移總是在 softmax 後應用,因此不包括在此。",
2359 "Aphrodite only. Determines the order of samplers.": "僅限 Aphrodite 使用。決定採樣器的順序。",2358 "Aphrodite only. Determines the order of samplers.": "僅限 Aphrodite 使用。決定採樣器的順序。",
2360 "Show model reasoning": "顯示模型思維鏈",2359 "Request model reasoning": "請求模型思維鏈",
2361 "Display the model's internal thoughts in the response.": "在回應中顯示模型的思維鏈(內部思考過程)。",2360 "Allows the model to return its thinking process.": "讓模型回傳其思考過程。",
2362 "Generic (OpenAI-compatible) [LM Studio, LiteLLM, etc.]": "通用(兼容 OpenAI)[LM Studio, LiteLLM 等]",2361 "Generic (OpenAI-compatible) [LM Studio, LiteLLM, etc.]": "通用(兼容 OpenAI)[LM Studio, LiteLLM 等]",
2363 "Model ID (optional)": "模型 ID(可選)",2362 "Model ID (optional)": "模型 ID(可選)",
2364 "DeepSeek API Key": "DeepSeek API 金鑰",2363 "DeepSeek API Key": "DeepSeek API 金鑰",
public/script.js+267 -205
@@ -95,6 +95,7 @@ import {
95 resetMovableStyles,95 resetMovableStyles,
96 forceCharacterEditorTokenize,96 forceCharacterEditorTokenize,
97 applyPowerUserSettings,97 applyPowerUserSettings,
98 generatedTextFiltered,
98} from './scripts/power-user.js';99} from './scripts/power-user.js';
99100
100import {101import {
@@ -169,6 +170,7 @@ import {
169 toggleDrawer,170 toggleDrawer,
170 isElementInViewport,171 isElementInViewport,
171 copyText,172 copyText,
173 escapeHtml,
172} from './scripts/utils.js';174} from './scripts/utils.js';
173import { debounce_timeout } from './scripts/constants.js';175import { debounce_timeout } from './scripts/constants.js';
174176
@@ -272,7 +274,8 @@ import { initSettingsSearch } from './scripts/setting-search.js';
272import { initBulkEdit } from './scripts/bulk-edit.js';274import { initBulkEdit } from './scripts/bulk-edit.js';
273import { deriveTemplatesFromChatTemplate } from './scripts/chat-templates.js';275import { deriveTemplatesFromChatTemplate } from './scripts/chat-templates.js';
274import { getContext } from './scripts/st-context.js';276import { getContext } from './scripts/st-context.js';
275import { initReasoning, PromptReasoning } from './scripts/reasoning.js';277import { extractReasoningFromData, initReasoning, PromptReasoning, ReasoningHandler, removeReasoningFromString, updateReasoningUI } from './scripts/reasoning.js';
278import { accountStorage } from './scripts/util/AccountStorage.js';
276279
277// API OBJECT FOR EXTERNAL WIRING280// API OBJECT FOR EXTERNAL WIRING
278globalThis.SillyTavern = {281globalThis.SillyTavern = {
@@ -368,6 +371,10 @@ DOMPurify.addHook('uponSanitizeElement', (node, _, config) => {
368 return;371 return;
369 }372 }
370373
374 if (!(node instanceof Element)) {
375 return;
376 }
377
371 let mediaBlocked = false;378 let mediaBlocked = false;
372379
373 switch (node.tagName) {380 switch (node.tagName) {
@@ -422,7 +429,7 @@ DOMPurify.addHook('uponSanitizeElement', (node, _, config) => {
422 const entityId = getCurrentEntityId();429 const entityId = getCurrentEntityId();
423 const warningShownKey = `mediaWarningShown:${entityId}`;430 const warningShownKey = `mediaWarningShown:${entityId}`;
424431
425 if (localStorage.getItem(warningShownKey) === null) {432 if (accountStorage.getItem(warningShownKey) === null) {
426 const warningToast = toastr.warning(433 const warningToast = toastr.warning(
427 t`Use the 'Ext. Media' button to allow it. Click on this message to dismiss.`,434 t`Use the 'Ext. Media' button to allow it. Click on this message to dismiss.`,
428 t`External media has been blocked`,435 t`External media has been blocked`,
@@ -433,7 +440,7 @@ DOMPurify.addHook('uponSanitizeElement', (node, _, config) => {
433 },440 },
434 );441 );
435442
436 localStorage.setItem(warningShownKey, 'true');443 accountStorage.setItem(warningShownKey, 'true');
437 }444 }
438 }445 }
439});446});
@@ -495,9 +502,11 @@ export const event_types = {
495 // TODO: Naming convention is inconsistent with other events502 // TODO: Naming convention is inconsistent with other events
496 CHARACTER_DELETED: 'characterDeleted',503 CHARACTER_DELETED: 'characterDeleted',
497 CHARACTER_DUPLICATED: 'character_duplicated',504 CHARACTER_DUPLICATED: 'character_duplicated',
505 CHARACTER_RENAMED: 'character_renamed',
498 /** @deprecated The event is aliased to STREAM_TOKEN_RECEIVED. */506 /** @deprecated The event is aliased to STREAM_TOKEN_RECEIVED. */
499 SMOOTH_STREAM_TOKEN_RECEIVED: 'stream_token_received',507 SMOOTH_STREAM_TOKEN_RECEIVED: 'stream_token_received',
500 STREAM_TOKEN_RECEIVED: 'stream_token_received',508 STREAM_TOKEN_RECEIVED: 'stream_token_received',
509 STREAM_REASONING_DONE: 'stream_reasoning_done',
501 FILE_ATTACHMENT_DELETED: 'file_attachment_deleted',510 FILE_ATTACHMENT_DELETED: 'file_attachment_deleted',
502 WORLDINFO_FORCE_ACTIVATE: 'worldinfo_force_activate',511 WORLDINFO_FORCE_ACTIVATE: 'worldinfo_force_activate',
503 OPEN_CHARACTER_LIBRARY: 'open_character_library',512 OPEN_CHARACTER_LIBRARY: 'open_character_library',
@@ -508,7 +517,7 @@ export const event_types = {
508 TOOL_CALLS_RENDERED: 'tool_calls_rendered',517 TOOL_CALLS_RENDERED: 'tool_calls_rendered',
509};518};
510519
511export const eventSource = new EventEmitter();520export const eventSource = new EventEmitter([event_types.APP_READY]);
512521
513eventSource.on(event_types.CHAT_CHANGED, processChatSlashCommands);522eventSource.on(event_types.CHAT_CHANGED, processChatSlashCommands);
514523
@@ -1028,12 +1037,22 @@ export function setAnimationDuration(ms = null) {
1028 document.documentElement.style.setProperty('--animation-duration', `${animation_duration}ms`);1037 document.documentElement.style.setProperty('--animation-duration', `${animation_duration}ms`);
1029}1038}
10301039
1040/**
1041 * Sets the currently active character
1042 * @param {object|number|string} [entityOrKey] - An entity with id property (character, group, tag), or directly an id or tag key. If not provided, the active character is reset to `null`.
1043 */
1031export function setActiveCharacter(entityOrKey) {1044export function setActiveCharacter(entityOrKey) {
1032 active_character = getTagKeyForEntity(entityOrKey);1045 active_character = entityOrKey ? getTagKeyForEntity(entityOrKey) : null;
1046 if (active_character) active_group = null;
1033}1047}
10341048
1049/**
1050 * Sets the currently active group.
1051 * @param {object|number|string} [entityOrKey] - An entity with id property (character, group, tag), or directly an id or tag key. If not provided, the active group is reset to `null`.
1052 */
1035export function setActiveGroup(entityOrKey) {1053export function setActiveGroup(entityOrKey) {
1036 active_group = getTagKeyForEntity(entityOrKey);1054 active_group = entityOrKey ? getTagKeyForEntity(entityOrKey) : null;
1055 if (active_group) active_character = null;
1037}1056}
10381057
1039/**1058/**
@@ -1500,7 +1519,7 @@ export async function printCharacters(fullRefresh = false) {
15001519
1501 $('#rm_print_characters_pagination').pagination({1520 $('#rm_print_characters_pagination').pagination({
1502 dataSource: entities,1521 dataSource: entities,
1503 pageSize: Number(localStorage.getItem(storageKey)) || per_page_default,1522 pageSize: Number(accountStorage.getItem(storageKey)) || per_page_default,
1504 sizeChangerOptions: [10, 25, 50, 100, 250, 500, 1000],1523 sizeChangerOptions: [10, 25, 50, 100, 250, 500, 1000],
1505 pageRange: 1,1524 pageRange: 1,
1506 pageNumber: saveCharactersPage || 1,1525 pageNumber: saveCharactersPage || 1,
@@ -1544,7 +1563,7 @@ export async function printCharacters(fullRefresh = false) {
1544 eventSource.emit(event_types.CHARACTER_PAGE_LOADED);1563 eventSource.emit(event_types.CHARACTER_PAGE_LOADED);
1545 },1564 },
1546 afterSizeSelectorChange: function (e) {1565 afterSizeSelectorChange: function (e) {
1547 localStorage.setItem(storageKey, e.target.value);1566 accountStorage.setItem(storageKey, e.target.value);
1548 },1567 },
1549 afterPaging: function (e) {1568 afterPaging: function (e) {
1550 saveCharactersPage = e;1569 saveCharactersPage = e;
@@ -2007,14 +2026,15 @@ export async function sendTextareaMessage() {
2007 * @param {boolean} isUser If the message was sent by the user2026 * @param {boolean} isUser If the message was sent by the user
2008 * @param {number} messageId Message index in chat array2027 * @param {number} messageId Message index in chat array
2009 * @param {object} [sanitizerOverrides] DOMPurify sanitizer option overrides2028 * @param {object} [sanitizerOverrides] DOMPurify sanitizer option overrides
2029 * @param {boolean} [isReasoning] If the message is reasoning output
2010 * @returns {string} HTML string2030 * @returns {string} HTML string
2011 */2031 */
2012export function messageFormatting(mes, ch_name, isSystem, isUser, messageId, sanitizerOverrides = {}) {2032export function messageFormatting(mes, ch_name, isSystem, isUser, messageId, sanitizerOverrides = {}, isReasoning = false) {
2013 if (!mes) {2033 if (!mes) {
2014 return '';2034 return '';
2015 }2035 }
20162036
2017 if (Number(messageId) === 0 && !isSystem && !isUser) {2037 if (Number(messageId) === 0 && !isSystem && !isUser && !isReasoning) {
2018 const mesBeforeReplace = mes;2038 const mesBeforeReplace = mes;
2019 const chatMessage = chat[messageId];2039 const chatMessage = chat[messageId];
2020 mes = substituteParams(mes, undefined, ch_name);2040 mes = substituteParams(mes, undefined, ch_name);
@@ -2043,6 +2063,9 @@ export function messageFormatting(mes, ch_name, isSystem, isUser, messageId, san
2043 if (!isSystem) {2063 if (!isSystem) {
2044 function getRegexPlacement() {2064 function getRegexPlacement() {
2045 try {2065 try {
2066 if (isReasoning) {
2067 return regex_placement.REASONING;
2068 }
2046 if (isUser) {2069 if (isUser) {
2047 return regex_placement.USER_INPUT;2070 return regex_placement.USER_INPUT;
2048 } else if (chat[messageId]?.extra?.type === 'narrator') {2071 } else if (chat[messageId]?.extra?.type === 'narrator') {
@@ -2076,6 +2099,17 @@ export function messageFormatting(mes, ch_name, isSystem, isUser, messageId, san
2076 mes = mes.replaceAll('<', '&lt;').replaceAll('>', '&gt;');2099 mes = mes.replaceAll('<', '&lt;').replaceAll('>', '&gt;');
2077 }2100 }
20782101
2102 // Make sure reasoning strings are always shown, even if they include "<" or ">"
2103 [power_user.reasoning.prefix, power_user.reasoning.suffix].forEach((reasoningString) => {
2104 if (!reasoningString || !reasoningString.trim().length) {
2105 return;
2106 }
2107 // Only replace the first occurrence of the reasoning string
2108 if (mes.includes(reasoningString)) {
2109 mes = mes.replace(reasoningString, escapeHtml(reasoningString));
2110 }
2111 });
2112
2079 if (!isSystem) {2113 if (!isSystem) {
2080 // Save double quotes in tags as a special character to prevent them from being encoded2114 // Save double quotes in tags as a special character to prevent them from being encoded
2081 if (!power_user.encode_tags) {2115 if (!power_user.encode_tags) {
@@ -2186,26 +2220,29 @@ function insertSVGIcon(mes, extra) {
2186 modelName = extra.api;2220 modelName = extra.api;
2187 }2221 }
21882222
2189 const image = new Image();2223 const insertOrReplaceSVG = (image, className, targetSelector, insertBefore) => {
2190 // Add classes for styling and identification
2191 image.classList.add('icon-svg', 'timestamp-icon');
2192 image.src = `/img/${modelName}.svg`;
2193 image.title = `${extra?.api ? extra.api + ' - ' : ''}${extra?.model ?? ''}`;
2194
2195 image.onload = async function () {2224 image.onload = async function () {
2196 // Check if an SVG already exists adjacent to the timestamp2225 let existingSVG = insertBefore ? mes.find(targetSelector).prev(`.${className}`) : mes.find(targetSelector).next(`.${className}`);
2197 let existingSVG = mes.find('.timestamp').next('.timestamp-icon');
2198
2199 if (existingSVG.length) {2226 if (existingSVG.length) {
2200 // Replace existing SVG
2201 existingSVG.replaceWith(image);2227 existingSVG.replaceWith(image);
2202 } else {2228 } else {
2203 // Append the new SVG if none exists2229 if (insertBefore) mes.find(targetSelector).before(image);
2204 mes.find('.timestamp').after(image);2230 else mes.find(targetSelector).after(image);
2205 }2231 }
2206
2207 await SVGInject(image);2232 await SVGInject(image);
2208 };2233 };
2234 };
2235
2236 const createModelImage = (className, targetSelector, insertBefore) => {
2237 const image = new Image();
2238 image.classList.add('icon-svg', className);
2239 image.src = `/img/${modelName}.svg`;
2240 image.title = `${extra?.api ? extra.api + ' - ' : ''}${extra?.model ?? ''}`;
2241 insertOrReplaceSVG(image, className, targetSelector, insertBefore);
2242 };
2243
2244 createModelImage('timestamp-icon', '.timestamp');
2245 createModelImage('thinking-icon', '.mes_reasoning_header_title', true);
2209}2246}
22102247
22112248
@@ -2216,7 +2253,6 @@ function getMessageFromTemplate({
2216 isUser,2253 isUser,
2217 avatarImg,2254 avatarImg,
2218 bias,2255 bias,
2219 reasoning,
2220 isSystem,2256 isSystem,
2221 title,2257 title,
2222 timerValue,2258 timerValue,
@@ -2241,7 +2277,6 @@ function getMessageFromTemplate({
2241 mes.find('.avatar img').attr('src', avatarImg);2277 mes.find('.avatar img').attr('src', avatarImg);
2242 mes.find('.ch_name .name_text').text(characterName);2278 mes.find('.ch_name .name_text').text(characterName);
2243 mes.find('.mes_bias').html(bias);2279 mes.find('.mes_bias').html(bias);
2244 mes.find('.mes_reasoning').html(reasoning);
2245 mes.find('.timestamp').text(timestamp).attr('title', `${extra?.api ? extra.api + ' - ' : ''}${extra?.model ?? ''}`);2280 mes.find('.timestamp').text(timestamp).attr('title', `${extra?.api ? extra.api + ' - ' : ''}${extra?.model ?? ''}`);
2246 mes.find('.mesIDDisplay').text(`#${mesId}`);2281 mes.find('.mesIDDisplay').text(`#${mesId}`);
2247 tokenCount && mes.find('.tokenCounterDisplay').text(`${tokenCount}t`);2282 tokenCount && mes.find('.tokenCounterDisplay').text(`${tokenCount}t`);
@@ -2249,6 +2284,8 @@ function getMessageFromTemplate({
2249 timerValue && mes.find('.mes_timer').attr('title', timerTitle).text(timerValue);2284 timerValue && mes.find('.mes_timer').attr('title', timerTitle).text(timerValue);
2250 bookmarkLink && updateBookmarkDisplay(mes);2285 bookmarkLink && updateBookmarkDisplay(mes);
22512286
2287 updateReasoningUI(mes);
2288
2252 if (power_user.timestamp_model_icon && extra?.api) {2289 if (power_user.timestamp_model_icon && extra?.api) {
2253 insertSVGIcon(mes, extra);2290 insertSVGIcon(mes, extra);
2254 }2291 }
@@ -2260,12 +2297,18 @@ function getMessageFromTemplate({
2260 * Re-renders a message block with updated content.2297 * Re-renders a message block with updated content.
2261 * @param {number} messageId Message ID2298 * @param {number} messageId Message ID
2262 * @param {object} message Message object2299 * @param {object} message Message object
2300 * @param {object} [options={}] Optional arguments
2301 * @param {boolean} [options.rerenderMessage=true] Whether to re-render the message content (inside <c>.mes_text</c>)
2263 */2302 */
2264export function updateMessageBlock(messageId, message) {2303export function updateMessageBlock(messageId, message, { rerenderMessage = true } = {}) {
2265 const messageElement = $(`#chat [mesid="${messageId}"]`);2304 const messageElement = $(`#chat [mesid="${messageId}"]`);
2305 if (rerenderMessage) {
2266 const text = message?.extra?.display_text ?? message.mes;2306 const text = message?.extra?.display_text ?? message.mes;
2267 messageElement.find('.mes_text').html(messageFormatting(text, message.name, message.is_system, message.is_user, messageId));2307 messageElement.find('.mes_text').html(messageFormatting(text, message.name, message.is_system, message.is_user, messageId, {}, false));
2268 messageElement.find('.mes_reasoning').html(messageFormatting(message.extra?.reasoning ?? '', '', false, false, -1));2308 }
2309
2310 updateReasoningUI(messageElement);
2311
2269 addCopyToCodeBlocks(messageElement);2312 addCopyToCodeBlocks(messageElement);
2270 appendMediaToMessage(message, messageElement);2313 appendMediaToMessage(message, messageElement);
2271}2314}
@@ -2422,9 +2465,9 @@ export function addOneMessage(mes, { type = 'normal', insertAfter = null, scroll
2422 mes.is_user,2465 mes.is_user,
2423 chat.indexOf(mes),2466 chat.indexOf(mes),
2424 sanitizerOverrides,2467 sanitizerOverrides,
2468 false,
2425 );2469 );
2426 const bias = messageFormatting(mes.extra?.bias ?? '', '', false, false, -1);2470 const bias = messageFormatting(mes.extra?.bias ?? '', '', false, false, -1, {}, false);
2427 const reasoning = messageFormatting(mes.extra?.reasoning ?? '', '', false, false, -1);
2428 let bookmarkLink = mes?.extra?.bookmark_link ?? '';2471 let bookmarkLink = mes?.extra?.bookmark_link ?? '';
24292472
2430 let params = {2473 let params = {
@@ -2434,7 +2477,6 @@ export function addOneMessage(mes, { type = 'normal', insertAfter = null, scroll
2434 isUser: mes.is_user,2477 isUser: mes.is_user,
2435 avatarImg: avatarImg,2478 avatarImg: avatarImg,
2436 bias: bias,2479 bias: bias,
2437 reasoning: reasoning,
2438 isSystem: isSystem,2480 isSystem: isSystem,
2439 title: title,2481 title: title,
2440 bookmarkLink: bookmarkLink,2482 bookmarkLink: bookmarkLink,
@@ -2442,7 +2484,7 @@ export function addOneMessage(mes, { type = 'normal', insertAfter = null, scroll
2442 timestamp: timestamp,2484 timestamp: timestamp,
2443 extra: mes.extra,2485 extra: mes.extra,
2444 tokenCount: mes.extra?.token_count ?? 0,2486 tokenCount: mes.extra?.token_count ?? 0,
2445 ...formatGenerationTimer(mes.gen_started, mes.gen_finished, mes.extra?.token_count),2487 ...formatGenerationTimer(mes.gen_started, mes.gen_finished, mes.extra?.token_count, mes.extra?.reasoning_duration),
2446 };2488 };
24472489
2448 const renderedMessage = getMessageFromTemplate(params);2490 const renderedMessage = getMessageFromTemplate(params);
@@ -2494,8 +2536,8 @@ export function addOneMessage(mes, { type = 'normal', insertAfter = null, scroll
2494 const swipeMessage = chatElement.find(`[mesid="${chat.length - 1}"]`);2536 const swipeMessage = chatElement.find(`[mesid="${chat.length - 1}"]`);
2495 swipeMessage.attr('swipeid', params.swipeId);2537 swipeMessage.attr('swipeid', params.swipeId);
2496 swipeMessage.find('.mes_text').html(messageText).attr('title', title);2538 swipeMessage.find('.mes_text').html(messageText).attr('title', title);
2497 swipeMessage.find('.mes_reasoning').html(reasoning);
2498 swipeMessage.find('.timestamp').text(timestamp).attr('title', `${params.extra.api} - ${params.extra.model}`);2539 swipeMessage.find('.timestamp').text(timestamp).attr('title', `${params.extra.api} - ${params.extra.model}`);
2540 updateReasoningUI(swipeMessage);
2499 appendMediaToMessage(mes, swipeMessage);2541 appendMediaToMessage(mes, swipeMessage);
2500 if (power_user.timestamp_model_icon && params.extra?.api) {2542 if (power_user.timestamp_model_icon && params.extra?.api) {
2501 insertSVGIcon(swipeMessage, params.extra);2543 insertSVGIcon(swipeMessage, params.extra);
@@ -2562,13 +2604,14 @@ export function formatCharacterAvatar(characterAvatar) {
2562 * @param {Date} gen_started Date when generation was started2604 * @param {Date} gen_started Date when generation was started
2563 * @param {Date} gen_finished Date when generation was finished2605 * @param {Date} gen_finished Date when generation was finished
2564 * @param {number} tokenCount Number of tokens generated (0 if not available)2606 * @param {number} tokenCount Number of tokens generated (0 if not available)
2607 * @param {number?} [reasoningDuration=null] Reasoning duration (null if no reasoning was done)
2565 * @returns {Object} Object containing the formatted timer value and title2608 * @returns {Object} Object containing the formatted timer value and title
2566 * @example2609 * @example
2567 * const { timerValue, timerTitle } = formatGenerationTimer(gen_started, gen_finished, tokenCount);2610 * const { timerValue, timerTitle } = formatGenerationTimer(gen_started, gen_finished, tokenCount);
2568 * console.log(timerValue); // 1.2s2611 * console.log(timerValue); // 1.2s
2569 * console.log(timerTitle); // Generation queued: 12:34:56 7 Jan 2021\nReply received: 12:34:57 7 Jan 2021\nTime to generate: 1.2 seconds\nToken rate: 5 t/s2612 * console.log(timerTitle); // Generation queued: 12:34:56 7 Jan 2021\nReply received: 12:34:57 7 Jan 2021\nTime to generate: 1.2 seconds\nToken rate: 5 t/s
2570 */2613 */
2571function formatGenerationTimer(gen_started, gen_finished, tokenCount) {2614function formatGenerationTimer(gen_started, gen_finished, tokenCount, reasoningDuration = null) {
2572 if (!gen_started || !gen_finished) {2615 if (!gen_started || !gen_finished) {
2573 return {};2616 return {};
2574 }2617 }
@@ -2582,8 +2625,9 @@ function formatGenerationTimer(gen_started, gen_finished, tokenCount) {
2582 `Generation queued: ${start.format(dateFormat)}`,2625 `Generation queued: ${start.format(dateFormat)}`,
2583 `Reply received: ${finish.format(dateFormat)}`,2626 `Reply received: ${finish.format(dateFormat)}`,
2584 `Time to generate: ${seconds} seconds`,2627 `Time to generate: ${seconds} seconds`,
2628 reasoningDuration > 0 ? `Time to think: ${reasoningDuration / 1000} seconds` : '',
2585 tokenCount > 0 ? `Token rate: ${Number(tokenCount / seconds).toFixed(1)} t/s` : '',2629 tokenCount > 0 ? `Token rate: ${Number(tokenCount / seconds).toFixed(1)} t/s` : '',
2586 ].join('\n');2630 ].filter(x => x).join('\n').trim();
25872631
2588 if (isNaN(seconds) || seconds < 0) {2632 if (isNaN(seconds) || seconds < 0) {
2589 return { timerValue: '', timerTitle };2633 return { timerValue: '', timerTitle };
@@ -2771,7 +2815,8 @@ export async function generateQuietPrompt(quiet_prompt, quietToLoud, skipWIAN, q
2771 TempResponseLength.save(main_api, responseLength);2815 TempResponseLength.save(main_api, responseLength);
2772 eventHook = TempResponseLength.setupEventHook(main_api);2816 eventHook = TempResponseLength.setupEventHook(main_api);
2773 }2817 }
2774 return await Generate('quiet', options);2818 const result = await Generate('quiet', options);
2819 return removeReasoningFromString(result);
2775 } finally {2820 } finally {
2776 if (responseLengthCustomized && TempResponseLength.isCustomized()) {2821 if (responseLengthCustomized && TempResponseLength.isCustomized()) {
2777 TempResponseLength.restore(main_api);2822 TempResponseLength.restore(main_api);
@@ -3071,8 +3116,8 @@ export function isStreamingEnabled() {
3071 (main_api == 'openai' &&3116 (main_api == 'openai' &&
3072 oai_settings.stream_openai &&3117 oai_settings.stream_openai &&
3073 !noStreamSources.includes(oai_settings.chat_completion_source) &&3118 !noStreamSources.includes(oai_settings.chat_completion_source) &&
3074 !(oai_settings.chat_completion_source == chat_completion_sources.OPENAI && oai_settings.openai_model.startsWith('o1-')) &&3119 !(oai_settings.chat_completion_source == chat_completion_sources.OPENAI && ['o1-2024-12-17', 'o1'].includes(oai_settings.openai_model))
3075 !(oai_settings.chat_completion_source == chat_completion_sources.MAKERSUITE && oai_settings.google_model.includes('bison')))3120 )
3076 || (main_api == 'kobold' && kai_settings.streaming_kobold && kai_flags.can_use_streaming)3121 || (main_api == 'kobold' && kai_settings.streaming_kobold && kai_flags.can_use_streaming)
3077 || (main_api == 'novel' && nai_settings.streaming_novel)3122 || (main_api == 'novel' && nai_settings.streaming_novel)
3078 || (main_api == 'textgenerationwebui' && textgen_settings.streaming));3123 || (main_api == 'textgenerationwebui' && textgen_settings.streaming));
@@ -3101,11 +3146,14 @@ class StreamingProcessor {
3101 constructor(type, forceName2, timeStarted, continueMessage) {3146 constructor(type, forceName2, timeStarted, continueMessage) {
3102 this.result = '';3147 this.result = '';
3103 this.messageId = -1;3148 this.messageId = -1;
3149 /** @type {HTMLElement} */
3104 this.messageDom = null;3150 this.messageDom = null;
3151 /** @type {HTMLElement} */
3105 this.messageTextDom = null;3152 this.messageTextDom = null;
3153 /** @type {HTMLElement} */
3106 this.messageTimerDom = null;3154 this.messageTimerDom = null;
3155 /** @type {HTMLElement} */
3107 this.messageTokenCounterDom = null;3156 this.messageTokenCounterDom = null;
3108 this.messageReasoningDom = null;
3109 /** @type {HTMLTextAreaElement} */3157 /** @type {HTMLTextAreaElement} */
3110 this.sendTextarea = document.querySelector('#send_textarea');3158 this.sendTextarea = document.querySelector('#send_textarea');
3111 this.type = type;3159 this.type = type;
@@ -3121,7 +3169,8 @@ class StreamingProcessor {
3121 /** @type {import('./scripts/logprobs.js').TokenLogprobs[]} */3169 /** @type {import('./scripts/logprobs.js').TokenLogprobs[]} */
3122 this.messageLogprobs = [];3170 this.messageLogprobs = [];
3123 this.toolCalls = [];3171 this.toolCalls = [];
3124 this.reasoning = '';3172 // Initialize reasoning in its own handler
3173 this.reasoningHandler = new ReasoningHandler(timeStarted);
3125 }3174 }
31263175
3127 #checkDomElements(messageId) {3176 #checkDomElements(messageId) {
@@ -3130,8 +3179,8 @@ class StreamingProcessor {
3130 this.messageTextDom = this.messageDom?.querySelector('.mes_text');3179 this.messageTextDom = this.messageDom?.querySelector('.mes_text');
3131 this.messageTimerDom = this.messageDom?.querySelector('.mes_timer');3180 this.messageTimerDom = this.messageDom?.querySelector('.mes_timer');
3132 this.messageTokenCounterDom = this.messageDom?.querySelector('.tokenCounterDisplay');3181 this.messageTokenCounterDom = this.messageDom?.querySelector('.tokenCounterDisplay');
3133 this.messageReasoningDom = this.messageDom?.querySelector('.mes_reasoning');
3134 }3182 }
3183 this.reasoningHandler.updateDom(messageId);
3135 }3184 }
31363185
3137 #updateMessageBlockVisibility() {3186 #updateMessageBlockVisibility() {
@@ -3141,22 +3190,12 @@ class StreamingProcessor {
3141 }3190 }
3142 }3191 }
31433192
3144 showMessageButtons(messageId) {3193 markUIGenStarted() {
3145 if (messageId == -1) {3194 deactivateSendButtons();
3146 return;
3147 }
3148
3149 showStopButton();
3150 $(`#chat .mes[mesid="${messageId}"] .mes_buttons`).css({ 'display': 'none' });
3151 }
3152
3153 hideMessageButtons(messageId) {
3154 if (messageId == -1) {
3155 return;
3156 }3195 }
31573196
3158 hideStopButton();3197 markUIGenStopped() {
3159 $(`#chat .mes[mesid="${messageId}"] .mes_buttons`).css({ 'display': 'flex' });3198 activateSendButtons();
3160 }3199 }
31613200
3162 async onStartStreaming(text) {3201 async onStartStreaming(text) {
@@ -3165,20 +3204,18 @@ class StreamingProcessor {
3165 if (this.type == 'impersonate') {3204 if (this.type == 'impersonate') {
3166 this.sendTextarea.value = '';3205 this.sendTextarea.value = '';
3167 this.sendTextarea.dispatchEvent(new Event('input', { bubbles: true }));3206 this.sendTextarea.dispatchEvent(new Event('input', { bubbles: true }));
3168 }3207 } else {
3169 else {3208 await saveReply(this.type, text, true, '', [], '');
3170 await saveReply(this.type, text, true);
3171 messageId = chat.length - 1;3209 messageId = chat.length - 1;
3172 this.#checkDomElements(messageId);3210 this.#checkDomElements(messageId);
3173 this.showMessageButtons(messageId);3211 this.markUIGenStarted();
3174 }3212 }
3175
3176 hideSwipeButtons();3213 hideSwipeButtons();
3177 scrollChatToBottom();3214 scrollChatToBottom();
3178 return messageId;3215 return messageId;
3179 }3216 }
31803217
3181 onProgressStreaming(messageId, text, isFinal) {3218 async onProgressStreaming(messageId, text, isFinal) {
3182 const isImpersonate = this.type == 'impersonate';3219 const isImpersonate = this.type == 'impersonate';
3183 const isContinue = this.type == 'continue';3220 const isContinue = this.type == 'continue';
31843221
@@ -3190,11 +3227,9 @@ class StreamingProcessor {
31903227
3191 let processedText = cleanUpMessage(text, isImpersonate, isContinue, !isFinal, this.stoppingStrings);3228 let processedText = cleanUpMessage(text, isImpersonate, isContinue, !isFinal, this.stoppingStrings);
31923229
3193 // Predict unbalanced asterisks / quotes during streaming
3194 const charsToBalance = ['*', '"', '```'];3230 const charsToBalance = ['*', '"', '```'];
3195 for (const char of charsToBalance) {3231 for (const char of charsToBalance) {
3196 if (!isFinal && isOdd(countOccurrences(processedText, char))) {3232 if (!isFinal && isOdd(countOccurrences(processedText, char))) {
3197 // Add character at the end to balance it
3198 const separator = char.length > 1 ? '\n' : '';3233 const separator = char.length > 1 ? '\n' : '';
3199 processedText = processedText.trimEnd() + separator + char;3234 processedText = processedText.trimEnd() + separator + char;
3200 }3235 }
@@ -3203,31 +3238,25 @@ class StreamingProcessor {
3203 if (isImpersonate) {3238 if (isImpersonate) {
3204 this.sendTextarea.value = processedText;3239 this.sendTextarea.value = processedText;
3205 this.sendTextarea.dispatchEvent(new Event('input', { bubbles: true }));3240 this.sendTextarea.dispatchEvent(new Event('input', { bubbles: true }));
3206 }3241 } else {
3207 else {3242 const mesChanged = chat[messageId]['mes'] !== processedText;
3208 this.#checkDomElements(messageId);3243 this.#checkDomElements(messageId);
3209 this.#updateMessageBlockVisibility();3244 this.#updateMessageBlockVisibility();
3210 const currentTime = new Date();3245 const currentTime = new Date();
3211 chat[messageId]['mes'] = processedText;3246 chat[messageId]['mes'] = processedText;
3212 chat[messageId]['gen_started'] = this.timeStarted;3247 chat[messageId]['gen_started'] = this.timeStarted;
3213 chat[messageId]['gen_finished'] = currentTime;3248 chat[messageId]['gen_finished'] = currentTime;
3214
3215 if (!chat[messageId]['extra']) {3249 if (!chat[messageId]['extra']) {
3216 chat[messageId]['extra'] = {};3250 chat[messageId]['extra'] = {};
3217 }3251 }
32183252
3219 if (this.reasoning) {3253 // Update reasoning
3220 chat[messageId]['extra']['reasoning'] = this.reasoning;3254 await this.reasoningHandler.process(messageId, mesChanged);
3221 if (this.messageReasoningDom instanceof HTMLElement) {3255 processedText = chat[messageId]['mes'];
3222 const formattedReasoning = messageFormatting(this.reasoning, '', false, false, -1);
3223 this.messageReasoningDom.innerHTML = formattedReasoning;
3224 }
3225 }
32263256
3227 // Don't waste time calculating token count for streaming3257 // Token count update.
3228 const tokenCountText = (this.reasoning || '') + processedText;3258 const tokenCountText = this.reasoningHandler.reasoning + processedText;
3229 const currentTokenCount = isFinal && power_user.message_token_count_enabled ? getTokenCount(tokenCountText, 0) : 0;3259 const currentTokenCount = isFinal && power_user.message_token_count_enabled ? getTokenCount(tokenCountText, 0) : 0;
3230
3231 if (currentTokenCount) {3260 if (currentTokenCount) {
3232 chat[messageId]['extra']['token_count'] = currentTokenCount;3261 chat[messageId]['extra']['token_count'] = currentTokenCount;
3233 if (this.messageTokenCounterDom instanceof HTMLElement) {3262 if (this.messageTokenCounterDom instanceof HTMLElement) {
@@ -3246,12 +3275,14 @@ class StreamingProcessor {
3246 chat[messageId].is_system,3275 chat[messageId].is_system,
3247 chat[messageId].is_user,3276 chat[messageId].is_user,
3248 messageId,3277 messageId,
3278 {},
3279 false,
3249 );3280 );
3250 if (this.messageTextDom instanceof HTMLElement) {3281 if (this.messageTextDom instanceof HTMLElement) {
3251 this.messageTextDom.innerHTML = formattedText;3282 this.messageTextDom.innerHTML = formattedText;
3252 }3283 }
32533284
3254 const timePassed = formatGenerationTimer(this.timeStarted, currentTime, currentTokenCount);3285 const timePassed = formatGenerationTimer(this.timeStarted, currentTime, currentTokenCount, this.reasoningHandler.getDuration());
3255 if (this.messageTimerDom instanceof HTMLElement) {3286 if (this.messageTimerDom instanceof HTMLElement) {
3256 this.messageTimerDom.textContent = timePassed.timerValue;3287 this.messageTimerDom.textContent = timePassed.timerValue;
3257 this.messageTimerDom.title = timePassed.timerTitle;3288 this.messageTimerDom.title = timePassed.timerTitle;
@@ -3266,10 +3297,12 @@ class StreamingProcessor {
3266 }3297 }
32673298
3268 async onFinishStreaming(messageId, text) {3299 async onFinishStreaming(messageId, text) {
3269 this.hideMessageButtons(this.messageId);3300 this.markUIGenStopped();
3270 this.onProgressStreaming(messageId, text, true);3301 await this.onProgressStreaming(messageId, text, true);
3271 addCopyToCodeBlocks($(`#chat .mes[mesid="${messageId}"]`));3302 addCopyToCodeBlocks($(`#chat .mes[mesid="${messageId}"]`));
32723303
3304 await this.reasoningHandler.finish(messageId);
3305
3273 if (Array.isArray(this.swipes) && this.swipes.length > 0) {3306 if (Array.isArray(this.swipes) && this.swipes.length > 0) {
3274 const message = chat[messageId];3307 const message = chat[messageId];
3275 const swipeInfo = {3308 const swipeInfo = {
@@ -3297,39 +3330,11 @@ class StreamingProcessor {
3297 unblockGeneration();3330 unblockGeneration();
3298 generatedPromptCache = '';3331 generatedPromptCache = '';
32993332
3300 //console.log("Generated text size:", text.length, text)
3301
3302 const isAborted = this.abortController.signal.aborted;3333 const isAborted = this.abortController.signal.aborted;
3303 if (power_user.auto_swipe && !isAborted) {3334 if (!isAborted && power_user.auto_swipe && generatedTextFiltered(text)) {
3304 function containsBlacklistedWords(str, blacklist, threshold) {3335 return swipe_right();
3305 const regex = new RegExp(`\\b(${blacklist.join('|')})\\b`, 'gi');
3306 const matches = str.match(regex) || [];
3307 return matches.length >= threshold;
3308 }
3309
3310 const generatedTextFiltered = (text) => {
3311 if (text) {
3312 if (power_user.auto_swipe_minimum_length) {
3313 if (text.length < power_user.auto_swipe_minimum_length && text.length !== 0) {
3314 console.log('Generated text size too small');
3315 return true;
3316 }
3317 }
3318 if (power_user.auto_swipe_blacklist_threshold) {
3319 if (containsBlacklistedWords(text, power_user.auto_swipe_blacklist, power_user.auto_swipe_blacklist_threshold)) {
3320 console.log('Generated text has blacklisted words');
3321 return true;
3322 }3336 }
3323 }
3324 }
3325 return false;
3326 };
33273337
3328 if (generatedTextFiltered(text)) {
3329 swipe_right();
3330 return;
3331 }
3332 }
3333 playMessageSound();3338 playMessageSound();
3334 }3339 }
33353340
@@ -3337,7 +3342,7 @@ class StreamingProcessor {
3337 this.abortController.abort();3342 this.abortController.abort();
3338 this.isStopped = true;3343 this.isStopped = true;
33393344
3340 this.hideMessageButtons(this.messageId);3345 this.markUIGenStopped();
3341 generatedPromptCache = '';3346 generatedPromptCache = '';
3342 unblockGeneration();3347 unblockGeneration();
33433348
@@ -3387,8 +3392,8 @@ class StreamingProcessor {
3387 const timestamps = [];3392 const timestamps = [];
3388 for await (const { text, swipes, logprobs, toolCalls, state } of this.generator()) {3393 for await (const { text, swipes, logprobs, toolCalls, state } of this.generator()) {
3389 timestamps.push(Date.now());3394 timestamps.push(Date.now());
3390 if (this.isStopped) {3395 if (this.isStopped || this.abortController.signal.aborted) {
3391 return;3396 return this.result;
3392 }3397 }
33933398
3394 this.toolCalls = toolCalls;3399 this.toolCalls = toolCalls;
@@ -3397,9 +3402,10 @@ class StreamingProcessor {
3397 if (logprobs) {3402 if (logprobs) {
3398 this.messageLogprobs.push(...(Array.isArray(logprobs) ? logprobs : [logprobs]));3403 this.messageLogprobs.push(...(Array.isArray(logprobs) ? logprobs : [logprobs]));
3399 }3404 }
3400 this.reasoning = state?.reasoning ?? '';3405 // Get the updated reasoning string into the handler
3406 this.reasoningHandler.updateReasoning(this.messageId, state?.reasoning);
3401 await eventSource.emit(event_types.STREAM_TOKEN_RECEIVED, text);3407 await eventSource.emit(event_types.STREAM_TOKEN_RECEIVED, text);
3402 await sw.tick(() => this.onProgressStreaming(this.messageId, this.continueMessage + text));3408 await sw.tick(async () => await this.onProgressStreaming(this.messageId, this.continueMessage + text));
3403 }3409 }
3404 const seconds = (timestamps[timestamps.length - 1] - timestamps[0]) / 1000;3410 const seconds = (timestamps[timestamps.length - 1] - timestamps[0]) / 1000;
3405 console.warn(`Stream stats: ${timestamps.length} tokens, ${seconds.toFixed(2)} seconds, rate: ${Number(timestamps.length / seconds).toFixed(2)} TPS`);3411 console.warn(`Stream stats: ${timestamps.length} tokens, ${seconds.toFixed(2)} seconds, rate: ${Number(timestamps.length / seconds).toFixed(2)} TPS`);
@@ -3475,7 +3481,7 @@ export async function generateRaw(prompt, api, instructOverride, quietToLoud, sy
3475 break;3481 break;
3476 }3482 }
3477 case 'textgenerationwebui':3483 case 'textgenerationwebui':
3478 generateData = getTextGenGenerationData(prompt, amount_gen, false, false, null, 'quiet');3484 generateData = await getTextGenGenerationData(prompt, amount_gen, false, false, null, 'quiet');
3479 TempResponseLength.restore(api);3485 TempResponseLength.restore(api);
3480 break;3486 break;
3481 case 'openai': {3487 case 'openai': {
@@ -3864,14 +3870,6 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
3864 coreChat.pop();3870 coreChat.pop();
3865 }3871 }
38663872
3867 const reasoning = new PromptReasoning();
3868 for (let i = coreChat.length - 1; i >= 0; i--) {
3869 if (reasoning.isLimitReached()) {
3870 break;
3871 }
3872 coreChat[i] = { ...coreChat[i], mes: reasoning.addToMessage(coreChat[i].mes, coreChat[i].extra?.reasoning) };
3873 }
3874
3875 coreChat = await Promise.all(coreChat.map(async (chatItem, index) => {3873 coreChat = await Promise.all(coreChat.map(async (chatItem, index) => {
3876 let message = chatItem.mes;3874 let message = chatItem.mes;
3877 let regexType = chatItem.is_user ? regex_placement.USER_INPUT : regex_placement.AI_OUTPUT;3875 let regexType = chatItem.is_user ? regex_placement.USER_INPUT : regex_placement.AI_OUTPUT;
@@ -3891,6 +3889,27 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
3891 };3889 };
3892 }));3890 }));
38933891
3892 const reasoning = new PromptReasoning();
3893 for (let i = coreChat.length - 1; i >= 0; i--) {
3894 const depth = coreChat.length - i - 1;
3895 const isPrefix = isContinue && i === coreChat.length - 1;
3896 coreChat[i] = {
3897 ...coreChat[i],
3898 mes: reasoning.addToMessage(
3899 coreChat[i].mes,
3900 getRegexedString(
3901 String(coreChat[i].extra?.reasoning ?? ''),
3902 regex_placement.REASONING,
3903 { isPrompt: true, depth: depth },
3904 ),
3905 isPrefix,
3906 ),
3907 };
3908 if (reasoning.isLimitReached()) {
3909 break;
3910 }
3911 }
3912
3894 // Determine token limit3913 // Determine token limit
3895 let this_max_context = getMaxContextSize();3914 let this_max_context = getMaxContextSize();
38963915
@@ -4449,7 +4468,7 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
4449 // For prompt bit itemization4468 // For prompt bit itemization
4450 let mesSendString = '';4469 let mesSendString = '';
44514470
4452 function getCombinedPrompt(isNegative) {4471 async function getCombinedPrompt(isNegative) {
4453 // Only return if the guidance scale doesn't exist or the value is 14472 // Only return if the guidance scale doesn't exist or the value is 1
4454 // Also don't return if constructing the neutral prompt4473 // Also don't return if constructing the neutral prompt
4455 if (isNegative && !useCfgPrompt) {4474 if (isNegative && !useCfgPrompt) {
@@ -4476,10 +4495,16 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
4476 // TODO: Make all extension prompts use an array/splice method4495 // TODO: Make all extension prompts use an array/splice method
4477 const lengthDiff = mesSend.length - cfgPrompt.depth;4496 const lengthDiff = mesSend.length - cfgPrompt.depth;
4478 const cfgDepth = lengthDiff >= 0 ? lengthDiff : 0;4497 const cfgDepth = lengthDiff >= 0 ? lengthDiff : 0;
4498 const cfgMessage = finalMesSend[cfgDepth];
4499 if (cfgMessage) {
4500 if (!Array.isArray(finalMesSend[cfgDepth].extensionPrompts)) {
4501 finalMesSend[cfgDepth].extensionPrompts = [];
4502 }
4479 finalMesSend[cfgDepth].extensionPrompts.push(`${cfgPrompt.value}\n`);4503 finalMesSend[cfgDepth].extensionPrompts.push(`${cfgPrompt.value}\n`);
4480 }4504 }
4481 }4505 }
4482 }4506 }
4507 }
44834508
4484 // Add prompt bias after everything else4509 // Add prompt bias after everything else
4485 // Always run with continue4510 // Always run with continue
@@ -4552,13 +4577,13 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
4552 };4577 };
45534578
4554 // Before returning the combined prompt, give available context related information to all subscribers.4579 // Before returning the combined prompt, give available context related information to all subscribers.
4555 eventSource.emitAndWait(event_types.GENERATE_BEFORE_COMBINE_PROMPTS, data);4580 await eventSource.emit(event_types.GENERATE_BEFORE_COMBINE_PROMPTS, data);
45564581
4557 // If one or multiple subscribers return a value, forfeit the responsibillity of flattening the context.4582 // If one or multiple subscribers return a value, forfeit the responsibillity of flattening the context.
4558 return !data.combinedPrompt ? combine() : data.combinedPrompt;4583 return !data.combinedPrompt ? combine() : data.combinedPrompt;
4559 }4584 }
45604585
4561 let finalPrompt = getCombinedPrompt(false);4586 let finalPrompt = await getCombinedPrompt(false);
45624587
4563 const eventData = { prompt: finalPrompt, dryRun: dryRun };4588 const eventData = { prompt: finalPrompt, dryRun: dryRun };
4564 await eventSource.emit(event_types.GENERATE_AFTER_COMBINE_PROMPTS, eventData);4589 await eventSource.emit(event_types.GENERATE_AFTER_COMBINE_PROMPTS, eventData);
@@ -4592,8 +4617,8 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
4592 }4617 }
4593 break;4618 break;
4594 case 'textgenerationwebui': {4619 case 'textgenerationwebui': {
4595 const cfgValues = useCfgPrompt ? { guidanceScale: cfgGuidanceScale, negativePrompt: getCombinedPrompt(true) } : null;4620 const cfgValues = useCfgPrompt ? { guidanceScale: cfgGuidanceScale, negativePrompt: await getCombinedPrompt(true) } : null;
4596 generate_data = getTextGenGenerationData(finalPrompt, maxLength, isImpersonate, isContinue, cfgValues, type);4621 generate_data = await getTextGenGenerationData(finalPrompt, maxLength, isImpersonate, isContinue, cfgValues, type);
4597 break;4622 break;
4598 }4623 }
4599 case 'novel': {4624 case 'novel': {
@@ -4799,6 +4824,11 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
4799 const swipes = extractMultiSwipes(data, type);4824 const swipes = extractMultiSwipes(data, type);
48004825
4801 messageChunk = cleanUpMessage(getMessage, isImpersonate, isContinue, false);4826 messageChunk = cleanUpMessage(getMessage, isImpersonate, isContinue, false);
4827 reasoning = getRegexedString(reasoning, regex_placement.REASONING);
4828
4829 if (power_user.trim_spaces) {
4830 reasoning = reasoning.trim();
4831 }
48024832
4803 if (isContinue) {4833 if (isContinue) {
4804 getMessage = continue_mag + getMessage;4834 getMessage = continue_mag + getMessage;
@@ -4857,32 +4887,9 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
4857 }4887 }
48584888
4859 const isAborted = abortController && abortController.signal.aborted;4889 const isAborted = abortController && abortController.signal.aborted;
4860 if (power_user.auto_swipe && !isAborted) {4890 if (!isAborted && power_user.auto_swipe && generatedTextFiltered(getMessage)) {
4861 console.debug('checking for autoswipeblacklist on non-streaming message');
4862 function containsBlacklistedWords(getMessage, blacklist, threshold) {
4863 console.debug('checking blacklisted words');
4864 const regex = new RegExp(`\\b(${blacklist.join('|')})\\b`, 'gi');
4865 const matches = getMessage.match(regex) || [];
4866 return matches.length >= threshold;
4867 }
4868
4869 const generatedTextFiltered = (getMessage) => {
4870 if (power_user.auto_swipe_blacklist_threshold) {
4871 if (containsBlacklistedWords(getMessage, power_user.auto_swipe_blacklist, power_user.auto_swipe_blacklist_threshold)) {
4872 console.debug('Generated text has blacklisted words');
4873 return true;
4874 }
4875 }
4876
4877 return false;
4878 };
4879 if (generatedTextFiltered(getMessage)) {
4880 console.debug('swiping right automatically');
4881 is_send_press = false;4891 is_send_press = false;
4882 swipe_right();4892 return swipe_right();
4883 // TODO: do we want to resolve after an auto-swipe?
4884 return;
4885 }
4886 }4893 }
48874894
4888 console.debug('/api/chats/save called by /Generate');4895 console.debug('/api/chats/save called by /Generate');
@@ -5537,7 +5544,7 @@ async function promptItemize(itemizedPrompts, requestedMesId) {
5537 toastr.info(t`Copied!`);5544 toastr.info(t`Copied!`);
5538 });5545 });
55395546
5540 popup.dlg.querySelector('#showRawPrompt').addEventListener('click', function () {5547 popup.dlg.querySelector('#showRawPrompt').addEventListener('click', async function () {
5541 //console.log(itemizedPrompts[PromptArrayItemForRawPromptDisplay].rawPrompt);5548 //console.log(itemizedPrompts[PromptArrayItemForRawPromptDisplay].rawPrompt);
5542 console.log(PromptArrayItemForRawPromptDisplay);5549 console.log(PromptArrayItemForRawPromptDisplay);
5543 console.log(itemizedPrompts);5550 console.log(itemizedPrompts);
@@ -5545,6 +5552,17 @@ async function promptItemize(itemizedPrompts, requestedMesId) {
55455552
5546 const rawPrompt = flatten(itemizedPrompts[PromptArrayItemForRawPromptDisplay].rawPrompt);5553 const rawPrompt = flatten(itemizedPrompts[PromptArrayItemForRawPromptDisplay].rawPrompt);
55475554
5555 // Mobile needs special handholding. The side-view on the popup wouldn't work,
5556 // so we just show an additional popup for this.
5557 if (isMobile()) {
5558 const content = document.createElement('div');
5559 content.classList.add('tokenItemizingMaintext');
5560 content.innerText = rawPrompt;
5561 const popup = new Popup(content, POPUP_TYPE.TEXT, null, { allowVerticalScrolling: true, leftAlign: true });
5562 await popup.show();
5563 return;
5564 }
5565
5548 //let DisplayStringifiedPrompt = JSON.stringify(itemizedPrompts[PromptArrayItemForRawPromptDisplay].rawPrompt).replace(/\n+/g, '<br>');5566 //let DisplayStringifiedPrompt = JSON.stringify(itemizedPrompts[PromptArrayItemForRawPromptDisplay].rawPrompt).replace(/\n+/g, '<br>');
5549 const rawPromptWrapper = document.getElementById('rawPromptWrapper');5567 const rawPromptWrapper = document.getElementById('rawPromptWrapper');
5550 rawPromptWrapper.innerText = rawPrompt;5568 rawPromptWrapper.innerText = rawPrompt;
@@ -5728,26 +5746,6 @@ function extractMessageFromData(data) {
5728}5746}
57295747
5730/**5748/**
5731 * Extracts the reasoning from the response data.
5732 * @param {object} data Response data
5733 * @returns {string} Extracted reasoning
5734 */
5735function extractReasoningFromData(data) {
5736 if (main_api === 'openai' && oai_settings.show_thoughts) {
5737 switch (oai_settings.chat_completion_source) {
5738 case chat_completion_sources.DEEPSEEK:
5739 return data?.choices?.[0]?.message?.reasoning_content ?? '';
5740 case chat_completion_sources.OPENROUTER:
5741 return data?.choices?.[0]?.message?.reasoning ?? '';
5742 case chat_completion_sources.MAKERSUITE:
5743 return data?.responseContent?.parts?.filter(part => part.thought)?.map(part => part.text)?.join('\n\n') ?? '';
5744 }
5745 }
5746
5747 return '';
5748}
5749
5750/**
5751 * Extracts multiswipe swipes from the response data.5749 * Extracts multiswipe swipes from the response data.
5752 * @param {Object} data Response data5750 * @param {Object} data Response data
5753 * @param {string} type Type of generation5751 * @param {string} type Type of generation
@@ -5937,6 +5935,15 @@ export async function saveReply(type, getMessage, fromStreaming, title, swipes,
5937 chat[chat.length - 1]['extra'] = {};5935 chat[chat.length - 1]['extra'] = {};
5938 }5936 }
59395937
5938 // Coerce null/undefined to empty string
5939 if (chat.length && !chat[chat.length - 1]['extra']['reasoning']) {
5940 chat[chat.length - 1]['extra']['reasoning'] = '';
5941 }
5942
5943 if (!reasoning) {
5944 reasoning = '';
5945 }
5946
5940 let oldMessage = '';5947 let oldMessage = '';
5941 const generationFinished = new Date();5948 const generationFinished = new Date();
5942 const img = extractImageFromMessage(getMessage);5949 const img = extractImageFromMessage(getMessage);
@@ -5953,6 +5960,7 @@ export async function saveReply(type, getMessage, fromStreaming, title, swipes,
5953 chat[chat.length - 1]['extra']['api'] = getGeneratingApi();5960 chat[chat.length - 1]['extra']['api'] = getGeneratingApi();
5954 chat[chat.length - 1]['extra']['model'] = getGeneratingModel();5961 chat[chat.length - 1]['extra']['model'] = getGeneratingModel();
5955 chat[chat.length - 1]['extra']['reasoning'] = reasoning;5962 chat[chat.length - 1]['extra']['reasoning'] = reasoning;
5963 chat[chat.length - 1]['extra']['reasoning_duration'] = null;
5956 if (power_user.message_token_count_enabled) {5964 if (power_user.message_token_count_enabled) {
5957 const tokenCountText = (reasoning || '') + chat[chat.length - 1]['mes'];5965 const tokenCountText = (reasoning || '') + chat[chat.length - 1]['mes'];
5958 chat[chat.length - 1]['extra']['token_count'] = await getTokenCountAsync(tokenCountText, 0);5966 chat[chat.length - 1]['extra']['token_count'] = await getTokenCountAsync(tokenCountText, 0);
@@ -5974,7 +5982,8 @@ export async function saveReply(type, getMessage, fromStreaming, title, swipes,
5974 chat[chat.length - 1]['send_date'] = getMessageTimeStamp();5982 chat[chat.length - 1]['send_date'] = getMessageTimeStamp();
5975 chat[chat.length - 1]['extra']['api'] = getGeneratingApi();5983 chat[chat.length - 1]['extra']['api'] = getGeneratingApi();
5976 chat[chat.length - 1]['extra']['model'] = getGeneratingModel();5984 chat[chat.length - 1]['extra']['model'] = getGeneratingModel();
5977 chat[chat.length - 1]['extra']['reasoning'] += reasoning;5985 chat[chat.length - 1]['extra']['reasoning'] = reasoning;
5986 chat[chat.length - 1]['extra']['reasoning_duration'] = null;
5978 if (power_user.message_token_count_enabled) {5987 if (power_user.message_token_count_enabled) {
5979 const tokenCountText = (reasoning || '') + chat[chat.length - 1]['mes'];5988 const tokenCountText = (reasoning || '') + chat[chat.length - 1]['mes'];
5980 chat[chat.length - 1]['extra']['token_count'] = await getTokenCountAsync(tokenCountText, 0);5989 chat[chat.length - 1]['extra']['token_count'] = await getTokenCountAsync(tokenCountText, 0);
@@ -5994,6 +6003,7 @@ export async function saveReply(type, getMessage, fromStreaming, title, swipes,
5994 chat[chat.length - 1]['extra']['api'] = getGeneratingApi();6003 chat[chat.length - 1]['extra']['api'] = getGeneratingApi();
5995 chat[chat.length - 1]['extra']['model'] = getGeneratingModel();6004 chat[chat.length - 1]['extra']['model'] = getGeneratingModel();
5996 chat[chat.length - 1]['extra']['reasoning'] += reasoning;6005 chat[chat.length - 1]['extra']['reasoning'] += reasoning;
6006 // We don't know if the reasoning duration extended, so we don't update it here on purpose.
5997 if (power_user.message_token_count_enabled) {6007 if (power_user.message_token_count_enabled) {
5998 const tokenCountText = (reasoning || '') + chat[chat.length - 1]['mes'];6008 const tokenCountText = (reasoning || '') + chat[chat.length - 1]['mes'];
5999 chat[chat.length - 1]['extra']['token_count'] = await getTokenCountAsync(tokenCountText, 0);6009 chat[chat.length - 1]['extra']['token_count'] = await getTokenCountAsync(tokenCountText, 0);
@@ -6013,6 +6023,7 @@ export async function saveReply(type, getMessage, fromStreaming, title, swipes,
6013 chat[chat.length - 1]['extra']['api'] = getGeneratingApi();6023 chat[chat.length - 1]['extra']['api'] = getGeneratingApi();
6014 chat[chat.length - 1]['extra']['model'] = getGeneratingModel();6024 chat[chat.length - 1]['extra']['model'] = getGeneratingModel();
6015 chat[chat.length - 1]['extra']['reasoning'] = reasoning;6025 chat[chat.length - 1]['extra']['reasoning'] = reasoning;
6026 chat[chat.length - 1]['extra']['reasoning_duration'] = null;
6016 if (power_user.trim_spaces) {6027 if (power_user.trim_spaces) {
6017 getMessage = getMessage.trim();6028 getMessage = getMessage.trim();
6018 }6029 }
@@ -6153,20 +6164,21 @@ function extractImageFromMessage(getMessage) {
6153 return { getMessage, image, title };6164 return { getMessage, image, title };
6154}6165}
61556166
6167/**
6168 * A function mainly used to switch 'generating' state - setting it to false and activating the buttons again
6169 */
6156export function activateSendButtons() {6170export function activateSendButtons() {
6157 is_send_press = false;6171 is_send_press = false;
6158 $('#send_but').removeClass('displayNone');
6159 $('#mes_continue').removeClass('displayNone');
6160 $('#mes_impersonate').removeClass('displayNone');
6161 $('.mes_buttons:last').show();
6162 hideStopButton();6172 hideStopButton();
6173 delete document.body.dataset.generating;
6163}6174}
61646175
6176/**
6177 * A function mainly used to switch 'generating' state - setting it to true and deactivating the buttons
6178 */
6165export function deactivateSendButtons() {6179export function deactivateSendButtons() {
6166 $('#send_but').addClass('displayNone');
6167 $('#mes_continue').addClass('displayNone');
6168 $('#mes_impersonate').addClass('displayNone');
6169 showStopButton();6180 showStopButton();
6181 document.body.dataset.generating = 'true';
6170}6182}
61716183
6172export function resetChatState() {6184export function resetChatState() {
@@ -6259,9 +6271,35 @@ export async function renameCharacter(name = null, { silent = false, renameChats
6259 const data = await response.json();6271 const data = await response.json();
6260 const newAvatar = data.avatar;6272 const newAvatar = data.avatar;
62616273
6262 // Replace tags list6274 const oldName = getCharaFilename(null, { manualAvatarKey: oldAvatar });
6275 const newName = getCharaFilename(null, { manualAvatarKey: newAvatar });
6276
6277 // Replace other auxillery fields where was referenced by avatar key
6278 // Tag List
6263 renameTagKey(oldAvatar, newAvatar);6279 renameTagKey(oldAvatar, newAvatar);
62646280
6281 // Addtional lore books
6282 const charLore = world_info.charLore?.find(x => x.name == oldName);
6283 if (charLore) {
6284 charLore.name = newName;
6285 saveSettingsDebounced();
6286 }
6287
6288 // Char-bound Author's Notes
6289 const charNote = extension_settings.note.chara?.find(x => x.name == oldName);
6290 if (charNote) {
6291 charNote.name = newName;
6292 saveSettingsDebounced();
6293 }
6294
6295 // Update active character, if the current one was the currently active one
6296 if (active_character === oldAvatar) {
6297 active_character = newAvatar;
6298 saveSettingsDebounced();
6299 }
6300
6301 await eventSource.emit(event_types.CHARACTER_RENAMED, oldAvatar, newAvatar);
6302
6265 // Reload characters list6303 // Reload characters list
6266 await getCharacters();6304 await getCharacters();
62676305
@@ -6868,10 +6906,11 @@ export async function getSettings() {
6868 $('#your_name').text(name1);6906 $('#your_name').text(name1);
6869 }6907 }
68706908
6909 accountStorage.init(settings?.accountStorage);
6871 await setUserControls(data.enable_accounts);6910 await setUserControls(data.enable_accounts);
68726911
6873 // Allow subscribers to mutate settings6912 // Allow subscribers to mutate settings
6874 eventSource.emit(event_types.SETTINGS_LOADED_BEFORE, settings);6913 await eventSource.emit(event_types.SETTINGS_LOADED_BEFORE, settings);
68756914
6876 //Load KoboldAI settings6915 //Load KoboldAI settings
6877 koboldai_setting_names = data.koboldai_setting_names;6916 koboldai_setting_names = data.koboldai_setting_names;
@@ -6968,7 +7007,7 @@ export async function getSettings() {
6968 loadProxyPresets(settings);7007 loadProxyPresets(settings);
69697008
6970 // Allow subscribers to mutate settings7009 // Allow subscribers to mutate settings
6971 eventSource.emit(event_types.SETTINGS_LOADED_AFTER, settings);7010 await eventSource.emit(event_types.SETTINGS_LOADED_AFTER, settings);
69727011
6973 // Set context size after loading power user (may override the max value)7012 // Set context size after loading power user (may override the max value)
6974 $('#max_context').val(max_context);7013 $('#max_context').val(max_context);
@@ -7028,7 +7067,7 @@ export async function getSettings() {
7028 }7067 }
7029 await validateDisabledSamplers();7068 await validateDisabledSamplers();
7030 settingsReady = true;7069 settingsReady = true;
7031 eventSource.emit(event_types.SETTINGS_LOADED);7070 await eventSource.emit(event_types.SETTINGS_LOADED);
7032}7071}
70337072
7034function selectKoboldGuiPreset() {7073function selectKoboldGuiPreset() {
@@ -7039,7 +7078,8 @@ function selectKoboldGuiPreset() {
70397078
7040export async function saveSettings(loopCounter = 0) {7079export async function saveSettings(loopCounter = 0) {
7041 if (!settingsReady) {7080 if (!settingsReady) {
7042 console.warn('Settings not ready, aborting save');7081 console.warn('Settings not ready, scheduling another save');
7082 saveSettingsDebounced();
7043 return;7083 return;
7044 }7084 }
70457085
@@ -7060,6 +7100,7 @@ export async function saveSettings(loopCounter = 0) {
7060 url: '/api/settings/save',7100 url: '/api/settings/save',
7061 data: JSON.stringify({7101 data: JSON.stringify({
7062 firstRun: firstRun,7102 firstRun: firstRun,
7103 accountStorage: accountStorage.getState(),
7063 currentVersion: currentVersion,7104 currentVersion: currentVersion,
7064 username: name1,7105 username: name1,
7065 active_character: active_character,7106 active_character: active_character,
@@ -7125,8 +7166,10 @@ export function setGenerationParamsFromPreset(preset) {
7125// Common code for message editor done and auto-save7166// Common code for message editor done and auto-save
7126function updateMessage(div) {7167function updateMessage(div) {
7127 const mesBlock = div.closest('.mes_block');7168 const mesBlock = div.closest('.mes_block');
7128 let text = mesBlock.find('.edit_textarea').val();7169 let text = mesBlock.find('.edit_textarea').val()
7129 const mes = chat[this_edit_mes_id];7170 ?? mesBlock.find('.mes_text').text();
7171 const mesElement = div.closest('.mes');
7172 const mes = chat[mesElement.attr('mesid')];
71307173
7131 let regexPlacement;7174 let regexPlacement;
7132 if (mes.is_user) {7175 if (mes.is_user) {
@@ -7210,9 +7253,11 @@ function messageEditAuto(div) {
7210 mes.is_system,7253 mes.is_system,
7211 mes.is_user,7254 mes.is_user,
7212 this_edit_mes_id,7255 this_edit_mes_id,
7256 {},
7257 false,
7213 ));7258 ));
7214 mesBlock.find('.mes_bias').empty();7259 mesBlock.find('.mes_bias').empty();
7215 mesBlock.find('.mes_bias').append(messageFormatting(bias, '', false, false, -1));7260 mesBlock.find('.mes_bias').append(messageFormatting(bias, '', false, false, -1, {}, false));
7216 saveChatDebounced();7261 saveChatDebounced();
7217}7262}
72187263
@@ -7234,13 +7279,20 @@ async function messageEditDone(div) {
7234 mes.is_system,7279 mes.is_system,
7235 mes.is_user,7280 mes.is_user,
7236 this_edit_mes_id,7281 this_edit_mes_id,
7282 {},
7283 false,
7237 ),7284 ),
7238 );7285 );
7239 mesBlock.find('.mes_bias').empty();7286 mesBlock.find('.mes_bias').empty();
7240 mesBlock.find('.mes_bias').append(messageFormatting(bias, '', false, false, -1));7287 mesBlock.find('.mes_bias').append(messageFormatting(bias, '', false, false, -1, {}, false));
7241 appendMediaToMessage(mes, div.closest('.mes'));7288 appendMediaToMessage(mes, div.closest('.mes'));
7242 addCopyToCodeBlocks(div.closest('.mes'));7289 addCopyToCodeBlocks(div.closest('.mes'));
72437290
7291 const reasoningEditDone = mesBlock.find('.mes_reasoning_edit_done:visible');
7292 if (reasoningEditDone.length > 0) {
7293 reasoningEditDone.trigger('click');
7294 }
7295
7244 await eventSource.emit(event_types.MESSAGE_UPDATED, this_edit_mes_id);7296 await eventSource.emit(event_types.MESSAGE_UPDATED, this_edit_mes_id);
7245 this_edit_mes_id = undefined;7297 this_edit_mes_id = undefined;
7246 await saveChatConditional();7298 await saveChatConditional();
@@ -7504,7 +7556,7 @@ export function select_rm_info(type, charId, previousCharId = null) {
7504 }7556 }
75057557
7506 try {7558 try {
7507 const perPage = Number(localStorage.getItem('Characters_PerPage')) || per_page_default;7559 const perPage = Number(accountStorage.getItem('Characters_PerPage')) || per_page_default;
7508 const page = Math.floor(charIndex / perPage) + 1;7560 const page = Math.floor(charIndex / perPage) + 1;
7509 const selector = `#rm_print_characters_block [title*="${avatarFileName}"]`;7561 const selector = `#rm_print_characters_block [title*="${avatarFileName}"]`;
7510 $('#rm_print_characters_pagination').pagination('go', page);7562 $('#rm_print_characters_pagination').pagination('go', page);
@@ -7536,7 +7588,7 @@ export function select_rm_info(type, charId, previousCharId = null) {
7536 return;7588 return;
7537 }7589 }
75387590
7539 const perPage = Number(localStorage.getItem('Characters_PerPage')) || per_page_default;7591 const perPage = Number(accountStorage.getItem('Characters_PerPage')) || per_page_default;
7540 const page = Math.floor(charIndex / perPage) + 1;7592 const page = Math.floor(charIndex / perPage) + 1;
7541 $('#rm_print_characters_pagination').pagination('go', page);7593 $('#rm_print_characters_pagination').pagination('go', page);
7542 const selector = `#rm_print_characters_block [grid="${charId}"]`;7594 const selector = `#rm_print_characters_block [grid="${charId}"]`;
@@ -8741,11 +8793,6 @@ const swipe_right = () => {
8741 easing: animation_easing,8793 easing: animation_easing,
8742 queue: false,8794 queue: false,
8743 complete: async function () {8795 complete: async function () {
8744 /*if (!selected_group) {
8745 var typingIndicator = $("#typing_indicator_template .typing_indicator").clone();
8746 typingIndicator.find(".typing_indicator_name").text(characters[this_chid].name);
8747 } */
8748 /* $("#chat").append(typingIndicator); */
8749 const is_animation_scroll = ($('#chat').scrollTop() >= ($('#chat').prop('scrollHeight') - $('#chat').outerHeight()) - 10);8796 const is_animation_scroll = ($('#chat').scrollTop() >= ($('#chat').prop('scrollHeight') - $('#chat').outerHeight()) - 10);
8750 //console.log(parseInt(chat[chat.length-1]['swipe_id']));8797 //console.log(parseInt(chat[chat.length-1]['swipe_id']));
8751 //console.log(chat[chat.length-1]['swipes'].length);8798 //console.log(chat[chat.length-1]['swipes'].length);
@@ -8756,7 +8803,7 @@ const swipe_right = () => {
8756 // resets the timer8803 // resets the timer
8757 swipeMessage.find('.mes_timer').html('');8804 swipeMessage.find('.mes_timer').html('');
8758 swipeMessage.find('.tokenCounterDisplay').text('');8805 swipeMessage.find('.tokenCounterDisplay').text('');
8759 swipeMessage.find('.mes_reasoning').html('');8806 updateReasoningUI(swipeMessage, { reset: true });
8760 } else {8807 } else {
8761 //console.log('showing previously generated swipe candidate, or "..."');8808 //console.log('showing previously generated swipe candidate, or "..."');
8762 //console.log('onclick right swipe calling addOneMessage');8809 //console.log('onclick right swipe calling addOneMessage');
@@ -8806,7 +8853,6 @@ const swipe_right = () => {
8806 if (run_generate && !is_send_press && parseInt(chat[chat.length - 1]['swipe_id']) === chat[chat.length - 1]['swipes'].length) {8853 if (run_generate && !is_send_press && parseInt(chat[chat.length - 1]['swipe_id']) === chat[chat.length - 1]['swipes'].length) {
8807 console.debug('caught here 2');8854 console.debug('caught here 2');
8808 is_send_press = true;8855 is_send_press = true;
8809 $('.mes_buttons:last').hide();
8810 await Generate('swipe');8856 await Generate('swipe');
8811 } else {8857 } else {
8812 if (parseInt(chat[chat.length - 1]['swipe_id']) !== chat[chat.length - 1]['swipes'].length) {8858 if (parseInt(chat[chat.length - 1]['swipe_id']) !== chat[chat.length - 1]['swipes'].length) {
@@ -9408,6 +9454,9 @@ export async function deleteCharacter(characterKey, { deleteChats = true } = {})
9408 continue;9454 continue;
9409 }9455 }
94109456
9457 accountStorage.removeItem(`AlertWI_${character.avatar}`);
9458 accountStorage.removeItem(`AlertRegex_${character.avatar}`);
9459 accountStorage.removeItem(`mediaWarningShown:${character.avatar}`);
9411 delete tag_map[character.avatar];9460 delete tag_map[character.avatar];
9412 select_rm_info('char_delete', character.name);9461 select_rm_info('char_delete', character.name);
94139462
@@ -9610,8 +9659,8 @@ function addDebugFunctions() {
9610 });9659 });
96119660
9612 registerDebugFunction('toggleRegenerateWarning', 'Toggle Ctrl+Enter regeneration confirmation', 'Toggle the warning when regenerating a message with a Ctrl+Enter hotkey.', () => {9661 registerDebugFunction('toggleRegenerateWarning', 'Toggle Ctrl+Enter regeneration confirmation', 'Toggle the warning when regenerating a message with a Ctrl+Enter hotkey.', () => {
9613 localStorage.setItem('RegenerateWithCtrlEnter', localStorage.getItem('RegenerateWithCtrlEnter') === 'true' ? 'false' : 'true');9662 accountStorage.setItem('RegenerateWithCtrlEnter', accountStorage.getItem('RegenerateWithCtrlEnter') === 'true' ? 'false' : 'true');
9614 toastr.info('Regenerate warning is now ' + (localStorage.getItem('RegenerateWithCtrlEnter') === 'true' ? 'disabled' : 'enabled'));9663 toastr.info('Regenerate warning is now ' + (accountStorage.getItem('RegenerateWithCtrlEnter') === 'true' ? 'disabled' : 'enabled'));
9615 });9664 });
96169665
9617 registerDebugFunction('copySetup', 'Copy ST setup to clipboard [WIP]', 'Useful data when reporting bugs', async () => {9666 registerDebugFunction('copySetup', 'Copy ST setup to clipboard [WIP]', 'Useful data when reporting bugs', async () => {
@@ -10796,6 +10845,12 @@ jQuery(async function () {
10796 var edit_mes_id = $(this).closest('.mes').attr('mesid');10845 var edit_mes_id = $(this).closest('.mes').attr('mesid');
10797 this_edit_mes_id = edit_mes_id;10846 this_edit_mes_id = edit_mes_id;
1079810847
10848 // Also edit reasoning, if it exists
10849 const reasoningEdit = $(this).closest('.mes_block').find('.mes_reasoning_edit:visible');
10850 if (reasoningEdit.length > 0) {
10851 reasoningEdit.trigger('click');
10852 }
10853
10799 var text = chat[edit_mes_id]['mes'];10854 var text = chat[edit_mes_id]['mes'];
10800 if (chat[edit_mes_id]['is_user']) {10855 if (chat[edit_mes_id]['is_user']) {
10801 this_edit_mes_chname = name1;10856 this_edit_mes_chname = name1;
@@ -10923,10 +10978,17 @@ jQuery(async function () {
10923 chat[this_edit_mes_id].is_system,10978 chat[this_edit_mes_id].is_system,
10924 chat[this_edit_mes_id].is_user,10979 chat[this_edit_mes_id].is_user,
10925 this_edit_mes_id,10980 this_edit_mes_id,
10981 {},
10982 false,
10926 ));10983 ));
10927 appendMediaToMessage(chat[this_edit_mes_id], $(this).closest('.mes'));10984 appendMediaToMessage(chat[this_edit_mes_id], $(this).closest('.mes'));
10928 addCopyToCodeBlocks($(this).closest('.mes'));10985 addCopyToCodeBlocks($(this).closest('.mes'));
1092910986
10987 const reasoningEditDone = $(this).closest('.mes_block').find('.mes_reasoning_edit_cancel:visible');
10988 if (reasoningEditDone.length > 0) {
10989 reasoningEditDone.trigger('click');
10990 }
10991
10930 await eventSource.emit(event_types.MESSAGE_UPDATED, this_edit_mes_id);10992 await eventSource.emit(event_types.MESSAGE_UPDATED, this_edit_mes_id);
10931 this_edit_mes_id = undefined;10993 this_edit_mes_id = undefined;
10932 });10994 });
@@ -10990,7 +11052,7 @@ jQuery(async function () {
10990 });11052 });
1099111053
10992 $(document).on('click', '.mes_edit_copy', async function () {11054 $(document).on('click', '.mes_edit_copy', async function () {
10993 const confirmation = await callGenericPopup('Create a copy of this message?', POPUP_TYPE.CONFIRM);11055 const confirmation = await callGenericPopup(t`Create a copy of this message?`, POPUP_TYPE.CONFIRM);
10994 if (!confirmation) {11056 if (!confirmation) {
10995 return;11057 return;
10996 }11058 }
@@ -11469,7 +11531,7 @@ jQuery(async function () {
11469 );11531 );
11470 break;*/11532 break;*/
11471 default:11533 default:
11472 eventSource.emit('charManagementDropdown', target);11534 await eventSource.emit('charManagementDropdown', target);
11473 }11535 }
11474 $('#char-management-dropdown').prop('selectedIndex', 0);11536 $('#char-management-dropdown').prop('selectedIndex', 0);
11475 });11537 });
@@ -11631,7 +11693,7 @@ jQuery(async function () {
1163111693
11632 $(document).on('click', '.open_characters_library', async function () {11694 $(document).on('click', '.open_characters_library', async function () {
11633 await getCharacters();11695 await getCharacters();
11634 eventSource.emit(event_types.OPEN_CHARACTER_LIBRARY);11696 await eventSource.emit(event_types.OPEN_CHARACTER_LIBRARY);
11635 });11697 });
1163611698
11637 // Added here to prevent execution before script.js is loaded and get rid of quirky timeouts11699 // Added here to prevent execution before script.js is loaded and get rid of quirky timeouts
public/scripts/RossAscends-mods.js+58 -30
@@ -27,7 +27,6 @@ import {
27 send_on_enter_options,27 send_on_enter_options,
28} from './power-user.js';28} from './power-user.js';
2929
30import { LoadLocal, SaveLocal, LoadLocalBool } from './f-localStorage.js';
31import { selected_group, is_group_generating, openGroupById } from './group-chats.js';30import { selected_group, is_group_generating, openGroupById } from './group-chats.js';
32import { getTagKeyForEntity, applyTagsOnCharacterSelect } from './tags.js';31import { getTagKeyForEntity, applyTagsOnCharacterSelect } from './tags.js';
33import {32import {
@@ -41,6 +40,8 @@ import { textgen_types, textgenerationwebui_settings as textgen_settings, getTex
41import { debounce_timeout } from './constants.js';40import { debounce_timeout } from './constants.js';
4241
43import { Popup } from './popup.js';42import { Popup } from './popup.js';
43import { accountStorage } from './util/AccountStorage.js';
44import { getCurrentUserHandle } from './user.js';
4445
45var RPanelPin = document.getElementById('rm_button_panel_pin');46var RPanelPin = document.getElementById('rm_button_panel_pin');
46var LPanelPin = document.getElementById('lm_button_panel_pin');47var LPanelPin = document.getElementById('lm_button_panel_pin');
@@ -279,17 +280,32 @@ async function RA_autoloadchat() {
279 // active character is the name, we should look it up in the character list and get the id280 // active character is the name, we should look it up in the character list and get the id
280 if (active_character !== null && active_character !== undefined) {281 if (active_character !== null && active_character !== undefined) {
281 const active_character_id = characters.findIndex(x => getTagKeyForEntity(x) === active_character);282 const active_character_id = characters.findIndex(x => getTagKeyForEntity(x) === active_character);
282 if (active_character_id !== null) {283 if (active_character_id !== -1) {
283 await selectCharacterById(active_character_id);284 await selectCharacterById(active_character_id);
284285
285 // Do a little tomfoolery to spoof the tag selector286 // Do a little tomfoolery to spoof the tag selector
286 const selectedCharElement = $(`#rm_print_characters_block .character_select[chid="${active_character_id}"]`);287 const selectedCharElement = $(`#rm_print_characters_block .character_select[chid="${active_character_id}"]`);
287 applyTagsOnCharacterSelect.call(selectedCharElement);288 applyTagsOnCharacterSelect.call(selectedCharElement);
289 } else {
290 setActiveCharacter(null);
291 saveSettingsDebounced();
292 console.warn(`Currently active character with ID ${active_character} not found. Resetting to no active character.`);
288 }293 }
289 }294 }
290295
291 if (active_group !== null && active_group !== undefined) {296 if (active_group !== null && active_group !== undefined) {
292 await openGroupById(String(active_group));297 if (active_character) {
298 console.warn('Active character and active group are both set. Only active character will be loaded. Resetting active group.');
299 setActiveGroup(null);
300 saveSettingsDebounced();
301 } else {
302 const result = await openGroupById(String(active_group));
303 if (!result) {
304 setActiveGroup(null);
305 saveSettingsDebounced();
306 console.warn(`Currently active group with ID ${active_group} not found. Resetting to no active group.`);
307 }
308 }
293 }309 }
294310
295 // if the character list hadn't been loaded yet, try again.311 // if the character list hadn't been loaded yet, try again.
@@ -409,32 +425,34 @@ function RA_autoconnect(PrevApi) {
409function OpenNavPanels() {425function OpenNavPanels() {
410 if (!isMobile()) {426 if (!isMobile()) {
411 //auto-open R nav if locked and previously open427 //auto-open R nav if locked and previously open
412 if (LoadLocalBool('NavLockOn') == true && LoadLocalBool('NavOpened') == true) {428 if (accountStorage.getItem('NavLockOn') == 'true' && accountStorage.getItem('NavOpened') == 'true') {
413 //console.log("RA -- clicking right nav to open");429 //console.log("RA -- clicking right nav to open");
414 $('#rightNavDrawerIcon').click();430 $('#rightNavDrawerIcon').click();
415 }431 }
416432
417 //auto-open L nav if locked and previously open433 //auto-open L nav if locked and previously open
418 if (LoadLocalBool('LNavLockOn') == true && LoadLocalBool('LNavOpened') == true) {434 if (accountStorage.getItem('LNavLockOn') == 'true' && accountStorage.getItem('LNavOpened') == 'true') {
419 console.debug('RA -- clicking left nav to open');435 console.debug('RA -- clicking left nav to open');
420 $('#leftNavDrawerIcon').click();436 $('#leftNavDrawerIcon').click();
421 }437 }
422438
423 //auto-open WI if locked and previously open439 //auto-open WI if locked and previously open
424 if (LoadLocalBool('WINavLockOn') == true && LoadLocalBool('WINavOpened') == true) {440 if (accountStorage.getItem('WINavLockOn') == 'true' && accountStorage.getItem('WINavOpened') == 'true') {
425 console.debug('RA -- clicking WI to open');441 console.debug('RA -- clicking WI to open');
426 $('#WIDrawerIcon').click();442 $('#WIDrawerIcon').click();
427 }443 }
428 }444 }
429}445}
430446
447const getUserInputKey = () => getCurrentUserHandle() + '_userInput';
448
431function restoreUserInput() {449function restoreUserInput() {
432 if (!power_user.restore_user_input) {450 if (!power_user.restore_user_input) {
433 console.debug('restoreUserInput disabled');451 console.debug('restoreUserInput disabled');
434 return;452 return;
435 }453 }
436454
437 const userInput = LoadLocal('userInput');455 const userInput = localStorage.getItem(getUserInputKey());
438 if (userInput) {456 if (userInput) {
439 $('#send_textarea').val(userInput)[0].dispatchEvent(new Event('input', { bubbles: true }));457 $('#send_textarea').val(userInput)[0].dispatchEvent(new Event('input', { bubbles: true }));
440 }458 }
@@ -442,7 +460,8 @@ function restoreUserInput() {
442460
443function saveUserInput() {461function saveUserInput() {
444 const userInput = String($('#send_textarea').val());462 const userInput = String($('#send_textarea').val());
445 SaveLocal('userInput', userInput);463 localStorage.setItem(getUserInputKey(), userInput);
464 console.debug('User Input -- ', userInput);
446}465}
447const saveUserInputDebounced = debounce(saveUserInput);466const saveUserInputDebounced = debounce(saveUserInput);
448467
@@ -739,7 +758,7 @@ export function initRossMods() {
739758
740 //toggle pin class when lock toggle clicked759 //toggle pin class when lock toggle clicked
741 $(RPanelPin).on('click', function () {760 $(RPanelPin).on('click', function () {
742 SaveLocal('NavLockOn', $(RPanelPin).prop('checked'));761 accountStorage.setItem('NavLockOn', $(RPanelPin).prop('checked'));
743 if ($(RPanelPin).prop('checked') == true) {762 if ($(RPanelPin).prop('checked') == true) {
744 //console.log('adding pin class to right nav');763 //console.log('adding pin class to right nav');
745 $(RightNavPanel).addClass('pinnedOpen');764 $(RightNavPanel).addClass('pinnedOpen');
@@ -757,7 +776,7 @@ export function initRossMods() {
757 }776 }
758 });777 });
759 $(LPanelPin).on('click', function () {778 $(LPanelPin).on('click', function () {
760 SaveLocal('LNavLockOn', $(LPanelPin).prop('checked'));779 accountStorage.setItem('LNavLockOn', $(LPanelPin).prop('checked'));
761 if ($(LPanelPin).prop('checked') == true) {780 if ($(LPanelPin).prop('checked') == true) {
762 //console.log('adding pin class to Left nav');781 //console.log('adding pin class to Left nav');
763 $(LeftNavPanel).addClass('pinnedOpen');782 $(LeftNavPanel).addClass('pinnedOpen');
@@ -776,7 +795,7 @@ export function initRossMods() {
776 });795 });
777796
778 $(WIPanelPin).on('click', function () {797 $(WIPanelPin).on('click', function () {
779 SaveLocal('WINavLockOn', $(WIPanelPin).prop('checked'));798 accountStorage.setItem('WINavLockOn', $(WIPanelPin).prop('checked'));
780 if ($(WIPanelPin).prop('checked') == true) {799 if ($(WIPanelPin).prop('checked') == true) {
781 console.debug('adding pin class to WI');800 console.debug('adding pin class to WI');
782 $(WorldInfo).addClass('pinnedOpen');801 $(WorldInfo).addClass('pinnedOpen');
@@ -796,8 +815,8 @@ export function initRossMods() {
796 });815 });
797816
798 // read the state of right Nav Lock and apply to rightnav classlist817 // read the state of right Nav Lock and apply to rightnav classlist
799 $(RPanelPin).prop('checked', LoadLocalBool('NavLockOn'));818 $(RPanelPin).prop('checked', accountStorage.getItem('NavLockOn') == 'true');
800 if (LoadLocalBool('NavLockOn') == true) {819 if (accountStorage.getItem('NavLockOn') == 'true') {
801 //console.log('setting pin class via local var');820 //console.log('setting pin class via local var');
802 $(RightNavPanel).addClass('pinnedOpen');821 $(RightNavPanel).addClass('pinnedOpen');
803 $(RightNavDrawerIcon).addClass('drawerPinnedOpen');822 $(RightNavDrawerIcon).addClass('drawerPinnedOpen');
@@ -808,8 +827,8 @@ export function initRossMods() {
808 $(RightNavDrawerIcon).addClass('drawerPinnedOpen');827 $(RightNavDrawerIcon).addClass('drawerPinnedOpen');
809 }828 }
810 // read the state of left Nav Lock and apply to leftnav classlist829 // read the state of left Nav Lock and apply to leftnav classlist
811 $(LPanelPin).prop('checked', LoadLocalBool('LNavLockOn'));830 $(LPanelPin).prop('checked', accountStorage.getItem('LNavLockOn') === 'true');
812 if (LoadLocalBool('LNavLockOn') == true) {831 if (accountStorage.getItem('LNavLockOn') == 'true') {
813 //console.log('setting pin class via local var');832 //console.log('setting pin class via local var');
814 $(LeftNavPanel).addClass('pinnedOpen');833 $(LeftNavPanel).addClass('pinnedOpen');
815 $(LeftNavDrawerIcon).addClass('drawerPinnedOpen');834 $(LeftNavDrawerIcon).addClass('drawerPinnedOpen');
@@ -821,8 +840,8 @@ export function initRossMods() {
821 }840 }
822841
823 // read the state of left Nav Lock and apply to leftnav classlist842 // read the state of left Nav Lock and apply to leftnav classlist
824 $(WIPanelPin).prop('checked', LoadLocalBool('WINavLockOn'));843 $(WIPanelPin).prop('checked', accountStorage.getItem('WINavLockOn') === 'true');
825 if (LoadLocalBool('WINavLockOn') == true) {844 if (accountStorage.getItem('WINavLockOn') == 'true') {
826 //console.log('setting pin class via local var');845 //console.log('setting pin class via local var');
827 $(WorldInfo).addClass('pinnedOpen');846 $(WorldInfo).addClass('pinnedOpen');
828 $(WIDrawerIcon).addClass('drawerPinnedOpen');847 $(WIDrawerIcon).addClass('drawerPinnedOpen');
@@ -837,22 +856,22 @@ export function initRossMods() {
837 //save state of Right nav being open or closed856 //save state of Right nav being open or closed
838 $('#rightNavDrawerIcon').on('click', function () {857 $('#rightNavDrawerIcon').on('click', function () {
839 if (!$('#rightNavDrawerIcon').hasClass('openIcon')) {858 if (!$('#rightNavDrawerIcon').hasClass('openIcon')) {
840 SaveLocal('NavOpened', 'true');859 accountStorage.setItem('NavOpened', 'true');
841 } else { SaveLocal('NavOpened', 'false'); }860 } else { accountStorage.setItem('NavOpened', 'false'); }
842 });861 });
843862
844 //save state of Left nav being open or closed863 //save state of Left nav being open or closed
845 $('#leftNavDrawerIcon').on('click', function () {864 $('#leftNavDrawerIcon').on('click', function () {
846 if (!$('#leftNavDrawerIcon').hasClass('openIcon')) {865 if (!$('#leftNavDrawerIcon').hasClass('openIcon')) {
847 SaveLocal('LNavOpened', 'true');866 accountStorage.setItem('LNavOpened', 'true');
848 } else { SaveLocal('LNavOpened', 'false'); }867 } else { accountStorage.setItem('LNavOpened', 'false'); }
849 });868 });
850869
851 //save state of Left nav being open or closed870 //save state of Left nav being open or closed
852 $('#WorldInfo').on('click', function () {871 $('#WorldInfo').on('click', function () {
853 if (!$('#WorldInfo').hasClass('openIcon')) {872 if (!$('#WorldInfo').hasClass('openIcon')) {
854 SaveLocal('WINavOpened', 'true');873 accountStorage.setItem('WINavOpened', 'true');
855 } else { SaveLocal('WINavOpened', 'false'); }874 } else { accountStorage.setItem('WINavOpened', 'false'); }
856 });875 });
857876
858 var chatbarInFocus = false;877 var chatbarInFocus = false;
@@ -868,8 +887,8 @@ export function initRossMods() {
868 OpenNavPanels();887 OpenNavPanels();
869 }, 300);888 }, 300);
870889
871 $(SelectedCharacterTab).click(function () { SaveLocal('SelectedNavTab', 'rm_button_selected_ch'); });890 $(SelectedCharacterTab).click(function () { accountStorage.setItem('SelectedNavTab', 'rm_button_selected_ch'); });
872 $('#rm_button_characters').click(function () { SaveLocal('SelectedNavTab', 'rm_button_characters'); });891 $('#rm_button_characters').click(function () { accountStorage.setItem('SelectedNavTab', 'rm_button_characters'); });
873892
874 // when a char is selected from the list, save them as the auto-load character for next page load893 // when a char is selected from the list, save them as the auto-load character for next page load
875894
@@ -1063,14 +1082,21 @@ export function initRossMods() {
1063 // Ctrl+Enter for Regeneration Last Response. If editing, accept the edits instead1082 // Ctrl+Enter for Regeneration Last Response. If editing, accept the edits instead
1064 if (event.ctrlKey && event.key == 'Enter') {1083 if (event.ctrlKey && event.key == 'Enter') {
1065 const editMesDone = $('.mes_edit_done:visible');1084 const editMesDone = $('.mes_edit_done:visible');
1085 const reasoningMesDone = $('.mes_reasoning_edit_done:visible');
1066 if (editMesDone.length > 0) {1086 if (editMesDone.length > 0) {
1067 console.debug('Accepting edits with Ctrl+Enter');1087 console.debug('Accepting edits with Ctrl+Enter');
1068 $('#send_textarea').focus();1088 $('#send_textarea').trigger('focus');
1069 editMesDone.trigger('click');1089 editMesDone.trigger('click');
1070 return;1090 return;
1071 } else if (is_send_press == false) {1091 } else if (reasoningMesDone.length > 0) {
1092 console.debug('Accepting edits with Ctrl+Enter');
1093 $('#send_textarea').trigger('focus');
1094 reasoningMesDone.trigger('click');
1095 return;
1096 }
1097 else if (is_send_press == false) {
1072 const skipConfirmKey = 'RegenerateWithCtrlEnter';1098 const skipConfirmKey = 'RegenerateWithCtrlEnter';
1073 const skipConfirm = LoadLocalBool(skipConfirmKey);1099 const skipConfirm = accountStorage.getItem(skipConfirmKey) === 'true';
1074 function doRegenerate() {1100 function doRegenerate() {
1075 console.debug('Regenerating with Ctrl+Enter');1101 console.debug('Regenerating with Ctrl+Enter');
1076 $('#option_regenerate').trigger('click');1102 $('#option_regenerate').trigger('click');
@@ -1082,13 +1108,15 @@ export function initRossMods() {
1082 let regenerateWithCtrlEnter = false;1108 let regenerateWithCtrlEnter = false;
1083 const result = await Popup.show.confirm('Regenerate Message', 'Are you sure you want to regenerate the latest message?', {1109 const result = await Popup.show.confirm('Regenerate Message', 'Are you sure you want to regenerate the latest message?', {
1084 customInputs: [{ id: 'regenerateWithCtrlEnter', label: 'Don\'t ask again' }],1110 customInputs: [{ id: 'regenerateWithCtrlEnter', label: 'Don\'t ask again' }],
1085 onClose: (popup) => regenerateWithCtrlEnter = popup.inputResults.get('regenerateWithCtrlEnter') ?? false,1111 onClose: (popup) => {
1112 regenerateWithCtrlEnter = popup.inputResults.get('regenerateWithCtrlEnter') ?? false;
1113 },
1086 });1114 });
1087 if (!result) {1115 if (!result) {
1088 return;1116 return;
1089 }1117 }
10901118
1091 SaveLocal(skipConfirmKey, regenerateWithCtrlEnter);1119 accountStorage.setItem(skipConfirmKey, String(regenerateWithCtrlEnter));
1092 doRegenerate();1120 doRegenerate();
1093 }1121 }
1094 return;1122 return;
public/scripts/authors-note.js+1 -1
@@ -566,7 +566,7 @@ export function initAuthorsNote() {
566 namedArgumentList: [],566 namedArgumentList: [],
567 unnamedArgumentList: [567 unnamedArgumentList: [
568 new SlashCommandArgument(568 new SlashCommandArgument(
569 'position', [ARGUMENT_TYPE.STRING], false, false, null, ['system', 'user', 'assistant'],569 'role', [ARGUMENT_TYPE.STRING], false, false, null, ['system', 'user', 'assistant'],
570 ),570 ),
571 ],571 ],
572 helpString: `572 helpString: `
public/scripts/backgrounds.js+15 -5
@@ -96,8 +96,13 @@ function highlightLockedBackground() {
96 });96 });
97}97}
9898
99/**
100 * Locks the background for the current chat
101 * @param {Event} e Click event
102 * @returns {string} Empty string
103 */
99function onLockBackgroundClick(e) {104function onLockBackgroundClick(e) {
100 e.stopPropagation();105 e?.stopPropagation();
101106
102 const chatName = getCurrentChatId();107 const chatName = getCurrentChatId();
103108
@@ -106,7 +111,7 @@ function onLockBackgroundClick(e) {
106 return '';111 return '';
107 }112 }
108113
109 const relativeBgImage = getUrlParameter(this);114 const relativeBgImage = getUrlParameter(this) ?? background_settings.url;
110115
111 saveBackgroundMetadata(relativeBgImage);116 saveBackgroundMetadata(relativeBgImage);
112 setCustomBackground();117 setCustomBackground();
@@ -114,8 +119,13 @@ function onLockBackgroundClick(e) {
114 return '';119 return '';
115}120}
116121
122/**
123 * Locks the background for the current chat
124 * @param {Event} e Click event
125 * @returns {string} Empty string
126 */
117function onUnlockBackgroundClick(e) {127function onUnlockBackgroundClick(e) {
118 e.stopPropagation();128 e?.stopPropagation();
119 removeBackgroundMetadata();129 removeBackgroundMetadata();
120 unsetCustomBackground();130 unsetCustomBackground();
121 highlightLockedBackground();131 highlightLockedBackground();
@@ -513,12 +523,12 @@ export function initBackgrounds() {
513 $('#add_bg_button').on('change', onBackgroundUploadSelected);523 $('#add_bg_button').on('change', onBackgroundUploadSelected);
514 $('#bg-filter').on('input', onBackgroundFilterInput);524 $('#bg-filter').on('input', onBackgroundFilterInput);
515 SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'lockbg',525 SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'lockbg',
516 callback: onLockBackgroundClick,526 callback: () => onLockBackgroundClick(new CustomEvent('click')),
517 aliases: ['bglock'],527 aliases: ['bglock'],
518 helpString: 'Locks a background for the currently selected chat',528 helpString: 'Locks a background for the currently selected chat',
519 }));529 }));
520 SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'unlockbg',530 SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'unlockbg',
521 callback: onUnlockBackgroundClick,531 callback: () => onUnlockBackgroundClick(new CustomEvent('click')),
522 aliases: ['bgunlock'],532 aliases: ['bgunlock'],
523 helpString: 'Unlocks a background for the currently selected chat',533 helpString: 'Unlocks a background for the currently selected chat',
524 }));534 }));
public/scripts/chat-templates.js+2 -1
@@ -69,6 +69,7 @@ const hash_derivations = {
69 // DeepSeek R169 // DeepSeek R1
70 'b6835114b7303ddd78919a82e4d9f7d8c26ed0d7dfc36beeb12d524f6144eab1':70 'b6835114b7303ddd78919a82e4d9f7d8c26ed0d7dfc36beeb12d524f6144eab1':
71 'DeepSeek-V2.5'71 'DeepSeek-V2.5'
72 ,
72};73};
7374
74const substr_derivations = {75const substr_derivations = {
@@ -97,6 +98,6 @@ export async function deriveTemplatesFromChatTemplate(chat_template, hash) {
97 }98 }
98 }99 }
99100
100 console.log(`Unknown chat template hash: ${hash} for [${chat_template}]`);101 console.warn(`Unknown chat template hash: ${hash} for [${chat_template}]`);
101 return null;102 return null;
102}103}
public/scripts/chats.js+43 -7
@@ -45,6 +45,7 @@ import { DragAndDropHandler } from './dragdrop.js';
45import { renderTemplateAsync } from './templates.js';45import { renderTemplateAsync } from './templates.js';
46import { t } from './i18n.js';46import { t } from './i18n.js';
47import { humanizedDateTime } from './RossAscends-mods.js';47import { humanizedDateTime } from './RossAscends-mods.js';
48import { accountStorage } from './util/AccountStorage.js';
4849
49/**50/**
50 * @typedef {Object} FileAttachment51 * @typedef {Object} FileAttachment
@@ -621,21 +622,56 @@ async function enlargeMessageImage() {
621}622}
622623
623async function deleteMessageImage() {624async function deleteMessageImage() {
624 const value = await callGenericPopup('<h3>Delete image from message?<br>This action can\'t be undone.</h3>', POPUP_TYPE.CONFIRM);625 const value = await callGenericPopup('<h3>Delete image from message?<br>This action can\'t be undone.</h3>', POPUP_TYPE.TEXT, '', {
626 okButton: t`Delete one`,
627 customButtons: [
628 {
629 text: t`Delete all`,
630 appendAtEnd: true,
631 result: POPUP_RESULT.CUSTOM1,
632 },
633 {
634 text: t`Cancel`,
635 appendAtEnd: true,
636 result: POPUP_RESULT.CANCELLED,
637 },
638 ],
639 });
625640
626 if (value !== POPUP_RESULT.AFFIRMATIVE) {641 if (!value) {
627 return;642 return;
628 }643 }
629644
630 const mesBlock = $(this).closest('.mes');645 const mesBlock = $(this).closest('.mes');
631 const mesId = mesBlock.attr('mesid');646 const mesId = mesBlock.attr('mesid');
632 const message = chat[mesId];647 const message = chat[mesId];
648
649 let isLastImage = true;
650
651 if (Array.isArray(message.extra.image_swipes)) {
652 const indexOf = message.extra.image_swipes.indexOf(message.extra.image);
653 if (indexOf > -1) {
654 message.extra.image_swipes.splice(indexOf, 1);
655 isLastImage = message.extra.image_swipes.length === 0;
656 if (!isLastImage) {
657 const newIndex = Math.min(indexOf, message.extra.image_swipes.length - 1);
658 message.extra.image = message.extra.image_swipes[newIndex];
659 }
660 }
661 }
662
663 if (isLastImage || value === POPUP_RESULT.CUSTOM1) {
633 delete message.extra.image;664 delete message.extra.image;
634 delete message.extra.inline_image;665 delete message.extra.inline_image;
635 delete message.extra.title;666 delete message.extra.title;
636 delete message.extra.append_title;667 delete message.extra.append_title;
668 delete message.extra.image_swipes;
637 mesBlock.find('.mes_img_container').removeClass('img_extra');669 mesBlock.find('.mes_img_container').removeClass('img_extra');
638 mesBlock.find('.mes_img').attr('src', '');670 mesBlock.find('.mes_img').attr('src', '');
671 } else {
672 appendMediaToMessage(message, mesBlock);
673 }
674
639 await saveChatConditional();675 await saveChatConditional();
640}676}
641677
@@ -1043,8 +1079,8 @@ async function openAttachmentManager() {
1043 renderAttachments();1079 renderAttachments();
1044 });1080 });
10451081
1046 let sortField = localStorage.getItem('DataBank_sortField') || 'created';1082 let sortField = accountStorage.getItem('DataBank_sortField') || 'created';
1047 let sortOrder = localStorage.getItem('DataBank_sortOrder') || 'desc';1083 let sortOrder = accountStorage.getItem('DataBank_sortOrder') || 'desc';
1048 let filterString = '';1084 let filterString = '';
10491085
1050 const template = $(await renderExtensionTemplateAsync('attachments', 'manager', {}));1086 const template = $(await renderExtensionTemplateAsync('attachments', 'manager', {}));
@@ -1060,8 +1096,8 @@ async function openAttachmentManager() {
10601096
1061 sortField = this.selectedOptions[0].dataset.sortField;1097 sortField = this.selectedOptions[0].dataset.sortField;
1062 sortOrder = this.selectedOptions[0].dataset.sortOrder;1098 sortOrder = this.selectedOptions[0].dataset.sortOrder;
1063 localStorage.setItem('DataBank_sortField', sortField);1099 accountStorage.setItem('DataBank_sortField', sortField);
1064 localStorage.setItem('DataBank_sortOrder', sortOrder);1100 accountStorage.setItem('DataBank_sortOrder', sortOrder);
1065 renderAttachments();1101 renderAttachments();
1066 });1102 });
1067 function handleBulkAction(action) {1103 function handleBulkAction(action) {
@@ -1451,7 +1487,7 @@ jQuery(function () {
1451 ...chat.filter(x => x?.extra?.type !== system_message_types.ASSISTANT_NOTE),1487 ...chat.filter(x => x?.extra?.type !== system_message_types.ASSISTANT_NOTE),
1452 ];1488 ];
14531489
1454 download(JSON.stringify(chatToSave, null, 4), `Assistant - ${humanizedDateTime()}.json`, 'application/json');1490 download(chatToSave.map((m) => JSON.stringify(m)).join('\n'), `Assistant - ${humanizedDateTime()}.jsonl`, 'application/json');
1455 });1491 });
14561492
1457 // Do not change. #attachFile is added by extension.1493 // Do not change. #attachFile is added by extension.
public/scripts/extensions.js+19 -8
@@ -9,6 +9,7 @@ import { getContext } from './st-context.js';
9import { isAdmin } from './user.js';9import { isAdmin } from './user.js';
10import { t } from './i18n.js';10import { t } from './i18n.js';
11import { debounce_timeout } from './constants.js';11import { debounce_timeout } from './constants.js';
12import { accountStorage } from './util/AccountStorage.js';
1213
13export {14export {
14 getContext,15 getContext,
@@ -153,8 +154,18 @@ export const extension_settings = {
153 refine_mode: false,154 refine_mode: false,
154 },155 },
155 expressions: {156 expressions: {
157 /** @type {number} see `EXPRESSION_API` */
158 api: undefined,
156 /** @type {string[]} */159 /** @type {string[]} */
157 custom: [],160 custom: [],
161 showDefault: false,
162 translate: false,
163 /** @type {string} */
164 fallback_expression: undefined,
165 /** @type {string} */
166 llmPrompt: undefined,
167 allowMultiple: true,
168 rerollIfSame: false,
158 },169 },
159 connectionManager: {170 connectionManager: {
160 selectedProfile: '',171 selectedProfile: '',
@@ -602,12 +613,12 @@ function generateExtensionHtml(name, manifest, isActive, isDisabled, isExternal,
602 }613 }
603614
604 let toggleElement = isActive || isDisabled ?615 let toggleElement = isActive || isDisabled ?
605 `<input type="checkbox" title="Click to toggle" data-name="${name}" class="${isActive ? 'toggle_disable' : 'toggle_enable'} ${checkboxClass}" ${isActive ? 'checked' : ''}>` :616 '<input type="checkbox" title="' + t`Click to toggle` + `" data-name="${name}" class="${isActive ? 'toggle_disable' : 'toggle_enable'} ${checkboxClass}" ${isActive ? 'checked' : ''}>` :
606 `<input type="checkbox" title="Cannot enable extension" data-name="${name}" class="extension_missing ${checkboxClass}" disabled>`;617 `<input type="checkbox" title="Cannot enable extension" data-name="${name}" class="extension_missing ${checkboxClass}" disabled>`;
607618
608 let deleteButton = isExternal ? `<button class="btn_delete menu_button" data-name="${externalId}" title="Delete"><i class="fa-fw fa-solid fa-trash-can"></i></button>` : '';619 let deleteButton = isExternal ? `<button class="btn_delete menu_button" data-name="${externalId}" data-i18n="[title]Delete" title="Delete"><i class="fa-fw fa-solid fa-trash-can"></i></button>` : '';
609 let updateButton = isExternal ? `<button class="btn_update menu_button displayNone" data-name="${externalId}" title="Update available"><i class="fa-solid fa-download fa-fw"></i></button>` : '';620 let updateButton = isExternal ? `<button class="btn_update menu_button displayNone" data-name="${externalId}" title="Update available"><i class="fa-solid fa-download fa-fw"></i></button>` : '';
610 let moveButton = isExternal && isUserAdmin ? `<button class="btn_move menu_button" data-name="${externalId}" title="Move"><i class="fa-solid fa-folder-tree fa-fw"></i></button>` : '';621 let moveButton = isExternal && isUserAdmin ? `<button class="btn_move menu_button" data-name="${externalId}" data-i18n="[title]Move" title="Move"><i class="fa-solid fa-folder-tree fa-fw"></i></button>` : '';
611 let modulesInfo = '';622 let modulesInfo = '';
612623
613 if (isActive && Array.isArray(manifest.optional)) {624 if (isActive && Array.isArray(manifest.optional)) {
@@ -615,7 +626,7 @@ function generateExtensionHtml(name, manifest, isActive, isDisabled, isExternal,
615 modules.forEach(x => optional.delete(x));626 modules.forEach(x => optional.delete(x));
616 if (optional.size > 0) {627 if (optional.size > 0) {
617 const optionalString = DOMPurify.sanitize([...optional].join(', '));628 const optionalString = DOMPurify.sanitize([...optional].join(', '));
618 modulesInfo = `<div class="extension_modules">Optional modules: <span class="optional">${optionalString}</span></div>`;629 modulesInfo = '<div class="extension_modules">' + t`Optional modules:` + ` <span class="optional">${optionalString}</span></div>`;
619 }630 }
620 } else if (!isDisabled) { // Neither active nor disabled631 } else if (!isDisabled) { // Neither active nor disabled
621 const requirements = new Set(manifest.requires);632 const requirements = new Set(manifest.requires);
@@ -714,7 +725,7 @@ async function showExtensionsDetails() {
714 htmlExternal.append(htmlLoading);725 htmlExternal.append(htmlLoading);
715726
716 const sortOrderKey = 'extensions_sortByName';727 const sortOrderKey = 'extensions_sortByName';
717 const sortByName = localStorage.getItem(sortOrderKey) === 'true';728 const sortByName = accountStorage.getItem(sortOrderKey) === 'true';
718 const sortFn = sortByName ? sortManifestsByName : sortManifestsByOrder;729 const sortFn = sortByName ? sortManifestsByName : sortManifestsByOrder;
719 const extensions = Object.entries(manifests).sort((a, b) => sortFn(a[1], b[1])).map(getExtensionData);730 const extensions = Object.entries(manifests).sort((a, b) => sortFn(a[1], b[1])).map(getExtensionData);
720731
@@ -745,7 +756,7 @@ async function showExtensionsDetails() {
745 text: sortByName ? t`Sort: Display Name` : t`Sort: Loading Order`,756 text: sortByName ? t`Sort: Display Name` : t`Sort: Loading Order`,
746 action: async () => {757 action: async () => {
747 abortController.abort();758 abortController.abort();
748 localStorage.setItem(sortOrderKey, sortByName ? 'false' : 'true');759 accountStorage.setItem(sortOrderKey, sortByName ? 'false' : 'true');
749 await showExtensionsDetails();760 await showExtensionsDetails();
750 },761 },
751 };762 };
@@ -1153,11 +1164,11 @@ async function checkForExtensionUpdates(force) {
1153 const currentDate = new Date().toDateString();1164 const currentDate = new Date().toDateString();
11541165
1155 // Don't nag more than once a day1166 // Don't nag more than once a day
1156 if (localStorage.getItem(STORAGE_NAG_KEY) === currentDate) {1167 if (accountStorage.getItem(STORAGE_NAG_KEY) === currentDate) {
1157 return;1168 return;
1158 }1169 }
11591170
1160 localStorage.setItem(STORAGE_NAG_KEY, currentDate);1171 accountStorage.setItem(STORAGE_NAG_KEY, currentDate);
1161 }1172 }
11621173
1163 const isCurrentUserAdmin = isAdmin();1174 const isCurrentUserAdmin = isAdmin();
public/scripts/extensions/assets/index.js+16 -17
@@ -8,7 +8,9 @@ import { getRequestHeaders, processDroppedFiles, eventSource, event_types } from
8import { deleteExtension, extensionNames, getContext, installExtension, renderExtensionTemplateAsync } from '../../extensions.js';8import { deleteExtension, extensionNames, getContext, installExtension, renderExtensionTemplateAsync } from '../../extensions.js';
9import { POPUP_TYPE, Popup, callGenericPopup } from '../../popup.js';9import { POPUP_TYPE, Popup, callGenericPopup } from '../../popup.js';
10import { executeSlashCommands } from '../../slash-commands.js';10import { executeSlashCommands } from '../../slash-commands.js';
11import { accountStorage } from '../../util/AccountStorage.js';
11import { flashHighlight, getStringHash, isValidUrl } from '../../utils.js';12import { flashHighlight, getStringHash, isValidUrl } from '../../utils.js';
13import { t } from '../../i18n.js';
12export { MODULE_NAME };14export { MODULE_NAME };
1315
14const MODULE_NAME = 'assets';16const MODULE_NAME = 'assets';
@@ -58,11 +60,11 @@ const KNOWN_TYPES = {
58 'blip': 'Blip sounds',60 'blip': 'Blip sounds',
59};61};
6062
61function downloadAssetsList(url) {63async function downloadAssetsList(url) {
62 updateCurrentAssets().then(function () {64 updateCurrentAssets().then(async function () {
63 fetch(url, { cache: 'no-cache' })65 fetch(url, { cache: 'no-cache' })
64 .then(response => response.json())66 .then(response => response.json())
65 .then(json => {67 .then(async function(json) {
6668
67 availableAssets = {};69 availableAssets = {};
68 $('#assets_menu').empty();70 $('#assets_menu').empty();
@@ -83,10 +85,10 @@ function downloadAssetsList(url) {
8385
84 $('#assets_type_select').empty();86 $('#assets_type_select').empty();
85 $('#assets_search').val('');87 $('#assets_search').val('');
86 $('#assets_type_select').append($('<option />', { value: '', text: 'All' }));88 $('#assets_type_select').append($('<option />', { value: '', text: t`All` }));
8789
88 for (const type of assetTypes) {90 for (const type of assetTypes) {
89 const option = $('<option />', { value: type, text: KNOWN_TYPES[type] || type });91 const option = $('<option />', { value: type, text: t([KNOWN_TYPES[type] || type]) });
90 $('#assets_type_select').append(option);92 $('#assets_type_select').append(option);
91 }93 }
9294
@@ -103,11 +105,7 @@ function downloadAssetsList(url) {
103 assetTypeMenu.append(`<h3>${KNOWN_TYPES[assetType] || assetType}</h3>`).hide();105 assetTypeMenu.append(`<h3>${KNOWN_TYPES[assetType] || assetType}</h3>`).hide();
104106
105 if (assetType == 'extension') {107 if (assetType == 'extension') {
106 assetTypeMenu.append(`108 assetTypeMenu.append(await renderExtensionTemplateAsync('assets', 'installation'));
107 <div class="assets-list-git">
108 To download extensions from this page, you need to have <a href="https://git-scm.com/downloads" target="_blank">Git</a> installed.<br>
109 Click the <i class="fa-solid fa-sm fa-arrow-up-right-from-square"></i> icon to visit the Extension's repo for tips on how to use it.
110 </div>`);
111 }109 }
112110
113 for (const i in availableAssets[assetType].sort((a, b) => a?.name && b?.name && a['name'].localeCompare(b['name']))) {111 for (const i in availableAssets[assetType].sort((a, b) => a?.name && b?.name && a['name'].localeCompare(b['name']))) {
@@ -183,7 +181,7 @@ function downloadAssetsList(url) {
183 const displayName = DOMPurify.sanitize(asset['name'] || asset['id']);181 const displayName = DOMPurify.sanitize(asset['name'] || asset['id']);
184 const description = DOMPurify.sanitize(asset['description'] || '');182 const description = DOMPurify.sanitize(asset['description'] || '');
185 const url = isValidUrl(asset['url']) ? asset['url'] : '';183 const url = isValidUrl(asset['url']) ? asset['url'] : '';
186 const title = assetType === 'extension' ? `Extension repo/guide: ${url}` : 'Preview in browser';184 const title = assetType === 'extension' ? t`Extension repo/guide:` + ` ${url}` : t`Preview in browser`;
187 const previewIcon = (assetType === 'extension' || assetType === 'character') ? 'fa-arrow-up-right-from-square' : 'fa-headphones-simple';185 const previewIcon = (assetType === 'extension' || assetType === 'character') ? 'fa-arrow-up-right-from-square' : 'fa-headphones-simple';
188 const toolTag = assetType === 'extension' && asset['tool'];186 const toolTag = assetType === 'extension' && asset['tool'];
189187
@@ -194,9 +192,10 @@ function downloadAssetsList(url) {
194 <b>${displayName}</b>192 <b>${displayName}</b>
195 <a class="asset_preview" href="${url}" target="_blank" title="${title}">193 <a class="asset_preview" href="${url}" target="_blank" title="${title}">
196 <i class="fa-solid fa-sm ${previewIcon}"></i>194 <i class="fa-solid fa-sm ${previewIcon}"></i>
197 </a>195 </a>` +
198 ${toolTag ? '<span class="tag" title="Adds a function tool"><i class="fa-solid fa-sm fa-wrench"></i> Tool</span>' : ''}196 (toolTag ? '<span class="tag" title="' + t`Adds a function tool` + '"><i class="fa-solid fa-sm fa-wrench"></i> ' +
199 </span>197 t`Tool` + '</span>' : '') +
198 `</span>
200 <small class="asset-description">199 <small class="asset-description">
201 ${description}200 ${description}
202 </small>201 </small>
@@ -432,14 +431,14 @@ jQuery(async () => {
432 connectButton.on('click', async function () {431 connectButton.on('click', async function () {
433 const url = DOMPurify.sanitize(String(assetsJsonUrl.val()));432 const url = DOMPurify.sanitize(String(assetsJsonUrl.val()));
434 const rememberKey = `Assets_SkipConfirm_${getStringHash(url)}`;433 const rememberKey = `Assets_SkipConfirm_${getStringHash(url)}`;
435 const skipConfirm = localStorage.getItem(rememberKey) === 'true';434 const skipConfirm = accountStorage.getItem(rememberKey) === 'true';
436435
437 const confirmation = skipConfirm || await Popup.show.confirm('Loading Asset List', `<span>Are you sure you want to connect to the following url?</span><var>${url}</var>`, {436 const confirmation = skipConfirm || await Popup.show.confirm(t`Loading Asset List`, '<span>' + t`Are you sure you want to connect to the following url?` + `</span><var>${url}</var>`, {
438 customInputs: [{ id: 'assets-remember', label: 'Don\'t ask again for this URL' }],437 customInputs: [{ id: 'assets-remember', label: 'Don\'t ask again for this URL' }],
439 onClose: popup => {438 onClose: popup => {
440 if (popup.result) {439 if (popup.result) {
441 const rememberValue = popup.inputResults.get('assets-remember');440 const rememberValue = popup.inputResults.get('assets-remember');
442 localStorage.setItem(rememberKey, String(rememberValue));441 accountStorage.setItem(rememberKey, String(rememberValue));
443 }442 }
444 },443 },
445 });444 });
public/scripts/extensions/assets/installation.html+4 -0
@@ -0,0 +1,4 @@
1<div class="assets-list-git">
2 <span data-i18n="extension_install_1">To download extensions from this page, you need to have </span><a href="https://git-scm.com/downloads" target="_blank">Git</a><span data-i18n="extension_install_2"> installed.</span><br>
3 <span data-i18n="extension_install_3">Click the </span><i class="fa-solid fa-sm fa-arrow-up-right-from-square"></i><span data-i18n="extension_install_4"> icon to visit the Extension's repo for tips on how to use it.</span>
4</div>
\ No newline at end of file4 \ No newline at end of file
public/scripts/extensions/assets/window.html+1 -1
@@ -33,7 +33,7 @@ To install a single 3rd party extension, use the &quot;Install Extensions&quot;
33 <div id="assets_filters" class="flex-container">33 <div id="assets_filters" class="flex-container">
34 <select id="assets_type_select" class="text_pole flex1">34 <select id="assets_type_select" class="text_pole flex1">
35 </select>35 </select>
36 <input id="assets_search" class="text_pole flex1" placeholder="Search" type="search">36 <input id="assets_search" class="text_pole flex1" data-i18n="[placeholder]Search" placeholder="Search" type="search">
37 <div id="assets-characters-button" class="menu_button menu_button_icon">37 <div id="assets-characters-button" class="menu_button menu_button_icon">
38 <i class="fa-solid fa-image-portrait"></i>38 <i class="fa-solid fa-image-portrait"></i>
39 <span data-i18n="Characters">Characters</span>39 <span data-i18n="Characters">Characters</span>
public/scripts/extensions/caption/settings.html+7 -1
@@ -10,7 +10,7 @@
10 <select id="caption_source" class="text_pole">10 <select id="caption_source" class="text_pole">
11 <option value="local" data-i18n="Local">Local</option>11 <option value="local" data-i18n="Local">Local</option>
12 <option value="multimodal" data-i18n="Multimodal (OpenAI / Anthropic / llama / Google)">Multimodal (OpenAI / Anthropic / llama / Google)</option>12 <option value="multimodal" data-i18n="Multimodal (OpenAI / Anthropic / llama / Google)">Multimodal (OpenAI / Anthropic / llama / Google)</option>
13 <option value="extras" data-i18n="Extras">Extras</option>13 <option value="extras" data-i18n="Extras">Extras (deprecated)</option>
14 <option value="horde" data-i18n="Horde">Horde</option>14 <option value="horde" data-i18n="Horde">Horde</option>
15 </select>15 </select>
16 <div id="caption_multimodal_block" class="flex-container wide100p">16 <div id="caption_multimodal_block" class="flex-container wide100p">
@@ -53,6 +53,12 @@
53 <option data-type="anthropic" value="claude-3-opus-20240229">claude-3-opus-20240229</option>53 <option data-type="anthropic" value="claude-3-opus-20240229">claude-3-opus-20240229</option>
54 <option data-type="anthropic" value="claude-3-sonnet-20240229">claude-3-sonnet-20240229</option>54 <option data-type="anthropic" value="claude-3-sonnet-20240229">claude-3-sonnet-20240229</option>
55 <option data-type="anthropic" value="claude-3-haiku-20240307">claude-3-haiku-20240307</option>55 <option data-type="anthropic" value="claude-3-haiku-20240307">claude-3-haiku-20240307</option>
56 <option data-type="google" value="gemini-2.0-pro-exp">gemini-2.0-pro-exp</option>
57 <option data-type="google" value="gemini-2.0-pro-exp-02-05">gemini-2.0-pro-exp-02-05</option>
58 <option data-type="google" value="gemini-2.0-flash-lite-preview">gemini-2.0-flash-lite-preview</option>
59 <option data-type="google" value="gemini-2.0-flash-lite-preview-02-05">gemini-2.0-flash-lite-preview-02-05</option>
60 <option data-type="google" value="gemini-2.0-flash">gemini-2.0-flash</option>
61 <option data-type="google" value="gemini-2.0-flash-001">gemini-2.0-flash-001</option>
56 <option data-type="google" value="gemini-2.0-flash-exp">gemini-2.0-flash-exp</option>62 <option data-type="google" value="gemini-2.0-flash-exp">gemini-2.0-flash-exp</option>
57 <option data-type="google" value="gemini-2.0-flash-thinking-exp">gemini-2.0-flash-thinking-exp</option>63 <option data-type="google" value="gemini-2.0-flash-thinking-exp">gemini-2.0-flash-thinking-exp</option>
58 <option data-type="google" value="gemini-2.0-flash-thinking-exp-01-21">gemini-2.0-flash-thinking-exp-01-21</option>64 <option data-type="google" value="gemini-2.0-flash-thinking-exp-01-21">gemini-2.0-flash-thinking-exp-01-21</option>
public/scripts/extensions/connection-manager/index.js+4 -0
@@ -30,6 +30,7 @@ const CC_COMMANDS = [
30 'api-url',30 'api-url',
31 'model',31 'model',
32 'proxy',32 'proxy',
33 'stop-strings',
33];34];
3435
35const TC_COMMANDS = [36const TC_COMMANDS = [
@@ -43,6 +44,7 @@ const TC_COMMANDS = [
43 'context',44 'context',
44 'instruct-state',45 'instruct-state',
45 'tokenizer',46 'tokenizer',
47 'stop-strings',
46];48];
4749
48const FANCY_NAMES = {50const FANCY_NAMES = {
@@ -57,6 +59,7 @@ const FANCY_NAMES = {
57 'instruct': 'Instruct Template',59 'instruct': 'Instruct Template',
58 'context': 'Context Template',60 'context': 'Context Template',
59 'tokenizer': 'Tokenizer',61 'tokenizer': 'Tokenizer',
62 'stop-strings': 'Custom Stopping Strings',
60};63};
6164
62/**65/**
@@ -138,6 +141,7 @@ const profilesProvider = () => [
138 * @property {string} [context] Context Template141 * @property {string} [context] Context Template
139 * @property {string} [instruct-state] Instruct Mode142 * @property {string} [instruct-state] Instruct Mode
140 * @property {string} [tokenizer] Tokenizer143 * @property {string} [tokenizer] Tokenizer
144 * @property {string} [stop-strings] Custom Stopping Strings
141 * @property {string[]} [exclude] Commands to exclude145 * @property {string[]} [exclude] Commands to exclude
142 */146 */
143147
public/scripts/extensions/expressions/index.js+788 -708
@@ -1,11 +1,11 @@
1import { Fuse } from '../../../lib.js';1import { Fuse } from '../../../lib.js';
22
3import { callPopup, eventSource, event_types, generateRaw, getRequestHeaders, main_api, online_status, saveSettingsDebounced, substituteParams, substituteParamsExtended, system_message_types } from '../../../script.js';3import { characters, eventSource, event_types, generateRaw, getRequestHeaders, main_api, online_status, saveSettingsDebounced, substituteParams, substituteParamsExtended, system_message_types, this_chid } from '../../../script.js';
4import { dragElement, isMobile } from '../../RossAscends-mods.js';4import { dragElement, isMobile } from '../../RossAscends-mods.js';
5import { getContext, getApiUrl, modules, extension_settings, ModuleWorkerWrapper, doExtrasFetch, renderExtensionTemplateAsync } from '../../extensions.js';5import { getContext, getApiUrl, modules, extension_settings, ModuleWorkerWrapper, doExtrasFetch, renderExtensionTemplateAsync } from '../../extensions.js';
6import { loadMovingUIState, power_user } from '../../power-user.js';6import { loadMovingUIState, performFuzzySearch, power_user } from '../../power-user.js';
7import { onlyUnique, debounce, getCharaFilename, trimToEndSentence, trimToStartSentence, waitUntilCondition, findChar } from '../../utils.js';7import { onlyUnique, debounce, getCharaFilename, trimToEndSentence, trimToStartSentence, waitUntilCondition, findChar } from '../../utils.js';
8import { hideMutedSprites } from '../../group-chats.js';8import { hideMutedSprites, selected_group } from '../../group-chats.js';
9import { isJsonSchemaSupported } from '../../textgen-settings.js';9import { isJsonSchemaSupported } from '../../textgen-settings.js';
10import { debounce_timeout } from '../../constants.js';10import { debounce_timeout } from '../../constants.js';
11import { SlashCommandParser } from '../../slash-commands/SlashCommandParser.js';11import { SlashCommandParser } from '../../slash-commands/SlashCommandParser.js';
@@ -15,16 +15,32 @@ import { SlashCommandEnumValue, enumTypes } from '../../slash-commands/SlashComm
15import { commonEnumProviders } from '../../slash-commands/SlashCommandCommonEnumsProvider.js';15import { commonEnumProviders } from '../../slash-commands/SlashCommandCommonEnumsProvider.js';
16import { slashCommandReturnHelper } from '../../slash-commands/SlashCommandReturnHelper.js';16import { slashCommandReturnHelper } from '../../slash-commands/SlashCommandReturnHelper.js';
17import { generateWebLlmChatPrompt, isWebLlmSupported } from '../shared.js';17import { generateWebLlmChatPrompt, isWebLlmSupported } from '../shared.js';
18import { Popup, POPUP_RESULT } from '../../popup.js';
19import { t } from '../../i18n.js';
18export { MODULE_NAME };20export { MODULE_NAME };
1921
22/**
23* @typedef {object} Expression Expression definition with label and file path
24* @property {string} label The label of the expression
25* @property {ExpressionImage[]} files One or more images to represent this expression
26*/
27
28/**
29 * @typedef {object} ExpressionImage An expression image
30 * @property {string} expression - The expression
31 * @property {boolean} [isCustom=false] - If the expression is added by user
32 * @property {string} fileName - The filename with extension
33 * @property {string} title - The title for the image
34 * @property {string} imageSrc - The image source / full path
35 * @property {'success' | 'additional' | 'failure'} type - The type of the image
36 */
37
20const MODULE_NAME = 'expressions';38const MODULE_NAME = 'expressions';
21const UPDATE_INTERVAL = 2000;39const UPDATE_INTERVAL = 2000;
22const STREAMING_UPDATE_INTERVAL = 10000;40const STREAMING_UPDATE_INTERVAL = 10000;
23const TALKINGCHECK_UPDATE_INTERVAL = 500;
24const DEFAULT_FALLBACK_EXPRESSION = 'joy';41const DEFAULT_FALLBACK_EXPRESSION = 'joy';
25const DEFAULT_LLM_PROMPT = 'Ignore previous instructions. Classify the emotion of the last message. Output just one word, e.g. "joy" or "anger". Choose only one of the following labels: {{labels}}';42const DEFAULT_LLM_PROMPT = 'Ignore previous instructions. Classify the emotion of the last message. Output just one word, e.g. "joy" or "anger". Choose only one of the following labels: {{labels}}';
26const DEFAULT_EXPRESSIONS = [43const DEFAULT_EXPRESSIONS = [
27 'talkinghead',
28 'admiration',44 'admiration',
29 'amusement',45 'amusement',
30 'anger',46 'anger',
@@ -54,6 +70,12 @@ const DEFAULT_EXPRESSIONS = [
54 'surprise',70 'surprise',
55 'neutral',71 'neutral',
56];72];
73
74const OPTION_NO_FALLBACK = '#none';
75const OPTION_EMOJI_FALLBACK = '#emoji';
76const RESET_SPRITE_LABEL = '#reset';
77
78
57/** @enum {number} */79/** @enum {number} */
58const EXPRESSION_API = {80const EXPRESSION_API = {
59 local: 0,81 local: 0,
@@ -65,35 +87,29 @@ const EXPRESSION_API = {
65let expressionsList = null;87let expressionsList = null;
66let lastCharacter = undefined;88let lastCharacter = undefined;
67let lastMessage = null;89let lastMessage = null;
68let lastTalkingState = false;90/** @type {{[characterKey: string]: Expression[]}} */
69let lastTalkingStateMessage = null; // last message as seen by `updateTalkingState` (tracked separately, different timer)
70let spriteCache = {};91let spriteCache = {};
71let inApiCall = false;92let inApiCall = false;
72let lastServerResponseTime = 0;93let lastServerResponseTime = 0;
73export let lastExpression = {};
74
75function isTalkingHeadEnabled() {
76 return extension_settings.expressions.talkinghead && extension_settings.expressions.api == EXPRESSION_API.extras;
77}
7894
79/**95/** @type {{[characterName: string]: string}} */
80 * Returns the fallback expression if explicitly chosen, otherwise the default one96export let lastExpression = {};
81 * @returns {string} expression name
82 */
83function getFallbackExpression() {
84 return extension_settings.expressions.fallback_expression ?? DEFAULT_FALLBACK_EXPRESSION;
85}
8697
87/**98/**
88 * Toggles Talkinghead mode on/off.99 * Returns a placeholder image object for a given expression
89 *100 * @param {string} expression - The expression label
90 * Implements the `/th` slash command, which is meant to be bound to a Quick Reply button101 * @param {boolean} [isCustom=false] - Whether the expression is custom
91 * as a quick way to switch Talkinghead on or off (e.g. to conserve GPU resources when AFK102 * @returns {ExpressionImage} The placeholder image object
92 * for a long time).
93 */103 */
94function toggleTalkingHeadCommand(_) {104function getPlaceholderImage(expression, isCustom = false) {
95 setTalkingHeadState(!extension_settings.expressions.talkinghead);105 return {
96 return String(extension_settings.expressions.talkinghead);106 expression: expression,
107 isCustom: isCustom,
108 title: 'No Image',
109 type: 'failure',
110 fileName: 'No-Image-Placeholder.svg',
111 imageSrc: '/img/No-Image-Placeholder.svg',
112 };
97}113}
98114
99function isVisualNovelMode() {115function isVisualNovelMode() {
@@ -108,21 +124,21 @@ async function forceUpdateVisualNovelMode() {
108124
109const updateVisualNovelModeDebounced = debounce(forceUpdateVisualNovelMode, debounce_timeout.quick);125const updateVisualNovelModeDebounced = debounce(forceUpdateVisualNovelMode, debounce_timeout.quick);
110126
111async function updateVisualNovelMode(name, expression) {127async function updateVisualNovelMode(spriteFolderName, expression) {
112 const container = $('#visual-novel-wrapper');128 const vnContainer = $('#visual-novel-wrapper');
113129
114 await visualNovelRemoveInactive(container);130 await visualNovelRemoveInactive(vnContainer);
115131
116 const setSpritePromises = await visualNovelSetCharacterSprites(container, name, expression);132 const setSpritePromises = await visualNovelSetCharacterSprites(vnContainer, spriteFolderName, expression);
117133
118 // calculate layer indices based on recent messages134 // calculate layer indices based on recent messages
119 await visualNovelUpdateLayers(container);135 await visualNovelUpdateLayers(vnContainer);
120136
121 await Promise.allSettled(setSpritePromises);137 await Promise.allSettled(setSpritePromises);
122138
123 // update again based on new sprites139 // update again based on new sprites
124 if (setSpritePromises.length > 0) {140 if (setSpritePromises.length > 0) {
125 await visualNovelUpdateLayers(container);141 await visualNovelUpdateLayers(vnContainer);
126 }142 }
127}143}
128144
@@ -153,52 +169,60 @@ async function visualNovelRemoveInactive(container) {
153 await Promise.allSettled(removeInactiveCharactersPromises);169 await Promise.allSettled(removeInactiveCharactersPromises);
154}170}
155171
156async function visualNovelSetCharacterSprites(container, name, expression) {172/**
173 * Sets the character sprites for visual novel mode based on the provided container, name, and expression.
174 *
175 * @param {JQuery<HTMLElement>} vnContainer - The container element where the sprites will be set
176 * @param {string} spriteFolderName - The name of the sprite folder
177 * @param {string} expression - The expression to set for the characters
178 * @returns {Promise<Array>} - An array of promises that resolve when the sprites are set
179 */
180async function visualNovelSetCharacterSprites(vnContainer, spriteFolderName, expression) {
181 const originalExpression = expression;
157 const context = getContext();182 const context = getContext();
158 const group = context.groups.find(x => x.id == context.groupId);183 const group = context.groups.find(x => x.id == context.groupId);
159 const labels = await getExpressionsList();
160184
161 const createCharacterPromises = [];
162 const setSpritePromises = [];185 const setSpritePromises = [];
163186
164 for (const avatar of group.members) {187 for (const avatar of group.members) {
165 const isDisabled = group.disabled_members.includes(avatar);
166
167 // skip disabled characters188 // skip disabled characters
189 const isDisabled = group.disabled_members.includes(avatar);
168 if (isDisabled && hideMutedSprites) {190 if (isDisabled && hideMutedSprites) {
169 continue;191 continue;
170 }192 }
171193
172 const character = context.characters.find(x => x.avatar == avatar);194 const character = context.characters.find(x => x.avatar == avatar);
173
174 if (!character) {195 if (!character) {
175 continue;196 continue;
176 }197 }
177198
178 const spriteFolderName = getSpriteFolderName({ original_avatar: character.avatar }, character.name);199 const expressionImage = vnContainer.find(`.expression-holder[data-avatar="${avatar}"]`);
200 /** @type {JQuery<HTMLElement>} */
201 let img;
202
203 const memberSpriteFolderName = getSpriteFolderName({ original_avatar: character.avatar }, character.name);
179204
180 // download images if not downloaded yet205 // download images if not downloaded yet
181 if (spriteCache[spriteFolderName] === undefined) {206 if (spriteCache[memberSpriteFolderName] === undefined) {
182 spriteCache[spriteFolderName] = await getSpritesList(spriteFolderName);207 spriteCache[memberSpriteFolderName] = await getSpritesList(memberSpriteFolderName);
183 }208 }
184209
185 const sprites = spriteCache[spriteFolderName];210 const prevExpressionSrc = expressionImage.find('img').attr('src') || null;
186 const expressionImage = container.find(`.expression-holder[data-avatar="${avatar}"]`);
187 const defaultExpression = getFallbackExpression();
188 const defaultSpritePath = sprites.find(x => x.label === defaultExpression)?.path;
189 const noSprites = sprites.length === 0;
190211
191 if (expressionImage.length > 0) {212 if (!originalExpression && Array.isArray(spriteCache[memberSpriteFolderName]) && spriteCache[memberSpriteFolderName].length > 0) {
192 if (name == spriteFolderName) {213 expression = await getLastMessageSprite(avatar);
193 await validateImages(spriteFolderName, true);214 }
194 setExpressionOverrideHtml(true); // <= force clear expression override input
195 const currentSpritePath = labels.includes(expression) ? sprites.find(x => x.label === expression)?.path : '';
196215
197 const path = currentSpritePath || defaultSpritePath || '';216 const spriteFile = chooseSpriteForExpression(memberSpriteFolderName, expression, { prevExpressionSrc: prevExpressionSrc });
198 const img = expressionImage.find('img');217 if (expressionImage.length) {
218 if (!spriteFolderName || spriteFolderName == memberSpriteFolderName) {
219 await validateImages(memberSpriteFolderName, true);
220 setExpressionOverrideHtml(true); // <= force clear expression override input
221 const path = spriteFile?.imageSrc || '';
222 img = expressionImage.find('img');
199 await setImage(img, path);223 await setImage(img, path);
200 }224 }
201 expressionImage.toggleClass('hidden', noSprites);225 expressionImage.toggleClass('hidden', !spriteFile);
202 } else {226 } else {
203 const template = $('#expression-holder').clone();227 const template = $('#expression-holder').clone();
204 template.attr('id', `expression-${avatar}`);228 template.attr('id', `expression-${avatar}`);
@@ -206,21 +230,49 @@ async function visualNovelSetCharacterSprites(container, name, expression) {
206 template.find('.drag-grabber').attr('id', `expression-${avatar}header`);230 template.find('.drag-grabber').attr('id', `expression-${avatar}header`);
207 $('#visual-novel-wrapper').append(template);231 $('#visual-novel-wrapper').append(template);
208 dragElement($(template[0]));232 dragElement($(template[0]));
209 template.toggleClass('hidden', noSprites);233 template.toggleClass('hidden', !spriteFile);
210 await setImage(template.find('img'), defaultSpritePath || '');234 img = template.find('img');
235 await setImage(img, spriteFile?.imageSrc || '');
211 const fadeInPromise = new Promise(resolve => {236 const fadeInPromise = new Promise(resolve => {
212 template.fadeIn(250, () => resolve());237 template.fadeIn(250, () => resolve());
213 });238 });
214 createCharacterPromises.push(fadeInPromise);239 setSpritePromises.push(fadeInPromise);
215 const setSpritePromise = setLastMessageSprite(template.find('img'), avatar, labels);
216 setSpritePromises.push(setSpritePromise);
217 }240 }
241
242 if (!img) {
243 continue;
244 }
245
246 img.attr('data-sprite-folder-name', spriteFolderName);
247 img.attr('data-expression', expression);
248 img.attr('data-sprite-filename', spriteFile?.fileName || null);
249 img.attr('title', expression);
250
251 if (spriteFile) console.info(`Expression set for group member ${character.name}`, { expression: spriteFile.expression, file: spriteFile.fileName });
252 else if (expressionImage.length) console.info(`Expression unset for group member ${character.name} - No sprite found`, { expression: expression });
253 else console.info(`Expression not available for group member ${character.name}`, { expression: expression });
218 }254 }
219255
220 await Promise.allSettled(createCharacterPromises);
221 return setSpritePromises;256 return setSpritePromises;
222}257}
223258
259/**
260 * Classifies the text of the latest message and returns the expression label.
261 * @param {string} avatar - The avatar of the character to get the last message for
262 * @returns {Promise<string>} - The expression label
263 */
264async function getLastMessageSprite(avatar) {
265 const context = getContext();
266 const lastMessage = context.chat.slice().reverse().find(x => x.original_avatar == avatar || (x.force_avatar && x.force_avatar.includes(encodeURIComponent(avatar))));
267
268 if (lastMessage) {
269 const text = lastMessage.mes || '';
270 return await getExpressionLabel(text);
271 }
272
273 return null;
274}
275
224async function visualNovelUpdateLayers(container) {276async function visualNovelUpdateLayers(container) {
225 const context = getContext();277 const context = getContext();
226 const group = context.groups.find(x => x.id == context.groupId);278 const group = context.groups.find(x => x.id == context.groupId);
@@ -256,11 +308,17 @@ async function visualNovelUpdateLayers(container) {
256 const containerWidth = container.width();308 const containerWidth = container.width();
257 const pivotalPoint = containerWidth * 0.5;309 const pivotalPoint = containerWidth * 0.5;
258310
259 let images = $('#visual-novel-wrapper .expression-holder');311 let images = Array.from($('#visual-novel-wrapper .expression-holder')).sort(sortFunction);
260 let imagesWidth = [];312 let imagesWidth = [];
261313
262 images.sort(sortFunction).each(function () {314 for (const image of images) {
263 imagesWidth.push($(this).width());315 if (image instanceof HTMLImageElement && !image.complete) {
316 await new Promise(resolve => image.addEventListener('load', resolve, { once: true }));
317 }
318 }
319
320 images.forEach(image => {
321 imagesWidth.push($(image).width());
264 });322 });
265323
266 let totalWidth = imagesWidth.reduce((a, b) => a + b, 0);324 let totalWidth = imagesWidth.reduce((a, b) => a + b, 0);
@@ -274,7 +332,7 @@ async function visualNovelUpdateLayers(container) {
274 currentPosition = 0; // Reset the initial position to 0332 currentPosition = 0; // Reset the initial position to 0
275 }333 }
276334
277 images.sort(sortFunction).each((index, current) => {335 images.forEach((current, index) => {
278 const element = $(current);336 const element = $(current);
279 const elementID = element.attr('id');337 const elementID = element.attr('id');
280338
@@ -294,9 +352,15 @@ async function visualNovelUpdateLayers(container) {
294 element.show();352 element.show();
295353
296 const promise = new Promise(resolve => {354 const promise = new Promise(resolve => {
355 if (power_user.reduced_motion) {
356 element.css('left', currentPosition + 'px');
357 requestAnimationFrame(() => resolve());
358 }
359 else {
297 element.animate({ left: currentPosition + 'px' }, 500, () => {360 element.animate({ left: currentPosition + 'px' }, 500, () => {
298 resolve();361 resolve();
299 });362 });
363 }
300 });364 });
301365
302 currentPosition += imagesWidth[index];366 currentPosition += imagesWidth[index];
@@ -307,23 +371,12 @@ async function visualNovelUpdateLayers(container) {
307 await Promise.allSettled(setLayerIndicesPromises);371 await Promise.allSettled(setLayerIndicesPromises);
308}372}
309373
310async function setLastMessageSprite(img, avatar, labels) {374/**
311 const context = getContext();375 * Sets the expression for the given character image.
312 const lastMessage = context.chat.slice().reverse().find(x => x.original_avatar == avatar || (x.force_avatar && x.force_avatar.includes(encodeURIComponent(avatar))));376 * @param {JQuery<HTMLElement>} img - The image element to set the image on
313377 * @param {string} path - The path to the image
314 if (lastMessage) {378 * @returns {Promise<void>} - A promise that resolves when the image is set
315 const text = lastMessage.mes || '';379 */
316 const spriteFolderName = getSpriteFolderName(lastMessage, lastMessage.name);
317 const sprites = spriteCache[spriteFolderName] || [];
318 const label = await getExpressionLabel(text);
319 const path = labels.includes(label) ? sprites.find(x => x.label === label)?.path : '';
320
321 if (path) {
322 setImage(img, path);
323 }
324 }
325}
326
327async function setImage(img, path) {380async function setImage(img, path) {
328 // Cohee: If something goes wrong, uncomment this to return to the old behavior381 // Cohee: If something goes wrong, uncomment this to return to the old behavior
329 /*382 /*
@@ -340,7 +393,7 @@ async function setImage(img, path) {
340 return new Promise(resolve => {393 return new Promise(resolve => {
341 const prevExpressionSrc = img.attr('src');394 const prevExpressionSrc = img.attr('src');
342 const expressionClone = img.clone();395 const expressionClone = img.clone();
343 const originalId = img.attr('id');396 const originalId = img.data('filename');
344397
345 //only swap expressions when necessary398 //only swap expressions when necessary
346 if (prevExpressionSrc !== path && !img.hasClass('expression-animating')) {399 if (prevExpressionSrc !== path && !img.hasClass('expression-animating')) {
@@ -348,7 +401,7 @@ async function setImage(img, path) {
348 expressionClone.addClass('expression-clone');401 expressionClone.addClass('expression-clone');
349 //make invisible and remove id to prevent double ids402 //make invisible and remove id to prevent double ids
350 //must be made invisible to start because they share the same Z-index403 //must be made invisible to start because they share the same Z-index
351 expressionClone.attr('id', '').css({ opacity: 0 });404 expressionClone.data('filename', '').css({ opacity: 0 });
352 //add new sprite path to clone src405 //add new sprite path to clone src
353 expressionClone.attr('src', path);406 expressionClone.attr('src', path);
354 //add invisible clone to html407 //add invisible clone to html
@@ -384,14 +437,18 @@ async function setImage(img, path) {
384 //remove old expression437 //remove old expression
385 img.remove();438 img.remove();
386 //replace ID so it becomes the new 'original' expression for next change439 //replace ID so it becomes the new 'original' expression for next change
387 expressionClone.attr('id', originalId);440 expressionClone.data('filename', originalId);
388 expressionClone.removeClass('expression-animating');441 expressionClone.removeClass('expression-animating');
389442
390 // Reset the expression holder min height and width443 // Reset the expression holder min height and width
391 expressionHolder.css('min-width', 100);444 expressionHolder.css('min-width', 100);
392 expressionHolder.css('min-height', 100);445 expressionHolder.css('min-height', 100);
393446
447 if (expressionClone.prop('complete')) {
394 resolve();448 resolve();
449 } else {
450 expressionClone.one('load', () => resolve());
451 }
395 });452 });
396453
397 expressionClone.removeClass('expression-clone');454 expressionClone.removeClass('expression-clone');
@@ -410,216 +467,9 @@ async function setImage(img, path) {
410 });467 });
411}468}
412469
413function onExpressionsShowDefaultInput() {470async function moduleWorker({ newChat = false } = {}) {
414 const value = $(this).prop('checked');
415 extension_settings.expressions.showDefault = value;
416 saveSettingsDebounced();
417
418 const existingImageSrc = $('img.expression').prop('src');
419 if (existingImageSrc !== undefined) { //if we have an image in src
420 if (!value && existingImageSrc.includes('/img/default-expressions/')) { //and that image is from /img/ (default)
421 $('img.expression').prop('src', ''); //remove it
422 lastMessage = null;
423 }
424 if (value) {
425 lastMessage = null;
426 }
427 }
428}
429
430/**
431 * Stops animating Talkinghead.
432 */
433async function unloadTalkingHead() {
434 if (!modules.includes('talkinghead')) {
435 console.debug('talkinghead module is disabled');
436 return;
437 }
438 console.debug('expressions: Stopping Talkinghead');
439
440 try {
441 const url = new URL(getApiUrl());
442 url.pathname = '/api/talkinghead/unload';
443 const loadResponse = await doExtrasFetch(url);
444 if (!loadResponse.ok) {
445 throw new Error(loadResponse.statusText);
446 }
447 //console.log(`Response: ${loadResponseText}`);
448 } catch (error) {
449 //console.error(`Error unloading - ${error}`);
450 }
451}
452
453/**
454 * Posts `talkinghead.png` of the current character to the talkinghead module in SillyTavern-extras, to start animating it.
455 */
456async function loadTalkingHead() {
457 if (!modules.includes('talkinghead')) {
458 console.debug('talkinghead module is disabled');
459 return;
460 }
461 console.debug('expressions: Starting Talkinghead');
462
463 const spriteFolderName = getSpriteFolderName();
464
465 const talkingheadPath = `/characters/${encodeURIComponent(spriteFolderName)}/talkinghead.png`;
466 const emotionsSettingsPath = `/characters/${encodeURIComponent(spriteFolderName)}/_emotions.json`;
467 const animatorSettingsPath = `/characters/${encodeURIComponent(spriteFolderName)}/_animator.json`;
468
469 try {
470 const spriteResponse = await fetch(talkingheadPath);
471
472 if (!spriteResponse.ok) {
473 throw new Error(spriteResponse.statusText);
474 }
475
476 const spriteBlob = await spriteResponse.blob();
477 const spriteFile = new File([spriteBlob], 'talkinghead.png', { type: 'image/png' });
478 const formData = new FormData();
479 formData.append('file', spriteFile);
480
481 const url = new URL(getApiUrl());
482 url.pathname = '/api/talkinghead/load';
483
484 const loadResponse = await doExtrasFetch(url, {
485 method: 'POST',
486 body: formData,
487 });
488
489 if (!loadResponse.ok) {
490 throw new Error(loadResponse.statusText);
491 }
492
493 const loadResponseText = await loadResponse.text();
494 console.log(`Load talkinghead response: ${loadResponseText}`);
495
496 // Optional: per-character emotion templates
497 let emotionsSettings;
498 try {
499 const emotionsResponse = await fetch(emotionsSettingsPath);
500 if (emotionsResponse.ok) {
501 emotionsSettings = await emotionsResponse.json();
502 console.log(`Loaded ${emotionsSettingsPath}`);
503 } else {
504 throw new Error();
505 }
506 }
507 catch (error) {
508 emotionsSettings = {}; // blank -> use server defaults (to unload the previous character's customizations)
509 console.log(`No valid config at ${emotionsSettingsPath}, using server defaults`);
510 }
511 try {
512 const url = new URL(getApiUrl());
513 url.pathname = '/api/talkinghead/load_emotion_templates';
514 const apiResult = await doExtrasFetch(url, {
515 method: 'POST',
516 headers: {
517 'Content-Type': 'application/json',
518 'Bypass-Tunnel-Reminder': 'bypass',
519 },
520 body: JSON.stringify(emotionsSettings),
521 });
522
523 if (!apiResult.ok) {
524 throw new Error(apiResult.statusText);
525 }
526 }
527 catch (error) {
528 // it's ok if not supported
529 console.log('Failed to send _emotions.json (backend too old?), ignoring');
530 }
531
532 // Optional: per-character animator and postprocessor config
533 let animatorSettings;
534 try {
535 const animatorResponse = await fetch(animatorSettingsPath);
536 if (animatorResponse.ok) {
537 animatorSettings = await animatorResponse.json();
538 console.log(`Loaded ${animatorSettingsPath}`);
539 } else {
540 throw new Error();
541 }
542 }
543 catch (error) {
544 animatorSettings = {}; // blank -> use server defaults (to unload the previous character's customizations)
545 console.log(`No valid config at ${animatorSettingsPath}, using server defaults`);
546 }
547 try {
548 const url = new URL(getApiUrl());
549 url.pathname = '/api/talkinghead/load_animator_settings';
550 const apiResult = await doExtrasFetch(url, {
551 method: 'POST',
552 headers: {
553 'Content-Type': 'application/json',
554 'Bypass-Tunnel-Reminder': 'bypass',
555 },
556 body: JSON.stringify(animatorSettings),
557 });
558
559 if (!apiResult.ok) {
560 throw new Error(apiResult.statusText);
561 }
562 }
563 catch (error) {
564 // it's ok if not supported
565 console.log('Failed to send _animator.json (backend too old?), ignoring');
566 }
567 } catch (error) {
568 console.error(`Error loading talkinghead image: ${talkingheadPath} - ${error}`);
569 }
570}
571
572function handleImageChange() {
573 const imgElement = document.querySelector('img#expression-image.expression');
574
575 if (!imgElement || !(imgElement instanceof HTMLImageElement)) {
576 console.log('Cannot find addExpressionImage()');
577 return;
578 }
579
580 if (isTalkingHeadEnabled() && modules.includes('talkinghead')) {
581 const talkingheadResultFeedSrc = `${getApiUrl()}/api/talkinghead/result_feed`;
582 $('#expression-holder').css({ display: '' });
583 if (imgElement.src !== talkingheadResultFeedSrc) {
584 const expressionImageElement = document.querySelector('.expression_list_image');
585
586 if (expressionImageElement && expressionImageElement instanceof HTMLImageElement) {
587 doExtrasFetch(expressionImageElement.src, {
588 method: 'HEAD',
589 })
590 .then(response => {
591 if (response.ok) {
592 imgElement.src = talkingheadResultFeedSrc;
593 }
594 })
595 .catch(error => {
596 console.error(error);
597 });
598 }
599 }
600 } else {
601 imgElement.src = ''; // remove in case char doesn't have expressions
602
603 // When switching Talkinghead off, force-set the character to the last known expression, if any.
604 // This preserves the same expression Talkinghead had at the moment it was switched off.
605 const charName = getContext().name2;
606 const last = lastExpression[charName];
607 const targetExpression = last ? last : getFallbackExpression();
608 setExpression(charName, targetExpression, true);
609 }
610}
611
612async function moduleWorker() {
613 const context = getContext();471 const context = getContext();
614472
615 // Hide and disable Talkinghead while not in extras
616 $('#image_type_block').toggle(extension_settings.expressions.api == EXPRESSION_API.extras);
617
618 if (extension_settings.expressions.api != EXPRESSION_API.extras && extension_settings.expressions.talkinghead) {
619 $('#image_type_toggle').prop('checked', false);
620 setTalkingHeadState(false);
621 }
622
623 // non-characters not supported473 // non-characters not supported
624 if (!context.groupId && context.characterId === undefined) {474 if (!context.groupId && context.characterId === undefined) {
625 removeExpression();475 removeExpression();
@@ -646,7 +496,7 @@ async function moduleWorker() {
646 }496 }
647497
648 const currentLastMessage = getLastCharacterMessage();498 const currentLastMessage = getLastCharacterMessage();
649 let spriteFolderName = context.groupId ? getSpriteFolderName(currentLastMessage, currentLastMessage.name) : getSpriteFolderName();499 let spriteFolderName = getSpriteFolderName(currentLastMessage, currentLastMessage.name);
650500
651 // character has no expressions or it is not loaded501 // character has no expressions or it is not loaded
652 if (Object.keys(spriteCache).length === 0) {502 if (Object.keys(spriteCache).length === 0) {
@@ -686,6 +536,10 @@ async function moduleWorker() {
686 offlineMode.css('display', 'none');536 offlineMode.css('display', 'none');
687 }537 }
688538
539 if (context.groupId && vnMode && newChat) {
540 await forceUpdateVisualNovelMode();
541 }
542
689 // Don't bother classifying if current char has no sprites and no default expressions are enabled543 // Don't bother classifying if current char has no sprites and no default expressions are enabled
690 if ((!Array.isArray(spriteCache[spriteFolderName]) || spriteCache[spriteFolderName].length === 0) && !extension_settings.expressions.showDefault) {544 if ((!Array.isArray(spriteCache[spriteFolderName]) || spriteCache[spriteFolderName].length === 0) && !extension_settings.expressions.showDefault) {
691 return;545 return;
@@ -732,11 +586,11 @@ async function moduleWorker() {
732 const force = !!context.groupId;586 const force = !!context.groupId;
733587
734 // Character won't be angry on you for swiping588 // Character won't be angry on you for swiping
735 if (currentLastMessage.mes == '...' && expressionsList.includes(getFallbackExpression())) {589 if (currentLastMessage.mes == '...' && expressionsList.includes(extension_settings.expressions.fallback_expression)) {
736 expression = getFallbackExpression();590 expression = extension_settings.expressions.fallback_expression;
737 }591 }
738592
739 await sendExpressionCall(spriteFolderName, expression, force, vnMode);593 await sendExpressionCall(spriteFolderName, expression, { force: force, vnMode: vnMode });
740 }594 }
741 catch (error) {595 catch (error) {
742 console.log(error);596 console.log(error);
@@ -749,91 +603,6 @@ async function moduleWorker() {
749 }603 }
750}604}
751605
752/**
753 * Starts/stops Talkinghead talking animation.
754 *
755 * Talking starts only when all the following conditions are met:
756 * - The LLM is currently streaming its output.
757 * - The AI's current last message is non-empty, and also not just '...' (as produced by a swipe).
758 * - The AI's current last message has changed from what we saw during the previous call.
759 *
760 * In all other cases, talking stops.
761 *
762 * A Talkinghead API call is made only when the talking state changes.
763 *
764 * Note that also the TTS system, if enabled, starts/stops the Talkinghead talking animation.
765 * See `talkingAnimation` in `SillyTavern/public/scripts/extensions/tts/index.js`.
766 */
767async function updateTalkingState() {
768 // Don't bother if Talkinghead is disabled or not loaded.
769 if (!isTalkingHeadEnabled() || !modules.includes('talkinghead')) {
770 return;
771 }
772
773 const context = getContext();
774 const currentLastMessage = getLastCharacterMessage();
775
776 try {
777 // TODO: Not sure if we need also "&& !context.groupId" here - the classify check in `moduleWorker`
778 // (that similarly checks the streaming processor state) does that for some reason.
779 // Talkinghead isn't currently designed to work with groups.
780 const lastMessageChanged = !((lastCharacter === context.characterId || lastCharacter === context.groupId) && lastTalkingStateMessage === currentLastMessage.mes);
781 const url = new URL(getApiUrl());
782 let newTalkingState;
783 if (context.streamingProcessor && !context.streamingProcessor.isFinished &&
784 currentLastMessage.mes.length !== 0 && currentLastMessage.mes !== '...' && lastMessageChanged) {
785 url.pathname = '/api/talkinghead/start_talking';
786 newTalkingState = true;
787 } else {
788 url.pathname = '/api/talkinghead/stop_talking';
789 newTalkingState = false;
790 }
791 try {
792 // Call the Talkinghead API only if the talking state changed.
793 if (newTalkingState !== lastTalkingState) {
794 console.debug(`updateTalkingState: calling ${url.pathname}`);
795 await doExtrasFetch(url);
796 }
797 }
798 catch (error) {
799 // it's ok if not supported
800 }
801 finally {
802 lastTalkingState = newTalkingState;
803 }
804 }
805 catch (error) {
806 // console.log(error);
807 }
808 finally {
809 lastTalkingStateMessage = currentLastMessage.mes;
810 }
811}
812
813/**
814 * Checks whether the current character has a talkinghead image available.
815 * @returns {Promise<boolean>} True if the character has a talkinghead image available, false otherwise.
816 */
817async function isTalkingHeadAvailable() {
818 let spriteFolderName = getSpriteFolderName();
819
820 try {
821 await validateImages(spriteFolderName);
822
823 let talkingheadObj = spriteCache[spriteFolderName].find(obj => obj.label === 'talkinghead');
824 let talkingheadPath = talkingheadObj ? talkingheadObj.path : null;
825
826 if (talkingheadPath != null) {
827 return true;
828 } else {
829 await unloadTalkingHead();
830 return false;
831 }
832 } catch (err) {
833 return err;
834 }
835}
836
837function getSpriteFolderName(characterMessage = null, characterName = null) {606function getSpriteFolderName(characterMessage = null, characterName = null) {
838 const context = getContext();607 const context = getContext();
839 let spriteFolderName = characterName ?? context.name2;608 let spriteFolderName = characterName ?? context.name2;
@@ -848,33 +617,6 @@ function getSpriteFolderName(characterMessage = null, characterName = null) {
848 return spriteFolderName;617 return spriteFolderName;
849}618}
850619
851function setTalkingHeadState(newState) {
852 console.debug(`expressions: New talkinghead state: ${newState}`);
853 extension_settings.expressions.talkinghead = newState; // Store setting
854 saveSettingsDebounced();
855
856 if ([EXPRESSION_API.local, EXPRESSION_API.llm, EXPRESSION_API.webllm].includes(extension_settings.expressions.api)) {
857 return;
858 }
859
860 isTalkingHeadAvailable().then(result => {
861 if (result) {
862 //console.log("talkinghead exists!");
863
864 if (extension_settings.expressions.talkinghead) {
865 loadTalkingHead();
866 } else {
867 unloadTalkingHead();
868 }
869 handleImageChange(); // Change image as needed
870
871
872 } else {
873 //console.log("talkinghead does not exist.");
874 }
875 });
876}
877
878function getFolderNameByMessage(message) {620function getFolderNameByMessage(message) {
879 const context = getContext();621 const context = getContext();
880 let avatarPath = '';622 let avatarPath = '';
@@ -894,48 +636,55 @@ function getFolderNameByMessage(message) {
894 return folderName;636 return folderName;
895}637}
896638
897async function sendExpressionCall(name, expression, force, vnMode) {639/**
898 lastExpression[name.split('/')[0]] = expression;640 * Update the expression for the given character.
899 if (!vnMode) {641 *
642 * @param {string} spriteFolderName The character name, optionally with a sprite folder override, e.g. "folder/expression".
643 * @param {string} expression The expression label, e.g. "amusement", "joy", etc.
644 * @param {Object} [options] Additional options
645 * @param {boolean} [options.force=false] If true, the expression will be sent even if it is the same as the current expression.
646 * @param {boolean} [options.vnMode=null] If true, the expression will be sent in Visual Novel mode. If null, it will be determined by the current chat mode.
647 * @param {string?} [options.overrideSpriteFile=null] - Set if a specific sprite file should be used. Must be sprite file name.
648 */
649export async function sendExpressionCall(spriteFolderName, expression, { force = false, vnMode = null, overrideSpriteFile = null } = {}) {
650 lastExpression[spriteFolderName.split('/')[0]] = expression;
651 if (vnMode === null) {
900 vnMode = isVisualNovelMode();652 vnMode = isVisualNovelMode();
901 }653 }
902654
903 if (vnMode) {655 if (vnMode) {
904 await updateVisualNovelMode(name, expression);656 await updateVisualNovelMode(spriteFolderName, expression);
905 } else {657 } else {
906 setExpression(name, expression, force);658 setExpression(spriteFolderName, expression, { force: force, overrideSpriteFile: overrideSpriteFile });
907 }659 }
908}660}
909661
910async function setSpriteSetCommand(_, folder) {662async function setSpriteFolderCommand(_, folder) {
911 if (!folder) {663 if (!folder) {
912 console.log('Clearing sprite set');664 console.log('Clearing sprite set');
913 folder = '';665 folder = '';
914 }666 }
915667
916 if (folder.startsWith('/') || folder.startsWith('\\')) {668 if (folder.startsWith('/') || folder.startsWith('\\')) {
917 folder = folder.slice(1);
918
919 const currentLastMessage = getLastCharacterMessage();669 const currentLastMessage = getLastCharacterMessage();
670 folder = folder.slice(1);
920 folder = `${currentLastMessage.name}/${folder}`;671 folder = `${currentLastMessage.name}/${folder}`;
921 }672 }
922673
923 $('#expression_override').val(folder.trim());674 $('#expression_override').val(folder.trim());
924 onClickExpressionOverrideButton();675 onClickExpressionOverrideButton();
925 // removeExpression();676
926 // moduleWorker();677 // No need to resend the expression, the folder override will automatically update the currently displayed one.
927 const vnMode = isVisualNovelMode();
928 await sendExpressionCall(folder, lastExpression, true, vnMode);
929 return '';678 return '';
930}679}
931680
932async function classifyCallback(/** @type {{api: string?, prompt: string?}} */ { api = null, prompt = null }, text) {681async function classifyCallback(/** @type {{api: string?, prompt: string?}} */ { api = null, prompt = null }, text) {
933 if (!text) {682 if (!text) {
934 toastr.warning('No text provided');683 toastr.error('No text provided');
935 return '';684 return '';
936 }685 }
937 if (api && !Object.keys(EXPRESSION_API).includes(api)) {686 if (api && !Object.keys(EXPRESSION_API).includes(api)) {
938 toastr.warning('Invalid API provided');687 toastr.error('Invalid API provided');
939 return '';688 return '';
940 }689 }
941690
@@ -951,37 +700,69 @@ async function classifyCallback(/** @type {{api: string?, prompt: string?}} */ {
951 return label;700 return label;
952}701}
953702
954async function setSpriteSlashCommand(_, spriteId) {703/** @type {(args: {type: 'expression' | 'sprite'}, searchTerm: string) => Promise<string>} */
955 if (!spriteId) {704async function setSpriteSlashCommand({ type }, searchTerm) {
956 console.log('No sprite id provided');705 type ??= 'expression';
706 searchTerm = searchTerm.trim().toLowerCase();
707 if (!searchTerm) {
708 toastr.error(t`No expression or sprite name provided`, t`Set Sprite`);
957 return '';709 return '';
958 }710 }
959711
960 spriteId = spriteId.trim().toLowerCase();712 const currentLastMessage = selected_group ? getLastCharacterMessage() : null;
713 const spriteFolderName = getSpriteFolderName(currentLastMessage, currentLastMessage?.name);
714
715 let label = searchTerm;
716
717 /** @type {string?} */
718 let spriteFile = null;
961719
962 // In Talkinghead mode, don't check for the existence of the sprite
963 // (emotion names are the same as for sprites, but it only needs "talkinghead.png").
964 const currentLastMessage = getLastCharacterMessage();
965 const spriteFolderName = getSpriteFolderName(currentLastMessage, currentLastMessage.name);
966 let label = spriteId;
967 if (!isTalkingHeadEnabled() || !modules.includes('talkinghead')) {
968 await validateImages(spriteFolderName);720 await validateImages(spriteFolderName);
969721
970 // Fuzzy search for sprite722 // Handle reset as a special term and just reset the sprite via expression call
971 const fuse = new Fuse(spriteCache[spriteFolderName], { keys: ['label'] });723 if (searchTerm === RESET_SPRITE_LABEL) {
972 const results = fuse.search(spriteId);724 await sendExpressionCall(spriteFolderName, label, { force: true });
973 const spriteItem = results[0]?.item;725 return lastExpression[spriteFolderName] ?? '';
726 }
727
728 switch (type) {
729 case 'expression': {
730 // Fuzzy search for expression
731 const existingExpressions = getCachedExpressions().map(x => ({ label: x }));
732 const results = performFuzzySearch('expression-expressions', existingExpressions, [
733 { name: 'label', weight: 1 },
734 ], searchTerm);
735 const matchedExpression = results[0]?.item;
736 if (!matchedExpression) {
737 toastr.warning(t`No expression found for search term ${searchTerm}`, t`Set Sprite`);
738 return '';
739 }
974740
975 if (!spriteItem) {741 label = matchedExpression.label;
976 console.log('No sprite found for search term ' + spriteId);742 break;
743 }
744 case 'sprite': {
745 // Fuzzy search for sprite file
746 const sprites = spriteCache[spriteFolderName].map(x => x.files).flat();
747 const results = performFuzzySearch('expression-expressions', sprites, [
748 { name: 'title', weight: 1 },
749 { name: 'fileName', weight: 1 },
750 ], searchTerm);
751 const matchedSprite = results[0]?.item;
752 if (!matchedSprite) {
753 toastr.warning(t`No sprite file found for search term ${searchTerm}`, t`Set Sprite`);
977 return '';754 return '';
978 }755 }
979756
980 label = spriteItem.label;757 label = matchedSprite.expression;
758 spriteFile = matchedSprite.fileName;
759 break;
760 }
761 default: throw Error('Invalid sprite set type: ' + type);
981 }762 }
982763
983 const vnMode = isVisualNovelMode();764 await sendExpressionCall(spriteFolderName, label, { force: true, overrideSpriteFile: spriteFile });
984 await sendExpressionCall(spriteFolderName, label, true, vnMode);765
985 return label;766 return label;
986}767}
987768
@@ -999,6 +780,21 @@ function spriteFolderNameFromCharacter(char) {
999}780}
1000781
1001/**782/**
783 * Generates a unique sprite name by appending an index to the given expression. *
784 * @param {string} expression - The base expression to be used as the prefix for the sprite name.
785 * @param {ExpressionImage[]} existingFiles - An array of existing file objects, each containing a fileName property.
786 * @returns {string} - A unique sprite name with the format "expression-index".
787 */
788function generateUniqueSpriteName(expression, existingFiles) {
789 let index = existingFiles.length;
790 let newSpriteName;
791 do {
792 newSpriteName = `${expression}-${index++}`;
793 } while (existingFiles.some(file => withoutExtension(file.fileName) === newSpriteName));
794 return newSpriteName;
795}
796
797/**
1002 * Slash command callback for /uploadsprite798 * Slash command callback for /uploadsprite
1003 *799 *
1004 * label= is required800 * label= is required
@@ -1011,16 +807,29 @@ function spriteFolderNameFromCharacter(char) {
1011 * @param {object} args807 * @param {object} args
1012 * @param {string} args.name Character name or avatar key, passed through findChar808 * @param {string} args.name Character name or avatar key, passed through findChar
1013 * @param {string} args.label Expression label809 * @param {string} args.label Expression label
1014 * @param {string} args.folder Sprite folder path, processed using backslash rules810 * @param {string} [args.folder=null] Optional sprite folder path, processed using backslash rules
811 * @param {string?} [args.spriteName=null] Optional sprite name
1015 * @param {string} imageUrl Image URI to fetch and upload812 * @param {string} imageUrl Image URI to fetch and upload
1016 * @returns {Promise<void>}813 * @returns {Promise<string>} the sprite name
1017 */814 */
1018async function uploadSpriteCommand({ name, label, folder }, imageUrl) {815async function uploadSpriteCommand({ name, label, folder = null, spriteName = null }, imageUrl) {
1019 if (!imageUrl) throw new Error('Image URL is required');816 if (!imageUrl) throw new Error('Image URL is required');
1020 if (!label || typeof label !== 'string') throw new Error('Expression label is required');817 if (!label || typeof label !== 'string') {
818 toastr.error(t`Expression label is required`, t`Error Uploading Sprite`);
819 return '';
820 }
1021821
1022 label = label.replace(/[^a-z]/gi, '').toLowerCase().trim();822 label = label.replace(/[^a-z]/gi, '').toLowerCase().trim();
1023 if (!label) throw new Error('Expression label must contain at least one letter');823 if (!label) {
824 toastr.error(t`Expression label must contain at least one letter`, t`Error Uploading Sprite`);
825 return '';
826 }
827
828 spriteName = spriteName || label;
829 if (!validateExpressionSpriteName(label, spriteName)) {
830 toastr.error(t`Invalid sprite name. Must follow the naming pattern for expression sprites.`, t`Error Uploading Sprite`);
831 return '';
832 }
1024833
1025 name = name || getLastCharacterMessage().original_avatar || getLastCharacterMessage().name;834 name = name || getLastCharacterMessage().original_avatar || getLastCharacterMessage().name;
1026 const char = findChar({ name });835 const char = findChar({ name });
@@ -1041,6 +850,7 @@ async function uploadSpriteCommand({ name, label, folder }, imageUrl) {
1041 formData.append('name', folder); // this is the folder or character name850 formData.append('name', folder); // this is the folder or character name
1042 formData.append('label', label); // this is the expression label851 formData.append('label', label); // this is the expression label
1043 formData.append('avatar', file); // this is the image file852 formData.append('avatar', file); // this is the image file
853 formData.append('spriteName', spriteName); // this is a redundant comment
1044854
1045 await handleFileUpload('/api/sprites/upload', formData);855 await handleFileUpload('/api/sprites/upload', formData);
1046 console.debug(`[${MODULE_NAME}] Upload of ${imageUrl} completed for ${name} with label ${label}`);856 console.debug(`[${MODULE_NAME}] Upload of ${imageUrl} completed for ${name} with label ${label}`);
@@ -1048,6 +858,8 @@ async function uploadSpriteCommand({ name, label, folder }, imageUrl) {
1048 console.error(`[${MODULE_NAME}] Error uploading file:`, error);858 console.error(`[${MODULE_NAME}] Error uploading file:`, error);
1049 throw error;859 throw error;
1050 }860 }
861
862 return spriteName;
1051}863}
1052864
1053/**865/**
@@ -1159,7 +971,7 @@ function getJsonSchema(emotions) {
1159function onTextGenSettingsReady(args) {971function onTextGenSettingsReady(args) {
1160 // Only call if inside an API call972 // Only call if inside an API call
1161 if (inApiCall && extension_settings.expressions.api === EXPRESSION_API.llm && isJsonSchemaSupported()) {973 if (inApiCall && extension_settings.expressions.api === EXPRESSION_API.llm && isJsonSchemaSupported()) {
1162 const emotions = DEFAULT_EXPRESSIONS.filter((e) => e != 'talkinghead');974 const emotions = DEFAULT_EXPRESSIONS;
1163 Object.assign(args, {975 Object.assign(args, {
1164 top_k: 1,976 top_k: 1,
1165 stop: [],977 stop: [],
@@ -1177,16 +989,16 @@ function onTextGenSettingsReady(args) {
1177 * @param {EXPRESSION_API} [expressionsApi=extension_settings.expressions.api] - The expressions API to use for classification.989 * @param {EXPRESSION_API} [expressionsApi=extension_settings.expressions.api] - The expressions API to use for classification.
1178 * @param {object} [options={}] - Optional arguments.990 * @param {object} [options={}] - Optional arguments.
1179 * @param {string?} [options.customPrompt=null] - The custom prompt to use for classification.991 * @param {string?} [options.customPrompt=null] - The custom prompt to use for classification.
1180 * @returns {Promise<string>} - The label of the expression.992 * @returns {Promise<string?>} - The label of the expression.
1181 */993 */
1182export async function getExpressionLabel(text, expressionsApi = extension_settings.expressions.api, { customPrompt = null } = {}) {994export async function getExpressionLabel(text, expressionsApi = extension_settings.expressions.api, { customPrompt = null } = {}) {
1183 // Return if text is undefined, saving a costly fetch request995 // Return if text is undefined, saving a costly fetch request
1184 if ((!modules.includes('classify') && expressionsApi == EXPRESSION_API.extras) || !text) {996 if ((!modules.includes('classify') && expressionsApi == EXPRESSION_API.extras) || !text) {
1185 return getFallbackExpression();997 return extension_settings.expressions.fallback_expression;
1186 }998 }
1187999
1188 if (extension_settings.expressions.translate && typeof window['translate'] === 'function') {1000 if (extension_settings.expressions.translate && typeof globalThis.translate === 'function') {
1189 text = await window['translate'](text, 'en');1001 text = await globalThis.translate(text, 'en');
1190 }1002 }
11911003
1192 text = sampleClassifyText(text);1004 text = sampleClassifyText(text);
@@ -1212,7 +1024,7 @@ export async function getExpressionLabel(text, expressionsApi = extension_settin
1212 await waitUntilCondition(() => online_status !== 'no_connection', 3000, 250);1024 await waitUntilCondition(() => online_status !== 'no_connection', 3000, 250);
1213 } catch (error) {1025 } catch (error) {
1214 console.warn('No LLM connection. Using fallback expression', error);1026 console.warn('No LLM connection. Using fallback expression', error);
1215 return getFallbackExpression();1027 return extension_settings.expressions.fallback_expression;
1216 }1028 }
12171029
1218 const expressionsList = await getExpressionsList();1030 const expressionsList = await getExpressionsList();
@@ -1225,7 +1037,7 @@ export async function getExpressionLabel(text, expressionsApi = extension_settin
1225 case EXPRESSION_API.webllm: {1037 case EXPRESSION_API.webllm: {
1226 if (!isWebLlmSupported()) {1038 if (!isWebLlmSupported()) {
1227 console.warn('WebLLM is not supported. Using fallback expression');1039 console.warn('WebLLM is not supported. Using fallback expression');
1228 return getFallbackExpression();1040 return extension_settings.expressions.fallback_expression;
1229 }1041 }
12301042
1231 const expressionsList = await getExpressionsList();1043 const expressionsList = await getExpressionsList();
@@ -1258,9 +1070,9 @@ export async function getExpressionLabel(text, expressionsApi = extension_settin
1258 } break;1070 } break;
1259 }1071 }
1260 } catch (error) {1072 } catch (error) {
1261 toastr.info('Could not classify expression. Check the console or your backend for more information.');1073 toastr.error('Could not classify expression. Check the console or your backend for more information.');
1262 console.error(error);1074 console.error(error);
1263 return getFallbackExpression();1075 return extension_settings.expressions.fallback_expression;
1264 }1076 }
1265}1077}
12661078
@@ -1288,75 +1100,155 @@ function removeExpression() {
1288 $('#no_chat_expressions').show();1100 $('#no_chat_expressions').show();
1289}1101}
12901102
1291async function validateImages(character, forceRedrawCached) {1103/**
1292 if (!character) {1104 * Validate a character's sprites, and redraw the sprites list if not done before or forced to redraw.
1105 * @param {string} spriteFolderName - The character sprite folder to validate
1106 * @param {boolean} [forceRedrawCached=false] - Whether to force redrawing the sprites list even if it's already been drawn before
1107 */
1108async function validateImages(spriteFolderName, forceRedrawCached = false) {
1109 if (!spriteFolderName) {
1293 return;1110 return;
1294 }1111 }
12951112
1296 const labels = await getExpressionsList();1113 const labels = await getExpressionsList();
12971114
1298 if (spriteCache[character]) {1115 if (spriteCache[spriteFolderName]) {
1299 if (forceRedrawCached && $('#image_list').data('name') !== character) {1116 if (forceRedrawCached && $('#image_list').data('name') !== spriteFolderName) {
1300 console.debug('force redrawing character sprites list');1117 console.debug('force redrawing character sprites list');
1301 await drawSpritesList(character, labels, spriteCache[character]);1118 await drawSpritesList(spriteFolderName, labels, spriteCache[spriteFolderName]);
1302 }1119 }
13031120
1304 return;1121 return;
1305 }1122 }
13061123
1307 const sprites = await getSpritesList(character);1124 const sprites = await getSpritesList(spriteFolderName);
1308 let validExpressions = await drawSpritesList(character, labels, sprites);1125 let validExpressions = await drawSpritesList(spriteFolderName, labels, sprites);
1309 spriteCache[character] = validExpressions;1126 spriteCache[spriteFolderName] = validExpressions;
1127}
1128
1129/**
1130 * Takes a given sprite as returned from the server, and enriches it with additional data for display/sorting
1131 * @param {{ path: string, label: string }} sprite
1132 * @returns {ExpressionImage}
1133 */
1134function getExpressionImageData(sprite) {
1135 const fileName = sprite.path.split('/').pop().split('?')[0];
1136 const fileNameWithoutExtension = fileName.replace(/\.[^/.]+$/, '');
1137 return {
1138 expression: sprite.label,
1139 fileName: fileName,
1140 title: fileNameWithoutExtension,
1141 imageSrc: sprite.path,
1142 type: 'success',
1143 isCustom: extension_settings.expressions.custom?.includes(sprite.label),
1144 };
1310}1145}
13111146
1312async function drawSpritesList(character, labels, sprites) {1147/**
1148 * Populate the character expression list with sprites for the given character.
1149 * @param {string} spriteFolderName - The name of the character to populate the list for
1150 * @param {string[]} labels - An array of expression labels that are valid
1151 * @param {Expression[]} sprites - An array of sprites
1152 * @returns {Promise<Expression[]>} An array of valid expression labels
1153 */
1154async function drawSpritesList(spriteFolderName, labels, sprites) {
1155 /** @type {Expression[]} */
1313 let validExpressions = [];1156 let validExpressions = [];
1157
1314 $('#no_chat_expressions').hide();1158 $('#no_chat_expressions').hide();
1315 $('#open_chat_expressions').show();1159 $('#open_chat_expressions').show();
1316 $('#image_list').empty();1160 $('#image_list').empty();
1317 $('#image_list').data('name', character);1161 $('#image_list').data('name', spriteFolderName);
1318 $('#image_list_header_name').text(character);1162 $('#image_list_header_name').text(spriteFolderName);
13191163
1320 if (!Array.isArray(labels)) {1164 if (!Array.isArray(labels)) {
1321 return [];1165 return [];
1322 }1166 }
13231167
1324 for (const item of labels.sort()) {1168 for (const expression of labels.sort()) {
1325 const sprite = sprites.find(x => x.label == item);1169 const isCustom = extension_settings.expressions.custom?.includes(expression);
1326 const isCustom = extension_settings.expressions.custom.includes(item);1170 const images = sprites
13271171 .filter(s => s.label === expression)
1328 if (sprite) {1172 .map(s => s.files)
1329 validExpressions.push(sprite);1173 .flat();
1330 const listItem = await getListItem(item, sprite.path, 'success', isCustom);1174
1175 if (images.length === 0) {
1176 const listItem = await getListItem(expression, {
1177 isCustom,
1178 images: [getPlaceholderImage(expression, isCustom)],
1179 });
1331 $('#image_list').append(listItem);1180 $('#image_list').append(listItem);
1181 continue;
1332 }1182 }
1333 else {1183
1334 const listItem = await getListItem(item, '/img/No-Image-Placeholder.svg', 'failure', isCustom);1184 validExpressions.push({ label: expression, files: images });
1185
1186 // Render main = first file, additional = rest
1187 let listItem = await getListItem(expression, {
1188 isCustom,
1189 images,
1190 });
1335 $('#image_list').append(listItem);1191 $('#image_list').append(listItem);
1336 }1192 }
1337 }
1338 return validExpressions;1193 return validExpressions;
1339}1194}
13401195
1341/**1196/**
1342 * Renders a list item template for the expressions list.1197 * Renders a list item template for the expressions list.
1343 * @param {string} item Expression name1198 * @param {string} expression Expression name
1344 * @param {string} imageSrc Path to image1199 * @param {object} args Arguments object
1345 * @param {'success' | 'failure'} textClass 'success' or 'failure'1200 * @param {ExpressionImage[]} [args.images] Array of image objects
1346 * @param {boolean} isCustom If expression is added by user1201 * @param {boolean} [args.isCustom=false] If expression is added by user
1347 * @returns {Promise<string>} Rendered list item template1202 * @returns {Promise<string>} Rendered list item template
1348 */1203 */
1349async function getListItem(item, imageSrc, textClass, isCustom) {1204async function getListItem(expression, { images, isCustom = false } = {}) {
1350 return renderExtensionTemplateAsync(MODULE_NAME, 'list-item', { item, imageSrc, textClass, isCustom });1205 return renderExtensionTemplateAsync(MODULE_NAME, 'list-item', { expression, images, isCustom: isCustom ?? false });
1351}1206}
13521207
1208/**
1209 * Fetches and processes the list of sprites for a given character name.
1210 * Retrieves sprite data from the server and organizes it into labeled groups.
1211 *
1212 * @param {string} name - The character name to fetch sprites for
1213 * @returns {Promise<Expression[]>} A promise that resolves to an array of grouped expression objects, each containing a label and associated image data
1214 */
1215
1353async function getSpritesList(name) {1216async function getSpritesList(name) {
1354 console.debug('getting sprites list');1217 console.debug('getting sprites list');
13551218
1356 try {1219 try {
1357 const result = await fetch(`/api/sprites/get?name=${encodeURIComponent(name)}`);1220 const result = await fetch(`/api/sprites/get?name=${encodeURIComponent(name)}`);
1221 /** @type {{ label: string, path: string }[]} */
1358 let sprites = result.ok ? (await result.json()) : [];1222 let sprites = result.ok ? (await result.json()) : [];
1359 return sprites;1223
1224 /** @type {Expression[]} */
1225 const grouped = sprites.reduce((acc, sprite) => {
1226 const imageData = getExpressionImageData(sprite);
1227 let existingExpression = acc.find(exp => exp.label === sprite.label);
1228 if (existingExpression) {
1229 existingExpression.files.push(imageData);
1230 } else {
1231 acc.push({ label: sprite.label, files: [imageData] });
1232 }
1233
1234 return acc;
1235 }, []);
1236
1237 // Sort the sprites for each expression alphabetically, but keep the main expression file at the front
1238 for (const expression of grouped) {
1239 expression.files.sort((a, b) => {
1240 if (a.title === expression.label) return -1;
1241 if (b.title === expression.label) return 1;
1242 return a.title.localeCompare(b.title);
1243 });
1244
1245 // Mark all besides the first sprite as 'additional'
1246 for (let i = 1; i < expression.files.length; i++) {
1247 expression.files[i].type = 'additional';
1248 }
1249 }
1250
1251 return grouped;
1360 }1252 }
1361 catch (err) {1253 catch (err) {
1362 console.log(err);1254 console.log(err);
@@ -1395,17 +1287,31 @@ async function renderFallbackExpressionPicker() {
1395 const defaultPicker = $('#expression_fallback');1287 const defaultPicker = $('#expression_fallback');
1396 defaultPicker.empty();1288 defaultPicker.empty();
13971289
1398 const fallbackExpression = getFallbackExpression();1290
1291 addOption(OPTION_NO_FALLBACK, '[ No fallback ]', !extension_settings.expressions.fallback_expression);
1292 addOption(OPTION_EMOJI_FALLBACK, '[ Default emojis ]', !!extension_settings.expressions.showDefault);
13991293
1400 for (const expression of expressions) {1294 for (const expression of expressions) {
1295 addOption(expression, expression, expression == extension_settings.expressions.fallback_expression);
1296 }
1297
1298 /** @type {(value: string, label: string, isSelected: boolean) => void} */
1299 function addOption(value, label, isSelected) {
1401 const option = document.createElement('option');1300 const option = document.createElement('option');
1402 option.value = expression;1301 option.value = value;
1403 option.text = expression;1302 option.text = label;
1404 option.selected = expression == fallbackExpression;1303 option.selected = isSelected;
1405 defaultPicker.append(option);1304 defaultPicker.append(option);
1406 }1305 }
1407}1306}
14081307
1308/**
1309 * Retrieves a unique list of cached expressions.
1310 * Combines the default expressions list with custom user-defined expressions.
1311 *
1312 * @returns {string[]} An array of unique expression labels
1313 */
1314
1409function getCachedExpressions() {1315function getCachedExpressions() {
1410 if (!Array.isArray(expressionsList)) {1316 if (!Array.isArray(expressionsList)) {
1411 return [];1317 return [];
@@ -1463,7 +1369,7 @@ export async function getExpressionsList() {
1463 }1369 }
14641370
1465 // If there was no specific list, or an error, just return the default expressions1371 // If there was no specific list, or an error, just return the default expressions
1466 expressionsList = DEFAULT_EXPRESSIONS.filter(e => e !== 'talkinghead').slice();1372 expressionsList = DEFAULT_EXPRESSIONS.slice();
1467 return expressionsList;1373 return expressionsList;
1468 }1374 }
14691375
@@ -1471,38 +1377,88 @@ export async function getExpressionsList() {
1471 return [...result, ...extension_settings.expressions.custom].filter(onlyUnique);1377 return [...result, ...extension_settings.expressions.custom].filter(onlyUnique);
1472}1378}
14731379
1474async function setExpression(character, expression, force) {1380/**
1475 if (!isTalkingHeadEnabled() || !modules.includes('talkinghead')) {1381 * Selects a sprite from the given sprite folder for the given expression.
1476 console.debug('entered setExpressions');1382 *
1477 await validateImages(character);1383 * If multiple sprites are allowed for the expression, it will randomly select one.
1384 * If the rerollIfSame option is enabled, it will only select a different sprite if the previous sprite was the same.
1385 * If the overrideSpriteFile option is set, it will look for the sprite with the given file name instead of randomly selecting one.
1386 *
1387 * @param {string} spriteFolderName - The name of the sprite folder
1388 * @param {string} expression - The expression to find the sprite for
1389 * @param {object} [options] - Options to select the sprite
1390 * @param {string} [options.prevExpressionSrc=null] - The source of the previous expression
1391 * @param {string} [options.overrideSpriteFile=null] - The file name of the sprite to select
1392 * @returns {ExpressionImage?} - The selected sprite
1393 */
1394function chooseSpriteForExpression(spriteFolderName, expression, { prevExpressionSrc = null, overrideSpriteFile = null } = {}) {
1395 if (!spriteCache[spriteFolderName]) return null;
1396 if (expression === RESET_SPRITE_LABEL) return null;
1397
1398 // Search for sprites of that expression - or fallback expression sprites if enabled
1399 let sprite = spriteCache[spriteFolderName].find(x => x.label === expression);
1400 if (!(sprite?.files.length > 0) && extension_settings.expressions.fallback_expression) {
1401 sprite = spriteCache[spriteFolderName].find(x => x.label === extension_settings.expressions.fallback_expression);
1402 console.debug('Expression', expression, 'not found. Using fallback expression', extension_settings.expressions.fallback_expression);
1403 }
1404 if (!(sprite?.files.length > 0)) return null;
1405
1406 let spriteFile = sprite.files[0];
1407
1408 // If a specific sprite file should be set, we are looking it up here
1409 if (overrideSpriteFile) {
1410 const searched = sprite.files.find(x => x.fileName === overrideSpriteFile);
1411 if (searched) spriteFile = searched;
1412 else toastr.warning(t`Couldn't find sprite file ${overrideSpriteFile} for expression ${expression}.`, t`Sprite Not Found`);
1413 }
1414 // Else calculate next expression, if multiple are allowed
1415 else if (extension_settings.expressions.allowMultiple && sprite.files.length > 1) {
1416 let possibleFiles = sprite.files;
1417 if (extension_settings.expressions.rerollIfSame) {
1418 possibleFiles = possibleFiles.filter(x => !prevExpressionSrc || x.imageSrc !== prevExpressionSrc);
1419 }
1420 spriteFile = possibleFiles[Math.floor(Math.random() * possibleFiles.length)];
1421 }
1422
1423 return spriteFile;
1424
1425}
1426
1427/**
1428 * Set the expression of a character.
1429 * @param {string} spriteFolderName - The name of the character (folder name - can also be a costume override)
1430 * @param {string} expression - The expression or sprite name to set
1431 * @param {Object} options - Optional parameters
1432 * @param {boolean} [options.force=false] - Whether to force the expression change even if Visual Novel mode is on
1433 * @param {string?} [options.overrideSpriteFile=null] - Set if a specific sprite file should be used. Must be sprite file name.
1434 * @returns {Promise<void>} A promise that resolves when the expression has been set.
1435 */
1436async function setExpression(spriteFolderName, expression, { force = false, overrideSpriteFile = null } = {}) {
1437 await validateImages(spriteFolderName);
1478 const img = $('img.expression');1438 const img = $('img.expression');
1479 const prevExpressionSrc = img.attr('src');1439 const prevExpressionSrc = img.attr('src');
1480 const expressionClone = img.clone();1440 const expressionClone = img.clone();
14811441
1482 const sprite = (spriteCache[character] && spriteCache[character].find(x => x.label === expression));1442 const spriteFile = chooseSpriteForExpression(spriteFolderName, expression, { prevExpressionSrc: prevExpressionSrc, overrideSpriteFile: overrideSpriteFile });
1483 console.debug('checking for expression images to show..');1443 if (spriteFile) {
1484 if (sprite) {
1485 console.debug('setting expression from character images folder');
1486
1487 if (force && isVisualNovelMode()) {1444 if (force && isVisualNovelMode()) {
1488 const context = getContext();1445 const context = getContext();
1489 const group = context.groups.find(x => x.id === context.groupId);1446 const group = context.groups.find(x => x.id === context.groupId);
14901447
1491 for (const member of group.members) {1448 // If it's a folder, make sure we find the group member based on the actual name
1492 const groupMember = context.characters.find(x => x.avatar === member);1449 const memberName = spriteFolderName.split('/')[0] ?? spriteFolderName;
1493
1494 if (!groupMember) {
1495 continue;
1496 }
14971450
1498 if (groupMember.name == character) {1451 const groupMember = group.members
1499 await setImage($(`.expression-holder[data-avatar="${member}"] img`), sprite.path);1452 .map(member => context.characters.find(x => x.avatar === member))
1453 .find(groupMember => groupMember && groupMember.name === memberName);
1454 if (groupMember) {
1455 await setImage($(`.expression-holder[data-avatar="${groupMember.avatar}"] img`), spriteFile.imageSrc);
1500 return;1456 return;
1501 }1457 }
1502 }1458 }
1503 }1459
1504 //only swap expressions when necessary1460 //only swap expressions when necessary
1505 if (prevExpressionSrc !== sprite.path1461 if (prevExpressionSrc !== spriteFile.imageSrc
1506 && !img.hasClass('expression-animating')) {1462 && !img.hasClass('expression-animating')) {
1507 //clone expression1463 //clone expression
1508 expressionClone.addClass('expression-clone');1464 expressionClone.addClass('expression-clone');
@@ -1510,7 +1466,12 @@ async function setExpression(character, expression, force) {
1510 //must be made invisible to start because they share the same Z-index1466 //must be made invisible to start because they share the same Z-index
1511 expressionClone.attr('id', '').css({ opacity: 0 });1467 expressionClone.attr('id', '').css({ opacity: 0 });
1512 //add new sprite path to clone src1468 //add new sprite path to clone src
1513 expressionClone.attr('src', sprite.path);1469 expressionClone.attr('src', spriteFile.imageSrc);
1470 //set relevant data tags
1471 expressionClone.attr('data-sprite-folder-name', spriteFolderName);
1472 expressionClone.attr('data-expression', expression);
1473 expressionClone.attr('data-sprite-filename', spriteFile.fileName);
1474 expressionClone.attr('title', expression);
1514 //add invisible clone to html1475 //add invisible clone to html
1515 expressionClone.appendTo($('#expression-holder'));1476 expressionClone.appendTo($('#expression-holder'));
15161477
@@ -1552,80 +1513,85 @@ async function setExpression(character, expression, force) {
1552 expressionHolder.css('min-height', 100);1513 expressionHolder.css('min-height', 100);
1553 });1514 });
15541515
1555
1556 expressionClone.removeClass('expression-clone');1516 expressionClone.removeClass('expression-clone');
15571517
1558 expressionClone.removeClass('default');1518 expressionClone.removeClass('default');
1559 expressionClone.off('error');1519 expressionClone.off('error');
1560 expressionClone.on('error', function () {1520 expressionClone.on('error', function (error) {
1561 console.debug('Expression image error', sprite.path);1521 console.debug('Expression image error', spriteFile.imageSrc, error);
1562 $(this).attr('src', '');1522 $(this).attr('src', '');
1563 $(this).off('error');1523 $(this).off('error');
1564 if (force && extension_settings.expressions.showDefault) {1524 if (force && extension_settings.expressions.showDefault) {
1565 setDefault();1525 setDefaultEmojiForImage(img, expression);
1566 }1526 }
1567 });1527 });
1568 }1528 }
1529
1530 console.info('Expression set', { expression: spriteFile.expression, file: spriteFile.fileName });
1569 }1531 }
1570 else {1532 else {
1571 if (extension_settings.expressions.showDefault) {1533 img.attr('data-sprite-folder-name', spriteFolderName);
1572 setDefault();
1573 }
1574 }
15751534
1576 function setDefault() {1535 img.off('error');
1577 console.debug('setting default');
1578 const defImgUrl = `/img/default-expressions/${expression}.png`;
1579 //console.log(defImgUrl);
1580 img.attr('src', defImgUrl);
1581 img.addClass('default');
1582 }
1583 document.getElementById('expression-holder').style.display = '';
15841536
1537 if (extension_settings.expressions.showDefault && expression !== RESET_SPRITE_LABEL) {
1538 setDefaultEmojiForImage(img, expression);
1585 } else {1539 } else {
1586 // Set the Talkinghead emotion to the specified expression1540 setNoneForImage(img, expression);
1587 // TODO: For now, Talkinghead emote only supported when VN mode is off; see also updateVisualNovelMode.
1588 try {
1589 let result = await isTalkingHeadAvailable();
1590 if (result) {
1591 const url = new URL(getApiUrl());
1592 url.pathname = '/api/talkinghead/set_emotion';
1593 await doExtrasFetch(url, {
1594 method: 'POST',
1595 headers: {
1596 'Content-Type': 'application/json',
1597 },
1598 body: JSON.stringify({ emotion_name: expression }),
1599 });
1600 }1541 }
1601 }1542 console.debug('Expression unset - No sprite found', { expression: expression });
1602 catch (error) {
1603 // `set_emotion` is not present in old versions, so let it 404.
1604 }1543 }
16051544
1606 try {1545 document.getElementById('expression-holder').style.display = '';
1607 // Find the <img> element with id="expression-image" and class="expression"
1608 const imgElement = document.querySelector('img#expression-image.expression');
1609 //console.log("searching");
1610 if (imgElement && imgElement instanceof HTMLImageElement) {
1611 //console.log("setting value");
1612 imgElement.src = getApiUrl() + '/api/talkinghead/result_feed';
1613 }
1614}1546}
1615 catch (error) {1547
1616 //console.log("The fetch failed!");1548/**
1549 * Sets the default expression image for the given image element and expression
1550 * @param {JQuery<HTMLElement>} img - The image element to set the default expression for
1551 * @param {string} expression - The expression label to use for the default image
1552 */
1553function setDefaultEmojiForImage(img, expression) {
1554 if (extension_settings.expressions.custom?.includes(expression)) {
1555 console.debug(`Can't set default emoji for a custom expression (${expression}). setting to ${DEFAULT_FALLBACK_EXPRESSION} instead.`);
1556 expression = DEFAULT_FALLBACK_EXPRESSION;
1617 }1557 }
1558
1559 const defImgUrl = `/img/default-expressions/${expression}.png`;
1560 img.attr('src', defImgUrl);
1561 img.attr('data-expression', expression);
1562 img.attr('data-sprite-filename', null);
1563 img.attr('title', expression);
1564 img.addClass('default');
1618}1565}
1566
1567/**
1568 * Sets the image element to display no expression by clearing its source attribute.
1569 * @param {JQuery<HTMLElement>} img - The image element to clear the expression for
1570 * @param {string} expression - The expression label to use
1571 */
1572function setNoneForImage(img, expression) {
1573 img.attr('src', '');
1574 img.attr('data-expression', expression);
1575 img.attr('data-sprite-filename', null);
1576 img.attr('title', expression);
1577 img.removeClass('default');
1619}1578}
16201579
1621function onClickExpressionImage() {1580function onClickExpressionImage() {
1622 const expression = $(this).attr('id');1581 // If there is no expression image and we clicked on the placeholder, we remove the sprite by calling via the expression label
1623 setSpriteSlashCommand({}, expression);1582 if ($(this).attr('data-expression-type') === 'failure') {
1583 const label = $(this).attr('data-expression');
1584 setSpriteSlashCommand({ type: 'expression' }, label);
1585 return;
1586 }
1587
1588 const spriteFile = $(this).attr('data-filename');
1589 setSpriteSlashCommand({ type: 'sprite' }, spriteFile);
1624}1590}
16251591
1626async function onClickExpressionAddCustom() {1592async function onClickExpressionAddCustom() {
1627 const template = await renderExtensionTemplateAsync(MODULE_NAME, 'add-custom-expression');1593 const template = await renderExtensionTemplateAsync(MODULE_NAME, 'add-custom-expression');
1628 let expressionName = await callPopup(template, 'input');1594 let expressionName = await Popup.show.input(null, template);
16291595
1630 if (!expressionName) {1596 if (!expressionName) {
1631 console.debug('No custom expression name provided');1597 console.debug('No custom expression name provided');
@@ -1636,19 +1602,15 @@ async function onClickExpressionAddCustom() {
16361602
1637 // a-z, 0-9, dashes and underscores only1603 // a-z, 0-9, dashes and underscores only
1638 if (!/^[a-z0-9-_]+$/.test(expressionName)) {1604 if (!/^[a-z0-9-_]+$/.test(expressionName)) {
1639 toastr.info('Invalid custom expression name provided');1605 toastr.warning('Invalid custom expression name provided', 'Add Custom Expression');
1640 return;1606 return;
1641 }1607 }
16421608 if (DEFAULT_EXPRESSIONS.includes(expressionName) || DEFAULT_EXPRESSIONS.some(x => expressionName.startsWith(x))) {
1643 // Check if expression name already exists in default expressions1609 toastr.warning('Expression name already exists', 'Add Custom Expression');
1644 if (DEFAULT_EXPRESSIONS.includes(expressionName)) {
1645 toastr.info('Expression name already exists');
1646 return;1610 return;
1647 }1611 }
1648
1649 // Check if expression name already exists in custom expressions
1650 if (extension_settings.expressions.custom.includes(expressionName)) {1612 if (extension_settings.expressions.custom.includes(expressionName)) {
1651 toastr.info('Custom expression already exists');1613 toastr.warning('Custom expression already exists', 'Add Custom Expression');
1652 return;1614 return;
1653 }1615 }
16541616
@@ -1665,14 +1627,15 @@ async function onClickExpressionAddCustom() {
16651627
1666async function onClickExpressionRemoveCustom() {1628async function onClickExpressionRemoveCustom() {
1667 const selectedExpression = String($('#expression_custom').val());1629 const selectedExpression = String($('#expression_custom').val());
1630 const noCustomExpressions = extension_settings.expressions.custom.length === 0;
16681631
1669 if (!selectedExpression) {1632 if (!selectedExpression || noCustomExpressions) {
1670 console.debug('No custom expression selected');1633 console.debug('No custom expression selected');
1671 return;1634 return;
1672 }1635 }
16731636
1674 const template = await renderExtensionTemplateAsync(MODULE_NAME, 'remove-custom-expression', { expression: selectedExpression });1637 const template = await renderExtensionTemplateAsync(MODULE_NAME, 'remove-custom-expression', { expression: selectedExpression });
1675 const confirmation = await callPopup(template, 'confirm');1638 const confirmation = await Popup.show.confirm(null, template);
16761639
1677 if (!confirmation) {1640 if (!confirmation) {
1678 console.debug('Custom expression removal cancelled');1641 console.debug('Custom expression removal cancelled');
@@ -1682,8 +1645,8 @@ async function onClickExpressionRemoveCustom() {
1682 // Remove custom expression from settings1645 // Remove custom expression from settings
1683 const index = extension_settings.expressions.custom.indexOf(selectedExpression);1646 const index = extension_settings.expressions.custom.indexOf(selectedExpression);
1684 extension_settings.expressions.custom.splice(index, 1);1647 extension_settings.expressions.custom.splice(index, 1);
1685 if (selectedExpression == getFallbackExpression()) {1648 if (selectedExpression == extension_settings.expressions.fallback_expression) {
1686 toastr.warning(`Deleted custom expression '${selectedExpression}' that was also selected as the fallback expression.\nFallback expression has been reset to '${DEFAULT_FALLBACK_EXPRESSION}'.`);1649 toastr.warning(`Deleted custom expression '${selectedExpression}' that was also selected as the fallback expression.\nFallback expression has been reset to '${DEFAULT_FALLBACK_EXPRESSION}'.`, 'Remove Custom Expression');
1687 extension_settings.expressions.fallback_expression = DEFAULT_FALLBACK_EXPRESSION;1650 extension_settings.expressions.fallback_expression = DEFAULT_FALLBACK_EXPRESSION;
1688 }1651 }
1689 await renderAdditionalExpressionSettings();1652 await renderAdditionalExpressionSettings();
@@ -1707,12 +1670,35 @@ function onExpressionApiChanged() {
1707 }1670 }
1708}1671}
17091672
1710function onExpressionFallbackChanged() {1673async function onExpressionFallbackChanged() {
1711 const expression = this.value;1674 /** @type {HTMLSelectElement} */
1712 if (expression) {1675 const select = this;
1713 extension_settings.expressions.fallback_expression = expression;1676 const selectedValue = select.value;
1714 saveSettingsDebounced();1677
1678 switch (selectedValue) {
1679 case OPTION_NO_FALLBACK:
1680 extension_settings.expressions.fallback_expression = null;
1681 extension_settings.expressions.showDefault = false;
1682 break;
1683 case OPTION_EMOJI_FALLBACK:
1684 extension_settings.expressions.fallback_expression = null;
1685 extension_settings.expressions.showDefault = true;
1686 break;
1687 default:
1688 extension_settings.expressions.fallback_expression = selectedValue;
1689 extension_settings.expressions.showDefault = false;
1690 break;
1691 }
1692
1693 const img = $('img.expression');
1694 const spriteFolderName = img.attr('data-sprite-folder-name');
1695 const expression = img.attr('data-expression');
1696
1697 if (spriteFolderName && expression) {
1698 await sendExpressionCall(spriteFolderName, expression, { force: true });
1715 }1699 }
1700
1701 saveSettingsDebounced();
1716}1702}
17171703
1718async function handleFileUpload(url, formData) {1704async function handleFileUpload(url, formData) {
@@ -1739,34 +1725,111 @@ async function handleFileUpload(url, formData) {
1739 }1725 }
1740}1726}
17411727
1728/**
1729 * Removes the file extension from a file name
1730 * @param {string} fileName The file name to remove the extension from
1731 * @returns {string} The file name without the extension
1732 */
1733function withoutExtension(fileName) {
1734 return fileName.replace(/\.[^/.]+$/, '');
1735}
1736
1737function validateExpressionSpriteName(expression, spriteName) {
1738 const filenameValidationRegex = new RegExp(`^${expression}(?:[-\\.].*?)?$`);
1739 const validFileName = filenameValidationRegex.test(spriteName);
1740 return validFileName;
1741}
1742
1742async function onClickExpressionUpload(event) {1743async function onClickExpressionUpload(event) {
1743 // Prevents the expression from being set1744 // Prevents the expression from being set
1744 event.stopPropagation();1745 event.stopPropagation();
17451746
1746 const id = $(this).closest('.expression_list_item').attr('id');1747 const expressionListItem = $(this).closest('.expression_list_item');
1748
1749 const clickedFileName = expressionListItem.attr('data-expression-type') !== 'failure' ? expressionListItem.attr('data-filename') : null;
1750 const expression = expressionListItem.data('expression');
1747 const name = $('#image_list').data('name');1751 const name = $('#image_list').data('name');
17481752
1749 const handleExpressionUploadChange = async (e) => {1753 const handleExpressionUploadChange = async (e) => {
1750 const file = e.target.files[0];1754 const file = e.target.files[0];
17511755
1752 if (!file) {1756 if (!file || !file.name) {
1757 console.debug('No valid file selected');
1758 return;
1759 }
1760
1761 const existingFiles = spriteCache[name]?.find(x => x.label === expression)?.files || [];
1762
1763 let spriteName = expression;
1764
1765 if (extension_settings.expressions.allowMultiple) {
1766 const matchesExisting = existingFiles.some(x => x.fileName === file.name);
1767 const fileNameWithoutExtension = withoutExtension(file.name);
1768 const validFileName = validateExpressionSpriteName(expression, fileNameWithoutExtension);
1769
1770 // If there is no expression yet and it's a valid expression, we just take it
1771 if (!clickedFileName && validFileName) {
1772 spriteName = fileNameWithoutExtension;
1773 }
1774 // If the filename matches the one that was clicked, we just take it and replace it
1775 else if (clickedFileName === file.name) {
1776 spriteName = fileNameWithoutExtension;
1777 }
1778 // If it's a valid filename and there's no existing file with the same name, we just take it
1779 else if (!matchesExisting && validFileName) {
1780 spriteName = fileNameWithoutExtension;
1781 }
1782 else {
1783 /** @type {import('../../popup.js').CustomPopupButton[]} */
1784 const customButtons = [];
1785 if (clickedFileName) {
1786 customButtons.push({
1787 text: t`Replace Existing`,
1788 result: POPUP_RESULT.NEGATIVE,
1789 action: () => {
1790 console.debug('Replacing existing sprite');
1791 spriteName = withoutExtension(clickedFileName);
1792 },
1793 });
1794 }
1795
1796 spriteName = null;
1797 const suggestedSpriteName = generateUniqueSpriteName(expression, existingFiles);
1798
1799 const message = await renderExtensionTemplateAsync(MODULE_NAME, 'templates/upload-expression', { expression, clickedFileName });
1800
1801 const input = await Popup.show.input(t`Upload Expression Sprite`, message,
1802 suggestedSpriteName, { customButtons: customButtons });
1803
1804 if (input) {
1805 if (!validateExpressionSpriteName(expression, input)) {
1806 toastr.warning(t`The name you entered does not follow the naming schema for the selected expression '${expression}'.`, t`Invalid Expression Sprite Name`);
1807 return;
1808 }
1809 spriteName = input;
1810 }
1811 }
1812 } else {
1813 spriteName = withoutExtension(clickedFileName);
1814 }
1815
1816 if (!spriteName) {
1817 toastr.warning(t`Cancelled uploading sprite.`, t`Upload Cancelled`);
1818 // Reset the input
1819 e.target.form.reset();
1753 return;1820 return;
1754 }1821 }
17551822
1756 const formData = new FormData();1823 const formData = new FormData();
1757 formData.append('name', name);1824 formData.append('name', name);
1758 formData.append('label', id);1825 formData.append('label', expression);
1759 formData.append('avatar', file);1826 formData.append('avatar', file);
1827 formData.append('spriteName', spriteName);
17601828
1761 await handleFileUpload('/api/sprites/upload', formData);1829 await handleFileUpload('/api/sprites/upload', formData);
17621830
1763 // Reset the input1831 // Reset the input
1764 e.target.form.reset();1832 e.target.form.reset();
1765
1766 // In Talkinghead mode, when a new talkinghead image is uploaded, refresh the live char.
1767 if (id === 'talkinghead' && isTalkingHeadEnabled() && modules.includes('talkinghead')) {
1768 await loadTalkingHead();
1769 }
1770 };1833 };
17711834
1772 $('#expression_upload')1835 $('#expression_upload')
@@ -1822,8 +1885,9 @@ async function onClickExpressionOverrideButton() {
1822 inApiCall = true;1885 inApiCall = true;
1823 $('#visual-novel-wrapper').empty();1886 $('#visual-novel-wrapper').empty();
1824 await validateImages(overridePath.length === 0 ? currentLastMessage.name : overridePath, true);1887 await validateImages(overridePath.length === 0 ? currentLastMessage.name : overridePath, true);
1888 const name = overridePath.length === 0 ? currentLastMessage.name : overridePath;
1825 const expression = await getExpressionLabel(currentLastMessage.mes);1889 const expression = await getExpressionLabel(currentLastMessage.mes);
1826 await sendExpressionCall(overridePath.length === 0 ? currentLastMessage.name : overridePath, expression, true);1890 await sendExpressionCall(name, expression, { force: true });
1827 forceUpdateVisualNovelMode();1891 forceUpdateVisualNovelMode();
1828 } catch (error) {1892 } catch (error) {
1829 console.debug(`Setting expression override for ${avatarFileName} failed with error: ${error}`);1893 console.debug(`Setting expression override for ${avatarFileName} failed with error: ${error}`);
@@ -1849,7 +1913,7 @@ async function onClickExpressionOverrideRemoveAllButton() {
1849 const currentLastMessage = getLastCharacterMessage();1913 const currentLastMessage = getLastCharacterMessage();
1850 await validateImages(currentLastMessage.name, true);1914 await validateImages(currentLastMessage.name, true);
1851 const expression = await getExpressionLabel(currentLastMessage.mes);1915 const expression = await getExpressionLabel(currentLastMessage.mes);
1852 await sendExpressionCall(currentLastMessage.name, expression, true);1916 await sendExpressionCall(currentLastMessage.name, expression, { force: true });
1853 forceUpdateVisualNovelMode();1917 forceUpdateVisualNovelMode();
18541918
1855 console.debug(extension_settings.expressionOverrides);1919 console.debug(extension_settings.expressionOverrides);
@@ -1872,16 +1936,13 @@ async function onClickExpressionUploadPackButton() {
1872 formData.append('name', name);1936 formData.append('name', name);
1873 formData.append('avatar', file);1937 formData.append('avatar', file);
18741938
1939 const uploadToast = toastr.info('Please wait...', 'Upload is processing', { timeOut: 0, extendedTimeOut: 0 });
1875 const { count } = await handleFileUpload('/api/sprites/upload-zip', formData);1940 const { count } = await handleFileUpload('/api/sprites/upload-zip', formData);
1941 toastr.clear(uploadToast);
1876 toastr.success(`Uploaded ${count} image(s) for ${name}`);1942 toastr.success(`Uploaded ${count} image(s) for ${name}`);
18771943
1878 // Reset the input1944 // Reset the input
1879 e.target.form.reset();1945 e.target.form.reset();
1880
1881 // In Talkinghead mode, refresh the live char.
1882 if (isTalkingHeadEnabled() && modules.includes('talkinghead')) {
1883 await loadTalkingHead();
1884 }
1885 };1946 };
18861947
1887 $('#expression_upload_pack')1948 $('#expression_upload_pack')
@@ -1894,20 +1955,28 @@ async function onClickExpressionDelete(event) {
1894 // Prevents the expression from being set1955 // Prevents the expression from being set
1895 event.stopPropagation();1956 event.stopPropagation();
18961957
1897 const confirmation = await callPopup('<h3>Are you sure?</h3>Once deleted, it\'s gone forever!', 'confirm');1958 const expressionListItem = $(this).closest('.expression_list_item');
1959 const expression = expressionListItem.data('expression');
1960
1961 if (expressionListItem.attr('data-expression-type') === 'failure') {
1962 return;
1963 }
18981964
1965 const confirmation = await Popup.show.confirm(t`Delete Expression`, t`Are you sure you want to delete this expression? Once deleted, it\'s gone forever!`
1966 + '<br /><br />'
1967 + t`Expression:` + ' <tt>' + expressionListItem.attr('data-filename') + '</tt>');
1899 if (!confirmation) {1968 if (!confirmation) {
1900 return;1969 return;
1901 }1970 }
19021971
1903 const id = $(this).closest('.expression_list_item').attr('id');1972 const fileName = withoutExtension(expressionListItem.attr('data-filename'));
1904 const name = $('#image_list').data('name');1973 const name = $('#image_list').data('name');
19051974
1906 try {1975 try {
1907 await fetch('/api/sprites/delete', {1976 await fetch('/api/sprites/delete', {
1908 method: 'POST',1977 method: 'POST',
1909 headers: getRequestHeaders(),1978 headers: getRequestHeaders(),
1910 body: JSON.stringify({ name, label: id }),1979 body: JSON.stringify({ name, label: expression, spriteName: fileName }),
1911 });1980 });
1912 } catch (error) {1981 } catch (error) {
1913 toastr.error('Failed to delete image. Try again later.');1982 toastr.error('Failed to delete image. Try again later.');
@@ -1984,6 +2053,16 @@ function migrateSettings() {
1984 extension_settings.expressions.llmPrompt = DEFAULT_LLM_PROMPT;2053 extension_settings.expressions.llmPrompt = DEFAULT_LLM_PROMPT;
1985 saveSettingsDebounced();2054 saveSettingsDebounced();
1986 }2055 }
2056
2057 if (extension_settings.expressions.allowMultiple === undefined) {
2058 extension_settings.expressions.allowMultiple = true;
2059 saveSettingsDebounced();
2060 }
2061
2062 if (extension_settings.expressions.showDefault && extension_settings.expressions.fallback_expression !== undefined) {
2063 extension_settings.expressions.showDefault = false;
2064 saveSettingsDebounced();
2065 }
1987}2066}
19882067
1989(async function () {2068(async function () {
@@ -2010,13 +2089,19 @@ function migrateSettings() {
2010 const template = await renderExtensionTemplateAsync(MODULE_NAME, 'settings');2089 const template = await renderExtensionTemplateAsync(MODULE_NAME, 'settings');
2011 $('#expressions_container').append(template);2090 $('#expressions_container').append(template);
2012 $('#expression_override_button').on('click', onClickExpressionOverrideButton);2091 $('#expression_override_button').on('click', onClickExpressionOverrideButton);
2013 $('#expressions_show_default').on('input', onExpressionsShowDefaultInput);
2014 $('#expression_upload_pack_button').on('click', onClickExpressionUploadPackButton);2092 $('#expression_upload_pack_button').on('click', onClickExpressionUploadPackButton);
2015 $('#expressions_show_default').prop('checked', extension_settings.expressions.showDefault).trigger('input');
2016 $('#expression_translate').prop('checked', extension_settings.expressions.translate).on('input', function () {2093 $('#expression_translate').prop('checked', extension_settings.expressions.translate).on('input', function () {
2017 extension_settings.expressions.translate = !!$(this).prop('checked');2094 extension_settings.expressions.translate = !!$(this).prop('checked');
2018 saveSettingsDebounced();2095 saveSettingsDebounced();
2019 });2096 });
2097 $('#expressions_allow_multiple').prop('checked', extension_settings.expressions.allowMultiple).on('input', function () {
2098 extension_settings.expressions.allowMultiple = !!$(this).prop('checked');
2099 saveSettingsDebounced();
2100 });
2101 $('#expressions_reroll_if_same').prop('checked', extension_settings.expressions.rerollIfSame).on('input', function () {
2102 extension_settings.expressions.rerollIfSame = !!$(this).prop('checked');
2103 saveSettingsDebounced();
2104 });
2020 $('#expression_override_cleanup_button').on('click', onClickExpressionOverrideRemoveAllButton);2105 $('#expression_override_cleanup_button').on('click', onClickExpressionOverrideRemoveAllButton);
2021 $(document).on('dragstart', '.expression', (e) => {2106 $(document).on('dragstart', '.expression', (e) => {
2022 e.preventDefault();2107 e.preventDefault();
@@ -2025,21 +2110,15 @@ function migrateSettings() {
2025 $(document).on('click', '.expression_list_item', onClickExpressionImage);2110 $(document).on('click', '.expression_list_item', onClickExpressionImage);
2026 $(document).on('click', '.expression_list_upload', onClickExpressionUpload);2111 $(document).on('click', '.expression_list_upload', onClickExpressionUpload);
2027 $(document).on('click', '.expression_list_delete', onClickExpressionDelete);2112 $(document).on('click', '.expression_list_delete', onClickExpressionDelete);
2028 $(window).on('resize', updateVisualNovelModeDebounced);2113 $(window).on('resize', () => updateVisualNovelModeDebounced());
2029 $('#open_chat_expressions').hide();2114 $('#open_chat_expressions').hide();
20302115
2031 $('#image_type_toggle').on('click', function () {
2032 if (this instanceof HTMLInputElement) {
2033 setTalkingHeadState(this.checked);
2034 }
2035 });
2036
2037 await renderAdditionalExpressionSettings();2116 await renderAdditionalExpressionSettings();
2038 $('#expression_api').val(extension_settings.expressions.api ?? EXPRESSION_API.extras);2117 $('#expression_api').val(extension_settings.expressions.api ?? EXPRESSION_API.extras);
2039 $('.expression_llm_prompt_block').toggle([EXPRESSION_API.llm, EXPRESSION_API.webllm].includes(extension_settings.expressions.api));2118 $('.expression_llm_prompt_block').toggle([EXPRESSION_API.llm, EXPRESSION_API.webllm].includes(extension_settings.expressions.api));
2040 $('#expression_llm_prompt').val(extension_settings.expressions.llmPrompt ?? '');2119 $('#expression_llm_prompt').val(extension_settings.expressions.llmPrompt ?? '');
2041 $('#expression_llm_prompt').on('input', function () {2120 $('#expression_llm_prompt').on('input', function () {
2042 extension_settings.expressions.llmPrompt = $(this).val();2121 extension_settings.expressions.llmPrompt = String($(this).val());
2043 saveSettingsDebounced();2122 saveSettingsDebounced();
2044 });2123 });
2045 $('#expression_llm_prompt_restore').on('click', function () {2124 $('#expression_llm_prompt_restore').on('click', function () {
@@ -2054,34 +2133,6 @@ function migrateSettings() {
2054 $('#expression_api').on('change', onExpressionApiChanged);2133 $('#expression_api').on('change', onExpressionApiChanged);
2055 }2134 }
20562135
2057 // Pause Talkinghead to save resources when the ST tab is not visible or the window is minimized.
2058 // We currently do this via loading/unloading. Could be improved by adding new pause/unpause endpoints to Extras.
2059 document.addEventListener('visibilitychange', function (event) {
2060 let pageIsVisible;
2061 if (document.hidden) {
2062 console.debug('expressions: SillyTavern is now hidden');
2063 pageIsVisible = false;
2064 } else {
2065 console.debug('expressions: SillyTavern is now visible');
2066 pageIsVisible = true;
2067 }
2068
2069 if (isTalkingHeadEnabled() && modules.includes('talkinghead')) {
2070 isTalkingHeadAvailable().then(result => {
2071 if (result) {
2072 if (pageIsVisible) {
2073 loadTalkingHead();
2074 } else {
2075 unloadTalkingHead();
2076 }
2077 handleImageChange(); // Change image as needed
2078 } else {
2079 //console.log("talkinghead does not exist.");
2080 }
2081 });
2082 }
2083 });
2084
2085 addExpressionImage();2136 addExpressionImage();
2086 addVisualNovelMode();2137 addVisualNovelMode();
2087 migrateSettings();2138 migrateSettings();
@@ -2090,11 +2141,6 @@ function migrateSettings() {
2090 const updateFunction = wrapper.update.bind(wrapper);2141 const updateFunction = wrapper.update.bind(wrapper);
2091 setInterval(updateFunction, UPDATE_INTERVAL);2142 setInterval(updateFunction, UPDATE_INTERVAL);
2092 moduleWorker();2143 moduleWorker();
2093 // For setting the Talkinghead talking animation on/off quickly enough for realtime use, we need another timer on a shorter schedule.
2094 const wrapperTalkingState = new ModuleWorkerWrapper(updateTalkingState);
2095 const updateTalkingStateFunction = wrapperTalkingState.update.bind(wrapperTalkingState);
2096 setInterval(updateTalkingStateFunction, TALKINGCHECK_UPDATE_INTERVAL);
2097 updateTalkingState();
2098 dragElement($('#expression-holder'));2144 dragElement($('#expression-holder'));
2099 eventSource.on(event_types.CHAT_CHANGED, () => {2145 eventSource.on(event_types.CHAT_CHANGED, () => {
2100 // character changed2146 // character changed
@@ -2108,111 +2154,137 @@ function migrateSettings() {
2108 imgElement.src = '';2154 imgElement.src = '';
2109 }2155 }
21102156
2111 //set checkbox to global var
2112 $('#image_type_toggle').prop('checked', extension_settings.expressions.talkinghead);
2113 if (extension_settings.expressions.talkinghead) {
2114 setTalkingHeadState(extension_settings.expressions.talkinghead);
2115 }
2116
2117 setExpressionOverrideHtml();2157 setExpressionOverrideHtml();
21182158
2119 if (isVisualNovelMode()) {2159 if (isVisualNovelMode()) {
2120 $('#visual-novel-wrapper').empty();2160 $('#visual-novel-wrapper').empty();
2121 }2161 }
21222162
2123 updateFunction();2163 updateFunction({ newChat: true });
2124 });2164 });
2125 eventSource.on(event_types.MOVABLE_PANELS_RESET, updateVisualNovelModeDebounced);2165 eventSource.on(event_types.MOVABLE_PANELS_RESET, updateVisualNovelModeDebounced);
2126 eventSource.on(event_types.GROUP_UPDATED, updateVisualNovelModeDebounced);2166 eventSource.on(event_types.GROUP_UPDATED, updateVisualNovelModeDebounced);
2127 eventSource.on(event_types.EXTRAS_CONNECTED, () => {
2128 if (extension_settings.expressions.talkinghead) {
2129 setTalkingHeadState(extension_settings.expressions.talkinghead);
2130 }
2131 });
21322167
2133 const localEnumProviders = {2168 const localEnumProviders = {
2134 expressions: () => getCachedExpressions().map(expression => {2169 expressions: () => {
2170 const currentLastMessage = selected_group ? getLastCharacterMessage() : null;
2171 const spriteFolderName = getSpriteFolderName(currentLastMessage, currentLastMessage?.name);
2172 const expressions = getCachedExpressions();
2173 return expressions.map(expression => {
2174 const spriteCount = spriteCache[spriteFolderName]?.find(x => x.label === expression)?.files.length ?? 0;
2135 const isCustom = extension_settings.expressions.custom?.includes(expression);2175 const isCustom = extension_settings.expressions.custom?.includes(expression);
2136 return new SlashCommandEnumValue(expression, null, isCustom ? enumTypes.name : enumTypes.enum, isCustom ? 'C' : 'D');2176 const subtitle = spriteCount == 0 ? '❌ No sprites available for this expression' :
2137 }),2177 spriteCount > 1 ? `${spriteCount} sprites` : null;
2178 return new SlashCommandEnumValue(expression,
2179 subtitle,
2180 isCustom ? enumTypes.name : enumTypes.enum,
2181 isCustom ? 'C' : 'D');
2182 });
2183 },
2184 sprites: () => {
2185 const currentLastMessage = selected_group ? getLastCharacterMessage() : null;
2186 const spriteFolderName = getSpriteFolderName(currentLastMessage, currentLastMessage?.name);
2187 const sprites = spriteCache[spriteFolderName]?.map(x => x.files)?.flat() ?? [];
2188 return sprites.map(x => {
2189 return new SlashCommandEnumValue(x.title,
2190 x.title !== x.expression ? x.expression : null,
2191 x.isCustom ? enumTypes.name : enumTypes.enum,
2192 x.isCustom ? 'C' : 'D');
2193 });
2194 },
2138 };2195 };
21392196
2140 SlashCommandParser.addCommandObject(SlashCommand.fromProps({2197 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
2141 name: 'sprite',2198 name: 'expression-set',
2142 aliases: ['emote'],2199 aliases: ['sprite', 'emote'],
2143 callback: setSpriteSlashCommand,2200 callback: setSpriteSlashCommand,
2201 namedArgumentList: [
2202 SlashCommandNamedArgument.fromProps({
2203 name: 'type',
2204 description: 'Whether to set an expression or a specific sprite.',
2205 typeList: [ARGUMENT_TYPE.STRING],
2206 isRequired: false,
2207 defaultValue: 'expression',
2208 enumList: ['expression', 'sprite'],
2209 }),
2210 ],
2144 unnamedArgumentList: [2211 unnamedArgumentList: [
2145 SlashCommandArgument.fromProps({2212 SlashCommandArgument.fromProps({
2146 description: 'spriteId',2213 description: 'expression label to set',
2147 typeList: [ARGUMENT_TYPE.STRING],2214 typeList: [ARGUMENT_TYPE.STRING],
2148 isRequired: true,2215 isRequired: true,
2149 enumProvider: localEnumProviders.expressions,2216 enumProvider: (executor, _) => {
2217 // Check if command is used to set a sprite, then use those enums
2218 const type = executor.namedArgumentList.find(it => it.name == 'type')?.value || 'expression';
2219 if (type == 'sprite') return localEnumProviders.sprites();
2220 else return [
2221 ...localEnumProviders.expressions(),
2222 new SlashCommandEnumValue(RESET_SPRITE_LABEL, 'Resets the expression (to either default or no sprite)', enumTypes.enum, '❌'),
2223 ];
2224 },
2150 }),2225 }),
2151 ],2226 ],
2152 helpString: 'Force sets the sprite for the current character.',2227 helpString: 'Force sets the expression for the current character.',
2153 returns: 'the currently set sprite label after setting it.',2228 returns: 'The currently set expression label after setting it.',
2154 }));2229 }));
2155 SlashCommandParser.addCommandObject(SlashCommand.fromProps({2230 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
2156 name: 'spriteoverride',2231 name: 'expression-folder-override',
2157 aliases: ['costume'],2232 aliases: ['spriteoverride', 'costume'],
2158 callback: setSpriteSetCommand,2233 callback: setSpriteFolderCommand,
2159 unnamedArgumentList: [2234 unnamedArgumentList: [
2160 new SlashCommandArgument(2235 new SlashCommandArgument(
2161 'optional folder', [ARGUMENT_TYPE.STRING], false,2236 'optional folder', [ARGUMENT_TYPE.STRING], false,
2162 ),2237 ),
2163 ],2238 ],
2164 helpString: 'Sets an override sprite folder for the current character. If the name starts with a slash or a backslash, selects a sub-folder in the character-named folder. Empty value to reset to default.',2239 helpString: `
2240 <div>
2241 Sets an override sprite folder for the current character.<br />
2242 In groups, this will apply to the character who last sent a message.
2243 </div>
2244 <div>
2245 If the name starts with a slash or a backslash, selects a sub-folder in the character-named folder. Empty value to reset to default.
2246 </div>
2247 `,
2165 }));2248 }));
2166 SlashCommandParser.addCommandObject(SlashCommand.fromProps({2249 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
2167 name: 'lastsprite',2250 name: 'expression-last',
2168 callback: (_, name) => {2251 aliases: ['lastsprite'],
2252 /** @type {(args: object, name: string) => Promise<string>} */
2253 callback: async (_, name) => {
2169 if (typeof name !== 'string') throw new Error('name must be a string');2254 if (typeof name !== 'string') throw new Error('name must be a string');
2255 if (!name) {
2256 if (selected_group) {
2257 toastr.error(t`In group chats, you must specify a character name.`, t`No character name specified`);
2258 return '';
2259 }
2260 name = characters[this_chid]?.avatar;
2261 }
2262
2170 const char = findChar({ name: name });2263 const char = findChar({ name: name });
2264 if (!char) toastr.warning(t`Couldn't find character ${name}.`, t`Character not found`);
2265
2171 const sprite = lastExpression[char?.name ?? name] ?? '';2266 const sprite = lastExpression[char?.name ?? name] ?? '';
2172 return sprite;2267 return sprite;
2173 },2268 },
2174 returns: 'the last set sprite / expression for the named character.',2269 returns: 'the last set expression for the named character.',
2175 unnamedArgumentList: [2270 unnamedArgumentList: [
2176 SlashCommandArgument.fromProps({2271 SlashCommandArgument.fromProps({
2177 description: 'Character name - or unique character identifier (avatar key)',2272 description: 'Character name - or unique character identifier (avatar key). If not provided, the current character for this chat will be used (does not work in group chats)',
2178 typeList: [ARGUMENT_TYPE.STRING],2273 typeList: [ARGUMENT_TYPE.STRING],
2179 isRequired: true,
2180 enumProvider: commonEnumProviders.characters('character'),2274 enumProvider: commonEnumProviders.characters('character'),
2181 forceEnum: true,
2182 }),2275 }),
2183 ],2276 ],
2184 helpString: 'Returns the last set sprite / expression for the named character.',2277 helpString: 'Returns the last set expression for the named character.',
2185 }));
2186 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
2187 name: 'th',
2188 callback: toggleTalkingHeadCommand,
2189 aliases: ['talkinghead'],
2190 helpString: 'Character Expressions: toggles <i>Image Type - talkinghead (extras)</i> on/off.',
2191 returns: 'the current state of the <i>Image Type - talkinghead (extras)</i> on/off.',
2192 }));2278 }));
2193 SlashCommandParser.addCommandObject(SlashCommand.fromProps({2279 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
2194 name: 'classify-expressions',2280 name: 'expression-list',
2195 aliases: ['expressions'],2281 aliases: ['expressions'],
2282 /** @type {(args: {return: string}) => Promise<string>} */
2196 callback: async (args) => {2283 callback: async (args) => {
2284 let returnType =
2197 /** @type {import('../../slash-commands/SlashCommandReturnHelper.js').SlashCommandReturnType} */2285 /** @type {import('../../slash-commands/SlashCommandReturnHelper.js').SlashCommandReturnType} */
2198 // @ts-ignore2286 (args.return);
2199 let returnType = args.return;
2200
2201 // Old legacy return type handling
2202 if (args.format) {
2203 toastr.warning(`Legacy argument 'format' with value '${args.format}' is deprecated. Please use 'return' instead. Routing to the correct return type...`, 'Deprecation warning');
2204 const type = String(args?.format).toLowerCase().trim();
2205 switch (type) {
2206 case 'json':
2207 returnType = 'object';
2208 break;
2209 default:
2210 returnType = 'pipe';
2211 break;
2212 }
2213 }
22142287
2215 // Now the actual new return type handling
2216 const list = await getExpressionsList();2288 const list = await getExpressionsList();
22172289
2218 return await slashCommandReturnHelper.doReturn(returnType ?? 'pipe', list, { objectToStringFunc: list => list.join(', ') });2290 return await slashCommandReturnHelper.doReturn(returnType ?? 'pipe', list, { objectToStringFunc: list => list.join(', ') });
@@ -2226,22 +2298,13 @@ function migrateSettings() {
2226 enumList: slashCommandReturnHelper.enumList({ allowObject: true }),2298 enumList: slashCommandReturnHelper.enumList({ allowObject: true }),
2227 forceEnum: true,2299 forceEnum: true,
2228 }),2300 }),
2229 // TODO remove some day
2230 SlashCommandNamedArgument.fromProps({
2231 name: 'format',
2232 description: '!!! DEPRECATED - use "return" instead !!! The format to return the list in: comma-separated plain text or JSON array. Default is plain text.',
2233 typeList: [ARGUMENT_TYPE.STRING],
2234 enumList: [
2235 new SlashCommandEnumValue('plain', null, enumTypes.enum, ', '),
2236 new SlashCommandEnumValue('json', null, enumTypes.enum, '[]'),
2237 ],
2238 }),
2239 ],2301 ],
2240 returns: 'The comma-separated list of available expressions, including custom expressions.',2302 returns: 'The comma-separated list of available expressions, including custom expressions.',
2241 helpString: 'Returns a list of available expressions, including custom expressions.',2303 helpString: 'Returns a list of available expressions, including custom expressions.',
2242 }));2304 }));
2243 SlashCommandParser.addCommandObject(SlashCommand.fromProps({2305 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
2244 name: 'classify',2306 name: 'expression-classify',
2307 aliases: ['classify'],
2245 callback: classifyCallback,2308 callback: classifyCallback,
2246 namedArgumentList: [2309 namedArgumentList: [
2247 SlashCommandNamedArgument.fromProps({2310 SlashCommandNamedArgument.fromProps({
@@ -2280,11 +2343,13 @@ function migrateSettings() {
2280 `,2343 `,
2281 }));2344 }));
2282 SlashCommandParser.addCommandObject(SlashCommand.fromProps({2345 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
2283 name: 'uploadsprite',2346 name: 'expression-upload',
2347 aliases: ['uploadsprite'],
2348 /** @type {(args: {name: string, label: string, folder: string?, spriteName: string?}, url: string) => Promise<string>} */
2284 callback: async (args, url) => {2349 callback: async (args, url) => {
2285 await uploadSpriteCommand(args, url);2350 return await uploadSpriteCommand(args, url);
2286 return '';
2287 },2351 },
2352 returns: 'the resulting sprite name',
2288 unnamedArgumentList: [2353 unnamedArgumentList: [
2289 SlashCommandArgument.fromProps({2354 SlashCommandArgument.fromProps({
2290 description: 'URL of the image to upload',2355 description: 'URL of the image to upload',
@@ -2298,7 +2363,6 @@ function migrateSettings() {
2298 description: 'Character name or avatar key (default is current character)',2363 description: 'Character name or avatar key (default is current character)',
2299 typeList: [ARGUMENT_TYPE.STRING],2364 typeList: [ARGUMENT_TYPE.STRING],
2300 isRequired: false,2365 isRequired: false,
2301 acceptsMultiple: false,
2302 }),2366 }),
2303 SlashCommandNamedArgument.fromProps({2367 SlashCommandNamedArgument.fromProps({
2304 name: 'label',2368 name: 'label',
@@ -2306,16 +2370,32 @@ function migrateSettings() {
2306 typeList: [ARGUMENT_TYPE.STRING],2370 typeList: [ARGUMENT_TYPE.STRING],
2307 enumProvider: localEnumProviders.expressions,2371 enumProvider: localEnumProviders.expressions,
2308 isRequired: true,2372 isRequired: true,
2309 acceptsMultiple: false,
2310 }),2373 }),
2311 SlashCommandNamedArgument.fromProps({2374 SlashCommandNamedArgument.fromProps({
2312 name: 'folder',2375 name: 'folder',
2313 description: 'Override folder to upload into',2376 description: 'Override folder to upload into',
2314 typeList: [ARGUMENT_TYPE.STRING],2377 typeList: [ARGUMENT_TYPE.STRING],
2315 isRequired: false,2378 isRequired: false,
2316 acceptsMultiple: false,2379 }),
2380 SlashCommandNamedArgument.fromProps({
2381 name: 'spriteName',
2382 description: 'Override sprite name to allow multiple sprites per expressions. Has to follow the naming pattern. If unspecified, the label will be used as sprite name.',
2383 typeList: [ARGUMENT_TYPE.STRING],
2384 isRequired: false,
2317 }),2385 }),
2318 ],2386 ],
2319 helpString: '<div>Upload a sprite from a URL.</div><div>Example:</div><pre><code>/uploadsprite name=Seraphina label=joy /user/images/Seraphina/Seraphina_2024-12-22@12h37m57s.png</code></pre>',2387 helpString: `
2388 <div>
2389 Upload a sprite from a URL.
2390 </div>
2391 <div>
2392 <strong>Example:</strong>
2393 <ul>
2394 <li>
2395 <pre><code>/uploadsprite name=Seraphina label=joy /user/images/Seraphina/Seraphina_2024-12-22@12h37m57s.png</code></pre>
2396 </li>
2397 </ul>
2398 </div>
2399 `,
2320 }));2400 }));
2321})();2401})();
public/scripts/extensions/expressions/list-item.html+9 -5
@@ -1,4 +1,5 @@
1<div id="{{item}}" class="expression_list_item">1{{#each images}}
2<div class="expression_list_item interactable" data-expression="{{../expression}}" data-expression-type="{{this.type}}" data-filename="{{this.fileName}}">
2 <div class="expression_list_buttons">3 <div class="expression_list_buttons">
3 <div class="menu_button expression_list_upload" title="Upload image">4 <div class="menu_button expression_list_upload" title="Upload image">
4 <i class="fa-solid fa-upload"></i>5 <i class="fa-solid fa-upload"></i>
@@ -7,11 +8,14 @@
7 <i class="fa-solid fa-trash"></i>8 <i class="fa-solid fa-trash"></i>
8 </div>9 </div>
9 </div>10 </div>
10 <div class="expression_list_title {{textClass}}">11 <div class="expression_list_title">
11 <span>{{item}}</span>12 <span>{{../expression}}</span>
12 {{#if isCustom}}13 {{#if ../isCustom}}
13 <small class="expression_list_custom">(custom)</small>14 <small class="expression_list_custom">(custom)</small>
14 {{/if}}15 {{/if}}
15 </div>16 </div>
16 <img class="expression_list_image" src="{{imageSrc}}" />17 <div class="expression_list_image_container" title="{{this.title}}">
18 <img class="expression_list_image" src="{{this.imageSrc}}" alt="{{this.title}}" data-epression="{{../expression}}" />
17 </div>19 </div>
20</div>
21{{/each}}
public/scripts/extensions/expressions/settings.html+22 -10
@@ -6,24 +6,24 @@
6 </div>6 </div>
77
8 <div class="inline-drawer-content">8 <div class="inline-drawer-content">
9 <label class="checkbox_label" for="expression_translate" title="Use the selected API from Chat Translation extension settings.">9 <label class="checkbox_label" for="expression_translate" title="Use the selected API from Chat Translation extension settings." data-i18n="[title]Use the selected API from Chat Translation extension settings.">
10 <input id="expression_translate" type="checkbox">10 <input id="expression_translate" type="checkbox">
11 <span data-i18n="Translate text to English before classification">Translate text to English before classification</span>11 <span data-i18n="Translate text to English before classification">Translate text to English before classification</span>
12 </label>12 </label>
13 <label class="checkbox_label" for="expressions_show_default">13 <label class="checkbox_label" for="expressions_allow_multiple" title="A single expression can have multiple sprites. Whenever the expression is chosen, a random sprite for this expression will be selected." data-i18n="[title]A single expression can have multiple sprites. Whenever the expression is chosen, a random sprite for this expression will be selected.">
14 <input id="expressions_show_default" type="checkbox">14 <input id="expressions_allow_multiple" type="checkbox">
15 <span data-i18n="Show default images (emojis) if sprite missing">Show default images (emojis) if sprite missing</span>15 <span data-i18n="Allow multiple sprites per expression">Allow multiple sprites per expression</span>
16 </label>16 </label>
17 <label id="image_type_block" class="checkbox_label" for="image_type_toggle">17 <label class="checkbox_label" for="expressions_reroll_if_same" title="If the same expression is used again, re-roll the sprite. This only applies to expressions that have multiple available sprites assigned." data-i18n="[title]If the same expression is used again, re-roll the sprite. This only applies to expressions that have multiple available sprites assigned.">
18 <input id="image_type_toggle" type="checkbox">18 <input id="expressions_reroll_if_same" type="checkbox">
19 <span data-i18n="Image Type - talkinghead (extras)">Image Type - talkinghead (extras)</span>19 <span data-i18n="Re-roll if same expression is used again">Re-roll if same sprite is used again</span>
20 </label>20 </label>
21 <div class="expression_api_block m-b-1 m-t-1">21 <div class="expression_api_block m-b-1 m-t-1">
22 <label for="expression_api" data-i18n="Classifier API">Classifier API</label>22 <label for="expression_api" data-i18n="Classifier API">Classifier API</label>
23 <small data-i18n="Select the API for classifying expressions.">Select the API for classifying expressions.</small>23 <small data-i18n="Select the API for classifying expressions.">Select the API for classifying expressions.</small>
24 <select id="expression_api" class="flex1 margin0">24 <select id="expression_api" class="flex1 margin0">
25 <option value="0" data-i18n="Local">Local</option>25 <option value="0" data-i18n="Local">Local</option>
26 <option value="1" data-i18n="Extras">Extras</option>26 <option value="1" data-i18n="Extras">Extras (deprecated)</option>
27 <option value="2" data-i18n="Main API">Main API</option>27 <option value="2" data-i18n="Main API">Main API</option>
28 <option value="3" data-i18n="WebLLM Extension">WebLLM Extension</option>28 <option value="3" data-i18n="WebLLM Extension">WebLLM Extension</option>
29 </select>29 </select>
@@ -75,8 +75,20 @@
75 <span data-i18n="Remove all image overrides">Remove all image overrides</span>75 <span data-i18n="Remove all image overrides">Remove all image overrides</span>
76 </div>76 </div>
77 </div>77 </div>
78 <p class="hint"><b data-i18n="Hint:">Hint:</b> <i><span data-i18n="Create new folder in the _space">Create new folder in the </span><b>/characters/</b> <span data-i18n="folder of your user data directory and name it as the name of the character.">folder of your user data directory and name it as the name of the character.</span>78 <p class="hint">
79 <span data-i18n="Put images with expressions there. File names should follow the pattern:">Put images with expressions there. File names should follow the pattern: </span><tt data-i18n="expression_label_pattern">[expression_label].[image_format]</tt></i></p>79 <b data-i18n="Hint:">Hint:</b>
80 <i>
81 <span data-i18n="Create new folder in the _space">Create new folder in the </span><b>/characters/</b> <span data-i18n="folder of your user data directory and name it as the name of the character.">folder of your user data directory and name it as the name of the character.</span>
82 <span data-i18n="Put images with expressions there. File names should follow the pattern:">Put images with expressions there. File names should follow the pattern: </span><tt data-i18n="expression_label_pattern">[expression_label].[image_format]</tt>
83 </i>
84 </p>
85 <p>
86 <i>
87 <span>In case of multiple files per expression, file names can contain a suffix, either separated by a dot or a
88 dash.
89 Examples: </span><tt>joy.png</tt>, <tt>joy-1.png</tt>, <tt>joy.expressive.png</tt>
90 </i>
91 </p>
80 <h3 id="image_list_header">92 <h3 id="image_list_header">
81 <strong data-i18n="Sprite set:">Sprite set:</strong>&nbsp;<span id="image_list_header_name"></span>93 <strong data-i18n="Sprite set:">Sprite set:</strong>&nbsp;<span id="image_list_header_name"></span>
82 </h3>94 </h3>
public/scripts/extensions/expressions/style.css+31 -2
@@ -111,6 +111,10 @@ img.expression.default {
111 justify-content: center;111 justify-content: center;
112}112}
113113
114.expression_list_image_container {
115 overflow: hidden;
116}
117
114.expression_list_title {118.expression_list_title {
115 position: absolute;119 position: absolute;
116 bottom: 0;120 bottom: 0;
@@ -126,6 +130,9 @@ img.expression.default {
126 flex-direction: column;130 flex-direction: column;
127 line-height: 1;131 line-height: 1;
128}132}
133.expression_list_custom {
134 font-size: 0.66rem;
135}
129136
130.expression_list_buttons {137.expression_list_buttons {
131 position: absolute;138 position: absolute;
@@ -162,11 +169,24 @@ img.expression.default {
162 row-gap: 1rem;169 row-gap: 1rem;
163}170}
164171
165#image_list .success {172#image_list .expression_list_item[data-expression-type="success"] .expression_list_title {
166 color: green;173 color: green;
167}174}
168175
169#image_list .failure {176#image_list .expression_list_item[data-expression-type="additional"] .expression_list_title {
177 color: darkolivegreen;
178}
179#image_list .expression_list_item[data-expression-type="additional"] .expression_list_title::before {
180 content: '➕';
181 position: absolute;
182 top: -7px;
183 left: -9px;
184 font-size: 14px;
185 color: transparent;
186 text-shadow: 0 0 0 darkolivegreen;
187}
188
189#image_list .expression_list_item[data-expression-type="failure"] .expression_list_title {
170 color: red;190 color: red;
171}191}
172192
@@ -189,3 +209,12 @@ img.expression.default {
189 flex-direction: row;209 flex-direction: row;
190}210}
191211
212#expressions_container:has(#expressions_allow_multiple:not(:checked)) #image_list .expression_list_item[data-expression-type="additional"],
213#expressions_container:has(#expressions_allow_multiple:not(:checked)) label[for="expressions_reroll_if_same"] {
214 opacity: 0.3;
215 transition: opacity var(--animation-duration) ease;
216}
217#expressions_container:has(#expressions_allow_multiple:not(:checked)) #image_list .expression_list_item[data-expression-type="additional"]:hover,
218#expressions_container:has(#expressions_allow_multiple:not(:checked)) #image_list .expression_list_item[data-expression-type="additional"]:focus {
219 opacity: unset;
220}
public/scripts/extensions/expressions/templates/upload-expression.html+12 -0
@@ -0,0 +1,12 @@
1<div class="m-b-1" data-i18n="upload_expression_request">Please enter a name for the sprite (without extension).</div>
2<div class="m-b-1" data-i18n="upload_expression_naming_1">
3 Sprite names must follow the naming schema for the selected expression: {{expression}}
4</div>
5<div data-i18n="upload_expression_naming_2">
6 For multiple expressions, the name must follow the expression name and a valid suffix. Allowed separators are '-' or dot '.'.
7</div>
8<span class="m-b-1" data-i18n="Examples:">Examples:</span> <tt>{{expression}}.png</tt>, <tt>{{expression}}-1.png</tt>, <tt>{{expression}}.expressive.png</tt>
9{{#if clickedFileName}}
10<div class="m-t-1" data-i18n="upload_expression_replace">Click 'Replace' to replace the existing expression:</div>
11<tt>{{clickedFileName}}</tt>
12{{/if}}
public/scripts/extensions/gallery/index.js+0 -1
@@ -441,7 +441,6 @@ SlashCommandParser.addCommandObject(SlashCommand.fromProps({
441 description: 'character name',441 description: 'character name',
442 typeList: [ARGUMENT_TYPE.STRING],442 typeList: [ARGUMENT_TYPE.STRING],
443 enumProvider: commonEnumProviders.characters('character'),443 enumProvider: commonEnumProviders.characters('character'),
444 forceEnum: true,
445 }),444 }),
446 SlashCommandNamedArgument.fromProps({445 SlashCommandNamedArgument.fromProps({
447 name: 'group',446 name: 'group',
public/scripts/extensions/memory/settings.html+1 -1
@@ -12,7 +12,7 @@
12 <label for="summary_source" data-i18n="ext_sum_with">Summarize with:</label>12 <label for="summary_source" data-i18n="ext_sum_with">Summarize with:</label>
13 <select id="summary_source">13 <select id="summary_source">
14 <option value="main" data-i18n="ext_sum_main_api">Main API</option>14 <option value="main" data-i18n="ext_sum_main_api">Main API</option>
15 <option value="extras">Extras API</option>15 <option value="extras">Extras API (deprecated)</option>
16 <option value="webllm" data-i18n="ext_sum_webllm">WebLLM Extension</option>16 <option value="webllm" data-i18n="ext_sum_webllm">WebLLM Extension</option>
17 </select><br>17 </select><br>
1818
public/scripts/extensions/quick-reply/src/QuickReply.js+9 -8
@@ -10,6 +10,7 @@ import { SlashCommandExecutor } from '../../../slash-commands/SlashCommandExecut
10import { SlashCommandParser } from '../../../slash-commands/SlashCommandParser.js';10import { SlashCommandParser } from '../../../slash-commands/SlashCommandParser.js';
11import { SlashCommandParserError } from '../../../slash-commands/SlashCommandParserError.js';11import { SlashCommandParserError } from '../../../slash-commands/SlashCommandParserError.js';
12import { SlashCommandScope } from '../../../slash-commands/SlashCommandScope.js';12import { SlashCommandScope } from '../../../slash-commands/SlashCommandScope.js';
13import { accountStorage } from '../../../util/AccountStorage.js';
13import { debounce, delay, getSortableDelay, showFontAwesomePicker } from '../../../utils.js';14import { debounce, delay, getSortableDelay, showFontAwesomePicker } from '../../../utils.js';
14import { log, quickReplyApi, warn } from '../index.js';15import { log, quickReplyApi, warn } from '../index.js';
15import { QuickReplyContextLink } from './QuickReplyContextLink.js';16import { QuickReplyContextLink } from './QuickReplyContextLink.js';
@@ -544,9 +545,9 @@ export class QuickReply {
544 this.editorSyntax = messageSyntaxInner;545 this.editorSyntax = messageSyntaxInner;
545 /**@type {HTMLInputElement}*/546 /**@type {HTMLInputElement}*/
546 const wrap = dom.querySelector('#qr--modal-wrap');547 const wrap = dom.querySelector('#qr--modal-wrap');
547 wrap.checked = JSON.parse(localStorage.getItem('qr--wrap') ?? 'false');548 wrap.checked = JSON.parse(accountStorage.getItem('qr--wrap') ?? 'false');
548 wrap.addEventListener('click', () => {549 wrap.addEventListener('click', () => {
549 localStorage.setItem('qr--wrap', JSON.stringify(wrap.checked));550 accountStorage.setItem('qr--wrap', JSON.stringify(wrap.checked));
550 updateWrap();551 updateWrap();
551 });552 });
552 const updateWrap = () => {553 const updateWrap = () => {
@@ -594,27 +595,27 @@ export class QuickReply {
594 };595 };
595 /**@type {HTMLInputElement}*/596 /**@type {HTMLInputElement}*/
596 const tabSize = dom.querySelector('#qr--modal-tabSize');597 const tabSize = dom.querySelector('#qr--modal-tabSize');
597 tabSize.value = JSON.parse(localStorage.getItem('qr--tabSize') ?? '4');598 tabSize.value = JSON.parse(accountStorage.getItem('qr--tabSize') ?? '4');
598 const updateTabSize = () => {599 const updateTabSize = () => {
599 message.style.tabSize = tabSize.value;600 message.style.tabSize = tabSize.value;
600 messageSyntaxInner.style.tabSize = tabSize.value;601 messageSyntaxInner.style.tabSize = tabSize.value;
601 updateScrollDebounced();602 updateScrollDebounced();
602 };603 };
603 tabSize.addEventListener('change', () => {604 tabSize.addEventListener('change', () => {
604 localStorage.setItem('qr--tabSize', JSON.stringify(Number(tabSize.value)));605 accountStorage.setItem('qr--tabSize', JSON.stringify(Number(tabSize.value)));
605 updateTabSize();606 updateTabSize();
606 });607 });
607 /**@type {HTMLInputElement}*/608 /**@type {HTMLInputElement}*/
608 const executeShortcut = dom.querySelector('#qr--modal-executeShortcut');609 const executeShortcut = dom.querySelector('#qr--modal-executeShortcut');
609 executeShortcut.checked = JSON.parse(localStorage.getItem('qr--executeShortcut') ?? 'true');610 executeShortcut.checked = JSON.parse(accountStorage.getItem('qr--executeShortcut') ?? 'true');
610 executeShortcut.addEventListener('click', () => {611 executeShortcut.addEventListener('click', () => {
611 localStorage.setItem('qr--executeShortcut', JSON.stringify(executeShortcut.checked));612 accountStorage.setItem('qr--executeShortcut', JSON.stringify(executeShortcut.checked));
612 });613 });
613 /**@type {HTMLInputElement}*/614 /**@type {HTMLInputElement}*/
614 const syntax = dom.querySelector('#qr--modal-syntax');615 const syntax = dom.querySelector('#qr--modal-syntax');
615 syntax.checked = JSON.parse(localStorage.getItem('qr--syntax') ?? 'true');616 syntax.checked = JSON.parse(accountStorage.getItem('qr--syntax') ?? 'true');
616 syntax.addEventListener('click', () => {617 syntax.addEventListener('click', () => {
617 localStorage.setItem('qr--syntax', JSON.stringify(syntax.checked));618 accountStorage.setItem('qr--syntax', JSON.stringify(syntax.checked));
618 updateSyntaxEnabled();619 updateSyntaxEnabled();
619 });620 });
620 if (navigator.keyboard) {621 if (navigator.keyboard) {
public/scripts/extensions/quick-reply/src/QuickReplySet.js+6 -28
@@ -1,15 +1,14 @@
1import { getRequestHeaders, substituteParams } from '../../../../script.js';1import { getRequestHeaders, substituteParams } from '../../../../script.js';
2import { Popup, POPUP_RESULT, POPUP_TYPE } from '../../../popup.js';2import { Popup, POPUP_RESULT, POPUP_TYPE } from '../../../popup.js';
3import { executeSlashCommands, executeSlashCommandsOnChatInput, executeSlashCommandsWithOptions } from '../../../slash-commands.js';3import { executeSlashCommandsOnChatInput, executeSlashCommandsWithOptions } from '../../../slash-commands.js';
4import { SlashCommandParser } from '../../../slash-commands/SlashCommandParser.js';
5import { SlashCommandScope } from '../../../slash-commands/SlashCommandScope.js';4import { SlashCommandScope } from '../../../slash-commands/SlashCommandScope.js';
6import { debounceAsync, log, warn } from '../index.js';5import { SlashCommandParser } from '../../../slash-commands/SlashCommandParser.js';
6import { debounceAsync, warn } from '../index.js';
7import { QuickReply } from './QuickReply.js';7import { QuickReply } from './QuickReply.js';
88
9export class QuickReplySet {9export class QuickReplySet {
10 /**@type {QuickReplySet[]}*/ static list = [];10 /**@type {QuickReplySet[]}*/ static list = [];
1111
12
13 static from(props) {12 static from(props) {
14 props.qrList = []; //props.qrList?.map(it=>QuickReply.from(it));13 props.qrList = []; //props.qrList?.map(it=>QuickReply.from(it));
15 const instance = Object.assign(new this(), props);14 const instance = Object.assign(new this(), props);
@@ -24,9 +23,6 @@ export class QuickReplySet {
24 return this.list.find(it=>it.name == name);23 return this.list.find(it=>it.name == name);
25 }24 }
2625
27
28
29
30 /**@type {string}*/ name;26 /**@type {string}*/ name;
31 /**@type {boolean}*/ disableSend = false;27 /**@type {boolean}*/ disableSend = false;
32 /**@type {boolean}*/ placeBeforeInput = false;28 /**@type {boolean}*/ placeBeforeInput = false;
@@ -34,19 +30,12 @@ export class QuickReplySet {
34 /**@type {string}*/ color = 'transparent';30 /**@type {string}*/ color = 'transparent';
35 /**@type {boolean}*/ onlyBorderColor = false;31 /**@type {boolean}*/ onlyBorderColor = false;
36 /**@type {QuickReply[]}*/ qrList = [];32 /**@type {QuickReply[]}*/ qrList = [];
37
38 /**@type {number}*/ idIndex = 0;33 /**@type {number}*/ idIndex = 0;
39
40 /**@type {boolean}*/ isDeleted = false;34 /**@type {boolean}*/ isDeleted = false;
41
42 /**@type {function}*/ save;35 /**@type {function}*/ save;
43
44 /**@type {HTMLElement}*/ dom;36 /**@type {HTMLElement}*/ dom;
45 /**@type {HTMLElement}*/ settingsDom;37 /**@type {HTMLElement}*/ settingsDom;
4638
47
48
49
50 constructor() {39 constructor() {
51 this.save = debounceAsync(()=>this.performSave(), 200);40 this.save = debounceAsync(()=>this.performSave(), 200);
52 }41 }
@@ -55,9 +44,6 @@ export class QuickReplySet {
55 this.qrList.forEach(qr=>this.hookQuickReply(qr));44 this.qrList.forEach(qr=>this.hookQuickReply(qr));
56 }45 }
5746
58
59
60
61 unrender() {47 unrender() {
62 this.dom?.remove();48 this.dom?.remove();
63 this.dom = null;49 this.dom = null;
@@ -100,9 +86,6 @@ export class QuickReplySet {
100 }86 }
101 }87 }
10288
103
104
105
106 renderSettings() {89 renderSettings() {
107 if (!this.settingsDom) {90 if (!this.settingsDom) {
108 this.settingsDom = document.createElement('div'); {91 this.settingsDom = document.createElement('div'); {
@@ -123,9 +106,6 @@ export class QuickReplySet {
123 this.settingsDom.append(qr.renderSettings(idx));106 this.settingsDom.append(qr.renderSettings(idx));
124 }107 }
125108
126
127
128
129 /**109 /**
130 *110 *
131 * @param {QuickReply} qr111 * @param {QuickReply} qr
@@ -138,6 +118,7 @@ export class QuickReplySet {
138 closure.scope.setMacro('arg::*', '');118 closure.scope.setMacro('arg::*', '');
139 return (await closure.execute())?.pipe;119 return (await closure.execute())?.pipe;
140 }120 }
121
141 /**122 /**
142 *123 *
143 * @param {QuickReply} qr The QR to execute.124 * @param {QuickReply} qr The QR to execute.
@@ -207,6 +188,7 @@ export class QuickReplySet {
207 document.querySelector('#send_but').click();188 document.querySelector('#send_but').click();
208 }189 }
209 }190 }
191
210 /**192 /**
211 * @param {QuickReply} qr193 * @param {QuickReply} qr
212 * @param {string} [message] - optional altered message to be used194 * @param {string} [message] - optional altered message to be used
@@ -220,9 +202,6 @@ export class QuickReplySet {
220 });202 });
221 }203 }
222204
223
224
225
226 addQuickReply(data = {}) {205 addQuickReply(data = {}) {
227 const id = Math.max(this.idIndex, this.qrList.reduce((max,qr)=>Math.max(max,qr.id),0)) + 1;206 const id = Math.max(this.idIndex, this.qrList.reduce((max,qr)=>Math.max(max,qr.id),0)) + 1;
228 data.id =207 data.id =
@@ -239,6 +218,7 @@ export class QuickReplySet {
239 this.save();218 this.save();
240 return qr;219 return qr;
241 }220 }
221
242 addQuickReplyFromText(qrJson) {222 addQuickReplyFromText(qrJson) {
243 let data;223 let data;
244 if (qrJson) {224 if (qrJson) {
@@ -371,7 +351,6 @@ export class QuickReplySet {
371 this.save();351 this.save();
372 }352 }
373353
374
375 toJSON() {354 toJSON() {
376 return {355 return {
377 version: 2,356 version: 2,
@@ -386,7 +365,6 @@ export class QuickReplySet {
386 };365 };
387 }366 }
388367
389
390 async performSave() {368 async performSave() {
391 const response = await fetch('/api/quick-replies/save', {369 const response = await fetch('/api/quick-replies/save', {
392 method: 'POST',370 method: 'POST',
public/scripts/extensions/quick-reply/src/SlashCommandHandler.js+4 -0
@@ -883,6 +883,10 @@ export class SlashCommandHandler {
883 }883 }
884 }884 }
885 getQuickReply(args) {885 getQuickReply(args) {
886 if (!args.id && !args.label) {
887 toastr.error('Please provide a valid id or label.');
888 return '';
889 }
886 try {890 try {
887 return JSON.stringify(this.api.getQrByLabel(args.set, args.id !== undefined ? Number(args.id) : args.label));891 return JSON.stringify(this.api.getQrByLabel(args.set, args.id !== undefined ? Number(args.id) : args.label));
888 } catch (ex) {892 } catch (ex) {
public/scripts/extensions/quick-reply/src/ui/SettingsUi.js+1 -1
@@ -346,7 +346,7 @@ export class SettingsUi {
346 }346 }
347347
348 async addQrSet() {348 async addQrSet() {
349 const name = await Popup.show.input('Create a new World Info', 'Enter a name for the new Quick Reply Set:');349 const name = await Popup.show.input('Create a new Quick Reply Set', 'Enter a name for the new Quick Reply Set:');
350 if (name && name.length > 0) {350 if (name && name.length > 0) {
351 const oldQrs = QuickReplySet.get(name);351 const oldQrs = QuickReplySet.get(name);
352 if (oldQrs) {352 if (oldQrs) {
public/scripts/extensions/regex/editor.html+6 -0
@@ -94,6 +94,12 @@
94 <span data-i18n="World Info">World Info</span>94 <span data-i18n="World Info">World Info</span>
95 </label>95 </label>
96 </div>96 </div>
97 <div data-i18n="[title]ext_regex_reasoning_desc" title="Reasoning block contents. When 'Only Format Prompt' is checked, it will also affect the reasoning contents added to the prompt.">
98 <label class="checkbox flex-container">
99 <input type="checkbox" name="replace_position" value="6">
100 <span data-i18n="Reasoning">Reasoning</span>
101 </label>
102 </div>
97 <div class="flex-container wide100p marginTop5">103 <div class="flex-container wide100p marginTop5">
98 <div class="flex1 flex-container flexNoGap">104 <div class="flex1 flex-container flexNoGap">
99 <small data-i18n="[title]ext_regex_min_depth_desc" title="When applied to prompts or display, only affect messages that are at least N levels deep. 0 = last message, 1 = penultimate message, etc. Only counts WI entries @Depth and usable messages, i.e. not hidden or system.">105 <small data-i18n="[title]ext_regex_min_depth_desc" title="When applied to prompts or display, only affect messages that are at least N levels deep. 0 = last message, 1 = penultimate message, etc. Only counts WI entries @Depth and usable messages, i.e. not hidden or system.">
public/scripts/extensions/regex/engine.js+2 -1
@@ -20,6 +20,7 @@ const regex_placement = {
20 SLASH_COMMAND: 3,20 SLASH_COMMAND: 3,
21 // 4 - sendAs (legacy)21 // 4 - sendAs (legacy)
22 WORLD_INFO: 5,22 WORLD_INFO: 5,
23 REASONING: 6,
23};24};
2425
25export const substitute_find_regex = {26export const substitute_find_regex = {
@@ -94,7 +95,7 @@ function getRegexedString(rawString, placement, { characterOverride, isMarkdown,
94 // Script applies to Generate and input is Generate95 // Script applies to Generate and input is Generate
95 (script.promptOnly && isPrompt) ||96 (script.promptOnly && isPrompt) ||
96 // Script applies to all cases when neither "only"s are true, but there's no need to do it when `isMarkdown`, the as source (chat history) should already be changed beforehand97 // Script applies to all cases when neither "only"s are true, but there's no need to do it when `isMarkdown`, the as source (chat history) should already be changed beforehand
97 (!script.markdownOnly && !script.promptOnly && !isMarkdown)98 (!script.markdownOnly && !script.promptOnly && !isMarkdown && !isPrompt)
98 ) {99 ) {
99 if (isEdit && !script.runOnEdit) {100 if (isEdit && !script.runOnEdit) {
100 console.debug(`getRegexedString: Skipping script ${script.scriptName} because it does not run on edit`);101 console.debug(`getRegexedString: Skipping script ${script.scriptName} because it does not run on edit`);
public/scripts/extensions/regex/index.js+4 -3
@@ -10,6 +10,7 @@ import { SlashCommandParser } from '../../slash-commands/SlashCommandParser.js';
10import { download, getFileText, getSortableDelay, uuidv4 } from '../../utils.js';10import { download, getFileText, getSortableDelay, uuidv4 } from '../../utils.js';
11import { regex_placement, runRegexScript, substitute_find_regex } from './engine.js';11import { regex_placement, runRegexScript, substitute_find_regex } from './engine.js';
12import { t } from '../../i18n.js';12import { t } from '../../i18n.js';
13import { accountStorage } from '../../util/AccountStorage.js';
1314
14/**15/**
15 * @typedef {object} RegexScript16 * @typedef {object} RegexScript
@@ -18,7 +19,7 @@ import { t } from '../../i18n.js';
18 * @property {string} replaceString - The replace string19 * @property {string} replaceString - The replace string
19 * @property {string[]} trimStrings - The trim strings20 * @property {string[]} trimStrings - The trim strings
20 * @property {string?} findRegex - The find regex21 * @property {string?} findRegex - The find regex
21 * @property {string?} substituteRegex - The substitute regex22 * @property {number?} substituteRegex - The substitute regex
22 */23 */
2324
24/**25/**
@@ -440,8 +441,8 @@ async function checkEmbeddedRegexScripts() {
440 if (avatar && !extension_settings.character_allowed_regex.includes(avatar)) {441 if (avatar && !extension_settings.character_allowed_regex.includes(avatar)) {
441 const checkKey = `AlertRegex_${characters[chid].avatar}`;442 const checkKey = `AlertRegex_${characters[chid].avatar}`;
442443
443 if (!localStorage.getItem(checkKey)) {444 if (!accountStorage.getItem(checkKey)) {
444 localStorage.setItem(checkKey, 'true');445 accountStorage.setItem(checkKey, 'true');
445 const template = await renderExtensionTemplateAsync('regex', 'embeddedScripts', {});446 const template = await renderExtensionTemplateAsync('regex', 'embeddedScripts', {});
446 const result = await callGenericPopup(template, POPUP_TYPE.CONFIRM, '', { okButton: 'Yes' });447 const result = await callGenericPopup(template, POPUP_TYPE.CONFIRM, '', { okButton: 'Yes' });
447448
public/scripts/extensions/stable-diffusion/index.js+67 -0
@@ -81,6 +81,7 @@ const sources = {
81 huggingface: 'huggingface',81 huggingface: 'huggingface',
82 nanogpt: 'nanogpt',82 nanogpt: 'nanogpt',
83 bfl: 'bfl',83 bfl: 'bfl',
84 falai: 'falai',
84};85};
8586
86const initiators = {87const initiators = {
@@ -1169,6 +1170,10 @@ async function onBflKeyClick() {
1169 return onApiKeyClick('BFL API Key:', SECRET_KEYS.BFL);1170 return onApiKeyClick('BFL API Key:', SECRET_KEYS.BFL);
1170}1171}
11711172
1173async function onFalaiKeyClick() {
1174 return onApiKeyClick('FALAI API Key:', SECRET_KEYS.FALAI);
1175}
1176
1172function onBflUpsamplingInput() {1177function onBflUpsamplingInput() {
1173 extension_settings.sd.bfl_upsampling = !!$('#sd_bfl_upsampling').prop('checked');1178 extension_settings.sd.bfl_upsampling = !!$('#sd_bfl_upsampling').prop('checked');
1174 saveSettingsDebounced();1179 saveSettingsDebounced();
@@ -1299,6 +1304,7 @@ async function onModelChange() {
1299 sources.huggingface,1304 sources.huggingface,
1300 sources.nanogpt,1305 sources.nanogpt,
1301 sources.bfl,1306 sources.bfl,
1307 sources.falai,
1302 ];1308 ];
13031309
1304 if (cloudSources.includes(extension_settings.sd.source)) {1310 if (cloudSources.includes(extension_settings.sd.source)) {
@@ -1707,6 +1713,9 @@ async function loadModels() {
1707 case sources.bfl:1713 case sources.bfl:
1708 models = await loadBflModels();1714 models = await loadBflModels();
1709 break;1715 break;
1716 case sources.falai:
1717 models = await loadFalaiModels();
1718 break;
1710 }1719 }
17111720
1712 for (const model of models) {1721 for (const model of models) {
@@ -1744,6 +1753,21 @@ async function loadBflModels() {
1744 ];1753 ];
1745}1754}
17461755
1756async function loadFalaiModels() {
1757 $('#sd_falai_key').toggleClass('success', !!secret_state[SECRET_KEYS.FALAI]);
1758
1759 const result = await fetch('/api/sd/falai/models', {
1760 method: 'POST',
1761 headers: getRequestHeaders(),
1762 });
1763
1764 if (result.ok) {
1765 return await result.json();
1766 }
1767
1768 return [];
1769}
1770
1747async function loadPollinationsModels() {1771async function loadPollinationsModels() {
1748 const result = await fetch('/api/sd/pollinations/models', {1772 const result = await fetch('/api/sd/pollinations/models', {
1749 method: 'POST',1773 method: 'POST',
@@ -2081,6 +2105,9 @@ async function loadSchedulers() {
2081 case sources.bfl:2105 case sources.bfl:
2082 schedulers = ['N/A'];2106 schedulers = ['N/A'];
2083 break;2107 break;
2108 case sources.falai:
2109 schedulers = ['N/A'];
2110 break;
2084 }2111 }
20852112
2086 for (const scheduler of schedulers) {2113 for (const scheduler of schedulers) {
@@ -2735,6 +2762,9 @@ async function sendGenerationRequest(generationType, prompt, additionalNegativeP
2735 case sources.bfl:2762 case sources.bfl:
2736 result = await generateBflImage(prefixedPrompt, signal);2763 result = await generateBflImage(prefixedPrompt, signal);
2737 break;2764 break;
2765 case sources.falai:
2766 result = await generateFalaiImage(prefixedPrompt, negativePrompt, signal);
2767 break;
2738 }2768 }
27392769
2740 if (!result.data) {2770 if (!result.data) {
@@ -3496,6 +3526,40 @@ async function generateBflImage(prompt, signal) {
3496 }3526 }
3497}3527}
34983528
3529/**
3530 * Generates an image using the FAL.AI API.
3531 * @param {string} prompt - The main instruction used to guide the image generation.
3532 * @param {string} negativePrompt - The negative prompt used to guide the image generation.
3533 * @param {AbortSignal} signal - An AbortSignal object that can be used to cancel the request.
3534 * @returns {Promise<{format: string, data: string}>} - A promise that resolves when the image generation and processing are complete.
3535 */
3536async function generateFalaiImage(prompt, negativePrompt, signal) {
3537 const result = await fetch('/api/sd/falai/generate', {
3538 method: 'POST',
3539 headers: getRequestHeaders(),
3540 signal: signal,
3541 body: JSON.stringify({
3542 prompt: prompt,
3543 negative_prompt: negativePrompt,
3544 model: extension_settings.sd.model,
3545 steps: clamp(extension_settings.sd.steps, 1, 50),
3546 guidance: clamp(extension_settings.sd.scale, 1.5, 5),
3547 width: clamp(extension_settings.sd.width, 256, 1440),
3548 height: clamp(extension_settings.sd.height, 256, 1440),
3549 seed: extension_settings.sd.seed >= 0 ? extension_settings.sd.seed : undefined,
3550 }),
3551 });
3552
3553 if (result.ok) {
3554 const data = await result.json();
3555 return { format: 'jpg', data: data.image };
3556 } else {
3557 const text = await result.text();
3558 console.log(text);
3559 throw new Error(text);
3560 }
3561}
3562
3499async function onComfyOpenWorkflowEditorClick() {3563async function onComfyOpenWorkflowEditorClick() {
3500 let workflow = await (await fetch('/api/sd/comfy/workflow', {3564 let workflow = await (await fetch('/api/sd/comfy/workflow', {
3501 method: 'POST',3565 method: 'POST',
@@ -3782,6 +3846,8 @@ function isValidState() {
3782 return secret_state[SECRET_KEYS.NANOGPT];3846 return secret_state[SECRET_KEYS.NANOGPT];
3783 case sources.bfl:3847 case sources.bfl:
3784 return secret_state[SECRET_KEYS.BFL];3848 return secret_state[SECRET_KEYS.BFL];
3849 case sources.falai:
3850 return secret_state[SECRET_KEYS.FALAI];
3785 }3851 }
3786}3852}
37873853
@@ -4443,6 +4509,7 @@ jQuery(async () => {
4443 $('#sd_function_tool').on('input', onFunctionToolInput);4509 $('#sd_function_tool').on('input', onFunctionToolInput);
4444 $('#sd_bfl_key').on('click', onBflKeyClick);4510 $('#sd_bfl_key').on('click', onBflKeyClick);
4445 $('#sd_bfl_upsampling').on('input', onBflUpsamplingInput);4511 $('#sd_bfl_upsampling').on('input', onBflUpsamplingInput);
4512 $('#sd_falai_key').on('click', onFalaiKeyClick);
44464513
4447 if (!CSS.supports('field-sizing', 'content')) {4514 if (!CSS.supports('field-sizing', 'content')) {
4448 $('.sd_settings .inline-drawer-toggle').on('click', function () {4515 $('.sd_settings .inline-drawer-toggle').on('click', function () {
public/scripts/extensions/stable-diffusion/settings.html+16 -1
@@ -41,7 +41,8 @@
41 <option value="blockentropy">Block Entropy</option>41 <option value="blockentropy">Block Entropy</option>
42 <option value="comfy">ComfyUI</option>42 <option value="comfy">ComfyUI</option>
43 <option value="drawthings">DrawThings HTTP API</option>43 <option value="drawthings">DrawThings HTTP API</option>
44 <option value="extras">Extras API (local / remote)</option>44 <option value="extras">Extras API (deprecated)</option>
45 <option value="falai">FAL.AI</option>
45 <option value="huggingface">HuggingFace Inference API (serverless)</option>46 <option value="huggingface">HuggingFace Inference API (serverless)</option>
46 <option value="nanogpt">NanoGPT</option>47 <option value="nanogpt">NanoGPT</option>
47 <option value="novel">NovelAI Diffusion</option>48 <option value="novel">NovelAI Diffusion</option>
@@ -256,6 +257,20 @@
256 </label>257 </label>
257 </div>258 </div>
258259
260 <div data-sd-source="falai">
261 <div class="flex-container flexnowrap alignItemsBaseline marginBot5">
262 <a href="https://fal.ai/dashboard" target="_blank" rel="noopener noreferrer">
263 <strong data-i18n="API Key">API Key</strong>
264 <i class="fa-solid fa-share-from-square"></i>
265 </a>
266 <span class="expander"></span>
267 <div id="sd_falai_key" class="menu_button menu_button_icon">
268 <i class="fa-fw fa-solid fa-key"></i>
269 <span data-i18n="Click to set">Click to set</span>
270 </div>
271 </div>
272 </div>
273
259 <div class="flex-container">274 <div class="flex-container">
260 <div class="flex1">275 <div class="flex1">
261 <label for="sd_model" data-i18n="Model">Model</label>276 <label for="sd_model" data-i18n="Model">Model</label>
public/scripts/extensions/token-counter/index.js+6 -20
@@ -6,6 +6,8 @@ import { getFriendlyTokenizerName, getTextTokens, getTokenCountAsync, tokenizers
6import { resetScrollHeight, debounce } from '../../utils.js';6import { resetScrollHeight, debounce } from '../../utils.js';
7import { debounce_timeout } from '../../constants.js';7import { debounce_timeout } from '../../constants.js';
8import { POPUP_TYPE, callGenericPopup } from '../../popup.js';8import { POPUP_TYPE, callGenericPopup } from '../../popup.js';
9import { renderExtensionTemplateAsync } from '../../extensions.js';
10import { t } from '../../i18n.js';
911
10function rgb2hex(rgb) {12function rgb2hex(rgb) {
11 rgb = rgb.match(/^rgba?[\s+]?\([\s+]?(\d+)[\s+]?,[\s+]?(\d+)[\s+]?,[\s+]?(\d+)[\s+]?/i);13 rgb = rgb.match(/^rgba?[\s+]?\([\s+]?(\d+)[\s+]?,[\s+]?(\d+)[\s+]?,[\s+]?(\d+)[\s+]?/i);
@@ -22,23 +24,7 @@ $('button').click(function () {
2224
23async function doTokenCounter() {25async function doTokenCounter() {
24 const { tokenizerName, tokenizerId } = getFriendlyTokenizerName(main_api);26 const { tokenizerName, tokenizerId } = getFriendlyTokenizerName(main_api);
25 const html = `27 const html = await renderExtensionTemplateAsync('token-counter', 'window', {tokenizerName});
26 <div class="wide100p">
27 <h3>Token Counter</h3>
28 <div class="justifyLeft flex-container flexFlowColumn">
29 <h4>Type / paste in the box below to see the number of tokens in the text.</h4>
30 <p>Selected tokenizer: ${tokenizerName}</p>
31 <div>Input:</div>
32 <textarea id="token_counter_textarea" class="wide100p textarea_compact" rows="1"></textarea>
33 <div>Tokens: <span id="token_counter_result">0</span></div>
34 <hr>
35 <div>Tokenized text:</div>
36 <div id="tokenized_chunks_display" class="wide100p">—</div>
37 <hr>
38 <div>Token IDs:</div>
39 <textarea id="token_counter_ids" class="wide100p textarea_compact" readonly rows="1">—</textarea>
40 </div>
41 </div>`;
4228
43 const dialog = $(html);29 const dialog = $(html);
44 const countDebounced = debounce(async () => {30 const countDebounced = debounce(async () => {
@@ -131,9 +117,9 @@ async function doCount() {
131jQuery(() => {117jQuery(() => {
132 const buttonHtml = `118 const buttonHtml = `
133 <div id="token_counter" class="list-group-item flex-container flexGap5">119 <div id="token_counter" class="list-group-item flex-container flexGap5">
134 <div class="fa-solid fa-1 extensionsMenuExtensionButton" /></div>120 <div class="fa-solid fa-1 extensionsMenuExtensionButton" /></div>` +
135 Token Counter121 t`Token Counter` +
136 </div>`;122 '</div>';
137 $('#token_counter_wand_container').append(buttonHtml);123 $('#token_counter_wand_container').append(buttonHtml);
138 $('#token_counter').on('click', doTokenCounter);124 $('#token_counter').on('click', doTokenCounter);
139 SlashCommandParser.addCommandObject(SlashCommand.fromProps({125 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
public/scripts/extensions/token-counter/window.html+16 -0
@@ -0,0 +1,16 @@
1<div class="wide100p">
2 <h3 data-i18n="Token Counter">Token Counter</h3>
3 <div class="justifyLeft flex-container flexFlowColumn">
4 <h4 data-i18n="Type / paste in the box below to see the number of tokens in the text.">Type / paste in the box below to see the number of tokens in the text.</h4>
5 <p><span data-i18n="Selected tokenizer:">Selected tokenizer:</span> {{tokenizerName}}</p>
6 <div data-i18n="Input:">Input:</div>
7 <textarea id="token_counter_textarea" class="wide100p textarea_compact" rows="1"></textarea>
8 <div><span data-i18n="Tokens:">Tokens:</span> <span id="token_counter_result">0</span></div>
9 <hr>
10 <div data-i18n="Tokenized text:">Tokenized text:</div>
11 <div id="tokenized_chunks_display" class="wide100p">—</div>
12 <hr>
13 <div data-i18n="Token IDs:">Token IDs:</div>
14 <textarea id="token_counter_ids" class="wide100p textarea_compact" readonly rows="1">—</textarea>
15 </div>
16</div>
\ No newline at end of file16 \ No newline at end of file
public/scripts/extensions/translate/index.js+1 -1
@@ -605,7 +605,7 @@ const handleOutgoingMessage = createEventHandler(translateOutgoingMessage, () =>
605const handleImpersonateReady = createEventHandler(translateImpersonate, () => shouldTranslate(incomingTypes));605const handleImpersonateReady = createEventHandler(translateImpersonate, () => shouldTranslate(incomingTypes));
606const handleMessageEdit = createEventHandler(translateMessageEdit, () => true);606const handleMessageEdit = createEventHandler(translateMessageEdit, () => true);
607607
608window['translate'] = translate;608globalThis.translate = translate;
609609
610jQuery(async () => {610jQuery(async () => {
611 const html = await renderExtensionTemplateAsync('translate', 'index');611 const html = await renderExtensionTemplateAsync('translate', 'index');
public/scripts/extensions/tts/alltalk.js+10 -6
@@ -388,7 +388,7 @@ class AllTalkTtsProvider {
388 }388 }
389389
390 async fetchRvcVoiceObjects() {390 async fetchRvcVoiceObjects() {
391 if (this.settings.server_version == 'v2') {391 if (this.settings.server_version == 'v1') {
392 console.log('Skipping RVC voices fetch for V1 server');392 console.log('Skipping RVC voices fetch for V1 server');
393 return [];393 return [];
394 }394 }
@@ -1031,14 +1031,18 @@ class AllTalkTtsProvider {
1031 console.error('fetchTtsGeneration Error Response Text:', errorText);1031 console.error('fetchTtsGeneration Error Response Text:', errorText);
1032 throw new Error(`HTTP ${response.status}: ${errorText}`);1032 throw new Error(`HTTP ${response.status}: ${errorText}`);
1033 }1033 }
1034
1034 const data = await response.json();1035 const data = await response.json();
10351036
1036 // Handle V1/V2 URL differences1037 // V1 returns a complete URL, V2 returns a relative path
1037 const outputUrl = this.settings.server_version === 'v1'1038 if (this.settings.server_version === 'v1') {
1038 ? data.output_file_url // V1 returns full URL1039 // V1: Use the complete URL directly from the response
1039 : `${this.settings.provider_endpoint}${data.output_file_url}`; // V2 returns relative path1040 return data.output_file_url;
1041 } else {
1042 // V2: Combine the endpoint with the relative path
1043 return `${this.settings.provider_endpoint}${data.output_file_url}`;
1044 }
10401045
1041 return outputUrl;
1042 } catch (error) {1046 } catch (error) {
1043 console.error('[fetchTtsGeneration] Exception caught:', error);1047 console.error('[fetchTtsGeneration] Exception caught:', error);
1044 throw error;1048 throw error;
public/scripts/extensions/tts/index.js+47 -30
@@ -27,13 +27,12 @@ import { SlashCommandEnumValue, enumTypes } from '../../slash-commands/SlashComm
27import { enumIcons } from '../../slash-commands/SlashCommandCommonEnumsProvider.js';27import { enumIcons } from '../../slash-commands/SlashCommandCommonEnumsProvider.js';
28import { POPUP_TYPE, callGenericPopup } from '../../popup.js';28import { POPUP_TYPE, callGenericPopup } from '../../popup.js';
29import { GoogleTranslateTtsProvider } from './google-translate.js';29import { GoogleTranslateTtsProvider } from './google-translate.js';
30export { talkingAnimation };
3130
32const UPDATE_INTERVAL = 1000;31const UPDATE_INTERVAL = 1000;
32const wrapper = new ModuleWorkerWrapper(moduleWorker);
3333
34let voiceMapEntries = [];34let voiceMapEntries = [];
35let voiceMap = {}; // {charName:voiceid, charName2:voiceid2}35let voiceMap = {}; // {charName:voiceid, charName2:voiceid2}
36let talkingHeadState = false;
37let lastChatId = null;36let lastChatId = null;
38let lastMessage = null;37let lastMessage = null;
39let lastMessageHash = null;38let lastMessageHash = null;
@@ -120,7 +119,7 @@ async function onNarrateOneMessage() {
120 }119 }
121120
122 resetTtsPlayback();121 resetTtsPlayback();
123 ttsJobQueue.push(message);122 processAndQueueTtsMessage(message);
124 moduleWorker();123 moduleWorker();
125}124}
126125
@@ -147,7 +146,7 @@ async function onNarrateText(args, text) {
147 }146 }
148147
149 resetTtsPlayback();148 resetTtsPlayback();
150 ttsJobQueue.push({ mes: text, name: name });149 processAndQueueTtsMessage({ mes: text, name: name });
151 await moduleWorker();150 await moduleWorker();
152151
153 // Return back to the chat voices152 // Return back to the chat voices
@@ -165,27 +164,6 @@ async function moduleWorker() {
165 updateUiAudioPlayState();164 updateUiAudioPlayState();
166}165}
167166
168function talkingAnimation(switchValue) {
169 if (!modules.includes('talkinghead')) {
170 console.debug('Talking Animation module not loaded');
171 return;
172 }
173
174 const apiUrl = getApiUrl();
175 const animationType = switchValue ? 'start' : 'stop';
176
177 if (switchValue !== talkingHeadState) {
178 try {
179 console.log(animationType + ' Talking Animation');
180 doExtrasFetch(`${apiUrl}/api/talkinghead/${animationType}_talking`);
181 talkingHeadState = switchValue;
182 } catch (error) {
183 // Handle the error here or simply ignore it to prevent logging
184 }
185 }
186 updateUiAudioPlayState();
187}
188
189function resetTtsPlayback() {167function resetTtsPlayback() {
190 // Stop system TTS utterance168 // Stop system TTS utterance
191 cancelTtsPlay();169 cancelTtsPlay();
@@ -220,6 +198,36 @@ function isTtsProcessing() {
220 return processing;198 return processing;
221}199}
222200
201/**
202 * Splits a message into lines and adds each non-empty line to the TTS job queue.
203 * @param {Object} message - The message object to be processed.
204 * @param {string} message.mes - The text of the message to be split into lines.
205 * @param {string} message.name - The name associated with the message.
206 * @returns {void}
207 */
208function processAndQueueTtsMessage(message) {
209 if (!extension_settings.tts.narrate_by_paragraphs) {
210 ttsJobQueue.push(message);
211 return;
212 }
213
214 const lines = message.mes.split('\n');
215
216 for (let i = 0; i < lines.length; i++) {
217 const line = lines[i];
218
219 if (line.length === 0) {
220 continue;
221 }
222
223 ttsJobQueue.push(
224 Object.assign({}, message, {
225 mes: line,
226 }),
227 );
228 }
229}
230
223function debugTtsPlayback() {231function debugTtsPlayback() {
224 console.log(JSON.stringify(232 console.log(JSON.stringify(
225 {233 {
@@ -347,10 +355,9 @@ function onAudioControlClicked() {
347 // Not pausing, doing a full stop to anything TTS is doing. Better UX as pause is not as useful355 // Not pausing, doing a full stop to anything TTS is doing. Better UX as pause is not as useful
348 if (!audioElement.paused || isTtsProcessing()) {356 if (!audioElement.paused || isTtsProcessing()) {
349 resetTtsPlayback();357 resetTtsPlayback();
350 talkingAnimation(false);
351 } else {358 } else {
352 // Default play behavior if not processing or playing is to play the last message.359 // Default play behavior if not processing or playing is to play the last message.
353 ttsJobQueue.push(context.chat[context.chat.length - 1]);360 processAndQueueTtsMessage(context.chat[context.chat.length - 1]);
354 }361 }
355 updateUiAudioPlayState();362 updateUiAudioPlayState();
356}363}
@@ -374,8 +381,8 @@ function addAudioControl() {
374function completeCurrentAudioJob() {381function completeCurrentAudioJob() {
375 audioQueueProcessorReady = true;382 audioQueueProcessorReady = true;
376 currentAudioJob = null;383 currentAudioJob = null;
377 talkingAnimation(false); //stop lip animation
378 // updateUiPlayState();384 // updateUiPlayState();
385 wrapper.update();
379}386}
380387
381/**388/**
@@ -404,7 +411,6 @@ async function processAudioJobQueue() {
404 audioQueueProcessorReady = false;411 audioQueueProcessorReady = false;
405 currentAudioJob = audioJobQueue.shift();412 currentAudioJob = audioJobQueue.shift();
406 playAudioData(currentAudioJob);413 playAudioData(currentAudioJob);
407 talkingAnimation(true);
408 } catch (error) {414 } catch (error) {
409 toastr.error(error.toString());415 toastr.error(error.toString());
410 console.error(error);416 console.error(error);
@@ -569,6 +575,7 @@ function loadSettings() {
569 $('#tts_narrate_quoted').prop('checked', extension_settings.tts.narrate_quoted_only);575 $('#tts_narrate_quoted').prop('checked', extension_settings.tts.narrate_quoted_only);
570 $('#tts_auto_generation').prop('checked', extension_settings.tts.auto_generation);576 $('#tts_auto_generation').prop('checked', extension_settings.tts.auto_generation);
571 $('#tts_periodic_auto_generation').prop('checked', extension_settings.tts.periodic_auto_generation);577 $('#tts_periodic_auto_generation').prop('checked', extension_settings.tts.periodic_auto_generation);
578 $('#tts_narrate_by_paragraphs').prop('checked', extension_settings.tts.narrate_by_paragraphs);
572 $('#tts_narrate_translated_only').prop('checked', extension_settings.tts.narrate_translated_only);579 $('#tts_narrate_translated_only').prop('checked', extension_settings.tts.narrate_translated_only);
573 $('#tts_narrate_user').prop('checked', extension_settings.tts.narrate_user);580 $('#tts_narrate_user').prop('checked', extension_settings.tts.narrate_user);
574 $('#tts_pass_asterisks').prop('checked', extension_settings.tts.pass_asterisks);581 $('#tts_pass_asterisks').prop('checked', extension_settings.tts.pass_asterisks);
@@ -638,6 +645,11 @@ function onPeriodicAutoGenerationClick() {
638 saveSettingsDebounced();645 saveSettingsDebounced();
639}646}
640647
648function onNarrateByParagraphsClick() {
649 extension_settings.tts.narrate_by_paragraphs = !!$('#tts_narrate_by_paragraphs').prop('checked');
650 saveSettingsDebounced();
651}
652
641653
642function onNarrateDialoguesClick() {654function onNarrateDialoguesClick() {
643 extension_settings.tts.narrate_dialogues_only = !!$('#tts_narrate_dialogues').prop('checked');655 extension_settings.tts.narrate_dialogues_only = !!$('#tts_narrate_dialogues').prop('checked');
@@ -816,7 +828,12 @@ async function onMessageEvent(messageId, lastCharIndex) {
816 lastChatId = context.chatId;828 lastChatId = context.chatId;
817829
818 console.debug(`Adding message from ${message.name} for TTS processing: "${message.mes}"`);830 console.debug(`Adding message from ${message.name} for TTS processing: "${message.mes}"`);
831
832 if (extension_settings.tts.periodic_auto_generation) {
819 ttsJobQueue.push(message);833 ttsJobQueue.push(message);
834 } else {
835 processAndQueueTtsMessage(message);
836 }
820}837}
821838
822async function onMessageDeleted() {839async function onMessageDeleted() {
@@ -1156,6 +1173,7 @@ jQuery(async function () {
1156 $('#tts_pass_asterisks').on('click', onPassAsterisksClick);1173 $('#tts_pass_asterisks').on('click', onPassAsterisksClick);
1157 $('#tts_auto_generation').on('click', onAutoGenerationClick);1174 $('#tts_auto_generation').on('click', onAutoGenerationClick);
1158 $('#tts_periodic_auto_generation').on('click', onPeriodicAutoGenerationClick);1175 $('#tts_periodic_auto_generation').on('click', onPeriodicAutoGenerationClick);
1176 $('#tts_narrate_by_paragraphs').on('click', onNarrateByParagraphsClick);
1159 $('#tts_narrate_user').on('click', onNarrateUserClick);1177 $('#tts_narrate_user').on('click', onNarrateUserClick);
11601178
1161 $('#playback_rate').on('input', function () {1179 $('#playback_rate').on('input', function () {
@@ -1177,7 +1195,6 @@ jQuery(async function () {
1177 loadSettings(); // Depends on Extension Controls and loadTtsProvider1195 loadSettings(); // Depends on Extension Controls and loadTtsProvider
1178 loadTtsProvider(extension_settings.tts.currentProvider); // No dependencies1196 loadTtsProvider(extension_settings.tts.currentProvider); // No dependencies
1179 addAudioControl(); // Depends on Extension Controls1197 addAudioControl(); // Depends on Extension Controls
1180 const wrapper = new ModuleWorkerWrapper(moduleWorker);
1181 setInterval(wrapper.update.bind(wrapper), UPDATE_INTERVAL); // Init depends on all the things1198 setInterval(wrapper.update.bind(wrapper), UPDATE_INTERVAL); // Init depends on all the things
1182 eventSource.on(event_types.MESSAGE_SWIPED, resetTtsPlayback);1199 eventSource.on(event_types.MESSAGE_SWIPED, resetTtsPlayback);
1183 eventSource.on(event_types.CHAT_CHANGED, onChatChanged);1200 eventSource.on(event_types.CHAT_CHANGED, onChatChanged);
public/scripts/extensions/tts/openai-compatible.js+3 -3
@@ -25,7 +25,7 @@ class OpenAICompatibleTtsProvider {
25 <label for="openai_compatible_tts_endpoint">Provider Endpoint:</label>25 <label for="openai_compatible_tts_endpoint">Provider Endpoint:</label>
26 <div class="flex-container alignItemsCenter">26 <div class="flex-container alignItemsCenter">
27 <div class="flex1">27 <div class="flex1">
28 <input id="openai_compatible_tts_endpoint" type="text" class="text_pole" maxlength="250" value="${this.defaultSettings.provider_endpoint}"/>28 <input id="openai_compatible_tts_endpoint" type="text" class="text_pole" maxlength="500" value="${this.defaultSettings.provider_endpoint}"/>
29 </div>29 </div>
30 <div id="openai_compatible_tts_key" class="menu_button menu_button_icon">30 <div id="openai_compatible_tts_key" class="menu_button menu_button_icon">
31 <i class="fa-solid fa-key"></i>31 <i class="fa-solid fa-key"></i>
@@ -33,9 +33,9 @@ class OpenAICompatibleTtsProvider {
33 </div>33 </div>
34 </div>34 </div>
35 <label for="openai_compatible_model">Model:</label>35 <label for="openai_compatible_model">Model:</label>
36 <input id="openai_compatible_model" type="text" class="text_pole" maxlength="250" value="${this.defaultSettings.model}"/>36 <input id="openai_compatible_model" type="text" class="text_pole" maxlength="500" value="${this.defaultSettings.model}"/>
37 <label for="openai_compatible_tts_voices">Available Voices (comma separated):</label>37 <label for="openai_compatible_tts_voices">Available Voices (comma separated):</label>
38 <input id="openai_compatible_tts_voices" type="text" class="text_pole" maxlength="250" value="${this.defaultSettings.available_voices.join()}"/>38 <input id="openai_compatible_tts_voices" type="text" class="text_pole" value="${this.defaultSettings.available_voices.join()}"/>
39 <label for="openai_compatible_tts_speed">Speed: <span id="openai_compatible_tts_speed_output"></span></label>39 <label for="openai_compatible_tts_speed">Speed: <span id="openai_compatible_tts_speed_output"></span></label>
40 <input type="range" id="openai_compatible_tts_speed" value="1" min="0.25" max="4" step="0.05">`;40 <input type="range" id="openai_compatible_tts_speed" value="1" min="0.25" max="4" step="0.05">`;
41 return html;41 return html;
public/scripts/extensions/tts/settings.html+4 -0
@@ -30,6 +30,10 @@
30 <input type="checkbox" id="tts_periodic_auto_generation">30 <input type="checkbox" id="tts_periodic_auto_generation">
31 <small data-i18n="Narrate by paragraphs (when streaming)">Narrate by paragraphs (when streaming)</small>31 <small data-i18n="Narrate by paragraphs (when streaming)">Narrate by paragraphs (when streaming)</small>
32 </label>32 </label>
33 <label class="checkbox_label" for="tts_narrate_by_paragraphs">
34 <input type="checkbox" id="tts_narrate_by_paragraphs">
35 <small data-i18n="Narrate by paragraphs (when not streaming)">Narrate by paragraphs (when not streaming)</small>
36 </label>
33 <label class="checkbox_label" for="tts_narrate_quoted">37 <label class="checkbox_label" for="tts_narrate_quoted">
34 <input type="checkbox" id="tts_narrate_quoted">38 <input type="checkbox" id="tts_narrate_quoted">
35 <small data-i18n="Only narrate quotes">Only narrate "quotes"</small>39 <small data-i18n="Only narrate quotes">Only narrate "quotes"</small>
public/scripts/extensions/tts/system.js+0 -3
@@ -1,6 +1,5 @@
1import { isMobile } from '../../RossAscends-mods.js';1import { isMobile } from '../../RossAscends-mods.js';
2import { getPreviewString } from './index.js';2import { getPreviewString } from './index.js';
3import { talkingAnimation } from './index.js';
4import { saveTtsProviderSettings } from './index.js';3import { saveTtsProviderSettings } from './index.js';
5export { SystemTtsProvider };4export { SystemTtsProvider };
65
@@ -70,7 +69,6 @@ var speechUtteranceChunker = function (utt, settings, callback) {
70 //placing the speak invocation inside a callback fixes ordering and onend issues.69 //placing the speak invocation inside a callback fixes ordering and onend issues.
71 setTimeout(function () {70 setTimeout(function () {
72 speechSynthesis.speak(newUtt);71 speechSynthesis.speak(newUtt);
73 talkingAnimation(true);
74 }, 0);72 }, 0);
75};73};
7674
@@ -240,7 +238,6 @@ class SystemTtsProvider {
240 //some code to execute when done238 //some code to execute when done
241 resolve(silence);239 resolve(silence);
242 console.log('System TTS done');240 console.log('System TTS done');
243 talkingAnimation(false);
244 });241 });
245 });242 });
246 }243 }
public/scripts/extensions/vectors/index.js+60 -68
@@ -561,9 +561,9 @@ async function retrieveFileChunks(queryText, collectionId) {
561 */561 */
562async function vectorizeFile(fileText, fileName, collectionId, chunkSize, overlapPercent) {562async function vectorizeFile(fileText, fileName, collectionId, chunkSize, overlapPercent) {
563 try {563 try {
564 if (settings.translate_files && typeof window['translate'] === 'function') {564 if (settings.translate_files && typeof globalThis.translate === 'function') {
565 console.log(`Vectors: Translating file ${fileName} to English...`);565 console.log(`Vectors: Translating file ${fileName} to English...`);
566 const translatedText = await window['translate'](fileText, 'en');566 const translatedText = await globalThis.translate(fileText, 'en');
567 fileText = translatedText;567 fileText = translatedText;
568 }568 }
569569
@@ -746,74 +746,65 @@ async function getQueryText(chat, initiator) {
746}746}
747747
748/**748/**
749 * Gets the saved hashes for a collection749 * Gets common body parameters for vector requests.
750* @param {string} collectionId750 * @returns {object}
751* @returns {Promise<number[]>} Saved hashes
752 */751 */
753async function getSavedHashes(collectionId) {752function getVectorsRequestBody() {
754 const response = await fetch('/api/vector/list', {753 const body = {};
755 method: 'POST',
756 headers: getVectorHeaders(),
757 body: JSON.stringify({
758 collectionId: collectionId,
759 source: settings.source,
760 }),
761 });
762
763 if (!response.ok) {
764 throw new Error(`Failed to get saved hashes for collection ${collectionId}`);
765 }
766
767 const hashes = await response.json();
768 return hashes;
769}
770
771function getVectorHeaders() {
772 const headers = getRequestHeaders();
773 switch (settings.source) {754 switch (settings.source) {
774 case 'extras':755 case 'extras':
775 Object.assign(headers, {756 body.extrasUrl = extension_settings.apiUrl;
776 'X-Extras-Url': extension_settings.apiUrl,757 body.extrasKey = extension_settings.apiKey;
777 'X-Extras-Key': extension_settings.apiKey,
778 });
779 break;758 break;
780 case 'togetherai':759 case 'togetherai':
781 Object.assign(headers, {760 body.model = extension_settings.vectors.togetherai_model;
782 'X-Togetherai-Model': extension_settings.vectors.togetherai_model,
783 });
784 break;761 break;
785 case 'openai':762 case 'openai':
786 Object.assign(headers, {763 body.model = extension_settings.vectors.openai_model;
787 'X-OpenAI-Model': extension_settings.vectors.openai_model,
788 });
789 break;764 break;
790 case 'cohere':765 case 'cohere':
791 Object.assign(headers, {766 body.model = extension_settings.vectors.cohere_model;
792 'X-Cohere-Model': extension_settings.vectors.cohere_model,
793 });
794 break;767 break;
795 case 'ollama':768 case 'ollama':
796 Object.assign(headers, {769 body.model = extension_settings.vectors.ollama_model;
797 'X-Ollama-Model': extension_settings.vectors.ollama_model,770 body.apiUrl = textgenerationwebui_settings.server_urls[textgen_types.OLLAMA];
798 'X-Ollama-URL': textgenerationwebui_settings.server_urls[textgen_types.OLLAMA],771 body.keep = !!extension_settings.vectors.ollama_keep;
799 'X-Ollama-Keep': !!extension_settings.vectors.ollama_keep,
800 });
801 break;772 break;
802 case 'llamacpp':773 case 'llamacpp':
803 Object.assign(headers, {774 body.apiUrl = textgenerationwebui_settings.server_urls[textgen_types.LLAMACPP];
804 'X-LlamaCpp-URL': textgenerationwebui_settings.server_urls[textgen_types.LLAMACPP],
805 });
806 break;775 break;
807 case 'vllm':776 case 'vllm':
808 Object.assign(headers, {777 body.apiUrl = textgenerationwebui_settings.server_urls[textgen_types.VLLM];
809 'X-Vllm-URL': textgenerationwebui_settings.server_urls[textgen_types.VLLM],778 body.model = extension_settings.vectors.vllm_model;
810 'X-Vllm-Model': extension_settings.vectors.vllm_model,
811 });
812 break;779 break;
813 default:780 default:
814 break;781 break;
815 }782 }
816 return headers;783 return body;
784}
785
786/**
787 * Gets the saved hashes for a collection
788* @param {string} collectionId
789* @returns {Promise<number[]>} Saved hashes
790*/
791async function getSavedHashes(collectionId) {
792 const response = await fetch('/api/vector/list', {
793 method: 'POST',
794 headers: getRequestHeaders(),
795 body: JSON.stringify({
796 ...getVectorsRequestBody(),
797 collectionId: collectionId,
798 source: settings.source,
799 }),
800 });
801
802 if (!response.ok) {
803 throw new Error(`Failed to get saved hashes for collection ${collectionId}`);
804 }
805
806 const hashes = await response.json();
807 return hashes;
817}808}
818809
819/**810/**
@@ -825,12 +816,11 @@ function getVectorHeaders() {
825async function insertVectorItems(collectionId, items) {816async function insertVectorItems(collectionId, items) {
826 throwIfSourceInvalid();817 throwIfSourceInvalid();
827818
828 const headers = getVectorHeaders();
829
830 const response = await fetch('/api/vector/insert', {819 const response = await fetch('/api/vector/insert', {
831 method: 'POST',820 method: 'POST',
832 headers: headers,821 headers: getRequestHeaders(),
833 body: JSON.stringify({822 body: JSON.stringify({
823 ...getVectorsRequestBody(),
834 collectionId: collectionId,824 collectionId: collectionId,
835 items: items,825 items: items,
836 source: settings.source,826 source: settings.source,
@@ -879,8 +869,9 @@ function throwIfSourceInvalid() {
879async function deleteVectorItems(collectionId, hashes) {869async function deleteVectorItems(collectionId, hashes) {
880 const response = await fetch('/api/vector/delete', {870 const response = await fetch('/api/vector/delete', {
881 method: 'POST',871 method: 'POST',
882 headers: getVectorHeaders(),872 headers: getRequestHeaders(),
883 body: JSON.stringify({873 body: JSON.stringify({
874 ...getVectorsRequestBody(),
884 collectionId: collectionId,875 collectionId: collectionId,
885 hashes: hashes,876 hashes: hashes,
886 source: settings.source,877 source: settings.source,
@@ -899,12 +890,11 @@ async function deleteVectorItems(collectionId, hashes) {
899 * @returns {Promise<{ hashes: number[], metadata: object[]}>} - Hashes of the results890 * @returns {Promise<{ hashes: number[], metadata: object[]}>} - Hashes of the results
900 */891 */
901async function queryCollection(collectionId, searchText, topK) {892async function queryCollection(collectionId, searchText, topK) {
902 const headers = getVectorHeaders();
903
904 const response = await fetch('/api/vector/query', {893 const response = await fetch('/api/vector/query', {
905 method: 'POST',894 method: 'POST',
906 headers: headers,895 headers: getRequestHeaders(),
907 body: JSON.stringify({896 body: JSON.stringify({
897 ...getVectorsRequestBody(),
908 collectionId: collectionId,898 collectionId: collectionId,
909 searchText: searchText,899 searchText: searchText,
910 topK: topK,900 topK: topK,
@@ -929,12 +919,11 @@ async function queryCollection(collectionId, searchText, topK) {
929 * @returns {Promise<Record<string, { hashes: number[], metadata: object[] }>>} - Results mapped to collection IDs919 * @returns {Promise<Record<string, { hashes: number[], metadata: object[] }>>} - Results mapped to collection IDs
930 */920 */
931async function queryMultipleCollections(collectionIds, searchText, topK, threshold) {921async function queryMultipleCollections(collectionIds, searchText, topK, threshold) {
932 const headers = getVectorHeaders();
933
934 const response = await fetch('/api/vector/query-multi', {922 const response = await fetch('/api/vector/query-multi', {
935 method: 'POST',923 method: 'POST',
936 headers: headers,924 headers: getRequestHeaders(),
937 body: JSON.stringify({925 body: JSON.stringify({
926 ...getVectorsRequestBody(),
938 collectionIds: collectionIds,927 collectionIds: collectionIds,
939 searchText: searchText,928 searchText: searchText,
940 topK: topK,929 topK: topK,
@@ -965,8 +954,9 @@ async function purgeFileVectorIndex(fileUrl) {
965954
966 const response = await fetch('/api/vector/purge', {955 const response = await fetch('/api/vector/purge', {
967 method: 'POST',956 method: 'POST',
968 headers: getVectorHeaders(),957 headers: getRequestHeaders(),
969 body: JSON.stringify({958 body: JSON.stringify({
959 ...getVectorsRequestBody(),
970 collectionId: collectionId,960 collectionId: collectionId,
971 }),961 }),
972 });962 });
@@ -994,8 +984,9 @@ async function purgeVectorIndex(collectionId) {
994984
995 const response = await fetch('/api/vector/purge', {985 const response = await fetch('/api/vector/purge', {
996 method: 'POST',986 method: 'POST',
997 headers: getVectorHeaders(),987 headers: getRequestHeaders(),
998 body: JSON.stringify({988 body: JSON.stringify({
989 ...getVectorsRequestBody(),
999 collectionId: collectionId,990 collectionId: collectionId,
1000 }),991 }),
1001 });992 });
@@ -1019,7 +1010,10 @@ async function purgeAllVectorIndexes() {
1019 try {1010 try {
1020 const response = await fetch('/api/vector/purge-all', {1011 const response = await fetch('/api/vector/purge-all', {
1021 method: 'POST',1012 method: 'POST',
1022 headers: getVectorHeaders(),1013 headers: getRequestHeaders(),
1014 body: JSON.stringify({
1015 ...getVectorsRequestBody(),
1016 }),
1023 });1017 });
10241018
1025 if (!response.ok) {1019 if (!response.ok) {
@@ -1638,14 +1632,12 @@ jQuery(async () => {
1638 }1632 }
1639 return textResult;1633 return textResult;
1640 };1634 };
1641
1642 if (args.return === 'chunks') {1635 if (args.return === 'chunks') {
1643 return getChunksText();1636 return getChunksText();
1644 }1637 }
16451638
1646 // @ts-ignore1639 // @ts-ignore
1647 return slashCommandReturnHelper.doReturn(args.return ?? 'object', urls, { objectToStringFunc: list => list.join('\n') });1640 return slashCommandReturnHelper.doReturn(args.return ?? 'object', urls, { objectToStringFunc: list => list.join('\n') });
1648
1649 },1641 },
1650 aliases: ['databank-search', 'data-bank-search'],1642 aliases: ['databank-search', 'data-bank-search'],
1651 helpString: 'Search the Data Bank for a specific query using vector similarity. Returns a list of file URLs with the most relevant content.',1643 helpString: 'Search the Data Bank for a specific query using vector similarity. Returns a list of file URLs with the most relevant content.',
@@ -1660,10 +1652,10 @@ jQuery(async () => {
1660 defaultValue: 'object',1652 defaultValue: 'object',
1661 enumList: [1653 enumList: [
1662 new SlashCommandEnumValue('chunks', 'Return the actual content chunks', enumTypes.enum, '{}'),1654 new SlashCommandEnumValue('chunks', 'Return the actual content chunks', enumTypes.enum, '{}'),
1663 ...slashCommandReturnHelper.enumList({ allowObject: true })1655 ...slashCommandReturnHelper.enumList({ allowObject: true }),
1664 ],1656 ],
1665 forceEnum: true,1657 forceEnum: true,
1666 })1658 }),
1667 ],1659 ],
1668 unnamedArgumentList: [1660 unnamedArgumentList: [
1669 new SlashCommandArgument('Query to search by.', ARGUMENT_TYPE.STRING, true, false),1661 new SlashCommandArgument('Query to search by.', ARGUMENT_TYPE.STRING, true, false),
public/scripts/extensions/vectors/settings.html+1 -1
@@ -11,7 +11,7 @@
11 </label>11 </label>
12 <select id="vectors_source" class="text_pole">12 <select id="vectors_source" class="text_pole">
13 <option value="cohere">Cohere</option>13 <option value="cohere">Cohere</option>
14 <option value="extras">Extras</option>14 <option value="extras">Extras (deprecated)</option>
15 <option value="palm">Google AI Studio</option>15 <option value="palm">Google AI Studio</option>
16 <option value="llamacpp">llama.cpp</option>16 <option value="llamacpp">llama.cpp</option>
17 <option value="transformers" data-i18n="Local (Transformers)">Local (Transformers)</option>17 <option value="transformers" data-i18n="Local (Transformers)">Local (Transformers)</option>
public/scripts/f-localStorage.js+15 -0
@@ -1,18 +1,30 @@
1////////////////// LOCAL STORAGE HANDLING /////////////////////1////////////////// LOCAL STORAGE HANDLING /////////////////////
22
3/**
4 * @deprecated THIS FUNCTION IS OBSOLETE. DO NOT USE
5 */
3export function SaveLocal(target, val) {6export function SaveLocal(target, val) {
4 localStorage.setItem(target, val);7 localStorage.setItem(target, val);
5 console.debug('SaveLocal -- ' + target + ' : ' + val);8 console.debug('SaveLocal -- ' + target + ' : ' + val);
6}9}
10/**
11 * @deprecated THIS FUNCTION IS OBSOLETE. DO NOT USE
12 */
7export function LoadLocal(target) {13export function LoadLocal(target) {
8 console.debug('LoadLocal -- ' + target);14 console.debug('LoadLocal -- ' + target);
9 return localStorage.getItem(target);15 return localStorage.getItem(target);
1016
11}17}
18/**
19 * @deprecated THIS FUNCTION IS OBSOLETE. DO NOT USE
20 */
12export function LoadLocalBool(target) {21export function LoadLocalBool(target) {
13 let result = localStorage.getItem(target) === 'true';22 let result = localStorage.getItem(target) === 'true';
14 return result;23 return result;
15}24}
25/**
26 * @deprecated THIS FUNCTION IS OBSOLETE. DO NOT USE
27 */
16export function CheckLocal() {28export function CheckLocal() {
17 console.log('----------local storage---------');29 console.log('----------local storage---------');
18 var i;30 var i;
@@ -22,6 +34,9 @@ export function CheckLocal() {
22 console.log('------------------------------');34 console.log('------------------------------');
23}35}
2436
37/**
38 * @deprecated THIS FUNCTION IS OBSOLETE. DO NOT USE
39 */
25export function ClearLocal() { localStorage.clear(); console.log('Removed All Local Storage'); }40export function ClearLocal() { localStorage.clear(); console.log('Removed All Local Storage'); }
2641
27/////////////////////////////////////////////////////////////////////////42/////////////////////////////////////////////////////////////////////////
public/scripts/group-chats.js+37 -39
@@ -78,6 +78,7 @@ import { FILTER_TYPES, FilterHelper } from './filters.js';
78import { isExternalMediaAllowed } from './chats.js';78import { isExternalMediaAllowed } from './chats.js';
79import { POPUP_TYPE, Popup, callGenericPopup } from './popup.js';79import { POPUP_TYPE, Popup, callGenericPopup } from './popup.js';
80import { t } from './i18n.js';80import { t } from './i18n.js';
81import { accountStorage } from './util/AccountStorage.js';
8182
82export {83export {
83 selected_group,84 selected_group,
@@ -292,10 +293,11 @@ export function getGroupNames() {
292293
293/**294/**
294 * Finds the character ID for a group member.295 * Finds the character ID for a group member.
295 * @param {string} arg 0-based member index or character name296 * @param {number|string} arg 0-based member index or character name
296 * @returns {number} 0-based character ID297 * @param {Boolean} full Whether to return a key-value object containing extra data
298 * @returns {number|Object} 0-based character ID or key-value object if full is true
297 */299 */
298export function findGroupMemberId(arg) {300export function findGroupMemberId(arg, full = false) {
299 arg = arg?.trim();301 arg = arg?.trim();
300302
301 if (!arg) {303 if (!arg) {
@@ -311,15 +313,19 @@ export function findGroupMemberId(arg) {
311 }313 }
312314
313 const index = parseInt(arg);315 const index = parseInt(arg);
314 const searchByName = isNaN(index);316 const searchByString = isNaN(index);
315317
316 if (searchByName) {318 if (searchByString) {
317 const memberNames = group.members.map(x => ({ name: characters.find(y => y.avatar === x)?.name, index: characters.findIndex(y => y.avatar === x) }));319 const memberNames = group.members.map(x => ({
318 const fuse = new Fuse(memberNames, { keys: ['name'] });320 avatar: x,
321 name: characters.find(y => y.avatar === x)?.name,
322 index: characters.findIndex(y => y.avatar === x),
323 }));
324 const fuse = new Fuse(memberNames, { keys: ['avatar', 'name'] });
319 const result = fuse.search(arg);325 const result = fuse.search(arg);
320326
321 if (!result.length) {327 if (!result.length) {
322 console.warn(`WARN: No group member found with name ${arg}`);328 console.warn(`WARN: No group member found using string ${arg}`);
323 return;329 return;
324 }330 }
325331
@@ -330,9 +336,11 @@ export function findGroupMemberId(arg) {
330 return;336 return;
331 }337 }
332338
333 console.log(`Triggering group member ${chid} (${arg}) from search result`, result[0]);339 console.log(`Targeting group member ${chid} (${arg}) from search result`, result[0]);
334 return chid;340
335 } else {341 return !full ? chid : { ...{ id: chid }, ...result[0].item };
342 }
343 else {
336 const memberAvatar = group.members[index];344 const memberAvatar = group.members[index];
337345
338 if (memberAvatar === undefined) {346 if (memberAvatar === undefined) {
@@ -347,8 +355,14 @@ export function findGroupMemberId(arg) {
347 return;355 return;
348 }356 }
349357
350 console.log(`Triggering group member ${memberAvatar} at index ${index}`);358 console.log(`Targeting group member ${memberAvatar} at index ${index}`);
351 return chid;359
360 return !full ? chid : {
361 id: chid,
362 avatar: memberAvatar,
363 name: characters.find(y => y.avatar === memberAvatar)?.name,
364 index: index,
365 };
352 }366 }
353}367}
354368
@@ -805,7 +819,6 @@ async function generateGroupWrapper(by_auto_mode, type = null, params = {}) {
805819
806 /** @type {any} Caution: JS war crimes ahead */820 /** @type {any} Caution: JS war crimes ahead */
807 let textResult = '';821 let textResult = '';
808 let typingIndicator = $('#chat .typing_indicator');
809 const group = groups.find((x) => x.id === selected_group);822 const group = groups.find((x) => x.id === selected_group);
810823
811 if (!group || !Array.isArray(group.members) || !group.members.length) {824 if (!group || !Array.isArray(group.members) || !group.members.length) {
@@ -821,14 +834,6 @@ async function generateGroupWrapper(by_auto_mode, type = null, params = {}) {
821 setCharacterId(undefined);834 setCharacterId(undefined);
822 const userInput = String($('#send_textarea').val());835 const userInput = String($('#send_textarea').val());
823836
824 if (typingIndicator.length === 0 && !isStreamingEnabled()) {
825 typingIndicator = $(
826 '#typing_indicator_template .typing_indicator',
827 ).clone();
828 typingIndicator.hide();
829 $('#chat').append(typingIndicator);
830 }
831
832 // id of this specific batch for regeneration purposes837 // id of this specific batch for regeneration purposes
833 group_generation_id = Date.now();838 group_generation_id = Date.now();
834 const lastMessage = chat[chat.length - 1];839 const lastMessage = chat[chat.length - 1];
@@ -906,14 +911,6 @@ async function generateGroupWrapper(by_auto_mode, type = null, params = {}) {
906 }911 }
907 await eventSource.emit(event_types.GROUP_MEMBER_DRAFTED, chId);912 await eventSource.emit(event_types.GROUP_MEMBER_DRAFTED, chId);
908913
909 if (type !== 'swipe' && type !== 'impersonate' && !isStreamingEnabled()) {
910 // update indicator and scroll down
911 typingIndicator
912 .find('.typing_indicator_name')
913 .text(characters[chId].name);
914 typingIndicator.show();
915 }
916
917 // Wait for generation to finish914 // Wait for generation to finish
918 textResult = await Generate(generateType, { automatic_trigger: by_auto_mode, ...(params || {}) });915 textResult = await Generate(generateType, { automatic_trigger: by_auto_mode, ...(params || {}) });
919 let messageChunk = textResult?.messageChunk;916 let messageChunk = textResult?.messageChunk;
@@ -930,8 +927,6 @@ async function generateGroupWrapper(by_auto_mode, type = null, params = {}) {
930 }927 }
931 }928 }
932 } finally {929 } finally {
933 typingIndicator.hide();
934
935 is_group_generating = false;930 is_group_generating = false;
936 setSendButtonState(false);931 setSendButtonState(false);
937 setCharacterId(undefined);932 setCharacterId(undefined);
@@ -1315,10 +1310,10 @@ function printGroupCandidates() {
1315 formatNavigator: PAGINATION_TEMPLATE,1310 formatNavigator: PAGINATION_TEMPLATE,
1316 showNavigator: true,1311 showNavigator: true,
1317 showSizeChanger: true,1312 showSizeChanger: true,
1318 pageSize: Number(localStorage.getItem(storageKey)) || 5,1313 pageSize: Number(accountStorage.getItem(storageKey)) || 5,
1319 sizeChangerOptions: [5, 10, 25, 50, 100, 200, 500, 1000],1314 sizeChangerOptions: [5, 10, 25, 50, 100, 200, 500, 1000],
1320 afterSizeSelectorChange: function (e) {1315 afterSizeSelectorChange: function (e) {
1321 localStorage.setItem(storageKey, e.target.value);1316 accountStorage.setItem(storageKey, e.target.value);
1322 },1317 },
1323 callback: function (data) {1318 callback: function (data) {
1324 $('#rm_group_add_members').empty();1319 $('#rm_group_add_members').empty();
@@ -1342,10 +1337,10 @@ function printGroupMembers() {
1342 formatNavigator: PAGINATION_TEMPLATE,1337 formatNavigator: PAGINATION_TEMPLATE,
1343 showNavigator: true,1338 showNavigator: true,
1344 showSizeChanger: true,1339 showSizeChanger: true,
1345 pageSize: Number(localStorage.getItem(storageKey)) || 5,1340 pageSize: Number(accountStorage.getItem(storageKey)) || 5,
1346 sizeChangerOptions: [5, 10, 25, 50, 100, 200, 500, 1000],1341 sizeChangerOptions: [5, 10, 25, 50, 100, 200, 500, 1000],
1347 afterSizeSelectorChange: function (e) {1342 afterSizeSelectorChange: function (e) {
1348 localStorage.setItem(storageKey, e.target.value);1343 accountStorage.setItem(storageKey, e.target.value);
1349 },1344 },
1350 callback: function (data) {1345 callback: function (data) {
1351 $('.rm_group_members').empty();1346 $('.rm_group_members').empty();
@@ -1669,12 +1664,12 @@ function updateFavButtonState(state) {
1669export async function openGroupById(groupId) {1664export async function openGroupById(groupId) {
1670 if (isChatSaving) {1665 if (isChatSaving) {
1671 toastr.info(t`Please wait until the chat is saved before switching characters.`, t`Your chat is still saving...`);1666 toastr.info(t`Please wait until the chat is saved before switching characters.`, t`Your chat is still saving...`);
1672 return;1667 return false;
1673 }1668 }
16741669
1675 if (!groups.find(x => x.id === groupId)) {1670 if (!groups.find(x => x.id === groupId)) {
1676 console.log('Group not found', groupId);1671 console.log('Group not found', groupId);
1677 return;1672 return false;
1678 }1673 }
16791674
1680 if (!is_send_press && !is_group_generating) {1675 if (!is_send_press && !is_group_generating) {
@@ -1691,8 +1686,11 @@ export async function openGroupById(groupId) {
1691 updateChatMetadata({}, true);1686 updateChatMetadata({}, true);
1692 chat.length = 0;1687 chat.length = 0;
1693 await getGroupChat(groupId);1688 await getGroupChat(groupId);
1689 return true;
1694 }1690 }
1695 }1691 }
1692
1693 return false;
1696}1694}
16971695
1698function openCharacterDefinition(characterSelect) {1696function openCharacterDefinition(characterSelect) {
public/scripts/loader.js+27 -6
@@ -27,21 +27,42 @@ export async function hideLoader() {
27 }27 }
2828
29 return new Promise((resolve) => {29 return new Promise((resolve) => {
30 // Spinner blurs/fades out30 const spinner = $('#load-spinner');
31 $('#load-spinner').on('transitionend webkitTransitionEnd oTransitionEnd MSTransitionEnd', function () {31 if (!spinner.length) {
32 console.warn('Spinner element not found, skipping animation');
33 cleanup();
34 return;
35 }
36
37 // Check if transitions are enabled
38 const transitionDuration = spinner[0] ? getComputedStyle(spinner[0]).transitionDuration : '0s';
39 const hasTransitions = parseFloat(transitionDuration) > 0;
40
41 if (hasTransitions) {
42 Promise.race([
43 new Promise((r) => setTimeout(r, 500)), // Fallback timeout
44 new Promise((r) => spinner.one('transitionend webkitTransitionEnd oTransitionEnd MSTransitionEnd', r)),
45 ]).finally(cleanup);
46 } else {
47 cleanup();
48 }
49
50 function cleanup() {
32 $('#loader').remove();51 $('#loader').remove();
33 // Yoink preloader entirely; it only exists to cover up unstyled content while loading JS52 // Yoink preloader entirely; it only exists to cover up unstyled content while loading JS
34 // If it's present, we remove it once and then it's gone.53 // If it's present, we remove it once and then it's gone.
35 yoinkPreloader();54 yoinkPreloader();
3655
37 loaderPopup.complete(POPUP_RESULT.AFFIRMATIVE).then(() => {56 loaderPopup.complete(POPUP_RESULT.AFFIRMATIVE)
57 .catch((err) => console.error('Error completing loaderPopup:', err))
58 .finally(() => {
38 loaderPopup = null;59 loaderPopup = null;
39 resolve();60 resolve();
40 });61 });
41 });62 }
4263
43 $('#load-spinner')64 // Apply the styles
44 .css({65 spinner.css({
45 'filter': 'blur(15px)',66 'filter': 'blur(15px)',
46 'opacity': '0',67 'opacity': '0',
47 });68 });
public/scripts/openai.js+96 -62
@@ -73,6 +73,7 @@ import { SlashCommandEnumValue } from './slash-commands/SlashCommandEnumValue.js
73import { Popup, POPUP_RESULT } from './popup.js';73import { Popup, POPUP_RESULT } from './popup.js';
74import { t } from './i18n.js';74import { t } from './i18n.js';
75import { ToolManager } from './tool-calling.js';75import { ToolManager } from './tool-calling.js';
76import { accountStorage } from './util/AccountStorage.js';
7677
77export {78export {
78 openai_messages_count,79 openai_messages_count,
@@ -82,7 +83,6 @@ export {
82 setOpenAIMessageExamples,83 setOpenAIMessageExamples,
83 setupChatCompletionPromptManager,84 setupChatCompletionPromptManager,
84 sendOpenAIRequest,85 sendOpenAIRequest,
85 getChatCompletionModel,
86 TokenHandler,86 TokenHandler,
87 IdentifierNotFoundError,87 IdentifierNotFoundError,
88 Message,88 Message,
@@ -258,8 +258,8 @@ const default_settings = {
258 ai21_model: 'jamba-1.5-large',258 ai21_model: 'jamba-1.5-large',
259 mistralai_model: 'mistral-large-latest',259 mistralai_model: 'mistral-large-latest',
260 cohere_model: 'command-r-plus',260 cohere_model: 'command-r-plus',
261 perplexity_model: 'llama-3.1-70b-instruct',261 perplexity_model: 'sonar-pro',
262 groq_model: 'llama-3.1-70b-versatile',262 groq_model: 'llama-3.3-70b-versatile',
263 nanogpt_model: 'gpt-4o-mini',263 nanogpt_model: 'gpt-4o-mini',
264 zerooneai_model: 'yi-large',264 zerooneai_model: 'yi-large',
265 blockentropy_model: 'be-70b-base-llama3.1',265 blockentropy_model: 'be-70b-base-llama3.1',
@@ -298,7 +298,8 @@ const default_settings = {
298 names_behavior: character_names_behavior.DEFAULT,298 names_behavior: character_names_behavior.DEFAULT,
299 continue_postfix: continue_postfix_types.SPACE,299 continue_postfix: continue_postfix_types.SPACE,
300 custom_prompt_post_processing: custom_prompt_post_processing_types.NONE,300 custom_prompt_post_processing: custom_prompt_post_processing_types.NONE,
301 show_thoughts: false,301 show_thoughts: true,
302 reasoning_effort: 'medium',
302 seed: -1,303 seed: -1,
303 n: 1,304 n: 1,
304};305};
@@ -337,7 +338,7 @@ const oai_settings = {
337 ai21_model: 'jamba-1.5-large',338 ai21_model: 'jamba-1.5-large',
338 mistralai_model: 'mistral-large-latest',339 mistralai_model: 'mistral-large-latest',
339 cohere_model: 'command-r-plus',340 cohere_model: 'command-r-plus',
340 perplexity_model: 'llama-3.1-70b-instruct',341 perplexity_model: 'sonar-pro',
341 groq_model: 'llama-3.1-70b-versatile',342 groq_model: 'llama-3.1-70b-versatile',
342 nanogpt_model: 'gpt-4o-mini',343 nanogpt_model: 'gpt-4o-mini',
343 zerooneai_model: 'yi-large',344 zerooneai_model: 'yi-large',
@@ -377,7 +378,8 @@ const oai_settings = {
377 names_behavior: character_names_behavior.DEFAULT,378 names_behavior: character_names_behavior.DEFAULT,
378 continue_postfix: continue_postfix_types.SPACE,379 continue_postfix: continue_postfix_types.SPACE,
379 custom_prompt_post_processing: custom_prompt_post_processing_types.NONE,380 custom_prompt_post_processing: custom_prompt_post_processing_types.NONE,
380 show_thoughts: false,381 show_thoughts: true,
382 reasoning_effort: 'medium',
381 seed: -1,383 seed: -1,
382 n: 1,384 n: 1,
383};385};
@@ -412,7 +414,7 @@ async function validateReverseProxy() {
412 throw err;414 throw err;
413 }415 }
414 const rememberKey = `Proxy_SkipConfirm_${getStringHash(oai_settings.reverse_proxy)}`;416 const rememberKey = `Proxy_SkipConfirm_${getStringHash(oai_settings.reverse_proxy)}`;
415 const skipConfirm = localStorage.getItem(rememberKey) === 'true';417 const skipConfirm = accountStorage.getItem(rememberKey) === 'true';
416418
417 const confirmation = skipConfirm || await Popup.show.confirm(t`Connecting To Proxy`, await renderTemplateAsync('proxyConnectionWarning', { proxyURL: DOMPurify.sanitize(oai_settings.reverse_proxy) }));419 const confirmation = skipConfirm || await Popup.show.confirm(t`Connecting To Proxy`, await renderTemplateAsync('proxyConnectionWarning', { proxyURL: DOMPurify.sanitize(oai_settings.reverse_proxy) }));
418420
@@ -423,7 +425,7 @@ async function validateReverseProxy() {
423 throw new Error('Proxy connection denied.');425 throw new Error('Proxy connection denied.');
424 }426 }
425427
426 localStorage.setItem(rememberKey, String(true));428 accountStorage.setItem(rememberKey, String(true));
427}429}
428430
429/**431/**
@@ -1443,9 +1445,7 @@ async function sendWindowAIRequest(messages, signal, stream) {
1443 }1445 }
14441446
1445 const onStreamResult = (res, err) => {1447 const onStreamResult = (res, err) => {
1446 if (err) {1448 if (err) return;
1447 return;
1448 }
14491449
1450 const thisContent = res?.message?.content;1450 const thisContent = res?.message?.content;
14511451
@@ -1497,7 +1497,7 @@ async function sendWindowAIRequest(messages, signal, stream) {
1497 }1497 }
1498}1498}
14991499
1500function getChatCompletionModel() {1500export function getChatCompletionModel() {
1501 switch (oai_settings.chat_completion_source) {1501 switch (oai_settings.chat_completion_source) {
1502 case chat_completion_sources.CLAUDE:1502 case chat_completion_sources.CLAUDE:
1503 return oai_settings.claude_model;1503 return oai_settings.claude_model;
@@ -1869,7 +1869,7 @@ async function sendOpenAIRequest(type, messages, signal) {
1869 const isQuiet = type === 'quiet';1869 const isQuiet = type === 'quiet';
1870 const isImpersonate = type === 'impersonate';1870 const isImpersonate = type === 'impersonate';
1871 const isContinue = type === 'continue';1871 const isContinue = type === 'continue';
1872 const stream = oai_settings.stream_openai && !isQuiet && !isScale && !(isGoogle && oai_settings.google_model.includes('bison')) && !(isOAI && oai_settings.openai_model.startsWith('o1-'));1872 const stream = oai_settings.stream_openai && !isQuiet && !isScale && !(isOAI && ['o1-2024-12-17', 'o1'].includes(oai_settings.openai_model));
1873 const useLogprobs = !!power_user.request_token_probabilities;1873 const useLogprobs = !!power_user.request_token_probabilities;
1874 const canMultiSwipe = oai_settings.n > 1 && !isContinue && !isImpersonate && !isQuiet && (isOAI || isCustom);1874 const canMultiSwipe = oai_settings.n > 1 && !isContinue && !isImpersonate && !isQuiet && (isOAI || isCustom);
18751875
@@ -1913,9 +1913,14 @@ async function sendOpenAIRequest(type, messages, signal) {
1913 'user_name': name1,1913 'user_name': name1,
1914 'char_name': name2,1914 'char_name': name2,
1915 'group_names': getGroupNames(),1915 'group_names': getGroupNames(),
1916 'show_thoughts': Boolean(oai_settings.show_thoughts),1916 'include_reasoning': Boolean(oai_settings.show_thoughts),
1917 'reasoning_effort': String(oai_settings.reasoning_effort),
1917 };1918 };
19181919
1920 if (!canMultiSwipe && ToolManager.canPerformToolCalls(type)) {
1921 await ToolManager.registerFunctionToolsOpenAI(generate_data);
1922 }
1923
1919 // Empty array will produce a validation error1924 // Empty array will produce a validation error
1920 if (!Array.isArray(generate_data.stop) || !generate_data.stop.length) {1925 if (!Array.isArray(generate_data.stop) || !generate_data.stop.length) {
1921 delete generate_data.stop;1926 delete generate_data.stop;
@@ -2039,6 +2044,8 @@ async function sendOpenAIRequest(type, messages, signal) {
2039 delete generate_data.top_logprobs;2044 delete generate_data.top_logprobs;
2040 delete generate_data.logprobs;2045 delete generate_data.logprobs;
2041 delete generate_data.logit_bias;2046 delete generate_data.logit_bias;
2047 delete generate_data.tools;
2048 delete generate_data.tool_choice;
2042 }2049 }
2043 }2050 }
20442051
@@ -2046,11 +2053,7 @@ async function sendOpenAIRequest(type, messages, signal) {
2046 generate_data['seed'] = oai_settings.seed;2053 generate_data['seed'] = oai_settings.seed;
2047 }2054 }
20482055
2049 if (!canMultiSwipe && ToolManager.canPerformToolCalls(type)) {2056 if (isOAI && (oai_settings.openai_model.startsWith('o1') || oai_settings.openai_model.startsWith('o3'))) {
2050 await ToolManager.registerFunctionToolsOpenAI(generate_data);
2051 }
2052
2053 if (isOAI && oai_settings.openai_model.startsWith('o1-')) {
2054 generate_data.messages.forEach((msg) => {2057 generate_data.messages.forEach((msg) => {
2055 if (msg.role === 'system') {2058 if (msg.role === 'system') {
2056 msg.role = 'user';2059 msg.role = 'user';
@@ -2058,7 +2061,6 @@ async function sendOpenAIRequest(type, messages, signal) {
2058 });2061 });
2059 generate_data.max_completion_tokens = generate_data.max_tokens;2062 generate_data.max_completion_tokens = generate_data.max_tokens;
2060 delete generate_data.max_tokens;2063 delete generate_data.max_tokens;
2061 delete generate_data.stream;
2062 delete generate_data.logprobs;2064 delete generate_data.logprobs;
2063 delete generate_data.top_logprobs;2065 delete generate_data.top_logprobs;
2064 delete generate_data.n;2066 delete generate_data.n;
@@ -2069,8 +2071,7 @@ async function sendOpenAIRequest(type, messages, signal) {
2069 delete generate_data.tools;2071 delete generate_data.tools;
2070 delete generate_data.tool_choice;2072 delete generate_data.tool_choice;
2071 delete generate_data.stop;2073 delete generate_data.stop;
2072 // It does support logit_bias, but the tokenizer used and its effect is yet unknown.2074 delete generate_data.logit_bias;
2073 // delete generate_data.logit_bias;
2074 }2075 }
20752076
2076 await eventSource.emit(event_types.CHAT_COMPLETION_SETTINGS_READY, generate_data);2077 await eventSource.emit(event_types.CHAT_COMPLETION_SETTINGS_READY, generate_data);
@@ -2166,6 +2167,14 @@ function getStreamingReply(data, state) {
2166 state.reasoning += (data.choices?.filter(x => x?.delta?.reasoning)?.[0]?.delta?.reasoning || '');2167 state.reasoning += (data.choices?.filter(x => x?.delta?.reasoning)?.[0]?.delta?.reasoning || '');
2167 }2168 }
2168 return data.choices?.[0]?.delta?.content ?? data.choices?.[0]?.message?.content ?? data.choices?.[0]?.text ?? '';2169 return data.choices?.[0]?.delta?.content ?? data.choices?.[0]?.message?.content ?? data.choices?.[0]?.text ?? '';
2170 } else if (oai_settings.chat_completion_source === chat_completion_sources.CUSTOM) {
2171 if (oai_settings.show_thoughts) {
2172 state.reasoning +=
2173 data.choices?.filter(x => x?.delta?.reasoning_content)?.[0]?.delta?.reasoning_content ??
2174 data.choices?.filter(x => x?.delta?.reasoning)?.[0]?.delta?.reasoning ??
2175 '';
2176 }
2177 return data.choices?.[0]?.delta?.content ?? data.choices?.[0]?.message?.content ?? data.choices?.[0]?.text ?? '';
2169 } else {2178 } else {
2170 return data.choices?.[0]?.delta?.content ?? data.choices?.[0]?.message?.content ?? data.choices?.[0]?.text ?? '';2179 return data.choices?.[0]?.delta?.content ?? data.choices?.[0]?.message?.content ?? data.choices?.[0]?.text ?? '';
2171 }2180 }
@@ -3124,6 +3133,7 @@ function loadOpenAISettings(data, settings) {
3124 oai_settings.inline_image_quality = settings.inline_image_quality ?? default_settings.inline_image_quality;3133 oai_settings.inline_image_quality = settings.inline_image_quality ?? default_settings.inline_image_quality;
3125 oai_settings.bypass_status_check = settings.bypass_status_check ?? default_settings.bypass_status_check;3134 oai_settings.bypass_status_check = settings.bypass_status_check ?? default_settings.bypass_status_check;
3126 oai_settings.show_thoughts = settings.show_thoughts ?? default_settings.show_thoughts;3135 oai_settings.show_thoughts = settings.show_thoughts ?? default_settings.show_thoughts;
3136 oai_settings.reasoning_effort = settings.reasoning_effort ?? default_settings.reasoning_effort;
3127 oai_settings.seed = settings.seed ?? default_settings.seed;3137 oai_settings.seed = settings.seed ?? default_settings.seed;
3128 oai_settings.n = settings.n ?? default_settings.n;3138 oai_settings.n = settings.n ?? default_settings.n;
31293139
@@ -3253,6 +3263,9 @@ function loadOpenAISettings(data, settings) {
3253 $('#n_openai').val(oai_settings.n);3263 $('#n_openai').val(oai_settings.n);
3254 $('#openai_show_thoughts').prop('checked', oai_settings.show_thoughts);3264 $('#openai_show_thoughts').prop('checked', oai_settings.show_thoughts);
32553265
3266 $('#openai_reasoning_effort').val(oai_settings.reasoning_effort);
3267 $(`#openai_reasoning_effort option[value="${oai_settings.reasoning_effort}"]`).prop('selected', true);
3268
3256 if (settings.reverse_proxy !== undefined) oai_settings.reverse_proxy = settings.reverse_proxy;3269 if (settings.reverse_proxy !== undefined) oai_settings.reverse_proxy = settings.reverse_proxy;
3257 $('#openai_reverse_proxy').val(oai_settings.reverse_proxy);3270 $('#openai_reverse_proxy').val(oai_settings.reverse_proxy);
32583271
@@ -3513,6 +3526,7 @@ async function saveOpenAIPreset(name, settings, triggerUi = true) {
3513 continue_postfix: settings.continue_postfix,3526 continue_postfix: settings.continue_postfix,
3514 function_calling: settings.function_calling,3527 function_calling: settings.function_calling,
3515 show_thoughts: settings.show_thoughts,3528 show_thoughts: settings.show_thoughts,
3529 reasoning_effort: settings.reasoning_effort,
3516 seed: settings.seed,3530 seed: settings.seed,
3517 n: settings.n,3531 n: settings.n,
3518 };3532 };
@@ -3971,6 +3985,7 @@ function onSettingsPresetChange() {
3971 continue_postfix: ['#continue_postfix', 'continue_postfix', false],3985 continue_postfix: ['#continue_postfix', 'continue_postfix', false],
3972 function_calling: ['#openai_function_calling', 'function_calling', true],3986 function_calling: ['#openai_function_calling', 'function_calling', true],
3973 show_thoughts: ['#openai_show_thoughts', 'show_thoughts', true],3987 show_thoughts: ['#openai_show_thoughts', 'show_thoughts', true],
3988 reasoning_effort: ['#openai_reasoning_effort', 'reasoning_effort', false],
3974 seed: ['#seed_openai', 'seed', false],3989 seed: ['#seed_openai', 'seed', false],
3975 n: ['#n_openai', 'n', false],3990 n: ['#n_openai', 'n', false],
3976 };3991 };
@@ -4027,7 +4042,7 @@ function getMaxContextOpenAI(value) {
4027 if (oai_settings.max_context_unlocked) {4042 if (oai_settings.max_context_unlocked) {
4028 return unlocked_max;4043 return unlocked_max;
4029 }4044 }
4030 else if (value.startsWith('o1-')) {4045 else if (value.startsWith('o1') || value.startsWith('o3')) {
4031 return max_128k;4046 return max_128k;
4032 }4047 }
4033 else if (value.includes('chatgpt-4o-latest') || value.includes('gpt-4-turbo') || value.includes('gpt-4o') || value.includes('gpt-4-1106') || value.includes('gpt-4-0125') || value.includes('gpt-4-vision')) {4048 else if (value.includes('chatgpt-4o-latest') || value.includes('gpt-4-turbo') || value.includes('gpt-4o') || value.includes('gpt-4-1106') || value.includes('gpt-4-0125') || value.includes('gpt-4-vision')) {
@@ -4100,6 +4115,40 @@ function getMaxContextWindowAI(value) {
4100 }4115 }
4101}4116}
41024117
4118/**
4119 * Get the maximum context size for the Groq model
4120 * @param {string} model Model identifier
4121 * @param {boolean} isUnlocked Whether context limits are unlocked
4122 * @returns {number} Maximum context size in tokens
4123 */
4124function getGroqMaxContext(model, isUnlocked) {
4125 if (isUnlocked) {
4126 return unlocked_max;
4127 }
4128
4129 const contextMap = {
4130 'gemma2-9b-it': max_8k,
4131 'llama-3.3-70b-versatile': max_128k,
4132 'llama-3.1-8b-instant': max_128k,
4133 'llama3-70b-8192': max_8k,
4134 'llama3-8b-8192': max_8k,
4135 'llama-guard-3-8b': max_8k,
4136 'mixtral-8x7b-32768': max_32k,
4137 'deepseek-r1-distill-llama-70b': max_128k,
4138 'llama-3.3-70b-specdec': max_8k,
4139 'llama-3.2-1b-preview': max_128k,
4140 'llama-3.2-3b-preview': max_128k,
4141 'llama-3.2-11b-vision-preview': max_128k,
4142 'llama-3.2-90b-vision-preview': max_128k,
4143 'qwen-2.5-32b': max_128k,
4144 'deepseek-r1-distill-qwen-32b': max_128k,
4145 'deepseek-r1-distill-llama-70b-specdec': max_128k,
4146 };
4147
4148 // Return context size if model found, otherwise default to 128k
4149 return Object.entries(contextMap).find(([key]) => model.includes(key))?.[1] || max_128k;
4150}
4151
4103async function onModelChange() {4152async function onModelChange() {
4104 biasCache = undefined;4153 biasCache = undefined;
4105 let value = String($(this).val() || '');4154 let value = String($(this).val() || '');
@@ -4232,9 +4281,9 @@ async function onModelChange() {
4232 $('#openai_max_context').attr('max', max_2mil);4281 $('#openai_max_context').attr('max', max_2mil);
4233 } else if (value.includes('gemini-exp-1114') || value.includes('gemini-exp-1121') || value.includes('gemini-2.0-flash-thinking-exp-1219')) {4282 } else if (value.includes('gemini-exp-1114') || value.includes('gemini-exp-1121') || value.includes('gemini-2.0-flash-thinking-exp-1219')) {
4234 $('#openai_max_context').attr('max', max_32k);4283 $('#openai_max_context').attr('max', max_32k);
4235 } else if (value.includes('gemini-1.5-pro') || value.includes('gemini-exp-1206')) {4284 } else if (value.includes('gemini-1.5-pro') || value.includes('gemini-exp-1206') || value.includes('gemini-2.0-pro')) {
4236 $('#openai_max_context').attr('max', max_2mil);4285 $('#openai_max_context').attr('max', max_2mil);
4237 } else if (value.includes('gemini-1.5-flash') || value.includes('gemini-2.0-flash-exp') || value.includes('gemini-2.0-flash-thinking-exp')) {4286 } else if (value.includes('gemini-1.5-flash') || value.includes('gemini-2.0-flash')) {
4238 $('#openai_max_context').attr('max', max_1mil);4287 $('#openai_max_context').attr('max', max_1mil);
4239 } else if (value.includes('gemini-1.0-pro') || value === 'gemini-pro') {4288 } else if (value.includes('gemini-1.0-pro') || value === 'gemini-pro') {
4240 $('#openai_max_context').attr('max', max_32k);4289 $('#openai_max_context').attr('max', max_32k);
@@ -4380,28 +4429,19 @@ async function onModelChange() {
4380 if (oai_settings.max_context_unlocked) {4429 if (oai_settings.max_context_unlocked) {
4381 $('#openai_max_context').attr('max', unlocked_max);4430 $('#openai_max_context').attr('max', unlocked_max);
4382 }4431 }
4432 else if (['sonar', 'sonar-reasoning', 'sonar-reasoning-pro', 'r1-1776'].includes(oai_settings.perplexity_model)) {
4433 $('#openai_max_context').attr('max', 127000);
4434 }
4435 else if (['sonar-pro'].includes(oai_settings.perplexity_model)) {
4436 $('#openai_max_context').attr('max', 200000);
4437 }
4383 else if (oai_settings.perplexity_model.includes('llama-3.1')) {4438 else if (oai_settings.perplexity_model.includes('llama-3.1')) {
4384 const isOnline = oai_settings.perplexity_model.includes('online');4439 const isOnline = oai_settings.perplexity_model.includes('online');
4385 const contextSize = isOnline ? 128 * 1024 - 4000 : 128 * 1024;4440 const contextSize = isOnline ? 128 * 1024 - 4000 : 128 * 1024;
4386 $('#openai_max_context').attr('max', contextSize);4441 $('#openai_max_context').attr('max', contextSize);
4387 }4442 }
4388 else if (['llama-3-sonar-small-32k-chat', 'llama-3-sonar-large-32k-chat'].includes(oai_settings.perplexity_model)) {
4389 $('#openai_max_context').attr('max', max_32k);
4390 }
4391 else if (['llama-3-sonar-small-32k-online', 'llama-3-sonar-large-32k-online'].includes(oai_settings.perplexity_model)) {
4392 $('#openai_max_context').attr('max', 28000);
4393 }
4394 else if (['sonar-small-chat', 'sonar-medium-chat', 'codellama-70b-instruct', 'mistral-7b-instruct', 'mixtral-8x7b-instruct', 'mixtral-8x22b-instruct'].includes(oai_settings.perplexity_model)) {
4395 $('#openai_max_context').attr('max', max_16k);
4396 }
4397 else if (['llama-3-8b-instruct', 'llama-3-70b-instruct'].includes(oai_settings.perplexity_model)) {
4398 $('#openai_max_context').attr('max', max_8k);
4399 }
4400 else if (['sonar-small-online', 'sonar-medium-online'].includes(oai_settings.perplexity_model)) {
4401 $('#openai_max_context').attr('max', 12000);
4402 }
4403 else {4443 else {
4404 $('#openai_max_context').attr('max', max_4k);4444 $('#openai_max_context').attr('max', max_128k);
4405 }4445 }
4406 oai_settings.openai_max_context = Math.min(Number($('#openai_max_context').attr('max')), oai_settings.openai_max_context);4446 oai_settings.openai_max_context = Math.min(Number($('#openai_max_context').attr('max')), oai_settings.openai_max_context);
4407 $('#openai_max_context').val(oai_settings.openai_max_context).trigger('input');4447 $('#openai_max_context').val(oai_settings.openai_max_context).trigger('input');
@@ -4410,27 +4450,8 @@ async function onModelChange() {
4410 }4450 }
44114451
4412 if (oai_settings.chat_completion_source == chat_completion_sources.GROQ) {4452 if (oai_settings.chat_completion_source == chat_completion_sources.GROQ) {
4413 if (oai_settings.max_context_unlocked) {4453 const maxContext = getGroqMaxContext(oai_settings.groq_model, oai_settings.max_context_unlocked);
4414 $('#openai_max_context').attr('max', unlocked_max);4454 $('#openai_max_context').attr('max', maxContext);
4415 }
4416 else if (oai_settings.groq_model.includes('llama-3.2') && oai_settings.groq_model.includes('-preview')) {
4417 $('#openai_max_context').attr('max', max_8k);
4418 }
4419 else if (oai_settings.groq_model.includes('llama-3.3') || oai_settings.groq_model.includes('llama-3.2') || oai_settings.groq_model.includes('llama-3.1')) {
4420 $('#openai_max_context').attr('max', max_128k);
4421 }
4422 else if (oai_settings.groq_model.includes('llama3-groq')) {
4423 $('#openai_max_context').attr('max', max_8k);
4424 }
4425 else if (['llama3-8b-8192', 'llama3-70b-8192', 'gemma-7b-it', 'gemma2-9b-it'].includes(oai_settings.groq_model)) {
4426 $('#openai_max_context').attr('max', max_8k);
4427 }
4428 else if (['mixtral-8x7b-32768'].includes(oai_settings.groq_model)) {
4429 $('#openai_max_context').attr('max', max_32k);
4430 }
4431 else {
4432 $('#openai_max_context').attr('max', max_4k);
4433 }
4434 oai_settings.openai_max_context = Math.min(Number($('#openai_max_context').attr('max')), oai_settings.openai_max_context);4455 oai_settings.openai_max_context = Math.min(Number($('#openai_max_context').attr('max')), oai_settings.openai_max_context);
4435 $('#openai_max_context').val(oai_settings.openai_max_context).trigger('input');4456 $('#openai_max_context').val(oai_settings.openai_max_context).trigger('input');
4436 oai_settings.temp_openai = Math.min(oai_max_temp, oai_settings.temp_openai);4457 oai_settings.temp_openai = Math.min(oai_max_temp, oai_settings.temp_openai);
@@ -4930,6 +4951,12 @@ export function isImageInliningSupported() {
4930 // gultra just isn't being offered as multimodal, thanks google.4951 // gultra just isn't being offered as multimodal, thanks google.
4931 const visionSupportedModels = [4952 const visionSupportedModels = [
4932 'gpt-4-vision',4953 'gpt-4-vision',
4954 'gemini-2.0-pro-exp',
4955 'gemini-2.0-pro-exp-02-05',
4956 'gemini-2.0-flash-lite-preview',
4957 'gemini-2.0-flash-lite-preview-02-05',
4958 'gemini-2.0-flash',
4959 'gemini-2.0-flash-001',
4933 'gemini-2.0-flash-thinking-exp-1219',4960 'gemini-2.0-flash-thinking-exp-1219',
4934 'gemini-2.0-flash-thinking-exp-01-21',4961 'gemini-2.0-flash-thinking-exp-01-21',
4935 'gemini-2.0-flash-thinking-exp',4962 'gemini-2.0-flash-thinking-exp',
@@ -4957,6 +4984,8 @@ export function isImageInliningSupported() {
4957 'gpt-4-turbo',4984 'gpt-4-turbo',
4958 'gpt-4o',4985 'gpt-4o',
4959 'gpt-4o-mini',4986 'gpt-4o-mini',
4987 'o1',
4988 'o1-2024-12-17',
4960 'chatgpt-4o-latest',4989 'chatgpt-4o-latest',
4961 'yi-vision',4990 'yi-vision',
4962 'pixtral-latest',4991 'pixtral-latest',
@@ -5515,6 +5544,11 @@ export function initOpenAI() {
5515 saveSettingsDebounced();5544 saveSettingsDebounced();
5516 });5545 });
55175546
5547 $('#openai_reasoning_effort').on('input', function () {
5548 oai_settings.reasoning_effort = String($(this).val());
5549 saveSettingsDebounced();
5550 });
5551
5518 if (!CSS.supports('field-sizing', 'content')) {5552 if (!CSS.supports('field-sizing', 'content')) {
5519 $(document).on('input', '#openai_settings .autoSetHeight', function () {5553 $(document).on('input', '#openai_settings .autoSetHeight', function () {
5520 resetScrollHeight($(this));5554 resetScrollHeight($(this));
public/scripts/personas.js+6 -5
@@ -30,6 +30,7 @@ import { t } from './i18n.js';
30import { openWorldInfoEditor, world_names } from './world-info.js';30import { openWorldInfoEditor, world_names } from './world-info.js';
31import { renderTemplateAsync } from './templates.js';31import { renderTemplateAsync } from './templates.js';
32import { saveMetadataDebounced } from './extensions.js';32import { saveMetadataDebounced } from './extensions.js';
33import { accountStorage } from './util/AccountStorage.js';
3334
34/**35/**
35 * @typedef {object} PersonaConnection A connection between a character and a character or group entity36 * @typedef {object} PersonaConnection A connection between a character and a character or group entity
@@ -67,7 +68,7 @@ export function isPersonaPanelOpen() {
67}68}
6869
69function switchPersonaGridView() {70function switchPersonaGridView() {
70 const state = localStorage.getItem(GRID_STORAGE_KEY) === 'true';71 const state = accountStorage.getItem(GRID_STORAGE_KEY) === 'true';
71 $('#user_avatar_block').toggleClass('gridView', state);72 $('#user_avatar_block').toggleClass('gridView', state);
72}73}
7374
@@ -218,7 +219,7 @@ export async function getUserAvatars(doRender = true, openPageAt = '') {
218219
219 const storageKey = 'Personas_PerPage';220 const storageKey = 'Personas_PerPage';
220 const listId = '#user_avatar_block';221 const listId = '#user_avatar_block';
221 const perPage = Number(localStorage.getItem(storageKey)) || 5;222 const perPage = Number(accountStorage.getItem(storageKey)) || 5;
222223
223 $('#persona_pagination_container').pagination({224 $('#persona_pagination_container').pagination({
224 dataSource: entities,225 dataSource: entities,
@@ -241,7 +242,7 @@ export async function getUserAvatars(doRender = true, openPageAt = '') {
241 updatePersonaUIStates();242 updatePersonaUIStates();
242 },243 },
243 afterSizeSelectorChange: function (e) {244 afterSizeSelectorChange: function (e) {
244 localStorage.setItem(storageKey, e.target.value);245 accountStorage.setItem(storageKey, e.target.value);
245 },246 },
246 afterPaging: function (e) {247 afterPaging: function (e) {
247 savePersonasPage = e;248 savePersonasPage = e;
@@ -1631,8 +1632,8 @@ export function initPersonas() {
1631 saveSettingsDebounced();1632 saveSettingsDebounced();
1632 });1633 });
1633 $('#persona_grid_toggle').on('click', () => {1634 $('#persona_grid_toggle').on('click', () => {
1634 const state = localStorage.getItem(GRID_STORAGE_KEY) === 'true';1635 const state = accountStorage.getItem(GRID_STORAGE_KEY) === 'true';
1635 localStorage.setItem(GRID_STORAGE_KEY, String(!state));1636 accountStorage.setItem(GRID_STORAGE_KEY, String(!state));
1636 switchPersonaGridView();1637 switchPersonaGridView();
1637 });1638 });
16381639
public/scripts/popup.js+12 -1
@@ -24,6 +24,15 @@ export const POPUP_RESULT = {
24 AFFIRMATIVE: 1,24 AFFIRMATIVE: 1,
25 NEGATIVE: 0,25 NEGATIVE: 0,
26 CANCELLED: null,26 CANCELLED: null,
27 CUSTOM1: 1001,
28 CUSTOM2: 1002,
29 CUSTOM3: 1003,
30 CUSTOM4: 1004,
31 CUSTOM5: 1005,
32 CUSTOM6: 1006,
33 CUSTOM7: 1007,
34 CUSTOM8: 1008,
35 CUSTOM9: 1009,
27};36};
2837
29/**38/**
@@ -37,6 +46,7 @@ export const POPUP_RESULT = {
37 * @property {boolean?} [transparent=false] - Whether to display the popup in transparent mode (no background, border, shadow or anything, only its content)46 * @property {boolean?} [transparent=false] - Whether to display the popup in transparent mode (no background, border, shadow or anything, only its content)
38 * @property {boolean?} [allowHorizontalScrolling=false] - Whether to allow horizontal scrolling in the popup47 * @property {boolean?} [allowHorizontalScrolling=false] - Whether to allow horizontal scrolling in the popup
39 * @property {boolean?} [allowVerticalScrolling=false] - Whether to allow vertical scrolling in the popup48 * @property {boolean?} [allowVerticalScrolling=false] - Whether to allow vertical scrolling in the popup
49 * @property {boolean?} [leftAlign=false] - Whether the popup content should be left-aligned by default
40 * @property {'slow'|'fast'|'none'?} [animation='slow'] - Animation speed for the popup (opening, closing, ...)50 * @property {'slow'|'fast'|'none'?} [animation='slow'] - Animation speed for the popup (opening, closing, ...)
41 * @property {POPUP_RESULT|number?} [defaultResult=POPUP_RESULT.AFFIRMATIVE] - The default result of this popup when Enter is pressed. Can be changed from `POPUP_RESULT.AFFIRMATIVE`.51 * @property {POPUP_RESULT|number?} [defaultResult=POPUP_RESULT.AFFIRMATIVE] - The default result of this popup when Enter is pressed. Can be changed from `POPUP_RESULT.AFFIRMATIVE`.
42 * @property {CustomPopupButton[]|string[]?} [customButtons=null] - Custom buttons to add to the popup. If only strings are provided, the buttons will be added with default options, and their result will be in order from `2` onward.52 * @property {CustomPopupButton[]|string[]?} [customButtons=null] - Custom buttons to add to the popup. If only strings are provided, the buttons will be added with default options, and their result will be in order from `2` onward.
@@ -164,7 +174,7 @@ export class Popup {
164 * @param {string} [inputValue=''] - The initial value of the input field174 * @param {string} [inputValue=''] - The initial value of the input field
165 * @param {PopupOptions} [options={}] - Additional options for the popup175 * @param {PopupOptions} [options={}] - Additional options for the popup
166 */176 */
167 constructor(content, type, inputValue = '', { okButton = null, cancelButton = null, rows = 1, wide = false, wider = false, large = false, transparent = false, allowHorizontalScrolling = false, allowVerticalScrolling = false, animation = 'fast', defaultResult = POPUP_RESULT.AFFIRMATIVE, customButtons = null, customInputs = null, onClosing = null, onClose = null, cropAspect = null, cropImage = null } = {}) {177 constructor(content, type, inputValue = '', { okButton = null, cancelButton = null, rows = 1, wide = false, wider = false, large = false, transparent = false, allowHorizontalScrolling = false, allowVerticalScrolling = false, leftAlign = false, animation = 'fast', defaultResult = POPUP_RESULT.AFFIRMATIVE, customButtons = null, customInputs = null, onClosing = null, onClose = null, cropAspect = null, cropImage = null } = {}) {
168 Popup.util.popups.push(this);178 Popup.util.popups.push(this);
169179
170 // Make this popup uniquely identifiable180 // Make this popup uniquely identifiable
@@ -209,6 +219,7 @@ export class Popup {
209 if (transparent) this.dlg.classList.add('transparent_dialogue_popup');219 if (transparent) this.dlg.classList.add('transparent_dialogue_popup');
210 if (allowHorizontalScrolling) this.dlg.classList.add('horizontal_scrolling_dialogue_popup');220 if (allowHorizontalScrolling) this.dlg.classList.add('horizontal_scrolling_dialogue_popup');
211 if (allowVerticalScrolling) this.dlg.classList.add('vertical_scrolling_dialogue_popup');221 if (allowVerticalScrolling) this.dlg.classList.add('vertical_scrolling_dialogue_popup');
222 if (leftAlign) this.dlg.classList.add('left_aligned_dialogue_popup');
212 if (animation) this.dlg.classList.add('popup--animation-' + animation);223 if (animation) this.dlg.classList.add('popup--animation-' + animation);
213224
214 // If custom button captions are provided, we set them beforehand225 // If custom button captions are provided, we set them beforehand
public/scripts/power-user.js+95 -9
@@ -54,6 +54,7 @@ import { commonEnumProviders, enumIcons } from './slash-commands/SlashCommandCom
54import { POPUP_TYPE, callGenericPopup } from './popup.js';54import { POPUP_TYPE, callGenericPopup } from './popup.js';
55import { loadSystemPrompts } from './sysprompt.js';55import { loadSystemPrompts } from './sysprompt.js';
56import { fuzzySearchCategories } from './filters.js';56import { fuzzySearchCategories } from './filters.js';
57import { accountStorage } from './util/AccountStorage.js';
5758
58export {59export {
59 loadPowerUserSettings,60 loadPowerUserSettings,
@@ -254,7 +255,10 @@ let power_user = {
254 },255 },
255256
256 reasoning: {257 reasoning: {
258 auto_parse: false,
257 add_to_prompts: false,259 add_to_prompts: false,
260 auto_expand: false,
261 show_hidden: false,
258 prefix: '<think>\n',262 prefix: '<think>\n',
259 suffix: '\n</think>',263 suffix: '\n</think>',
260 separator: '\n\n',264 separator: '\n\n',
@@ -1843,14 +1847,15 @@ async function loadContextSettings() {
18431847
1844/**1848/**
1845 * Common function to perform fuzzy search with optional caching1849 * Common function to perform fuzzy search with optional caching
1850 * @template T
1846 * @param {string} type - Type of search from fuzzySearchCategories1851 * @param {string} type - Type of search from fuzzySearchCategories
1847 * @param {any[]} data - Data array to search in1852 * @param {T[]} data - Data array to search in
1848 * @param {Array<{name: string, weight: number, getFn?: (obj: any) => string}>} keys - Fuse.js keys configuration1853 * @param {Array<{name: string, weight: number, getFn?: (obj: T) => string}>} keys - Fuse.js keys configuration
1849 * @param {string} searchValue - The search term1854 * @param {string} searchValue - The search term
1850 * @param {Object.<string, { resultMap: Map<string, any> }>} [fuzzySearchCaches=null] - Optional fuzzy search caches1855 * @param {Object.<string, { resultMap: Map<string, any> }>} [fuzzySearchCaches=null] - Optional fuzzy search caches
1851 * @returns {import('fuse.js').FuseResult<any>[]} Results as items with their score1856 * @returns {import('fuse.js').FuseResult<T>[]} Results as items with their score
1852 */1857 */
1853function performFuzzySearch(type, data, keys, searchValue, fuzzySearchCaches = null) {1858export function performFuzzySearch(type, data, keys, searchValue, fuzzySearchCaches = null) {
1854 // Check cache if provided1859 // Check cache if provided
1855 if (fuzzySearchCaches) {1860 if (fuzzySearchCaches) {
1856 const cache = fuzzySearchCaches[type];1861 const cache = fuzzySearchCaches[type];
@@ -2019,7 +2024,7 @@ export function renderStoryString(params) {
2019 */2024 */
2020function validateStoryString(storyString, params) {2025function validateStoryString(storyString, params) {
2021 /** @type {{hashCache: {[hash: string]: {fieldsWarned: {[key: string]: boolean}}}}} */2026 /** @type {{hashCache: {[hash: string]: {fieldsWarned: {[key: string]: boolean}}}}} */
2022 const cache = JSON.parse(localStorage.getItem(storage_keys.storyStringValidationCache)) ?? { hashCache: {} };2027 const cache = JSON.parse(accountStorage.getItem(storage_keys.storyStringValidationCache)) ?? { hashCache: {} };
20232028
2024 const hash = getStringHash(storyString);2029 const hash = getStringHash(storyString);
20252030
@@ -2056,7 +2061,7 @@ function validateStoryString(storyString, params) {
2056 toastr.warning(`The story string does not contain the following fields, but they would contain content: ${fieldsList}`, 'Story String Validation');2061 toastr.warning(`The story string does not contain the following fields, but they would contain content: ${fieldsList}`, 'Story String Validation');
2057 }2062 }
20582063
2059 localStorage.setItem(storage_keys.storyStringValidationCache, JSON.stringify(cache));2064 accountStorage.setItem(storage_keys.storyStringValidationCache, JSON.stringify(cache));
2060}2065}
20612066
20622067
@@ -2451,7 +2456,7 @@ async function resetMovablePanels(type) {
2451 }2456 }
24522457
2453 saveSettingsDebounced();2458 saveSettingsDebounced();
2454 eventSource.emit(event_types.MOVABLE_PANELS_RESET);2459 await eventSource.emit(event_types.MOVABLE_PANELS_RESET);
24552460
2456 eventSource.once(event_types.SETTINGS_UPDATED, () => {2461 eventSource.once(event_types.SETTINGS_UPDATED, () => {
2457 $('.resizing').removeClass('resizing');2462 $('.resizing').removeClass('resizing');
@@ -2919,6 +2924,46 @@ export function flushEphemeralStoppingStrings() {
2919}2924}
29202925
2921/**2926/**
2927 * Checks if the generated text should be filtered based on the auto-swipe settings.
2928 * @param {string} text The text to check
2929 * @returns {boolean} If the generated text should be filtered
2930 */
2931export function generatedTextFiltered(text) {
2932 /**
2933 * Checks if the given text contains any of the blacklisted words.
2934 * @param {string} text The text to check
2935 * @param {string[]} blacklist The list of blacklisted words
2936 * @param {number} threshold The number of blacklisted words that need to be present to trigger the check
2937 * @returns {boolean} Whether the text contains blacklisted words
2938 */
2939 function containsBlacklistedWords(text, blacklist, threshold) {
2940 const regex = new RegExp(`\\b(${blacklist.join('|')})\\b`, 'gi');
2941 const matches = text.match(regex) || [];
2942 return matches.length >= threshold;
2943 }
2944
2945 // Make sure a generated text is non-empty
2946 // Otherwise we might get in a loop with a broken API
2947 text = text.trim();
2948 if (text.length > 0) {
2949 if (power_user.auto_swipe_minimum_length) {
2950 if (text.length < power_user.auto_swipe_minimum_length) {
2951 console.log('Generated text size too small');
2952 return true;
2953 }
2954 }
2955 if (power_user.auto_swipe_blacklist.length && power_user.auto_swipe_blacklist_threshold) {
2956 if (containsBlacklistedWords(text, power_user.auto_swipe_blacklist, power_user.auto_swipe_blacklist_threshold)) {
2957 console.log('Generated text has blacklisted words');
2958 return true;
2959 }
2960 }
2961 }
2962
2963 return false;
2964}
2965
2966/**
2922 * Gets the custom stopping strings from the power user settings.2967 * Gets the custom stopping strings from the power user settings.
2923 * @param {number | undefined} limit Number of strings to return. If 0 or undefined, returns all strings.2968 * @param {number | undefined} limit Number of strings to return. If 0 or undefined, returns all strings.
2924 * @returns {string[]} An array of custom stopping strings2969 * @returns {string[]} An array of custom stopping strings
@@ -3899,9 +3944,9 @@ $(document).ready(() => {
3899 helpString: 'Start a new chat with a random character. If an argument is provided, only considers characters that have the specified tag.',3944 helpString: 'Start a new chat with a random character. If an argument is provided, only considers characters that have the specified tag.',
3900 }));3945 }));
3901 SlashCommandParser.addCommandObject(SlashCommand.fromProps({3946 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
3902 name: 'delmode',3947 name: 'del',
3903 callback: doDelMode,3948 callback: doDelMode,
3904 aliases: ['del'],3949 aliases: ['delete', 'delmode'],
3905 unnamedArgumentList: [3950 unnamedArgumentList: [
3906 new SlashCommandArgument(3951 new SlashCommandArgument(
3907 'optional number', [ARGUMENT_TYPE.NUMBER], false,3952 'optional number', [ARGUMENT_TYPE.NUMBER], false,
@@ -4084,4 +4129,45 @@ $(document).ready(() => {
4084 ],4129 ],
4085 helpString: 'activates a movingUI preset by name',4130 helpString: 'activates a movingUI preset by name',
4086 }));4131 }));
4132 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
4133 name: 'stop-strings',
4134 aliases: ['stopping-strings', 'custom-stopping-strings', 'custom-stop-strings'],
4135 helpString: `
4136 <div>
4137 Sets a list of custom stopping strings. Gets the list if no value is provided.
4138 </div>
4139 <div>
4140 <strong>Examples:</strong>
4141 </div>
4142 <ul>
4143 <li>Value must be a JSON-serialized array: <pre><code class="language-stscript">/stop-strings ["goodbye", "farewell"]</code></pre></li>
4144 <li>Pipe characters must be escaped with a backslash: <pre><code class="language-stscript">/stop-strings ["left\\|right"]</code></pre></li>
4145 </ul>
4146 `,
4147 returns: ARGUMENT_TYPE.LIST,
4148 unnamedArgumentList: [
4149 SlashCommandArgument.fromProps({
4150 description: 'list of strings',
4151 typeList: [ARGUMENT_TYPE.LIST],
4152 acceptsMultiple: false,
4153 isRequired: false,
4154 }),
4155 ],
4156 callback: (_, value) => {
4157 if (String(value ?? '').trim()) {
4158 const parsedValue = ((x) => { try { return JSON.parse(x.toString()); } catch { return null; } })(value);
4159 if (!parsedValue || !Array.isArray(parsedValue)) {
4160 throw new Error('Invalid list format. The value must be a JSON-serialized array of strings.');
4161 }
4162 parsedValue.forEach((item, index) => {
4163 parsedValue[index] = String(item);
4164 });
4165 power_user.custom_stopping_strings = JSON.stringify(parsedValue);
4166 $('#custom_stopping_strings').val(power_user.custom_stopping_strings);
4167 saveSettingsDebounced();
4168 }
4169
4170 return power_user.custom_stopping_strings;
4171 },
4172 }));
4087});4173});
public/scripts/preset-manager.js+3 -0
@@ -586,6 +586,9 @@ class PresetManager {
586 'tabby_model',586 'tabby_model',
587 'derived',587 'derived',
588 'generic_model',588 'generic_model',
589 'include_reasoning',
590 'global_banned_tokens',
591 'send_banned_tokens',
589 ];592 ];
590 const settings = Object.assign({}, getSettingsByApiId(this.apiId));593 const settings = Object.assign({}, getSettingsByApiId(this.apiId));
591594
public/scripts/reasoning.js+765 -21
@@ -1,13 +1,32 @@
1import { chat, closeMessageEditor, saveChatConditional, saveSettingsDebounced, substituteParams, updateMessageBlock } from '../script.js';1import {
2import { t } from './i18n.js';2 moment,
3} from '../lib.js';
4import { chat, closeMessageEditor, event_types, eventSource, main_api, messageFormatting, saveChatConditional, saveSettingsDebounced, substituteParams, updateMessageBlock } from '../script.js';
5import { getRegexedString, regex_placement } from './extensions/regex/engine.js';
6import { getCurrentLocale, t, translate } from './i18n.js';
3import { MacrosParser } from './macros.js';7import { MacrosParser } from './macros.js';
8import { chat_completion_sources, getChatCompletionModel, oai_settings } from './openai.js';
4import { Popup } from './popup.js';9import { Popup } from './popup.js';
5import { power_user } from './power-user.js';10import { power_user } from './power-user.js';
6import { SlashCommand } from './slash-commands/SlashCommand.js';11import { SlashCommand } from './slash-commands/SlashCommand.js';
7import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from './slash-commands/SlashCommandArgument.js';12import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from './slash-commands/SlashCommandArgument.js';
8import { commonEnumProviders } from './slash-commands/SlashCommandCommonEnumsProvider.js';13import { commonEnumProviders, enumIcons } from './slash-commands/SlashCommandCommonEnumsProvider.js';
14import { enumTypes, SlashCommandEnumValue } from './slash-commands/SlashCommandEnumValue.js';
9import { SlashCommandParser } from './slash-commands/SlashCommandParser.js';15import { SlashCommandParser } from './slash-commands/SlashCommandParser.js';
10import { copyText } from './utils.js';16import { textgen_types, textgenerationwebui_settings } from './textgen-settings.js';
17import { copyText, escapeRegex, isFalseBoolean, setDatasetProperty, trimSpaces } from './utils.js';
18
19/**
20 * Enum representing the type of the reasoning for a message (where it came from)
21 * @enum {string}
22 * @readonly
23 */
24export const ReasoningType = {
25 Model: 'model',
26 Parsed: 'parsed',
27 Manual: 'manual',
28 Edited: 'edited',
29};
1130
12/**31/**
13 * Gets a message from a jQuery element.32 * Gets a message from a jQuery element.
@@ -22,12 +41,473 @@ function getMessageFromJquery(element) {
22}41}
2342
24/**43/**
44 * Toggles the auto-expand state of reasoning blocks.
45 */
46function toggleReasoningAutoExpand() {
47 const reasoningBlocks = document.querySelectorAll('details.mes_reasoning_details');
48 reasoningBlocks.forEach((block) => {
49 if (block instanceof HTMLDetailsElement) {
50 block.open = power_user.reasoning.auto_expand;
51 }
52 });
53}
54
55/**
56 * Extracts the reasoning from the response data.
57 * @param {object} data Response data
58 * @returns {string} Extracted reasoning
59 */
60export function extractReasoningFromData(data) {
61 switch (main_api) {
62 case 'textgenerationwebui':
63 switch (textgenerationwebui_settings.type) {
64 case textgen_types.OPENROUTER:
65 return data?.choices?.[0]?.reasoning ?? '';
66 }
67 break;
68
69 case 'openai':
70 if (!oai_settings.show_thoughts) break;
71
72 switch (oai_settings.chat_completion_source) {
73 case chat_completion_sources.DEEPSEEK:
74 return data?.choices?.[0]?.message?.reasoning_content ?? '';
75 case chat_completion_sources.OPENROUTER:
76 return data?.choices?.[0]?.message?.reasoning ?? '';
77 case chat_completion_sources.MAKERSUITE:
78 return data?.responseContent?.parts?.filter(part => part.thought)?.map(part => part.text)?.join('\n\n') ?? '';
79 case chat_completion_sources.CUSTOM: {
80 return data?.choices?.[0]?.message?.reasoning_content
81 ?? data?.choices?.[0]?.message?.reasoning
82 ?? '';
83 }
84 }
85 break;
86 }
87
88 return '';
89}
90
91/**
92 * Check if the model supports reasoning, but does not send back the reasoning
93 * @returns {boolean} True if the model supports reasoning
94 */
95export function isHiddenReasoningModel() {
96 if (main_api !== 'openai') {
97 return false;
98 }
99
100 /** @typedef {{ (currentModel: string, supportedModel: string): boolean }} MatchingFunc */
101 /** @type {Record.<string, MatchingFunc>} */
102 const FUNCS = {
103 equals: (currentModel, supportedModel) => currentModel === supportedModel,
104 startsWith: (currentModel, supportedModel) => currentModel.startsWith(supportedModel),
105 };
106
107 /** @type {{ name: string; func: MatchingFunc; }[]} */
108 const hiddenReasoningModels = [
109 { name: 'o1', func: FUNCS.startsWith },
110 { name: 'o3', func: FUNCS.startsWith },
111 { name: 'gemini-2.0-flash-thinking-exp', func: FUNCS.startsWith },
112 { name: 'gemini-2.0-pro-exp', func: FUNCS.startsWith },
113 ];
114
115 const model = getChatCompletionModel() || '';
116
117 const isHidden = hiddenReasoningModels.some(({ name, func }) => func(model, name));
118 return isHidden;
119}
120
121/**
122 * Updates the Reasoning UI for a specific message
123 * @param {number|JQuery<HTMLElement>|HTMLElement} messageIdOrElement The message ID or the message element
124 * @param {Object} [options={}] - Optional arguments
125 * @param {boolean} [options.reset=false] - Whether to reset state, and not take the current mess properties (for example when swiping)
126 */
127export function updateReasoningUI(messageIdOrElement, { reset = false } = {}) {
128 const handler = new ReasoningHandler();
129 handler.initHandleMessage(messageIdOrElement, { reset });
130}
131
132
133/**
134 * Enum for representing the state of reasoning
135 * @enum {string}
136 * @readonly
137 */
138export const ReasoningState = {
139 None: 'none',
140 Thinking: 'thinking',
141 Done: 'done',
142 Hidden: 'hidden',
143};
144
145/**
146 * Handles reasoning-specific logic and DOM updates for messages.
147 * This class is used inside the {@link StreamingProcessor} to manage reasoning states and UI updates.
148 */
149export class ReasoningHandler {
150 /** @type {boolean} True if the model supports reasoning, but hides the reasoning output */
151 #isHiddenReasoningModel;
152 /** @type {boolean} True if the handler is currently handling a manual parse of reasoning blocks */
153 #isParsingReasoning = false;
154 /** @type {number?} When reasoning is being parsed manually, and the reasoning has ended, this will be the index at which the actual messages starts */
155 #parsingReasoningMesStartIndex = null;
156
157 /**
158 * @param {Date?} [timeStarted=null] - When the generation started
159 */
160 constructor(timeStarted = null) {
161 /** @type {ReasoningState} The current state of the reasoning process */
162 this.state = ReasoningState.None;
163 /** @type {ReasoningType?} The type of the reasoning (where it came from) */
164 this.type = null;
165 /** @type {string} The reasoning output */
166 this.reasoning = '';
167 /** @type {Date} When the reasoning started */
168 this.startTime = null;
169 /** @type {Date} When the reasoning ended */
170 this.endTime = null;
171
172 /** @type {Date} Initial starting time of the generation */
173 this.initialTime = timeStarted ?? new Date();
174
175 this.#isHiddenReasoningModel = isHiddenReasoningModel();
176
177 // Cached DOM elements for reasoning
178 /** @type {HTMLElement} Main message DOM element `.mes` */
179 this.messageDom = null;
180 /** @type {HTMLDetailsElement} Reasoning details DOM element `.mes_reasoning_details` */
181 this.messageReasoningDetailsDom = null;
182 /** @type {HTMLElement} Reasoning content DOM element `.mes_reasoning` */
183 this.messageReasoningContentDom = null;
184 /** @type {HTMLElement} Reasoning header DOM element `.mes_reasoning_header_title` */
185 this.messageReasoningHeaderDom = null;
186 }
187
188 /**
189 * Initializes the reasoning handler for a specific message.
190 *
191 * Can be used to update the DOM elements or read other reasoning states.
192 * It will internally take the message-saved data and write the states back into the handler, as if during streaming of the message.
193 * The state will always be either done/hidden or none.
194 *
195 * @param {number|JQuery<HTMLElement>|HTMLElement} messageIdOrElement - The message ID or the message element
196 * @param {Object} [options={}] - Optional arguments
197 * @param {boolean} [options.reset=false] - Whether to reset state of the handler, and not take the current mess properties (for example when swiping)
198 */
199 initHandleMessage(messageIdOrElement, { reset = false } = {}) {
200 /** @type {HTMLElement} */
201 const messageElement = typeof messageIdOrElement === 'number'
202 ? document.querySelector(`#chat [mesid="${messageIdOrElement}"]`)
203 : messageIdOrElement instanceof HTMLElement
204 ? messageIdOrElement
205 : $(messageIdOrElement)[0];
206 const messageId = Number(messageElement.getAttribute('mesid'));
207
208 if (isNaN(messageId) || !chat[messageId]) return;
209
210 if (!chat[messageId].extra) {
211 chat[messageId].extra = {};
212 }
213 const extra = chat[messageId].extra;
214
215 if (extra.reasoning) {
216 this.state = ReasoningState.Done;
217 } else if (extra.reasoning_duration) {
218 this.state = ReasoningState.Hidden;
219 }
220
221 this.type = extra?.reasoning_type;
222 this.reasoning = extra?.reasoning ?? '';
223
224 if (this.state !== ReasoningState.None) {
225 this.initialTime = new Date(chat[messageId].gen_started);
226 this.startTime = this.initialTime;
227 this.endTime = new Date(this.startTime.getTime() + (extra?.reasoning_duration ?? 0));
228 }
229
230 // Prefill main dom element, as message might not have been rendered yet
231 this.messageDom = messageElement;
232
233 // Make sure reset correctly clears all relevant states
234 if (reset) {
235 this.state = this.#isHiddenReasoningModel ? ReasoningState.Thinking : ReasoningState.None;
236 this.type = null;
237 this.reasoning = '';
238 this.initialTime = new Date();
239 this.startTime = null;
240 this.endTime = null;
241 }
242
243 this.updateDom(messageId);
244
245 if (power_user.reasoning.auto_expand && this.state !== ReasoningState.Hidden) {
246 this.messageReasoningDetailsDom.open = true;
247 }
248 }
249
250 /**
251 * Gets the duration of the reasoning in milliseconds.
252 *
253 * @returns {number?} The duration in milliseconds, or null if the start or end time is not set
254 */
255 getDuration() {
256 if (this.startTime && this.endTime) {
257 return this.endTime.getTime() - this.startTime.getTime();
258 }
259 return null;
260 }
261
262 /**
263 * Updates the reasoning text/string for a message.
264 *
265 * @param {number} messageId - The ID of the message to update
266 * @param {string?} [reasoning=null] - The reasoning text to update - If null or empty, uses the current reasoning
267 * @param {Object} [options={}] - Optional arguments
268 * @param {boolean} [options.persist=false] - Whether to persist the reasoning to the message object
269 * @param {boolean} [options.allowReset=false] - Whether to allow empty reasoning provided to reset the reasoning, instead of just taking the existing one
270 * @returns {boolean} - Returns true if the reasoning was changed, otherwise false
271 */
272 updateReasoning(messageId, reasoning = null, { persist = false, allowReset = false } = {}) {
273 if (messageId == -1 || !chat[messageId]) {
274 return false;
275 }
276
277 reasoning = allowReset ? reasoning ?? this.reasoning : reasoning || this.reasoning;
278 reasoning = trimSpaces(reasoning);
279
280 // Ensure the chat extra exists
281 if (!chat[messageId].extra) {
282 chat[messageId].extra = {};
283 }
284 const extra = chat[messageId].extra;
285
286 const reasoningChanged = extra.reasoning !== reasoning;
287 this.reasoning = getRegexedString(reasoning ?? '', regex_placement.REASONING);
288
289 this.type = (this.#isParsingReasoning || this.#parsingReasoningMesStartIndex) ? ReasoningType.Parsed : ReasoningType.Model;
290
291 if (persist) {
292 // Build and save the reasoning data to message extras
293 extra.reasoning = this.reasoning;
294 extra.reasoning_duration = this.getDuration();
295 extra.reasoning_type = (this.#isParsingReasoning || this.#parsingReasoningMesStartIndex) ? ReasoningType.Parsed : ReasoningType.Model;
296 }
297
298 return reasoningChanged;
299 }
300
301
302 /**
303 * Handles processing of reasoning for a message.
304 *
305 * This is usually called by the message processor when a message is changed.
306 *
307 * @param {number} messageId - The ID of the message to process
308 * @param {boolean} mesChanged - Whether the message has changed
309 * @returns {Promise<void>}
310 */
311 async process(messageId, mesChanged) {
312 mesChanged = this.#autoParseReasoningFromMessage(messageId, mesChanged);
313
314 if (!this.reasoning && !this.#isHiddenReasoningModel)
315 return;
316
317 // Ensure reasoning string is updated and regexes are applied correctly
318 const reasoningChanged = this.updateReasoning(messageId, null, { persist: true });
319
320 if ((this.#isHiddenReasoningModel || reasoningChanged) && this.state === ReasoningState.None) {
321 this.state = ReasoningState.Thinking;
322 this.startTime = this.initialTime;
323 }
324 if ((this.#isHiddenReasoningModel || !reasoningChanged) && mesChanged && this.state === ReasoningState.Thinking) {
325 this.endTime = new Date();
326 await this.finish(messageId);
327 }
328 }
329
330 #autoParseReasoningFromMessage(messageId, mesChanged) {
331 if (!power_user.reasoning.auto_parse)
332 return;
333 if (!power_user.reasoning.prefix || !power_user.reasoning.suffix)
334 return mesChanged;
335
336 /** @type {{ mes: string, [key: string]: any}} */
337 const message = chat[messageId];
338 if (!message) return mesChanged;
339
340 // If we are done with reasoning parse, we just split the message correctly so the reasoning doesn't show up inside of it.
341 if (this.#parsingReasoningMesStartIndex) {
342 message.mes = trimSpaces(message.mes.slice(this.#parsingReasoningMesStartIndex));
343 return mesChanged;
344 }
345
346 if (this.state === ReasoningState.None || this.#isHiddenReasoningModel) {
347 // If streamed message starts with the opening, cut it out and put all inside reasoning
348 if (message.mes.startsWith(power_user.reasoning.prefix) && message.mes.length > power_user.reasoning.prefix.length) {
349 this.#isParsingReasoning = true;
350
351 // Manually set starting state here, as we might already have received the ending suffix
352 this.state = ReasoningState.Thinking;
353 this.startTime = this.startTime ?? this.initialTime;
354 this.endTime = null;
355 }
356 }
357
358 if (!this.#isParsingReasoning)
359 return mesChanged;
360
361 // If we are in manual parsing mode, all currently streaming mes tokens will go the the reasoning block
362 const originalMes = message.mes;
363 this.reasoning = originalMes.slice(power_user.reasoning.prefix.length);
364 message.mes = '';
365
366 // If the reasoning contains the ending suffix, we cut that off and continue as message streaming
367 if (this.reasoning.includes(power_user.reasoning.suffix)) {
368 this.reasoning = this.reasoning.slice(0, this.reasoning.indexOf(power_user.reasoning.suffix));
369 this.#parsingReasoningMesStartIndex = originalMes.indexOf(power_user.reasoning.suffix) + power_user.reasoning.suffix.length;
370 message.mes = trimSpaces(originalMes.slice(this.#parsingReasoningMesStartIndex));
371 this.#isParsingReasoning = false;
372 }
373
374 // Only return the original mesChanged value if we haven't cut off the complete message
375 return message.mes.length ? mesChanged : false;
376 }
377
378 /**
379 * Completes the reasoning process for a message.
380 *
381 * Records the finish time if it was not set during streaming and updates the reasoning state.
382 * Emits an event to signal the completion of reasoning and updates the DOM elements accordingly.
383 *
384 * @param {number} messageId - The ID of the message to complete reasoning for
385 * @returns {Promise<void>}
386 */
387 async finish(messageId) {
388 if (this.state === ReasoningState.None) return;
389
390 // Make sure the finish time is recorded if a reasoning was in process and it wasn't ended correctly during streaming
391 if (this.startTime !== null && this.endTime === null) {
392 this.endTime = new Date();
393 }
394
395 if (this.state === ReasoningState.Thinking) {
396 this.state = this.#isHiddenReasoningModel ? ReasoningState.Hidden : ReasoningState.Done;
397 this.updateReasoning(messageId, null, { persist: true });
398 await eventSource.emit(event_types.STREAM_REASONING_DONE, this.reasoning, this.getDuration(), messageId, this.state);
399 }
400
401 this.updateDom(messageId);
402 }
403
404 /**
405 * Updates the reasoning UI elements for a message.
406 *
407 * Toggles the CSS class, updates states, reasoning message, and duration.
408 *
409 * @param {number} messageId - The ID of the message to update
410 */
411 updateDom(messageId) {
412 this.#checkDomElements(messageId);
413
414 // Main CSS class to show this message includes reasoning
415 this.messageDom.classList.toggle('reasoning', this.state !== ReasoningState.None);
416
417 // Update states to the relevant DOM elements
418 setDatasetProperty(this.messageDom, 'reasoningState', this.state !== ReasoningState.None ? this.state : null);
419 setDatasetProperty(this.messageReasoningDetailsDom, 'state', this.state);
420 setDatasetProperty(this.messageReasoningDetailsDom, 'type', this.type);
421
422 // Update the reasoning message
423 const reasoning = trimSpaces(this.reasoning);
424 const displayReasoning = messageFormatting(reasoning, '', false, false, messageId, {}, true);
425 this.messageReasoningContentDom.innerHTML = displayReasoning;
426
427 // Update tooltip for hidden reasoning edit
428 /** @type {HTMLElement} */
429 const button = this.messageDom.querySelector('.mes_edit_add_reasoning');
430 button.title = this.state === ReasoningState.Hidden ? t`Hidden reasoning - Add reasoning block` : t`Add reasoning block`;
431
432 // Make sure that hidden reasoning headers are collapsed by default, to not show a useless edit button
433 if (this.state === ReasoningState.Hidden) {
434 this.messageReasoningDetailsDom.open = false;
435 }
436
437 // Update the reasoning duration in the UI
438 this.#updateReasoningTimeUI();
439 }
440
441 /**
442 * Finds and caches reasoning-related DOM elements for the given message.
443 *
444 * @param {number} messageId - The ID of the message to cache the DOM elements for
445 */
446 #checkDomElements(messageId) {
447 // Make sure we reset dom elements if we are checking for a different message (shouldn't happen, but be sure)
448 if (this.messageDom !== null && this.messageDom.getAttribute('mesid') !== messageId.toString()) {
449 this.messageDom = null;
450 }
451
452 // Cache the DOM elements once
453 if (this.messageDom === null) {
454 this.messageDom = document.querySelector(`#chat .mes[mesid="${messageId}"]`);
455 if (this.messageDom === null) throw new Error('message dom does not exist');
456 }
457 if (this.messageReasoningDetailsDom === null) {
458 this.messageReasoningDetailsDom = this.messageDom.querySelector('.mes_reasoning_details');
459 }
460 if (this.messageReasoningContentDom === null) {
461 this.messageReasoningContentDom = this.messageDom.querySelector('.mes_reasoning');
462 }
463 if (this.messageReasoningHeaderDom === null) {
464 this.messageReasoningHeaderDom = this.messageDom.querySelector('.mes_reasoning_header_title');
465 }
466 }
467
468 /**
469 * Updates the reasoning time display in the UI.
470 *
471 * Shows the duration in a human-readable format with a tooltip for exact seconds.
472 * Displays "Thinking..." if still processing, or a generic message otherwise.
473 */
474 #updateReasoningTimeUI() {
475 const element = this.messageReasoningHeaderDom;
476 const duration = this.getDuration();
477 let data = null;
478 let title = '';
479 if (duration) {
480 const seconds = moment.duration(duration).asSeconds();
481
482 const durationStr = moment.duration(duration).locale(getCurrentLocale()).humanize({ s: 50, ss: 3 });
483 element.textContent = t`Thought for ${durationStr}`;
484 data = String(seconds);
485 title = `${seconds} seconds`;
486 } else if ([ReasoningState.Done, ReasoningState.Hidden].includes(this.state)) {
487 element.textContent = t`Thought for some time`;
488 data = 'unknown';
489 } else {
490 element.textContent = t`Thinking...`;
491 data = null;
492 }
493
494 if (this.type !== ReasoningType.Model) {
495 title += ` [${translate(this.type)}]`;
496 title = title.trim();
497 }
498 element.title = title;
499
500 setDatasetProperty(this.messageReasoningDetailsDom, 'duration', data);
501 setDatasetProperty(element, 'duration', data);
502 }
503}
504
505/**
25 * Helper class for adding reasoning to messages.506 * Helper class for adding reasoning to messages.
26 * Keeps track of the number of reasoning additions.507 * Keeps track of the number of reasoning additions.
27 */508 */
28export class PromptReasoning {509export class PromptReasoning {
29 static REASONING_PLACEHOLDER = '\u200B';510 static REASONING_PLACEHOLDER = '\u200B';
30 static REASONING_PLACEHOLDER_REGEX = new RegExp(`${PromptReasoning.REASONING_PLACEHOLDER}$`);
31511
32 constructor() {512 constructor() {
33 this.counter = 0;513 this.counter = 0;
@@ -49,15 +529,16 @@ export class PromptReasoning {
49 * Add reasoning to a message according to the power user settings.529 * Add reasoning to a message according to the power user settings.
50 * @param {string} content Message content530 * @param {string} content Message content
51 * @param {string} reasoning Message reasoning531 * @param {string} reasoning Message reasoning
532 * @param {boolean} isPrefix Whether this is the last message prefix
52 * @returns {string} Message content with reasoning533 * @returns {string} Message content with reasoning
53 */534 */
54 addToMessage(content, reasoning) {535 addToMessage(content, reasoning, isPrefix) {
55 // Disabled or reached limit of additions536 // Disabled or reached limit of additions
56 if (!power_user.reasoning.add_to_prompts || this.counter >= power_user.reasoning.max_additions) {537 if (!isPrefix && (!power_user.reasoning.add_to_prompts || this.counter >= power_user.reasoning.max_additions)) {
57 return content;538 return content;
58 }539 }
59540
60 // No reasoning provided or a placeholder541 // No reasoning provided or a legacy placeholder
61 if (!reasoning || reasoning === PromptReasoning.REASONING_PLACEHOLDER) {542 if (!reasoning || reasoning === PromptReasoning.REASONING_PLACEHOLDER) {
62 return content;543 return content;
63 }544 }
@@ -70,6 +551,11 @@ export class PromptReasoning {
70 const separator = substituteParams(power_user.reasoning.separator || '');551 const separator = substituteParams(power_user.reasoning.separator || '');
71 const suffix = substituteParams(power_user.reasoning.suffix || '');552 const suffix = substituteParams(power_user.reasoning.suffix || '');
72553
554 // Combine parts with reasoning only
555 if (isPrefix && !content) {
556 return `${prefix}${reasoning}`;
557 }
558
73 // Combine parts with reasoning and content559 // Combine parts with reasoning and content
74 return `${prefix}${reasoning}${suffix}${separator}${content}`;560 return `${prefix}${reasoning}${suffix}${separator}${content}`;
75 }561 }
@@ -105,11 +591,34 @@ function loadReasoningSettings() {
105 power_user.reasoning.max_additions = Number($(this).val());591 power_user.reasoning.max_additions = Number($(this).val());
106 saveSettingsDebounced();592 saveSettingsDebounced();
107 });593 });
594
595 $('#reasoning_auto_parse').prop('checked', power_user.reasoning.auto_parse);
596 $('#reasoning_auto_parse').on('change', function () {
597 power_user.reasoning.auto_parse = !!$(this).prop('checked');
598 saveSettingsDebounced();
599 });
600
601 $('#reasoning_auto_expand').prop('checked', power_user.reasoning.auto_expand);
602 $('#reasoning_auto_expand').on('change', function () {
603 power_user.reasoning.auto_expand = !!$(this).prop('checked');
604 toggleReasoningAutoExpand();
605 saveSettingsDebounced();
606 });
607 toggleReasoningAutoExpand();
608
609 $('#reasoning_show_hidden').prop('checked', power_user.reasoning.show_hidden);
610 $('#reasoning_show_hidden').on('change', function () {
611 power_user.reasoning.show_hidden = !!$(this).prop('checked');
612 $('#chat').attr('data-show-hidden-reasoning', power_user.reasoning.show_hidden ? 'true' : null);
613 saveSettingsDebounced();
614 });
615 $('#chat').attr('data-show-hidden-reasoning', power_user.reasoning.show_hidden ? 'true' : null);
108}616}
109617
110function registerReasoningSlashCommands() {618function registerReasoningSlashCommands() {
111 SlashCommandParser.addCommandObject(SlashCommand.fromProps({619 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
112 name: 'reasoning-get',620 name: 'reasoning-get',
621 aliases: ['get-reasoning'],
113 returns: ARGUMENT_TYPE.STRING,622 returns: ARGUMENT_TYPE.STRING,
114 helpString: t`Get the contents of a reasoning block of a message. Returns an empty string if the message does not have a reasoning block.`,623 helpString: t`Get the contents of a reasoning block of a message. Returns an empty string if the message does not have a reasoning block.`,
115 unnamedArgumentList: [624 unnamedArgumentList: [
@@ -120,15 +629,16 @@ function registerReasoningSlashCommands() {
120 }),629 }),
121 ],630 ],
122 callback: (_args, value) => {631 callback: (_args, value) => {
123 const messageId = !isNaN(Number(value)) ? Number(value) : chat.length - 1;632 const messageId = !isNaN(parseInt(value.toString())) ? parseInt(value.toString()) : chat.length - 1;
124 const message = chat[messageId];633 const message = chat[messageId];
125 const reasoning = String(message?.extra?.reasoning ?? '');634 const reasoning = String(message?.extra?.reasoning ?? '');
126 return reasoning.replace(PromptReasoning.REASONING_PLACEHOLDER_REGEX, '');635 return reasoning;
127 },636 },
128 }));637 }));
129638
130 SlashCommandParser.addCommandObject(SlashCommand.fromProps({639 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
131 name: 'reasoning-set',640 name: 'reasoning-set',
641 aliases: ['set-reasoning'],
132 returns: ARGUMENT_TYPE.STRING,642 returns: ARGUMENT_TYPE.STRING,
133 helpString: t`Set the reasoning block of a message. Returns the reasoning block content.`,643 helpString: t`Set the reasoning block of a message. Returns the reasoning block content.`,
134 namedArgumentList: [644 namedArgumentList: [
@@ -146,13 +656,18 @@ function registerReasoningSlashCommands() {
146 }),656 }),
147 ],657 ],
148 callback: async (args, value) => {658 callback: async (args, value) => {
149 const messageId = !isNaN(Number(args[0])) ? Number(args[0]) : chat.length - 1;659 const messageId = !isNaN(Number(args.at)) ? Number(args.at) : chat.length - 1;
150 const message = chat[messageId];660 const message = chat[messageId];
151 if (!message?.extra) {661 if (!message) {
152 return '';662 return '';
153 }663 }
664 // Make sure the message has an extra object
665 if (!message.extra || typeof message.extra !== 'object') {
666 message.extra = {};
667 }
154668
155 message.extra.reasoning = String(value ?? '');669 message.extra.reasoning = String(value ?? '');
670 message.extra.reasoning_type = ReasoningType.Manual;
156 await saveChatConditional();671 await saveChatConditional();
157672
158 closeMessageEditor('reasoning');673 closeMessageEditor('reasoning');
@@ -160,6 +675,77 @@ function registerReasoningSlashCommands() {
160 return message.extra.reasoning;675 return message.extra.reasoning;
161 },676 },
162 }));677 }));
678
679 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
680 name: 'reasoning-parse',
681 aliases: ['parse-reasoning'],
682 returns: 'reasoning string',
683 helpString: t`Extracts the reasoning block from a string using the Reasoning Formatting settings.`,
684 namedArgumentList: [
685 SlashCommandNamedArgument.fromProps({
686 name: 'regex',
687 description: 'Whether to apply regex scripts to the reasoning content.',
688 typeList: [ARGUMENT_TYPE.BOOLEAN],
689 defaultValue: 'true',
690 isRequired: false,
691 enumList: commonEnumProviders.boolean('trueFalse')(),
692 }),
693 SlashCommandNamedArgument.fromProps({
694 name: 'return',
695 description: 'Whether to return the parsed reasoning or the content without reasoning',
696 typeList: [ARGUMENT_TYPE.STRING],
697 defaultValue: 'reasoning',
698 isRequired: false,
699 enumList: [
700 new SlashCommandEnumValue('reasoning', null, enumTypes.enum, enumIcons.reasoning),
701 new SlashCommandEnumValue('content', null, enumTypes.enum, enumIcons.message),
702 ],
703 }),
704 SlashCommandNamedArgument.fromProps({
705 name: 'strict',
706 description: 'Whether to require the reasoning block to be at the beginning of the string (excluding whitespaces).',
707 typeList: [ARGUMENT_TYPE.BOOLEAN],
708 defaultValue: 'true',
709 isRequired: false,
710 enumList: commonEnumProviders.boolean('trueFalse')(),
711 }),
712 ],
713 unnamedArgumentList: [
714 SlashCommandArgument.fromProps({
715 description: 'input string',
716 typeList: [ARGUMENT_TYPE.STRING],
717 }),
718 ],
719 callback: (args, value) => {
720 if (!value || typeof value !== 'string') {
721 return '';
722 }
723
724 if (!power_user.reasoning.prefix || !power_user.reasoning.suffix) {
725 toastr.warning(t`Both prefix and suffix must be set in the Reasoning Formatting settings.`, t`Reasoning Parse`);
726 return value;
727 }
728 if (typeof args.return !== 'string' || !['reasoning', 'content'].includes(args.return)) {
729 toastr.warning(t`Invalid return type '${args.return}', defaulting to 'reasoning'.`, t`Reasoning Parse`);
730 }
731
732 const returnMessage = args.return === 'content';
733
734 const parsedReasoning = parseReasoningFromString(value, { strict: !isFalseBoolean(String(args.strict ?? '')) });
735 if (!parsedReasoning) {
736 return returnMessage ? value : '';
737 }
738
739 if (returnMessage) {
740 return parsedReasoning.content;
741 }
742
743 const applyRegex = !isFalseBoolean(String(args.regex ?? ''));
744 return applyRegex
745 ? getRegexedString(parsedReasoning.reasoning, regex_placement.REASONING)
746 : parsedReasoning.reasoning;
747 },
748 }));
163}749}
164750
165function registerReasoningMacros() {751function registerReasoningMacros() {
@@ -169,6 +755,31 @@ function registerReasoningMacros() {
169}755}
170756
171function setReasoningEventHandlers() {757function setReasoningEventHandlers() {
758 $(document).on('click', '.mes_reasoning_details', function (e) {
759 if (!e.target.closest('.mes_reasoning_actions') && !e.target.closest('.mes_reasoning_header')) {
760 e.preventDefault();
761 }
762 });
763
764 $(document).on('click', '.mes_reasoning_header', function (e) {
765 const details = $(this).closest('.mes_reasoning_details');
766 // Along with the CSS rules to mark blocks not toggle-able when they are empty, prevent them from actually being toggled, or being edited
767 if (details.find('.mes_reasoning').is(':empty')) {
768 e.preventDefault();
769 return;
770 }
771
772 // If we are in message edit mode and reasoning area is closed, a click opens and edits it
773 const mes = $(this).closest('.mes');
774 const mesEditArea = mes.find('#curEditTextarea');
775 if (mesEditArea.length) {
776 const summary = $(mes).find('.mes_reasoning_summary');
777 if (!summary.attr('open')) {
778 summary.find('.mes_reasoning_edit').trigger('click');
779 }
780 }
781 });
782
172 $(document).on('click', '.mes_reasoning_copy', (e) => {783 $(document).on('click', '.mes_reasoning_copy', (e) => {
173 e.stopPropagation();784 e.stopPropagation();
174 e.preventDefault();785 e.preventDefault();
@@ -187,7 +798,7 @@ function setReasoningEventHandlers(){
187 const textarea = document.createElement('textarea');798 const textarea = document.createElement('textarea');
188 const reasoningBlock = messageBlock.find('.mes_reasoning');799 const reasoningBlock = messageBlock.find('.mes_reasoning');
189 textarea.classList.add('reasoning_edit_textarea');800 textarea.classList.add('reasoning_edit_textarea');
190 textarea.value = reasoning.replace(PromptReasoning.REASONING_PLACEHOLDER_REGEX, '');801 textarea.value = reasoning;
191 $(textarea).insertBefore(reasoningBlock);802 $(textarea).insertBefore(reasoningBlock);
192803
193 if (!CSS.supports('field-sizing', 'content')) {804 if (!CSS.supports('field-sizing', 'content')) {
@@ -224,11 +835,14 @@ function setReasoningEventHandlers(){
224 }835 }
225836
226 const textarea = messageBlock.find('.reasoning_edit_textarea');837 const textarea = messageBlock.find('.reasoning_edit_textarea');
227 const reasoning = String(textarea.val());838 const reasoning = getRegexedString(String(textarea.val()), regex_placement.REASONING, { isEdit: true });
228 message.extra.reasoning = reasoning;839 message.extra.reasoning = reasoning;
840 message.extra.reasoning_type = message.extra.reasoning_type ? ReasoningType.Edited : ReasoningType.Manual;
229 await saveChatConditional();841 await saveChatConditional();
230 updateMessageBlock(messageId, message);842 updateMessageBlock(messageId, message);
231 textarea.remove();843 textarea.remove();
844
845 messageBlock.find('.mes_edit_done:visible').trigger('click');
232 });846 });
233847
234 $(document).on('click', '.mes_reasoning_edit_cancel', function (e) {848 $(document).on('click', '.mes_reasoning_edit_cancel', function (e) {
@@ -238,10 +852,14 @@ function setReasoningEventHandlers(){
238 const { messageBlock } = getMessageFromJquery(this);852 const { messageBlock } = getMessageFromJquery(this);
239 const textarea = messageBlock.find('.reasoning_edit_textarea');853 const textarea = messageBlock.find('.reasoning_edit_textarea');
240 textarea.remove();854 textarea.remove();
855
856 messageBlock.find('.mes_reasoning_edit_cancel:visible').trigger('click');
857
858 updateReasoningUI(messageBlock);
241 });859 });
242860
243 $(document).on('click', '.mes_edit_add_reasoning', async function () {861 $(document).on('click', '.mes_edit_add_reasoning', async function () {
244 const { message, messageId } = getMessageFromJquery(this);862 const { message, messageBlock } = getMessageFromJquery(this);
245 if (!message?.extra) {863 if (!message?.extra) {
246 return;864 return;
247 }865 }
@@ -251,34 +869,46 @@ function setReasoningEventHandlers(){
251 return;869 return;
252 }870 }
253871
254 message.extra.reasoning = PromptReasoning.REASONING_PLACEHOLDER;872 messageBlock.addClass('reasoning');
873
874 // To make hidden reasoning blocks editable, we just set them to "Done" here already.
875 // They will be done on save anyway - and on cancel the reasoning block gets rerendered too.
876 if (messageBlock.attr('data-reasoning-state') === ReasoningState.Hidden) {
877 messageBlock.attr('data-reasoning-state', ReasoningState.Done);
878 }
879
880 // Open the reasoning area so we can actually edit it
881 messageBlock.find('.mes_reasoning_details').attr('open', '');
882 messageBlock.find('.mes_reasoning_edit').trigger('click');
255 await saveChatConditional();883 await saveChatConditional();
256 closeMessageEditor();
257 updateMessageBlock(messageId, message);
258 });884 });
259885
260 $(document).on('click', '.mes_reasoning_delete', async function (e) {886 $(document).on('click', '.mes_reasoning_delete', async function (e) {
261 e.stopPropagation();887 e.stopPropagation();
262 e.preventDefault();888 e.preventDefault();
263889
264 const confirm = await Popup.show.confirm(t`Are you sure you want to clear the reasoning?`, t`Visible message contents will stay intact.`);890 const confirm = await Popup.show.confirm(t`Remove Reasoning`, t`Are you sure you want to clear the reasoning?<br />Visible message contents will stay intact.`);
265891
266 if (!confirm) {892 if (!confirm) {
267 return;893 return;
268 }894 }
269895
270 const { message, messageId } = getMessageFromJquery(this);896 const { message, messageId, messageBlock } = getMessageFromJquery(this);
271 if (!message?.extra) {897 if (!message?.extra) {
272 return;898 return;
273 }899 }
274 message.extra.reasoning = '';900 message.extra.reasoning = '';
901 delete message.extra.reasoning_type;
902 delete message.extra.reasoning_duration;
275 await saveChatConditional();903 await saveChatConditional();
276 updateMessageBlock(messageId, message);904 updateMessageBlock(messageId, message);
905 const textarea = messageBlock.find('.reasoning_edit_textarea');
906 textarea.remove();
277 });907 });
278908
279 $(document).on('pointerup', '.mes_reasoning_copy', async function () {909 $(document).on('pointerup', '.mes_reasoning_copy', async function () {
280 const { message } = getMessageFromJquery(this);910 const { message } = getMessageFromJquery(this);
281 const reasoning = String(message?.extra?.reasoning ?? '').replace(PromptReasoning.REASONING_PLACEHOLDER_REGEX, '');911 const reasoning = String(message?.extra?.reasoning ?? '');
282912
283 if (!reasoning) {913 if (!reasoning) {
284 return;914 return;
@@ -289,9 +919,123 @@ function setReasoningEventHandlers(){
289 });919 });
290}920}
291921
922/**
923 * Removes reasoning from a string if auto-parsing is enabled.
924 * @param {string} str Input string
925 * @returns {string} Output string
926 */
927export function removeReasoningFromString(str) {
928 if (!power_user.reasoning.auto_parse) {
929 return str;
930 }
931
932 const parsedReasoning = parseReasoningFromString(str);
933 return parsedReasoning?.content ?? str;
934}
935
936/**
937 * Parses reasoning from a string using the power user reasoning settings.
938 * @typedef {Object} ParsedReasoning
939 * @property {string} reasoning Reasoning block
940 * @property {string} content Message content
941 * @param {string} str Content of the message
942 * @param {Object} options Optional arguments
943 * @param {boolean} [options.strict=true] Whether the reasoning block **has** to be at the beginning of the provided string (excluding whitespaces), or can be anywhere in it
944 * @returns {ParsedReasoning|null} Parsed reasoning block and message content
945 */
946function parseReasoningFromString(str, { strict = true } = {}) {
947 // Both prefix and suffix must be defined
948 if (!power_user.reasoning.prefix || !power_user.reasoning.suffix) {
949 return null;
950 }
951
952 try {
953 const regex = new RegExp(`${(strict ? '^\\s*?' : '')}${escapeRegex(power_user.reasoning.prefix)}(.*?)${escapeRegex(power_user.reasoning.suffix)}`, 's');
954
955 let didReplace = false;
956 let reasoning = '';
957 let content = String(str).replace(regex, (_match, captureGroup) => {
958 didReplace = true;
959 reasoning = captureGroup;
960 return '';
961 });
962
963 if (didReplace) {
964 reasoning = trimSpaces(reasoning);
965 content = trimSpaces(content);
966 }
967
968 return { reasoning, content };
969 } catch (error) {
970 console.error('[Reasoning] Error parsing reasoning block', error);
971 return null;
972 }
973}
974
975function registerReasoningAppEvents() {
976 eventSource.makeFirst(event_types.MESSAGE_RECEIVED, (/** @type {number} */ idx) => {
977 if (!power_user.reasoning.auto_parse) {
978 return;
979 }
980
981 console.debug('[Reasoning] Auto-parsing reasoning block for message', idx);
982 const message = chat[idx];
983
984 if (!message) {
985 console.warn('[Reasoning] Message not found', idx);
986 return null;
987 }
988
989 if (!message.mes || message.mes === '...') {
990 console.debug('[Reasoning] Message content is empty or a placeholder', idx);
991 return null;
992 }
993
994 if (message.extra?.reasoning) {
995 console.debug('[Reasoning] Message already has reasoning', idx);
996 return null;
997 }
998
999 const parsedReasoning = parseReasoningFromString(message.mes);
1000
1001 // No reasoning block found
1002 if (!parsedReasoning) {
1003 return;
1004 }
1005
1006 // Make sure the message has an extra object
1007 if (!message.extra || typeof message.extra !== 'object') {
1008 message.extra = {};
1009 }
1010
1011 const contentUpdated = !!parsedReasoning.reasoning || parsedReasoning.content !== message.mes;
1012
1013 // If reasoning was found, add it to the message
1014 if (parsedReasoning.reasoning) {
1015 message.extra.reasoning = getRegexedString(parsedReasoning.reasoning, regex_placement.REASONING);
1016 message.extra.reasoning_type = ReasoningType.Parsed;
1017 }
1018
1019 // Update the message text if it was changed
1020 if (parsedReasoning.content !== message.mes) {
1021 message.mes = parsedReasoning.content;
1022 }
1023
1024 // Find if a message already exists in DOM and must be updated
1025 if (contentUpdated) {
1026 const messageRendered = document.querySelector(`.mes[mesid="${idx}"]`) !== null;
1027 if (messageRendered) {
1028 console.debug('[Reasoning] Updating message block', idx);
1029 updateMessageBlock(idx, message);
1030 }
1031 }
1032 });
1033}
1034
292export function initReasoning() {1035export function initReasoning() {
293 loadReasoningSettings();1036 loadReasoningSettings();
294 setReasoningEventHandlers();1037 setReasoningEventHandlers();
295 registerReasoningSlashCommands();1038 registerReasoningSlashCommands();
296 registerReasoningMacros();1039 registerReasoningMacros();
1040 registerReasoningAppEvents();
297}1041}
public/scripts/secrets.js+2 -0
@@ -40,6 +40,8 @@ export const SECRET_KEYS = {
40 BFL: 'api_key_bfl',40 BFL: 'api_key_bfl',
41 GENERIC: 'api_key_generic',41 GENERIC: 'api_key_generic',
42 DEEPSEEK: 'api_key_deepseek',42 DEEPSEEK: 'api_key_deepseek',
43 SERPER: 'api_key_serper',
44 FALAI: 'api_key_falai',
43};45};
4446
45const INPUT_MAP = {47const INPUT_MAP = {
public/scripts/slash-commands.js+127 -47
@@ -59,7 +59,7 @@ import { autoSelectPersona, isPersonaLocked, retriggerFirstMessageOnEmptyChat, s
59import { addEphemeralStoppingString, chat_styles, flushEphemeralStoppingStrings, power_user } from './power-user.js';59import { addEphemeralStoppingString, chat_styles, flushEphemeralStoppingStrings, power_user } from './power-user.js';
60import { SERVER_INPUTS, textgen_types, textgenerationwebui_settings } from './textgen-settings.js';60import { SERVER_INPUTS, textgen_types, textgenerationwebui_settings } from './textgen-settings.js';
61import { decodeTextTokens, getAvailableTokenizers, getFriendlyTokenizerName, getTextTokens, getTokenCountAsync, selectTokenizer } from './tokenizers.js';61import { decodeTextTokens, getAvailableTokenizers, getFriendlyTokenizerName, getTextTokens, getTokenCountAsync, selectTokenizer } from './tokenizers.js';
62import { debounce, delay, equalsIgnoreCaseAndAccents, findChar, getCharIndex, isFalseBoolean, isTrueBoolean, onlyUnique, showFontAwesomePicker, stringToRange, trimToEndSentence, trimToStartSentence, waitUntilCondition } from './utils.js';62import { debounce, delay, equalsIgnoreCaseAndAccents, findChar, getCharIndex, isFalseBoolean, isTrueBoolean, onlyUnique, regexFromString, showFontAwesomePicker, stringToRange, trimToEndSentence, trimToStartSentence, waitUntilCondition } from './utils.js';
63import { registerVariableCommands, resolveVariable } from './variables.js';63import { registerVariableCommands, resolveVariable } from './variables.js';
64import { background_settings } from './backgrounds.js';64import { background_settings } from './backgrounds.js';
65import { SlashCommandClosure } from './slash-commands/SlashCommandClosure.js';65import { SlashCommandClosure } from './slash-commands/SlashCommandClosure.js';
@@ -76,6 +76,7 @@ import { SlashCommandBreakController } from './slash-commands/SlashCommandBreakC
76import { SlashCommandExecutionError } from './slash-commands/SlashCommandExecutionError.js';76import { SlashCommandExecutionError } from './slash-commands/SlashCommandExecutionError.js';
77import { slashCommandReturnHelper } from './slash-commands/SlashCommandReturnHelper.js';77import { slashCommandReturnHelper } from './slash-commands/SlashCommandReturnHelper.js';
78import { t } from './i18n.js';78import { t } from './i18n.js';
79import { accountStorage } from './util/AccountStorage.js';
79export {80export {
80 executeSlashCommands, executeSlashCommandsWithOptions, getSlashCommandsHelp, registerSlashCommand,81 executeSlashCommands, executeSlashCommandsWithOptions, getSlashCommandsHelp, registerSlashCommand,
81};82};
@@ -283,7 +284,6 @@ export function initDefaultSlashCommands() {
283 description: 'Character name - or unique character identifier (avatar key)',284 description: 'Character name - or unique character identifier (avatar key)',
284 typeList: [ARGUMENT_TYPE.STRING],285 typeList: [ARGUMENT_TYPE.STRING],
285 enumProvider: commonEnumProviders.characters('character'),286 enumProvider: commonEnumProviders.characters('character'),
286 forceEnum: false,
287 }),287 }),
288 ],288 ],
289 helpString: `289 helpString: `
@@ -322,7 +322,6 @@ export function initDefaultSlashCommands() {
322 typeList: [ARGUMENT_TYPE.STRING],322 typeList: [ARGUMENT_TYPE.STRING],
323 isRequired: true,323 isRequired: true,
324 enumProvider: commonEnumProviders.characters('character'),324 enumProvider: commonEnumProviders.characters('character'),
325 forceEnum: false,
326 }),325 }),
327 SlashCommandNamedArgument.fromProps({326 SlashCommandNamedArgument.fromProps({
328 name: 'avatar',327 name: 'avatar',
@@ -566,7 +565,6 @@ export function initDefaultSlashCommands() {
566 typeList: [ARGUMENT_TYPE.STRING],565 typeList: [ARGUMENT_TYPE.STRING],
567 isRequired: true,566 isRequired: true,
568 enumProvider: commonEnumProviders.characters('all'),567 enumProvider: commonEnumProviders.characters('all'),
569 forceEnum: true,
570 }),568 }),
571 ],569 ],
572 helpString: 'Opens up a chat with the character or group by its name',570 helpString: 'Opens up a chat with the character or group by its name',
@@ -782,6 +780,57 @@ export function initDefaultSlashCommands() {
782 helpString: 'Unhides a message from the prompt.',780 helpString: 'Unhides a message from the prompt.',
783 }));781 }));
784 SlashCommandParser.addCommandObject(SlashCommand.fromProps({782 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
783 name: 'member-get',
784 aliases: ['getmember', 'memberget'],
785 callback: (async ({ field = 'name' }, arg) => {
786 if (!selected_group) {
787 toastr.warning('Cannot run /member-get command outside of a group chat.');
788 return '';
789 }
790 if (field === '') {
791 toastr.warning('\'/member-get field=\' argument required!');
792 return '';
793 }
794 field = field.toString();
795 arg = arg.toString();
796 if (!['name', 'index', 'id', 'avatar'].includes(field)) {
797 toastr.warning('\'/member-get field=\' argument required!');
798 return '';
799 }
800 const isId = !isNaN(parseInt(arg));
801 const groupMember = findGroupMemberId(arg, true);
802 if (!groupMember) {
803 toastr.warn(`No group member found using ${isId ? 'id' : 'string'} ${arg}`);
804 return '';
805 }
806 return groupMember[field];
807 }),
808 namedArgumentList: [
809 SlashCommandNamedArgument.fromProps({
810 name: 'field',
811 description: 'Whether to retrieve the name, index, id, or avatar.',
812 typeList: [ARGUMENT_TYPE.STRING],
813 isRequired: true,
814 defaultValue: 'name',
815 enumList: [
816 new SlashCommandEnumValue('name', 'Character name'),
817 new SlashCommandEnumValue('index', 'Group member index'),
818 new SlashCommandEnumValue('avatar', 'Character avatar'),
819 new SlashCommandEnumValue('id', 'Character index'),
820 ],
821 }),
822 ],
823 unnamedArgumentList: [
824 SlashCommandArgument.fromProps({
825 description: 'member index (starts with 0), name, or avatar',
826 typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.STRING],
827 isRequired: true,
828 enumProvider: commonEnumProviders.groupMembers(),
829 }),
830 ],
831 helpString: 'Retrieves a group member\'s name, index, id, or avatar.',
832 }));
833 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
785 name: 'member-disable',834 name: 'member-disable',
786 callback: disableGroupMemberCallback,835 callback: disableGroupMemberCallback,
787 aliases: ['disable', 'disablemember', 'memberdisable'],836 aliases: ['disable', 'disablemember', 'memberdisable'],
@@ -891,7 +940,8 @@ export function initDefaultSlashCommands() {
891 helpString: 'Moves a group member down in the group chat list.',940 helpString: 'Moves a group member down in the group chat list.',
892 }));941 }));
893 SlashCommandParser.addCommandObject(SlashCommand.fromProps({942 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
894 name: 'peek',943 name: 'member-peek',
944 aliases: ['peek', 'memberpeek', 'peekmember'],
895 callback: peekCallback,945 callback: peekCallback,
896 unnamedArgumentList: [946 unnamedArgumentList: [
897 SlashCommandArgument.fromProps({947 SlashCommandArgument.fromProps({
@@ -1057,7 +1107,6 @@ export function initDefaultSlashCommands() {
1057 typeList: [ARGUMENT_TYPE.STRING],1107 typeList: [ARGUMENT_TYPE.STRING],
1058 defaultValue: 'System',1108 defaultValue: 'System',
1059 enumProvider: () => [...commonEnumProviders.characters('character')(), new SlashCommandEnumValue('System', null, enumTypes.enum, enumIcons.assistant)],1109 enumProvider: () => [...commonEnumProviders.characters('character')(), new SlashCommandEnumValue('System', null, enumTypes.enum, enumIcons.assistant)],
1060 forceEnum: false,
1061 }),1110 }),
1062 new SlashCommandNamedArgument(1111 new SlashCommandNamedArgument(
1063 'length', 'API response length in tokens', [ARGUMENT_TYPE.NUMBER], false,1112 'length', 'API response length in tokens', [ARGUMENT_TYPE.NUMBER], false,
@@ -1951,7 +2000,7 @@ export function initDefaultSlashCommands() {
1951 returns: 'uppercase string',2000 returns: 'uppercase string',
1952 unnamedArgumentList: [2001 unnamedArgumentList: [
1953 new SlashCommandArgument(2002 new SlashCommandArgument(
1954 'string', [ARGUMENT_TYPE.STRING], true, false,2003 'text to affect', [ARGUMENT_TYPE.STRING], true, false,
1955 ),2004 ),
1956 ],2005 ],
1957 helpString: 'Converts the provided string to uppercase.',2006 helpString: 'Converts the provided string to uppercase.',
@@ -1963,7 +2012,7 @@ export function initDefaultSlashCommands() {
1963 returns: 'lowercase string',2012 returns: 'lowercase string',
1964 unnamedArgumentList: [2013 unnamedArgumentList: [
1965 new SlashCommandArgument(2014 new SlashCommandArgument(
1966 'string', [ARGUMENT_TYPE.STRING], true, false,2015 'text to affect', [ARGUMENT_TYPE.STRING], true, false,
1967 ),2016 ),
1968 ],2017 ],
1969 helpString: 'Converts the provided string to lowercase.',2018 helpString: 'Converts the provided string to lowercase.',
@@ -1983,7 +2032,7 @@ export function initDefaultSlashCommands() {
1983 ],2032 ],
1984 unnamedArgumentList: [2033 unnamedArgumentList: [
1985 new SlashCommandArgument(2034 new SlashCommandArgument(
1986 'string', [ARGUMENT_TYPE.STRING], true, false,2035 'text to affect', [ARGUMENT_TYPE.STRING], true, false,
1987 ),2036 ),
1988 ],2037 ],
1989 helpString: `2038 helpString: `
@@ -2047,6 +2096,62 @@ export function initDefaultSlashCommands() {
2047 return '';2096 return '';
2048 },2097 },
2049 }));2098 }));
2099 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
2100 name: 'replace',
2101 aliases: ['re'],
2102 callback: (async ({ mode = 'literal', pattern, replacer = '' }, text) => {
2103 if (pattern === '')
2104 throw new Error('Argument of \'pattern=\' cannot be empty');
2105 switch (mode) {
2106 case 'literal':
2107 return text.replaceAll(pattern, replacer);
2108 case 'regex':
2109 return text.replace(regexFromString(pattern), replacer);
2110 default:
2111 throw new Error('Invalid \'/replace mode=\' argument specified!');
2112 }
2113 }),
2114 returns: 'replaced text',
2115 namedArgumentList: [
2116 SlashCommandNamedArgument.fromProps({
2117 name: 'mode',
2118 description: 'Replaces occurrence(s) of a pattern',
2119 typeList: [ARGUMENT_TYPE.STRING],
2120 defaultValue: 'literal',
2121 enumList: ['literal', 'regex'],
2122 }),
2123 new SlashCommandNamedArgument(
2124 'pattern', 'pattern to search with', [ARGUMENT_TYPE.STRING], true, false,
2125 ),
2126 new SlashCommandNamedArgument(
2127 'replacer', 'replacement text for matches', [ARGUMENT_TYPE.STRING], false, false, '',
2128 ),
2129 ],
2130 unnamedArgumentList: [
2131 new SlashCommandArgument(
2132 'text to affect', [ARGUMENT_TYPE.STRING], true, false,
2133 ),
2134 ],
2135 helpString: `
2136 <div>
2137 Replaces text within the provided string based on the pattern.
2138 </div>
2139 <div>
2140 If <code>mode</code> is <code>literal</code> (or omitted), <code>pattern</code> is a literal search string (case-sensitive).<br />
2141 If <code>mode</code> is <code>regex</code>, <code>pattern</code> is parsed as an ECMAScript Regular Expression.<br />
2142 The <code>replacer</code> replaces based on the <code>pattern</code> in the input text.<br />
2143 If <code>replacer</code> is omitted, the replacement(s) will be an empty string.<br />
2144 </div>
2145 <div>
2146 <strong>Example:</strong>
2147 <pre>/let x Blue house and blue car || </pre>
2148 <pre>/replace pattern="blue" {{var::x}} | /echo |/# Blue house and car ||</pre>
2149 <pre>/replace pattern="blue" replacer="red" {{var::x}} | /echo |/# Blue house and red car ||</pre>
2150 <pre>/replace mode=regex pattern="/blue/i" replacer="red" {{var::x}} | /echo |/# red house and blue car ||</pre>
2151 <pre>/replace mode=regex pattern="/blue/gi" replacer="red" {{var::x}} | /echo |/# red house and red car ||</pre>
2152 </div>
2153 `,
2154 }));
20502155
2051 registerVariableCommands();2156 registerVariableCommands();
2052}2157}
@@ -3039,7 +3144,7 @@ function performGroupMemberAction(chid, action) {
30393144
3040async function disableGroupMemberCallback(_, arg) {3145async function disableGroupMemberCallback(_, arg) {
3041 if (!selected_group) {3146 if (!selected_group) {
3042 toastr.warning('Cannot run /disable command outside of a group chat.');3147 toastr.warning('Cannot run /member-disable command outside of a group chat.');
3043 return '';3148 return '';
3044 }3149 }
30453150
@@ -3056,7 +3161,7 @@ async function disableGroupMemberCallback(_, arg) {
30563161
3057async function enableGroupMemberCallback(_, arg) {3162async function enableGroupMemberCallback(_, arg) {
3058 if (!selected_group) {3163 if (!selected_group) {
3059 toastr.warning('Cannot run /enable command outside of a group chat.');3164 toastr.warning('Cannot run /member-enable command outside of a group chat.');
3060 return '';3165 return '';
3061 }3166 }
30623167
@@ -3073,7 +3178,7 @@ async function enableGroupMemberCallback(_, arg) {
30733178
3074async function moveGroupMemberUpCallback(_, arg) {3179async function moveGroupMemberUpCallback(_, arg) {
3075 if (!selected_group) {3180 if (!selected_group) {
3076 toastr.warning('Cannot run /memberup command outside of a group chat.');3181 toastr.warning('Cannot run /member-up command outside of a group chat.');
3077 return '';3182 return '';
3078 }3183 }
30793184
@@ -3090,7 +3195,7 @@ async function moveGroupMemberUpCallback(_, arg) {
30903195
3091async function moveGroupMemberDownCallback(_, arg) {3196async function moveGroupMemberDownCallback(_, arg) {
3092 if (!selected_group) {3197 if (!selected_group) {
3093 toastr.warning('Cannot run /memberdown command outside of a group chat.');3198 toastr.warning('Cannot run /member-down command outside of a group chat.');
3094 return '';3199 return '';
3095 }3200 }
30963201
@@ -3107,12 +3212,12 @@ async function moveGroupMemberDownCallback(_, arg) {
31073212
3108async function peekCallback(_, arg) {3213async function peekCallback(_, arg) {
3109 if (!selected_group) {3214 if (!selected_group) {
3110 toastr.warning('Cannot run /peek command outside of a group chat.');3215 toastr.warning('Cannot run /member-peek command outside of a group chat.');
3111 return '';3216 return '';
3112 }3217 }
31133218
3114 if (is_group_generating) {3219 if (is_group_generating) {
3115 toastr.warning('Cannot run /peek command while the group reply is generating.');3220 toastr.warning('Cannot run /member-peek command while the group reply is generating.');
3116 return '';3221 return '';
3117 }3222 }
31183223
@@ -3129,12 +3234,7 @@ async function peekCallback(_, arg) {
31293234
3130async function removeGroupMemberCallback(_, arg) {3235async function removeGroupMemberCallback(_, arg) {
3131 if (!selected_group) {3236 if (!selected_group) {
3132 toastr.warning('Cannot run /memberremove command outside of a group chat.');3237 toastr.warning('Cannot run /member-remove command outside of a group chat.');
3133 return '';
3134 }
3135
3136 if (is_group_generating) {
3137 toastr.warning('Cannot run /memberremove command while the group reply is generating.');
3138 return '';3238 return '';
3139 }3239 }
31403240
@@ -3242,12 +3342,7 @@ function findPersonaByName(name) {
3242}3342}
32433343
3244async function sendUserMessageCallback(args, text) {3344async function sendUserMessageCallback(args, text) {
3245 if (!text) {3345 text = String(text ?? '').trim();
3246 toastr.warning('You must specify text to send');
3247 return;
3248 }
3249
3250 text = text.trim();
3251 const compact = isTrueBoolean(args?.compact);3346 const compact = isTrueBoolean(args?.compact);
3252 const bias = extractMessageBias(text);3347 const bias = extractMessageBias(text);
32533348
@@ -3562,24 +3657,18 @@ export function getNameAndAvatarForMessage(character, name = null) {
3562}3657}
35633658
3564export async function sendMessageAs(args, text) {3659export async function sendMessageAs(args, text) {
3565 if (!text) {
3566 toastr.warning('You must specify text to send as');
3567 return '';
3568 }
3569
3570 let name = args.name?.trim();3660 let name = args.name?.trim();
3571 let mesText;
35723661
3573 if (!name) {3662 if (!name) {
3574 const namelessWarningKey = 'sendAsNamelessWarningShown';3663 const namelessWarningKey = 'sendAsNamelessWarningShown';
3575 if (localStorage.getItem(namelessWarningKey) !== 'true') {3664 if (accountStorage.getItem(namelessWarningKey) !== 'true') {
3576 toastr.warning('To avoid confusion, please use /sendas name="Character Name"', 'Name defaulted to {{char}}', { timeOut: 10000 });3665 toastr.warning('To avoid confusion, please use /sendas name="Character Name"', 'Name defaulted to {{char}}', { timeOut: 10000 });
3577 localStorage.setItem(namelessWarningKey, 'true');3666 accountStorage.setItem(namelessWarningKey, 'true');
3578 }3667 }
3579 name = name2;3668 name = name2;
3580 }3669 }
35813670
3582 mesText = text.trim();3671 let mesText = String(text ?? '').trim();
35833672
3584 // Requires a regex check after the slash command is pushed to output3673 // Requires a regex check after the slash command is pushed to output
3585 mesText = getRegexedString(mesText, regex_placement.SLASH_COMMAND, { characterOverride: name });3674 mesText = getRegexedString(mesText, regex_placement.SLASH_COMMAND, { characterOverride: name });
@@ -3657,11 +3746,7 @@ export async function sendMessageAs(args, text) {
3657}3746}
36583747
3659export async function sendNarratorMessage(args, text) {3748export async function sendNarratorMessage(args, text) {
3660 if (!text) {3749 text = String(text ?? '');
3661 toastr.warning('You must specify text to send');
3662 return '';
3663 }
3664
3665 const name = chat_metadata[NARRATOR_NAME_KEY] || NARRATOR_NAME_DEFAULT;3750 const name = chat_metadata[NARRATOR_NAME_KEY] || NARRATOR_NAME_DEFAULT;
3666 // Messages that do nothing but set bias will be hidden from the context3751 // Messages that do nothing but set bias will be hidden from the context
3667 const bias = extractMessageBias(text);3752 const bias = extractMessageBias(text);
@@ -3752,18 +3837,13 @@ export async function promptQuietForLoudResponse(who, text) {
3752}3837}
37533838
3754async function sendCommentMessage(args, text) {3839async function sendCommentMessage(args, text) {
3755 if (!text) {
3756 toastr.warning('You must specify text to send');
3757 return '';
3758 }
3759
3760 const compact = isTrueBoolean(args?.compact);3840 const compact = isTrueBoolean(args?.compact);
3761 const message = {3841 const message = {
3762 name: COMMENT_NAME_DEFAULT,3842 name: COMMENT_NAME_DEFAULT,
3763 is_user: false,3843 is_user: false,
3764 is_system: true,3844 is_system: true,
3765 send_date: getMessageTimeStamp(),3845 send_date: getMessageTimeStamp(),
3766 mes: substituteParams(text.trim()),3846 mes: substituteParams(String(text ?? '').trim()),
3767 force_avatar: comment_avatar,3847 force_avatar: comment_avatar,
3768 extra: {3848 extra: {
3769 type: system_message_types.COMMENT,3849 type: system_message_types.COMMENT,
public/scripts/slash-commands/SlashCommandCommonEnumsProvider.js+1 -0
@@ -34,6 +34,7 @@ export const enumIcons = {
34 preset: '⚙️',34 preset: '⚙️',
35 file: '📄',35 file: '📄',
36 message: '💬',36 message: '💬',
37 reasoning: '💡',
37 voice: '🎤',38 voice: '🎤',
38 server: '🖥️',39 server: '🖥️',
39 popup: '🗔',40 popup: '🗔',
public/scripts/st-context.js+18 -0
@@ -68,10 +68,14 @@ import { tag_map, tags } from './tags.js';
68import { textgenerationwebui_settings } from './textgen-settings.js';68import { textgenerationwebui_settings } from './textgen-settings.js';
69import { tokenizers, getTextTokens, getTokenCount, getTokenCountAsync, getTokenizerModel } from './tokenizers.js';69import { tokenizers, getTextTokens, getTokenCount, getTokenCountAsync, getTokenizerModel } from './tokenizers.js';
70import { ToolManager } from './tool-calling.js';70import { ToolManager } from './tool-calling.js';
71import { accountStorage } from './util/AccountStorage.js';
71import { timestampToMoment, uuidv4 } from './utils.js';72import { timestampToMoment, uuidv4 } from './utils.js';
73import { getGlobalVariable, getLocalVariable, setGlobalVariable, setLocalVariable } from './variables.js';
74import { convertCharacterBook, loadWorldInfo, saveWorldInfo, updateWorldInfoList } from './world-info.js';
7275
73export function getContext() {76export function getContext() {
74 return {77 return {
78 accountStorage,
75 chat,79 chat,
76 characters,80 characters,
77 groups,81 groups,
@@ -175,6 +179,20 @@ export function getContext() {
175 humanizedDateTime,179 humanizedDateTime,
176 updateMessageBlock,180 updateMessageBlock,
177 appendMediaToMessage,181 appendMediaToMessage,
182 variables: {
183 local: {
184 get: getLocalVariable,
185 set: setLocalVariable,
186 },
187 global: {
188 get: getGlobalVariable,
189 set: setGlobalVariable,
190 },
191 },
192 loadWorldInfo,
193 saveWorldInfo,
194 updateWorldInfoList,
195 convertCharacterBook,
178 };196 };
179}197}
180198
public/scripts/templates/itemizationChat.html+1 -1
@@ -146,5 +146,5 @@
146</div>146</div>
147<hr>147<hr>
148<div id="rawPromptPopup" class="list-group">148<div id="rawPromptPopup" class="list-group">
149 <div id="rawPromptWrapper" class="tokenItemizingSubclass"></div>149 <div id="rawPromptWrapper" class="tokenItemizingMaintext"></div>
150</div>150</div>
public/scripts/textgen-models.js+15 -3
@@ -6,6 +6,7 @@ import { tokenizers } from './tokenizers.js';
6import { renderTemplateAsync } from './templates.js';6import { renderTemplateAsync } from './templates.js';
7import { POPUP_TYPE, callGenericPopup } from './popup.js';7import { POPUP_TYPE, callGenericPopup } from './popup.js';
8import { t } from './i18n.js';8import { t } from './i18n.js';
9import { accountStorage } from './util/AccountStorage.js';
910
10let mancerModels = [];11let mancerModels = [];
11let togetherModels = [];12let togetherModels = [];
@@ -54,6 +55,17 @@ const OPENROUTER_PROVIDERS = [
54 'xAI',55 'xAI',
55 'Cloudflare',56 'Cloudflare',
56 'SF Compute',57 'SF Compute',
58 'Minimax',
59 'Nineteen',
60 'Liquid',
61 'InferenceNet',
62 'Friendli',
63 'AionLabs',
64 'Alibaba',
65 'Nebius',
66 'Chutes',
67 'Kluster',
68 'Targon',
57 '01.AI',69 '01.AI',
58 'HuggingFace',70 'HuggingFace',
59 'Mancer',71 'Mancer',
@@ -330,7 +342,7 @@ export async function loadFeatherlessModels(data) {
330 populateClassSelection(data);342 populateClassSelection(data);
331343
332 // Retrieve the stored number of items per page or default to 10344 // Retrieve the stored number of items per page or default to 10
333 const perPage = Number(localStorage.getItem(storageKey)) || 10;345 const perPage = Number(accountStorage.getItem(storageKey)) || 10;
334346
335 // Initialize pagination347 // Initialize pagination
336 applyFiltersAndSort();348 applyFiltersAndSort();
@@ -406,7 +418,7 @@ export async function loadFeatherlessModels(data) {
406 },418 },
407 afterSizeSelectorChange: function (e) {419 afterSizeSelectorChange: function (e) {
408 const newPerPage = e.target.value;420 const newPerPage = e.target.value;
409 localStorage.setItem('Models_PerPage', newPerPage);421 accountStorage.setItem(storageKey, newPerPage);
410 setupPagination(models, Number(newPerPage), featherlessCurrentPage); // Use the stored current page number422 setupPagination(models, Number(newPerPage), featherlessCurrentPage); // Use the stored current page number
411 },423 },
412 });424 });
@@ -507,7 +519,7 @@ export async function loadFeatherlessModels(data) {
507 const currentModelIndex = filteredModels.findIndex(x => x.id === textgen_settings.featherless_model);519 const currentModelIndex = filteredModels.findIndex(x => x.id === textgen_settings.featherless_model);
508 featherlessCurrentPage = currentModelIndex >= 0 ? (currentModelIndex / perPage) + 1 : 1;520 featherlessCurrentPage = currentModelIndex >= 0 ? (currentModelIndex / perPage) + 1 : 1;
509521
510 setupPagination(filteredModels, Number(localStorage.getItem(storageKey)) || perPage, featherlessCurrentPage);522 setupPagination(filteredModels, Number(accountStorage.getItem(storageKey)) || perPage, featherlessCurrentPage);
511 }523 }
512524
513 // Required to keep the /model command function525 // Required to keep the /model command function
public/scripts/textgen-settings.js+44 -9
@@ -10,6 +10,7 @@ import {
10 setOnlineStatus,10 setOnlineStatus,
11 substituteParams,11 substituteParams,
12} from '../script.js';12} from '../script.js';
13import { t } from './i18n.js';
13import { BIAS_CACHE, createNewLogitBiasEntry, displayLogitBias, getLogitBiasListResult } from './logit-bias.js';14import { BIAS_CACHE, createNewLogitBiasEntry, displayLogitBias, getLogitBiasListResult } from './logit-bias.js';
1415
15import { power_user, registerDebugFunction } from './power-user.js';16import { power_user, registerDebugFunction } from './power-user.js';
@@ -172,6 +173,7 @@ const settings = {
172 //truncation_length: 2048,173 //truncation_length: 2048,
173 ban_eos_token: false,174 ban_eos_token: false,
174 skip_special_tokens: true,175 skip_special_tokens: true,
176 include_reasoning: true,
175 streaming: false,177 streaming: false,
176 mirostat_mode: 0,178 mirostat_mode: 0,
177 mirostat_tau: 5,179 mirostat_tau: 5,
@@ -181,6 +183,8 @@ const settings = {
181 grammar_string: '',183 grammar_string: '',
182 json_schema: {},184 json_schema: {},
183 banned_tokens: '',185 banned_tokens: '',
186 global_banned_tokens: '',
187 send_banned_tokens: true,
184 sampler_priority: OOBA_DEFAULT_ORDER,188 sampler_priority: OOBA_DEFAULT_ORDER,
185 samplers: LLAMACPP_DEFAULT_ORDER,189 samplers: LLAMACPP_DEFAULT_ORDER,
186 samplers_priorities: APHRODITE_DEFAULT_ORDER,190 samplers_priorities: APHRODITE_DEFAULT_ORDER,
@@ -263,6 +267,7 @@ export const setting_names = [
263 'add_bos_token',267 'add_bos_token',
264 'ban_eos_token',268 'ban_eos_token',
265 'skip_special_tokens',269 'skip_special_tokens',
270 'include_reasoning',
266 'streaming',271 'streaming',
267 'mirostat_mode',272 'mirostat_mode',
268 'mirostat_tau',273 'mirostat_tau',
@@ -272,6 +277,8 @@ export const setting_names = [
272 'grammar_string',277 'grammar_string',
273 'json_schema',278 'json_schema',
274 'banned_tokens',279 'banned_tokens',
280 'global_banned_tokens',
281 'send_banned_tokens',
275 'ignore_eos_token',282 'ignore_eos_token',
276 'spaces_between_special_tokens',283 'spaces_between_special_tokens',
277 'speculative_ngram',284 'speculative_ngram',
@@ -304,7 +311,7 @@ export function validateTextGenUrl() {
304 const formattedUrl = formatTextGenURL(url);311 const formattedUrl = formatTextGenURL(url);
305312
306 if (!formattedUrl) {313 if (!formattedUrl) {
307 toastr.error('Enter a valid API URL', 'Text Completion API');314 toastr.error(t`Enter a valid API URL`, 'Text Completion API');
308 return;315 return;
309 }316 }
310317
@@ -392,7 +399,7 @@ function getTokenizerForTokenIds() {
392 * @returns {TokenBanResult} String with comma-separated banned token IDs399 * @returns {TokenBanResult} String with comma-separated banned token IDs
393 */400 */
394function getCustomTokenBans() {401function getCustomTokenBans() {
395 if (!settings.banned_tokens && !textgenerationwebui_banned_in_macros.length) {402 if (!settings.send_banned_tokens || (!settings.banned_tokens && !settings.global_banned_tokens && !textgenerationwebui_banned_in_macros.length)) {
396 return {403 return {
397 banned_tokens: '',404 banned_tokens: '',
398 banned_strings: [],405 banned_strings: [],
@@ -402,8 +409,9 @@ function getCustomTokenBans() {
402 const tokenizer = getTokenizerForTokenIds();409 const tokenizer = getTokenizerForTokenIds();
403 const banned_tokens = [];410 const banned_tokens = [];
404 const banned_strings = [];411 const banned_strings = [];
405 const sequences = settings.banned_tokens412 const sequences = []
406 .split('\n')413 .concat(settings.banned_tokens.split('\n'))
414 .concat(settings.global_banned_tokens.split('\n'))
407 .concat(textgenerationwebui_banned_in_macros)415 .concat(textgenerationwebui_banned_in_macros)
408 .filter(x => x.length > 0)416 .filter(x => x.length > 0)
409 .filter(onlyUnique);417 .filter(onlyUnique);
@@ -451,6 +459,18 @@ function getCustomTokenBans() {
451}459}
452460
453/**461/**
462 * Sets the banned strings kill switch toggle.
463 * @param {boolean} isEnabled Kill switch state
464 * @param {string} title Label title
465 */
466function toggleBannedStringsKillSwitch(isEnabled, title) {
467 $('#send_banned_tokens_textgenerationwebui').prop('checked', isEnabled);
468 $('#send_banned_tokens_label').find('.menu_button').toggleClass('toggleEnabled', isEnabled).prop('title', title);
469 settings.send_banned_tokens = isEnabled;
470 saveSettingsDebounced();
471}
472
473/**
454 * Calculates logit bias object from the logit bias list.474 * Calculates logit bias object from the logit bias list.
455 * @returns {object} Logit bias object475 * @returns {object} Logit bias object
456 */476 */
@@ -501,7 +521,7 @@ export function loadTextGenSettings(data, loadedSettings) {
501 for (const [type, selector] of Object.entries(SERVER_INPUTS)) {521 for (const [type, selector] of Object.entries(SERVER_INPUTS)) {
502 const control = $(selector);522 const control = $(selector);
503 control.val(settings.server_urls[type] ?? '').on('input', function () {523 control.val(settings.server_urls[type] ?? '').on('input', function () {
504 settings.server_urls[type] = String($(this).val());524 settings.server_urls[type] = String($(this).val()).trim();
505 saveSettingsDebounced();525 saveSettingsDebounced();
506 });526 });
507 }527 }
@@ -592,6 +612,14 @@ function sortAphroditeItemsByOrder(orderArray) {
592}612}
593613
594jQuery(function () {614jQuery(function () {
615 $('#send_banned_tokens_textgenerationwebui').on('change', function () {
616 const checked = !!$(this).prop('checked');
617 toggleBannedStringsKillSwitch(checked,
618 checked
619 ? t`Banned tokens/strings are being sent in the request.`
620 : t`Banned tokens/strings are NOT being sent in the request.`);
621 });
622
595 $('#koboldcpp_order').sortable({623 $('#koboldcpp_order').sortable({
596 delay: getSortableDelay(),624 delay: getSortableDelay(),
597 stop: function () {625 stop: function () {
@@ -740,6 +768,7 @@ jQuery(function () {
740 'add_bos_token_textgenerationwebui': true,768 'add_bos_token_textgenerationwebui': true,
741 'temperature_last_textgenerationwebui': true,769 'temperature_last_textgenerationwebui': true,
742 'skip_special_tokens_textgenerationwebui': true,770 'skip_special_tokens_textgenerationwebui': true,
771 'include_reasoning_textgenerationwebui': true,
743 'top_a_textgenerationwebui': 0,772 'top_a_textgenerationwebui': 0,
744 'top_a_counter_textgenerationwebui': 0,773 'top_a_counter_textgenerationwebui': 0,
745 'mirostat_mode_textgenerationwebui': 0,774 'mirostat_mode_textgenerationwebui': 0,
@@ -929,6 +958,10 @@ function setSettingByName(setting, value, trigger) {
929 if (isCheckbox) {958 if (isCheckbox) {
930 const val = Boolean(value);959 const val = Boolean(value);
931 $(`#${setting}_textgenerationwebui`).prop('checked', val);960 $(`#${setting}_textgenerationwebui`).prop('checked', val);
961
962 if ('send_banned_tokens' === setting) {
963 $(`#${setting}_textgenerationwebui`).trigger('change');
964 }
932 }965 }
933 else if (isText) {966 else if (isText) {
934 $(`#${setting}_textgenerationwebui`).val(value);967 $(`#${setting}_textgenerationwebui`).val(value);
@@ -986,7 +1019,7 @@ export async function generateTextGenWithStreaming(generate_data, signal) {
986 let logprobs = null;1019 let logprobs = null;
987 const swipes = [];1020 const swipes = [];
988 const toolCalls = [];1021 const toolCalls = [];
989 const state = {};1022 const state = { reasoning: '' };
990 while (true) {1023 while (true) {
991 const { done, value } = await reader.read();1024 const { done, value } = await reader.read();
992 if (done) return;1025 if (done) return;
@@ -1003,6 +1036,7 @@ export async function generateTextGenWithStreaming(generate_data, signal) {
1003 const newText = data?.choices?.[0]?.text || data?.content || '';1036 const newText = data?.choices?.[0]?.text || data?.content || '';
1004 text += newText;1037 text += newText;
1005 logprobs = parseTextgenLogprobs(newText, data.choices?.[0]?.logprobs || data?.completion_probabilities);1038 logprobs = parseTextgenLogprobs(newText, data.choices?.[0]?.logprobs || data?.completion_probabilities);
1039 state.reasoning += data?.choices?.[0]?.reasoning ?? '';
1006 }1040 }
10071041
1008 yield { text, swipes, logprobs, toolCalls, state };1042 yield { text, swipes, logprobs, toolCalls, state };
@@ -1153,7 +1187,7 @@ export function getTextGenModel() {
1153 return settings.aphrodite_model;1187 return settings.aphrodite_model;
1154 case OLLAMA:1188 case OLLAMA:
1155 if (!settings.ollama_model) {1189 if (!settings.ollama_model) {
1156 toastr.error('No Ollama model selected.', 'Text Completion API');1190 toastr.error(t`No Ollama model selected.`, 'Text Completion API');
1157 throw new Error('No Ollama model selected');1191 throw new Error('No Ollama model selected');
1158 }1192 }
1159 return settings.ollama_model;1193 return settings.ollama_model;
@@ -1217,7 +1251,7 @@ function replaceMacrosInList(str) {
1217 }1251 }
1218}1252}
12191253
1220export function getTextGenGenerationData(finalPrompt, maxTokens, isImpersonate, isContinue, cfgValues, type) {1254export async function getTextGenGenerationData(finalPrompt, maxTokens, isImpersonate, isContinue, cfgValues, type) {
1221 const canMultiSwipe = !isContinue && !isImpersonate && type !== 'quiet';1255 const canMultiSwipe = !isContinue && !isImpersonate && type !== 'quiet';
1222 const dynatemp = isDynamicTemperatureSupported();1256 const dynatemp = isDynamicTemperatureSupported();
1223 const { banned_tokens, banned_strings } = getCustomTokenBans();1257 const { banned_tokens, banned_strings } = getCustomTokenBans();
@@ -1266,6 +1300,7 @@ export function getTextGenGenerationData(finalPrompt, maxTokens, isImpersonate,
1266 'truncation_length': max_context,1300 'truncation_length': max_context,
1267 'ban_eos_token': settings.ban_eos_token,1301 'ban_eos_token': settings.ban_eos_token,
1268 'skip_special_tokens': settings.skip_special_tokens,1302 'skip_special_tokens': settings.skip_special_tokens,
1303 'include_reasoning': settings.include_reasoning,
1269 'top_a': settings.top_a,1304 'top_a': settings.top_a,
1270 'tfs': settings.tfs,1305 'tfs': settings.tfs,
1271 'epsilon_cutoff': [OOBA, MANCER].includes(settings.type) ? settings.epsilon_cutoff : undefined,1306 'epsilon_cutoff': [OOBA, MANCER].includes(settings.type) ? settings.epsilon_cutoff : undefined,
@@ -1444,7 +1479,7 @@ export function getTextGenGenerationData(finalPrompt, maxTokens, isImpersonate,
1444 }1479 }
1445 }1480 }
14461481
1447 eventSource.emitAndWait(event_types.TEXT_COMPLETION_SETTINGS_READY, params);1482 await eventSource.emit(event_types.TEXT_COMPLETION_SETTINGS_READY, params);
14481483
1449 // Grammar conflicts with with json_schema1484 // Grammar conflicts with with json_schema
1450 if (settings.type === LLAMACPP) {1485 if (settings.type === LLAMACPP) {
public/scripts/tokenizers.js+6 -0
@@ -679,6 +679,9 @@ export function getTokenizerModel() {
679 }679 }
680680
681 if (oai_settings.chat_completion_source === chat_completion_sources.PERPLEXITY) {681 if (oai_settings.chat_completion_source === chat_completion_sources.PERPLEXITY) {
682 if (oai_settings.perplexity_model.includes('sonar-reasoning') || oai_settings.perplexity_model.includes('r1-1776')) {
683 return deepseekTokenizer;
684 }
682 if (oai_settings.perplexity_model.includes('llama-3') || oai_settings.perplexity_model.includes('llama3')) {685 if (oai_settings.perplexity_model.includes('llama-3') || oai_settings.perplexity_model.includes('llama3')) {
683 return llama3Tokenizer;686 return llama3Tokenizer;
684 }687 }
@@ -691,6 +694,9 @@ export function getTokenizerModel() {
691 }694 }
692695
693 if (oai_settings.chat_completion_source === chat_completion_sources.GROQ) {696 if (oai_settings.chat_completion_source === chat_completion_sources.GROQ) {
697 if (oai_settings.groq_model.includes('qwen')) {
698 return qwen2Tokenizer;
699 }
694 if (oai_settings.groq_model.includes('llama-3') || oai_settings.groq_model.includes('llama3')) {700 if (oai_settings.groq_model.includes('llama-3') || oai_settings.groq_model.includes('llama3')) {
695 return llama3Tokenizer;701 return llama3Tokenizer;
696 }702 }
public/scripts/tool-calling.js+1 -0
@@ -563,6 +563,7 @@ export class ToolManager {
563 chat_completion_sources.OPENROUTER,563 chat_completion_sources.OPENROUTER,
564 chat_completion_sources.GROQ,564 chat_completion_sources.GROQ,
565 chat_completion_sources.COHERE,565 chat_completion_sources.COHERE,
566 chat_completion_sources.DEEPSEEK,
566 ];567 ];
567 return supportedSources.includes(oai_settings.chat_completion_source);568 return supportedSources.includes(oai_settings.chat_completion_source);
568 }569 }
public/scripts/user.js+34 -0
@@ -9,6 +9,9 @@ import { ensureImageFormatSupported, getBase64Async, humanFileSize } from './uti
9export let currentUser = null;9export let currentUser = null;
10export let accountsEnabled = false;10export let accountsEnabled = false;
1111
12// Extend the session every 30 minutes
13const SESSION_EXTEND_INTERVAL = 30 * 60 * 1000;
14
12/**15/**
13 * Enable or disable user account controls in the UI.16 * Enable or disable user account controls in the UI.
14 * @param {boolean} isEnabled User account controls enabled17 * @param {boolean} isEnabled User account controls enabled
@@ -44,6 +47,14 @@ export function isAdmin() {
44}47}
4548
46/**49/**
50 * Gets the handle string of the current user.
51 * @returns {string} User handle
52 */
53export function getCurrentUserHandle() {
54 return currentUser?.handle || 'default-user';
55}
56
57/**
47 * Get the current user.58 * Get the current user.
48 * @returns {Promise<void>}59 * @returns {Promise<void>}
49 */60 */
@@ -886,6 +897,24 @@ async function slugify(text) {
886 }897 }
887}898}
888899
900/**
901 * Pings the server to extend the user session.
902 */
903async function extendUserSession() {
904 try {
905 const response = await fetch('/api/ping?extend=1', {
906 method: 'GET',
907 headers: getRequestHeaders(),
908 });
909
910 if (!response.ok) {
911 throw new Error('Ping did not succeed', { cause: response.status });
912 }
913 } catch (error) {
914 console.error('Failed to extend user session', error);
915 }
916}
917
889jQuery(() => {918jQuery(() => {
890 $('#logout_button').on('click', () => {919 $('#logout_button').on('click', () => {
891 logout();920 logout();
@@ -896,4 +925,9 @@ jQuery(() => {
896 $('#account_button').on('click', () => {925 $('#account_button').on('click', () => {
897 openUserProfile();926 openUserProfile();
898 });927 });
928 setInterval(async () => {
929 if (currentUser) {
930 await extendUserSession();
931 }
932 }, SESSION_EXTEND_INTERVAL);
899});933});
public/scripts/util/AccountStorage.js+139 -0
@@ -0,0 +1,139 @@
1import { saveSettingsDebounced } from '../../script.js';
2
3const MIGRATED_MARKER = '__migrated';
4const MIGRATABLE_KEYS = [
5 /^AlertRegex_/,
6 /^AlertWI_/,
7 /^Assets_SkipConfirm_/,
8 /^Characters_PerPage$/,
9 /^DataBank_sortField$/,
10 /^DataBank_sortOrder$/,
11 /^extension_update_nag$/,
12 /^extensions_sortByName$/,
13 /^FeatherlessModels_PerPage$/,
14 /^GroupMembers_PerPage$/,
15 /^GroupCandidates_PerPage$/,
16 /^LNavLockOn$/,
17 /^LNavOpened$/,
18 /^mediaWarningShown:/,
19 /^NavLockOn$/,
20 /^NavOpened$/,
21 /^Personas_PerPage$/,
22 /^Personas_GridView$/,
23 /^Proxy_SkipConfirm_/,
24 /^qr--executeShortcut$/,
25 /^qr--syntax$/,
26 /^qr--tabSize$/,
27 /^qr--wrap$/,
28 /^RegenerateWithCtrlEnter$/,
29 /^SelectedNavTab$/,
30 /^sendAsNamelessWarningShown$/,
31 /^StoryStringValidationCache$/,
32 /^WINavOpened$/,
33 /^WI_PerPage$/,
34 /^world_info_sort_order$/,
35];
36
37/**
38 * Provides access to account storage of arbitrary key-value pairs.
39 */
40class AccountStorage {
41 /**
42 * @type {Record<string, string>} Storage state
43 */
44 #state = {};
45
46 /**
47 * @type {boolean} If the storage was initialized
48 */
49 #ready = false;
50
51 #migrateLocalStorage() {
52 const localStorageKeys = [];
53 for (let i = 0; i < globalThis.localStorage.length; i++) {
54 localStorageKeys.push(globalThis.localStorage.key(i));
55 }
56 for (const key of localStorageKeys) {
57 if (MIGRATABLE_KEYS.some(k => k.test(key))) {
58 const value = globalThis.localStorage.getItem(key);
59 this.#state[key] = value;
60 globalThis.localStorage.removeItem(key);
61 }
62 }
63 }
64
65 /**
66 * Initialize the account storage.
67 * @param {Object} state Initial state
68 */
69 init(state) {
70 if (state && typeof state === 'object') {
71 this.#state = Object.assign(this.#state, state);
72 }
73
74 if (!Object.hasOwn(this.#state, MIGRATED_MARKER)) {
75 this.#migrateLocalStorage();
76 this.#state[MIGRATED_MARKER] = '1';
77 saveSettingsDebounced();
78 }
79
80 this.#ready = true;
81 }
82
83 /**
84 * Get the value of a key in account storage.
85 * @param {string} key Key to get
86 * @returns {string|null} Value of the key
87 */
88 getItem(key) {
89 if (!this.#ready) {
90 console.warn(`AccountStorage not ready (trying to read from ${key})`);
91 }
92
93 return Object.hasOwn(this.#state, key) ? String(this.#state[key]) : null;
94 }
95
96 /**
97 * Set a key in account storage.
98 * @param {string} key Key to set
99 * @param {string} value Value to set
100 */
101 setItem(key, value) {
102 if (!this.#ready) {
103 console.warn(`AccountStorage not ready (trying to write to ${key})`);
104 }
105
106 this.#state[key] = String(value);
107 saveSettingsDebounced();
108 }
109
110 /**
111 * Remove a key from account storage.
112 * @param {string} key Key to remove
113 */
114 removeItem(key) {
115 if (!this.#ready) {
116 console.warn(`AccountStorage not ready (trying to remove ${key})`);
117 }
118
119 if (!Object.hasOwn(this.#state, key)) {
120 return;
121 }
122
123 delete this.#state[key];
124 saveSettingsDebounced();
125 }
126
127 /**
128 * Gets a snapshot of the storage state.
129 * @returns {Record<string, string>} A deep clone of the storage state
130 */
131 getState() {
132 return structuredClone(this.#state);
133 }
134}
135
136/**
137 * Account storage instance.
138 */
139export const accountStorage = new AccountStorage();
public/scripts/utils.js+48 -13
@@ -8,7 +8,7 @@ import {
8import { getContext } from './extensions.js';8import { getContext } from './extensions.js';
9import { characters, getRequestHeaders, this_chid } from '../script.js';9import { characters, getRequestHeaders, this_chid } from '../script.js';
10import { isMobile } from './RossAscends-mods.js';10import { isMobile } from './RossAscends-mods.js';
11import { collapseNewlines } from './power-user.js';11import { collapseNewlines, power_user } from './power-user.js';
12import { debounce_timeout } from './constants.js';12import { debounce_timeout } from './constants.js';
13import { Popup, POPUP_RESULT, POPUP_TYPE } from './popup.js';13import { Popup, POPUP_RESULT, POPUP_TYPE } from './popup.js';
14import { SlashCommandClosure } from './slash-commands/SlashCommandClosure.js';14import { SlashCommandClosure } from './slash-commands/SlashCommandClosure.js';
@@ -677,6 +677,19 @@ export function sortByCssOrder(a, b) {
677}677}
678678
679/**679/**
680 * Trims leading and trailing whitespace from the input string based on a configuration setting.
681 * @param {string} input - The string to be trimmed
682 * @returns {string} The trimmed string if trimming is enabled; otherwise, returns the original string
683 */
684
685export function trimSpaces(input) {
686 if (!input || typeof input !== 'string') {
687 return input;
688 }
689 return power_user.trim_spaces ? input.trim() : input;
690}
691
692/**
680 * Trims a string to the end of a nearest sentence.693 * Trims a string to the end of a nearest sentence.
681 * @param {string} input The string to trim.694 * @param {string} input The string to trim.
682 * @returns {string} The trimmed string.695 * @returns {string} The trimmed string.
@@ -994,13 +1007,18 @@ export function getImageSizeFromDataURL(dataUrl) {
994 });1007 });
995}1008}
9961009
997export function getCharaFilename(chid) {1010/**
1011 * Gets the filename of the character avatar without extension
1012 * @param {number?} [chid=null] - Character ID. If not provided, uses the current character ID
1013 * @param {object} [options={}] - Options arguments
1014 * @param {string?} [options.manualAvatarKey=null] - Manually take the following avatar key, instead of using the chid to determine the name
1015 * @returns {string?} The filename of the character avatar without extension, or null if the character ID is invalid
1016 */
1017export function getCharaFilename(chid = null, { manualAvatarKey = null } = {}) {
998 const context = getContext();1018 const context = getContext();
999 const fileName = context.characters[chid ?? context.characterId]?.avatar;1019 const fileName = manualAvatarKey ?? context.characters[chid ?? context.characterId]?.avatar;
10001020
1001 if (fileName) {1021 return fileName?.replace(/\.[^/.]+$/, '') ?? null;
1002 return fileName.replace(/\.[^/.]+$/, '');
1003 }
1004}1022}
10051023
1006/**1024/**
@@ -1733,17 +1751,17 @@ export function hasAnimation(control) {
17331751
1734/**1752/**
1735 * Run an action once an animation on a control ends. If the control has no animation, the action will be executed immediately.1753 * Run an action once an animation on a control ends. If the control has no animation, the action will be executed immediately.
1736 *1754 * The action will be executed after the animation ends or after the timeout, whichever comes first.
1737 * @param {HTMLElement} control - The control element to listen for animation end event1755 * @param {HTMLElement} control - The control element to listen for animation end event
1738 * @param {(control:*?) => void} callback - The callback function to be executed when the animation ends1756 * @param {(control:*?) => void} callback - The callback function to be executed when the animation ends
1757 * @param {number} [timeout=500] - The timeout in milliseconds to wait for the animation to end before executing the callback
1739 */1758 */
1740export function runAfterAnimation(control, callback) {1759export function runAfterAnimation(control, callback, timeout = 500) {
1741 if (hasAnimation(control)) {1760 if (hasAnimation(control)) {
1742 const onAnimationEnd = () => {1761 Promise.race([
1743 control.removeEventListener('animationend', onAnimationEnd);1762 new Promise((r) => setTimeout(r, timeout)), // Fallback timeout
1744 callback(control);1763 new Promise((r) => control.addEventListener('animationend', r, { once: true })),
1745 };1764 ]).finally(() => callback(control));
1746 control.addEventListener('animationend', onAnimationEnd);
1747 } else {1765 } else {
1748 callback(control);1766 callback(control);
1749 }1767 }
@@ -2059,6 +2077,23 @@ export function toggleDrawer(drawer, expand = true) {
2059 }2077 }
2060}2078}
20612079
2080/**
2081 * Sets or removes a dataset property on an HTMLElement
2082 *
2083 * Utility function to make it easier to reset dataset properties on null, without them being "null" as value.
2084 *
2085 * @param {HTMLElement} element - The element to modify
2086 * @param {string} name - The name of the dataset property
2087 * @param {string|null} value - The value to set - If null, the dataset property will be removed
2088 */
2089export function setDatasetProperty(element, name, value) {
2090 if (value === null) {
2091 delete element.dataset[name];
2092 } else {
2093 element.dataset[name] = value;
2094 }
2095}
2096
2062export async function fetchFaFile(name) {2097export async function fetchFaFile(name) {
2063 const style = document.createElement('style');2098 const style = document.createElement('style');
2064 style.innerHTML = await (await fetch(`/css/${name}`)).text();2099 style.innerHTML = await (await fetch(`/css/${name}`)).text();
public/scripts/variables.js+4 -4
@@ -19,7 +19,7 @@ import { isFalseBoolean, convertValueType, isTrueBoolean } from './utils.js';
1919
20const MAX_LOOPS = 100;20const MAX_LOOPS = 100;
2121
22function getLocalVariable(name, args = {}) {22export function getLocalVariable(name, args = {}) {
23 if (!chat_metadata.variables) {23 if (!chat_metadata.variables) {
24 chat_metadata.variables = {};24 chat_metadata.variables = {};
25 }25 }
@@ -45,7 +45,7 @@ function getLocalVariable(name, args = {}) {
45 return (localVariable?.trim?.() === '' || isNaN(Number(localVariable))) ? (localVariable || '') : Number(localVariable);45 return (localVariable?.trim?.() === '' || isNaN(Number(localVariable))) ? (localVariable || '') : Number(localVariable);
46}46}
4747
48function setLocalVariable(name, value, args = {}) {48export function setLocalVariable(name, value, args = {}) {
49 if (!name) {49 if (!name) {
50 throw new Error('Variable name cannot be empty or undefined.');50 throw new Error('Variable name cannot be empty or undefined.');
51 }51 }
@@ -80,7 +80,7 @@ function setLocalVariable(name, value, args = {}) {
80 return value;80 return value;
81}81}
8282
83function getGlobalVariable(name, args = {}) {83export function getGlobalVariable(name, args = {}) {
84 let globalVariable = extension_settings.variables.global[args.key ?? name];84 let globalVariable = extension_settings.variables.global[args.key ?? name];
85 if (args.index !== undefined) {85 if (args.index !== undefined) {
86 try {86 try {
@@ -102,7 +102,7 @@ function getGlobalVariable(name, args = {}) {
102 return (globalVariable?.trim?.() === '' || isNaN(Number(globalVariable))) ? (globalVariable || '') : Number(globalVariable);102 return (globalVariable?.trim?.() === '' || isNaN(Number(globalVariable))) ? (globalVariable || '') : Number(globalVariable);
103}103}
104104
105function setGlobalVariable(name, value, args = {}) {105export function setGlobalVariable(name, value, args = {}) {
106 if (!name) {106 if (!name) {
107 throw new Error('Variable name cannot be empty or undefined.');107 throw new Error('Variable name cannot be empty or undefined.');
108 }108 }
public/scripts/world-info.js+36 -18
@@ -21,6 +21,7 @@ import { callGenericPopup, Popup, POPUP_TYPE } from './popup.js';
21import { StructuredCloneMap } from './util/StructuredCloneMap.js';21import { StructuredCloneMap } from './util/StructuredCloneMap.js';
22import { renderTemplateAsync } from './templates.js';22import { renderTemplateAsync } from './templates.js';
23import { t } from './i18n.js';23import { t } from './i18n.js';
24import { accountStorage } from './util/AccountStorage.js';
2425
25export const world_info_insertion_strategy = {26export const world_info_insertion_strategy = {
26 evenly: 0,27 evenly: 0,
@@ -400,6 +401,12 @@ class WorldInfoTimedEffects {
400 #entries = [];401 #entries = [];
401402
402 /**403 /**
404 * Is this a dry run?
405 * @type {boolean}
406 */
407 #isDryRun = false;
408
409 /**
403 * Buffer for active timed effects.410 * Buffer for active timed effects.
404 * @type {Record<TimedEffectType, WIScanEntry[]>}411 * @type {Record<TimedEffectType, WIScanEntry[]>}
405 */412 */
@@ -448,10 +455,12 @@ class WorldInfoTimedEffects {
448 * Initialize the timed effects with the given messages.455 * Initialize the timed effects with the given messages.
449 * @param {string[]} chat Array of chat messages456 * @param {string[]} chat Array of chat messages
450 * @param {WIScanEntry[]} entries Array of entries457 * @param {WIScanEntry[]} entries Array of entries
458 * @param {boolean} isDryRun Whether the operation is a dry run
451 */459 */
452 constructor(chat, entries) {460 constructor(chat, entries, isDryRun = false) {
453 this.#chat = chat;461 this.#chat = chat;
454 this.#entries = entries;462 this.#entries = entries;
463 this.#isDryRun = isDryRun;
455 this.#ensureChatMetadata();464 this.#ensureChatMetadata();
456 }465 }
457466
@@ -583,8 +592,10 @@ class WorldInfoTimedEffects {
583 * Checks for timed effects on chat messages.592 * Checks for timed effects on chat messages.
584 */593 */
585 checkTimedEffects() {594 checkTimedEffects() {
595 if (!this.#isDryRun) {
586 this.#checkTimedEffectOfType('sticky', this.#buffer.sticky, this.#onEnded.sticky.bind(this));596 this.#checkTimedEffectOfType('sticky', this.#buffer.sticky, this.#onEnded.sticky.bind(this));
587 this.#checkTimedEffectOfType('cooldown', this.#buffer.cooldown, this.#onEnded.cooldown.bind(this));597 this.#checkTimedEffectOfType('cooldown', this.#buffer.cooldown, this.#onEnded.cooldown.bind(this));
598 }
588 this.#checkDelayEffect(this.#buffer.delay);599 this.#checkDelayEffect(this.#buffer.delay);
589 }600 }
590601
@@ -629,6 +640,7 @@ class WorldInfoTimedEffects {
629 * @param {WIScanEntry[]} activatedEntries Entries that were activated640 * @param {WIScanEntry[]} activatedEntries Entries that were activated
630 */641 */
631 setTimedEffects(activatedEntries) {642 setTimedEffects(activatedEntries) {
643 if (this.#isDryRun) return;
632 for (const entry of activatedEntries) {644 for (const entry of activatedEntries) {
633 this.#setTimedEffectOfType('sticky', entry);645 this.#setTimedEffectOfType('sticky', entry);
634 this.#setTimedEffectOfType('cooldown', entry);646 this.#setTimedEffectOfType('cooldown', entry);
@@ -645,6 +657,9 @@ class WorldInfoTimedEffects {
645 if (!this.isValidEffectType(type)) {657 if (!this.isValidEffectType(type)) {
646 return;658 return;
647 }659 }
660 if (this.#isDryRun && type !== 'delay') {
661 return;
662 }
648663
649 const key = this.#getEntryKey(entry);664 const key = this.#getEntryKey(entry);
650 delete chat_metadata.timedWorldInfo[type][key];665 delete chat_metadata.timedWorldInfo[type][key];
@@ -858,7 +873,7 @@ export function setWorldInfoSettings(settings, data) {
858 $('#world_editor_select').append(`<option value='${i}'>${item}</option>`);873 $('#world_editor_select').append(`<option value='${i}'>${item}</option>`);
859 });874 });
860875
861 $('#world_info_sort_order').val(localStorage.getItem(SORT_ORDER_KEY) || '0');876 $('#world_info_sort_order').val(accountStorage.getItem(SORT_ORDER_KEY) || '0');
862 $('#world_info').trigger('change');877 $('#world_info').trigger('change');
863 $('#world_editor_select').trigger('change');878 $('#world_editor_select').trigger('change');
864879
@@ -1708,7 +1723,7 @@ export async function loadWorldInfo(name) {
1708 return null;1723 return null;
1709}1724}
17101725
1711async function updateWorldInfoList() {1726export async function updateWorldInfoList() {
1712 const result = await fetch('/api/settings/get', {1727 const result = await fetch('/api/settings/get', {
1713 method: 'POST',1728 method: 'POST',
1714 headers: getRequestHeaders(),1729 headers: getRequestHeaders(),
@@ -1933,13 +1948,13 @@ function displayWorldEntries(name, data, navigation = navigation_option.none, fl
1933 if (typeof navigation === 'number' && Number(navigation) >= 0) {1948 if (typeof navigation === 'number' && Number(navigation) >= 0) {
1934 const data = getDataArray();1949 const data = getDataArray();
1935 const uidIndex = data.findIndex(x => x.uid === navigation);1950 const uidIndex = data.findIndex(x => x.uid === navigation);
1936 const perPage = Number(localStorage.getItem(storageKey)) || perPageDefault;1951 const perPage = Number(accountStorage.getItem(storageKey)) || perPageDefault;
1937 startPage = Math.floor(uidIndex / perPage) + 1;1952 startPage = Math.floor(uidIndex / perPage) + 1;
1938 }1953 }
19391954
1940 $('#world_info_pagination').pagination({1955 $('#world_info_pagination').pagination({
1941 dataSource: getDataArray,1956 dataSource: getDataArray,
1942 pageSize: Number(localStorage.getItem(storageKey)) || perPageDefault,1957 pageSize: Number(accountStorage.getItem(storageKey)) || perPageDefault,
1943 sizeChangerOptions: [10, 25, 50, 100, 500, 1000],1958 sizeChangerOptions: [10, 25, 50, 100, 500, 1000],
1944 showSizeChanger: true,1959 showSizeChanger: true,
1945 pageRange: 1,1960 pageRange: 1,
@@ -1969,7 +1984,7 @@ function displayWorldEntries(name, data, navigation = navigation_option.none, fl
1969 worldEntriesList.append(blocks);1984 worldEntriesList.append(blocks);
1970 },1985 },
1971 afterSizeSelectorChange: function (e) {1986 afterSizeSelectorChange: function (e) {
1972 localStorage.setItem(storageKey, e.target.value);1987 accountStorage.setItem(storageKey, e.target.value);
1973 },1988 },
1974 afterPaging: function () {1989 afterPaging: function () {
1975 $('#world_popup_entries_list textarea[name="comment"]').each(function () {1990 $('#world_popup_entries_list textarea[name="comment"]').each(function () {
@@ -2174,7 +2189,7 @@ function verifyWorldInfoSearchSortRule() {
2174 // If search got cleared, we make sure to hide the option and go back to the one before2189 // If search got cleared, we make sure to hide the option and go back to the one before
2175 if (!searchTerm && !isHidden) {2190 if (!searchTerm && !isHidden) {
2176 searchOption.attr('hidden', '');2191 searchOption.attr('hidden', '');
2177 selector.val(localStorage.getItem(SORT_ORDER_KEY) || '0');2192 selector.val(accountStorage.getItem(SORT_ORDER_KEY) || '0');
2178 }2193 }
2179}2194}
21802195
@@ -2423,7 +2438,9 @@ export async function getWorldEntry(name, data, entry) {
2423 setWIOriginalDataValue(data, uid, originalDataValueName, data.entries[uid][entryPropName]);2438 setWIOriginalDataValue(data, uid, originalDataValueName, data.entries[uid][entryPropName]);
2424 await saveWorldInfo(name, data);2439 await saveWorldInfo(name, data);
2425 }2440 }
2441 $(this).toggleClass('empty', !data.entries[uid][entryPropName].length);
2426 });2442 });
2443 input.toggleClass('empty', !entry[entryPropName].length);
2427 input.on('select2:select', /** @type {function(*):void} */ event => updateWorldEntryKeyOptionsCache([event.params.data]));2444 input.on('select2:select', /** @type {function(*):void} */ event => updateWorldEntryKeyOptionsCache([event.params.data]));
2428 input.on('select2:unselect', /** @type {function(*):void} */ event => updateWorldEntryKeyOptionsCache([event.params.data], { remove: true }));2445 input.on('select2:unselect', /** @type {function(*):void} */ event => updateWorldEntryKeyOptionsCache([event.params.data], { remove: true }));
24292446
@@ -2458,6 +2475,7 @@ export async function getWorldEntry(name, data, entry) {
2458 data.entries[uid][entryPropName] = splitKeywordsAndRegexes(value);2475 data.entries[uid][entryPropName] = splitKeywordsAndRegexes(value);
2459 setWIOriginalDataValue(data, uid, originalDataValueName, data.entries[uid][entryPropName]);2476 setWIOriginalDataValue(data, uid, originalDataValueName, data.entries[uid][entryPropName]);
2460 await saveWorldInfo(name, data);2477 await saveWorldInfo(name, data);
2478 $(this).toggleClass('empty', !data.entries[uid][entryPropName].length);
2461 }2479 }
2462 });2480 });
2463 input.val(entry[entryPropName].join(', ')).trigger('input', { skipReset: true });2481 input.val(entry[entryPropName].join(', ')).trigger('input', { skipReset: true });
@@ -3435,7 +3453,7 @@ async function _save(name, data) {
3435 headers: getRequestHeaders(),3453 headers: getRequestHeaders(),
3436 body: JSON.stringify({ name: name, data: data }),3454 body: JSON.stringify({ name: name, data: data }),
3437 });3455 });
3438 eventSource.emit(event_types.WORLDINFO_UPDATED, name, data);3456 await eventSource.emit(event_types.WORLDINFO_UPDATED, name, data);
3439}3457}
34403458
34413459
@@ -3847,7 +3865,7 @@ export async function checkWorldInfo(chat, maxContext, isDryRun) {
3847 const context = getContext();3865 const context = getContext();
3848 const buffer = new WorldInfoBuffer(chat);3866 const buffer = new WorldInfoBuffer(chat);
38493867
3850 console.debug(`[WI] --- START WI SCAN (on ${chat.length} messages) ---`);3868 console.debug(`[WI] --- START WI SCAN (on ${chat.length} messages)${isDryRun ? ' (DRY RUN)' : ''} ---`);
38513869
3852 // Combine the chat3870 // Combine the chat
38533871
@@ -3879,9 +3897,9 @@ export async function checkWorldInfo(chat, maxContext, isDryRun) {
38793897
3880 console.debug(`[WI] Context size: ${maxContext}; WI budget: ${budget} (max% = ${world_info_budget}%, cap = ${world_info_budget_cap})`);3898 console.debug(`[WI] Context size: ${maxContext}; WI budget: ${budget} (max% = ${world_info_budget}%, cap = ${world_info_budget_cap})`);
3881 const sortedEntries = await getSortedEntries();3899 const sortedEntries = await getSortedEntries();
3882 const timedEffects = new WorldInfoTimedEffects(chat, sortedEntries);3900 const timedEffects = new WorldInfoTimedEffects(chat, sortedEntries, isDryRun);
38833901
3884 !isDryRun && timedEffects.checkTimedEffects();3902 timedEffects.checkTimedEffects();
38853903
3886 if (sortedEntries.length === 0) {3904 if (sortedEntries.length === 0) {
3887 return { worldInfoBefore: '', worldInfoAfter: '', WIDepthEntries: [], EMEntries: [], allActivatedEntries: new Set() };3905 return { worldInfoBefore: '', worldInfoAfter: '', WIDepthEntries: [], EMEntries: [], allActivatedEntries: new Set() };
@@ -4324,12 +4342,12 @@ export async function checkWorldInfo(chat, maxContext, isDryRun) {
4324 context.setExtensionPrompt(NOTE_MODULE_NAME, ANWithWI, chat_metadata[metadata_keys.position], chat_metadata[metadata_keys.depth], extension_settings.note.allowWIScan, chat_metadata[metadata_keys.role]);4342 context.setExtensionPrompt(NOTE_MODULE_NAME, ANWithWI, chat_metadata[metadata_keys.position], chat_metadata[metadata_keys.depth], extension_settings.note.allowWIScan, chat_metadata[metadata_keys.role]);
4325 }4343 }
43264344
4327 !isDryRun && timedEffects.setTimedEffects(Array.from(allActivatedEntries.values()));4345 timedEffects.setTimedEffects(Array.from(allActivatedEntries.values()));
4328 buffer.resetExternalEffects();4346 buffer.resetExternalEffects();
4329 timedEffects.cleanUp();4347 timedEffects.cleanUp();
43304348
4331 console.log(`[WI] Adding ${allActivatedEntries.size} entries to prompt`, Array.from(allActivatedEntries.values()));4349 console.log(`[WI] ${isDryRun ? 'Hypothetically adding' : 'Adding'} ${allActivatedEntries.size} entries to prompt`, Array.from(allActivatedEntries.values()));
4332 console.debug('[WI] --- DONE ---');4350 console.debug(`[WI] --- DONE${isDryRun ? ' (DRY RUN)' : ''} ---`);
43334351
4334 return { worldInfoBefore, worldInfoAfter, EMEntries, WIDepthEntries, allActivatedEntries: new Set(allActivatedEntries.values()) };4352 return { worldInfoBefore, worldInfoAfter, EMEntries, WIDepthEntries, allActivatedEntries: new Set(allActivatedEntries.values()) };
4335}4353}
@@ -4658,7 +4676,7 @@ function convertNovelLorebook(inputObj) {
4658 return outputObj;4676 return outputObj;
4659}4677}
46604678
4661function convertCharacterBook(characterBook) {4679export function convertCharacterBook(characterBook) {
4662 const result = { entries: {}, originalData: characterBook };4680 const result = { entries: {}, originalData: characterBook };
46634681
4664 characterBook.entries.forEach((entry, index) => {4682 characterBook.entries.forEach((entry, index) => {
@@ -4736,8 +4754,8 @@ export function checkEmbeddedWorld(chid) {
4736 // Only show the alert once per character4754 // Only show the alert once per character
4737 const checkKey = `AlertWI_${characters[chid].avatar}`;4755 const checkKey = `AlertWI_${characters[chid].avatar}`;
4738 const worldName = characters[chid]?.data?.extensions?.world;4756 const worldName = characters[chid]?.data?.extensions?.world;
4739 if (!localStorage.getItem(checkKey) && (!worldName || !world_names.includes(worldName))) {4757 if (!accountStorage.getItem(checkKey) && (!worldName || !world_names.includes(worldName))) {
4740 localStorage.setItem(checkKey, 'true');4758 accountStorage.setItem(checkKey, 'true');
47414759
4742 if (power_user.world_import_dialog) {4760 if (power_user.world_import_dialog) {
4743 const html = `<h3>This character has an embedded World/Lorebook.</h3>4761 const html = `<h3>This character has an embedded World/Lorebook.</h3>
@@ -5181,7 +5199,7 @@ jQuery(() => {
5181 $('#world_info_sort_order').on('change', function () {5199 $('#world_info_sort_order').on('change', function () {
5182 const value = String($(this).find(':selected').val());5200 const value = String($(this).find(':selected').val());
5183 // Save sort order, but do not save search sorting, as this is a temporary sorting option5201 // Save sort order, but do not save search sorting, as this is a temporary sorting option
5184 if (value !== 'search') localStorage.setItem(SORT_ORDER_KEY, value);5202 if (value !== 'search') accountStorage.setItem(SORT_ORDER_KEY, value);
5185 updateEditor(navigation_option.none);5203 updateEditor(navigation_option.none);
5186 });5204 });
51875205
public/style.css+144 -56
@@ -55,6 +55,10 @@
55 --interactable-outline-color: var(--white100);55 --interactable-outline-color: var(--white100);
56 --interactable-outline-color-faint: var(--white20a);56 --interactable-outline-color-faint: var(--white20a);
5757
58 --reasoning-body-color: var(--SmartThemeEmColor);
59 --reasoning-em-color: color-mix(in srgb, var(--SmartThemeEmColor) 67%, var(--SmartThemeBlurTintColor) 33%);
60 --reasoning-saturation: 0.5;
61
5862
59 /*Default Theme, will be changed by ToolCool Color Picker*/63 /*Default Theme, will be changed by ToolCool Color Picker*/
60 --SmartThemeBodyColor: rgb(220, 220, 210);64 --SmartThemeBodyColor: rgb(220, 220, 210);
@@ -106,6 +110,8 @@
106 --tool-cool-color-picker-btn-bg: transparent;110 --tool-cool-color-picker-btn-bg: transparent;
107 --tool-cool-color-picker-btn-border-color: transparent;111 --tool-cool-color-picker-btn-border-color: transparent;
108112
113 --mes-right-spacing: 30px;
114
109 --avatar-base-height: 50px;115 --avatar-base-height: 50px;
110 --avatar-base-width: 50px;116 --avatar-base-width: 50px;
111 --avatar-base-border-radius: 2px;117 --avatar-base-border-radius: 2px;
@@ -291,6 +297,10 @@ input[type='checkbox']:focus-visible {
291 color: var(--SmartThemeEmColor);297 color: var(--SmartThemeEmColor);
292}298}
293299
300.tokenItemizingMaintext {
301 font-size: calc(var(--mainFontSize) * 0.8);
302}
303
294.tokenGraph {304.tokenGraph {
295 border-radius: 10px;305 border-radius: 10px;
296 border: 1px solid var(--SmartThemeBorderColor);306 border: 1px solid var(--SmartThemeBorderColor);
@@ -373,18 +383,56 @@ input[type='checkbox']:focus-visible {
373383
374.mes_reasoning {384.mes_reasoning {
375 display: block;385 display: block;
376 border: 1px solid var(--SmartThemeBorderColor);386 border-left: 2px solid var(--reasoning-body-color);
377 background-color: var(--black30a);387 border-radius: 2px;
378 border-radius: 5px;
379 padding: 5px;388 padding: 5px;
380 margin: 5px 0;389 padding-left: 14px;
390 margin-bottom: 0.5em;
381 overflow-y: auto;391 overflow-y: auto;
392 color: hsl(from var(--reasoning-body-color) h calc(s * var(--reasoning-saturation)) l);
393}
394
395.mes_reasoning_details {
396 margin-right: var(--mes-right-spacing);
382}397}
383398
384.mes_reasoning_summary {399.mes_reasoning_details .mes_reasoning_summary {
400 list-style: none;
401 margin-right: calc(var(--mes-right-spacing) * -1);
402}
403
404.mes_reasoning_details summary::-webkit-details-marker {
405 display: none;
406}
407
408.mes_reasoning *:last-child {
409 margin-bottom: 0;
410}
411
412.mes_reasoning_header_block {
413 flex-grow: 1;
414}
415
416.mes_reasoning_header {
385 cursor: pointer;417 cursor: pointer;
386 position: relative;418 position: relative;
387 margin: 2px;419 user-select: none;
420 margin: 0.5em 2px;
421 padding: 7px 14px;
422 padding-right: calc(0.7em + 14px);
423 border-radius: 5px;
424 background-color: var(--grey30);
425 font-size: calc(var(--mainFontSize) * 0.9);
426 align-items: baseline;
427}
428
429.mes:has(.mes_reasoning:empty) .mes_reasoning_header {
430 cursor: default;
431}
432
433/* TWIMC: Remove with custom CSS to show the icon */
434.mes_reasoning_header>.icon-svg {
435 display: none;
388}436}
389437
390@supports not selector(:has(*)) {438@supports not selector(:has(*)) {
@@ -394,29 +442,41 @@ input[type='checkbox']:focus-visible {
394}442}
395443
396.mes_bias:empty,444.mes_bias:empty,
397.mes_reasoning:empty,445.mes:not(.reasoning) .mes_reasoning_details,
398.mes_reasoning_details:has(.mes_reasoning:empty),
399.mes_block:has(.edit_textarea) .mes_reasoning_details,
400.mes_reasoning_details:not([open]) .mes_reasoning_actions,446.mes_reasoning_details:not([open]) .mes_reasoning_actions,
401.mes_reasoning_details:has(.reasoning_edit_textarea) .mes_reasoning,447.mes_reasoning_details:has(.reasoning_edit_textarea) .mes_reasoning,
402.mes_reasoning_details:not(:has(.reasoning_edit_textarea)) .mes_reasoning_actions .mes_button.mes_reasoning_edit_done,448.mes_reasoning_details:has(.reasoning_edit_textarea) .mes_reasoning_header,
403.mes_reasoning_details:not(:has(.reasoning_edit_textarea)) .mes_reasoning_actions .mes_button.mes_reasoning_edit_cancel,449.mes_reasoning_details:has(.reasoning_edit_textarea) .mes_reasoning_actions .mes_button:not(.edit_button),
404.mes_reasoning_details:has(.reasoning_edit_textarea) .mes_reasoning_actions .mes_button:not(.mes_reasoning_edit_done, .mes_reasoning_edit_cancel) {450.mes_reasoning_details:not(:has(.reasoning_edit_textarea)) .mes_reasoning_actions .edit_button,
451.mes_block:has(.edit_textarea):has(.reasoning_edit_textarea) .mes_reasoning_actions,
452.mes.reasoning:not([data-reasoning-state="hidden"]) .mes_edit_add_reasoning,
453.mes:has(.mes_reasoning:empty) .mes_reasoning_arrow,
454.mes:has(.mes_reasoning:empty) .mes_reasoning,
455.mes:has(.mes_reasoning:empty) .mes_reasoning_copy {
405 display: none;456 display: none;
406}457}
407458
408.mes_reasoning_actions {459.mes[data-reasoning-state="hidden"] .mes_edit_add_reasoning {
460 background-color: color-mix(in srgb, var(--SmartThemeQuoteColor) 33%, var(--SmartThemeBlurTintColor) 66%);
461}
462
463/** If hidden reasoning should not be shown, we hide all blocks that don't have content */
464#chat:not([data-show-hidden-reasoning="true"]):not(:has(.reasoning_edit_textarea)) .mes:has(.mes_reasoning:empty) .mes_reasoning_details {
465 display: none;
466}
467
468.mes_reasoning_details .mes_reasoning_arrow {
409 position: absolute;469 position: absolute;
410 right: 0;470 top: 50%;
411 top: 0;471 right: 7px;
472 transform: translateY(-50%);
473 font-size: calc(var(--mainFontSize) * 0.7);
474 width: calc(var(--mainFontSize) * 0.7);
475 height: calc(var(--mainFontSize) * 0.7);
476}
412477
413 display: flex;478.mes_reasoning_details:not([open]) .mes_reasoning_arrow {
414 gap: 4px;479 transform: translateY(-50%) rotate(180deg);
415 flex-wrap: nowrap;
416 justify-content: flex-end;
417 transition: all 200ms;
418 overflow-x: hidden;
419 padding: 1px;
420}480}
421481
422.mes_reasoning_summary>span {482.mes_reasoning_summary>span {
@@ -424,21 +484,36 @@ input[type='checkbox']:focus-visible {
424}484}
425485
426.mes_text i,486.mes_text i,
427.mes_text em,487.mes_text em {
488 color: var(--SmartThemeEmColor);
489}
428.mes_reasoning i,490.mes_reasoning i,
429.mes_reasoning em {491.mes_reasoning em {
430 color: var(--SmartThemeEmColor);492 color: hsl(from var(--reasoning-em-color) h calc(s * var(--reasoning-saturation)) l);
431}493}
432494
433.mes_text u,495.mes_text q i,
434.mes_reasoning u {496.mes_text q em {
497 color: inherit;
498}
499.mes_reasoning q i,
500.mes_reasoning q em {
501 color: hsl(from var(--SmartThemeQuoteColor) h calc(s * var(--reasoning-saturation)) l);
502}
503
504.mes_text u {
435 color: var(--SmartThemeUnderlineColor);505 color: var(--SmartThemeUnderlineColor);
436}506}
507.mes_reasoning u {
508 color: hsl(from var(--SmartThemeUnderlineColor) h calc(s * var(--reasoning-saturation)) l);
509}
437510
438.mes_text q,511.mes_text q {
439.mes_reasoning q {
440 color: var(--SmartThemeQuoteColor);512 color: var(--SmartThemeQuoteColor);
441}513}
514.mes_reasoning q {
515 color: hsl(from var(--SmartThemeQuoteColor) h calc(s * var(--reasoning-saturation)) l);
516}
442517
443.mes_text font[color] em,518.mes_text font[color] em,
444.mes_text font[color] i,519.mes_text font[color] i,
@@ -1126,13 +1201,8 @@ body .panelControlBar {
1126 /*only affects bubblechat to make it sit nicely at the bottom*/1201 /*only affects bubblechat to make it sit nicely at the bottom*/
1127}1202}
11281203
1129.last_mes:has(.mes_text:empty):has(.mes_reasoning_details[open]) .mes_reasoning:not(:empty) {1204.last_mes:has(.mes_text:empty):has(.mes_reasoning_details) .mes_reasoning:not(:empty) {
1130 margin-bottom: 30px;1205 margin-bottom: var(--mes-right-spacing);
1131}
1132
1133.last_mes .mes_reasoning,
1134.last_mes .mes_text {
1135 padding-right: 30px;
1136}1206}
11371207
1138/* SWIPE RELATED STYLES*/1208/* SWIPE RELATED STYLES*/
@@ -1363,6 +1433,7 @@ body.swipeAllMessages .mes:not(.last_mes) .swipes-counter {
1363 padding-left: 0;1433 padding-left: 0;
1364 padding-top: 5px;1434 padding-top: 5px;
1365 padding-bottom: 5px;1435 padding-bottom: 5px;
1436 padding-right: var(--mes-right-spacing);
1366}1437}
13671438
1368br {1439br {
@@ -2849,9 +2920,8 @@ select option:not(:checked) {
2849 color: var(--active) !important;2920 color: var(--active) !important;
2850}2921}
28512922
2852#instruct_enabled_label .menu_button:not(.toggleEnabled),2923.menu_button.togglable:not(.toggleEnabled) {
2853#sysprompt_enabled_label .menu_button:not(.toggleEnabled) {2924 color: red;
2854 color: Red;
2855}2925}
28562926
2857.displayBlock {2927.displayBlock {
@@ -3048,6 +3118,7 @@ input[type=search]:focus::-webkit-search-cancel-button {
3048.mes_block .ch_name {3118.mes_block .ch_name {
3049 max-width: 100%;3119 max-width: 100%;
3050 min-height: 22px;3120 min-height: 22px;
3121 align-items: flex-start;
3051}3122}
30523123
3053/*applies to both groups and solos chars in the char list*/3124/*applies to both groups and solos chars in the char list*/
@@ -4275,7 +4346,13 @@ input[type="range"]::-webkit-slider-thumb {
4275 transition: 0.3s ease-in-out;4346 transition: 0.3s ease-in-out;
4276}4347}
42774348
4278.mes_edit_buttons .menu_button {4349.mes_reasoning_actions {
4350 margin: 0;
4351 margin-top: 0.5em;
4352}
4353
4354.mes_edit_buttons .menu_button,
4355.mes_reasoning_actions .edit_button {
4279 opacity: 0.5;4356 opacity: 0.5;
4280 padding: 0px;4357 padding: 0px;
4281 font-size: 1rem;4358 font-size: 1rem;
@@ -4288,6 +4365,12 @@ input[type="range"]::-webkit-slider-thumb {
4288 align-items: center;4365 align-items: center;
4289}4366}
42904367
4368.mes_reasoning_actions .edit_button {
4369 margin-bottom: 0.5em;
4370 opacity: 1;
4371 filter: brightness(0.7);
4372}
4373
4291.mes_reasoning_edit_cancel,4374.mes_reasoning_edit_cancel,
4292.mes_edit_cancel.menu_button {4375.mes_edit_cancel.menu_button {
4293 background-color: var(--crimson70a);4376 background-color: var(--crimson70a);
@@ -4314,6 +4397,14 @@ input[type="range"]::-webkit-slider-thumb {
4314 field-sizing: content;4397 field-sizing: content;
4315}4398}
43164399
4400body[data-generating="true"] #send_but,
4401body[data-generating="true"] #mes_continue,
4402body[data-generating="true"] #mes_impersonate,
4403body[data-generating="true"] #chat .last_mes .mes_buttons,
4404body[data-generating="true"] #chat .last_mes .mes_reasoning_actions {
4405 display: none;
4406}
4407
4317#anchor_order {4408#anchor_order {
4318 margin-bottom: 15px;4409 margin-bottom: 15px;
4319}4410}
@@ -4653,23 +4744,6 @@ body .ui-widget-content li:hover {
4653 opacity: 1;4744 opacity: 1;
4654}4745}
46554746
4656.typing_indicator {
4657 position: sticky;
4658 bottom: 10px;
4659 margin: 10px;
4660 opacity: 0.85;
4661 text-shadow: 0px 0px calc(var(--shadowWidth) * 1px) var(--SmartThemeShadowColor);
4662 order: 9999;
4663}
4664
4665.typing_indicator:after {
4666 display: inline-block;
4667 vertical-align: bottom;
4668 animation: ellipsis steps(4, end) 1500ms infinite;
4669 content: "";
4670 width: 0px;
4671}
4672
4673#group_avatar_preview .missing-avatar {4747#group_avatar_preview .missing-avatar {
4674 display: inline;4748 display: inline;
4675 vertical-align: middle;4749 vertical-align: middle;
@@ -5758,11 +5832,13 @@ body:not(.movingUI) .drawer-content.maximized {
5758 overflow-wrap: anywhere;5832 overflow-wrap: anywhere;
5759}5833}
57605834
5835#SystemPromptColumn summary,
5761#InstructSequencesColumn summary {5836#InstructSequencesColumn summary {
5762 font-size: 0.95em;5837 font-size: 0.95em;
5763 cursor: pointer;5838 cursor: pointer;
5764}5839}
57655840
5841#SystemPromptColumn details,
5766#InstructSequencesColumn details:not(:last-of-type) {5842#InstructSequencesColumn details:not(:last-of-type) {
5767 margin-bottom: 5px;5843 margin-bottom: 5px;
5768}5844}
@@ -5927,6 +6003,18 @@ body:not(.movingUI) .drawer-content.maximized {
5927 flex: 1;6003 flex: 1;
5928}6004}
59296005
6006.oneline-dropdown label {
6007 margin-top: 3px;
6008 margin-bottom: 5px;
6009 flex-grow: 1;
6010 text-align: left;
6011}
6012
6013.oneline-dropdown select {
6014 min-width: fit-content;
6015 width: 40%;
6016}
6017
5930.multiline {6018.multiline {
5931 white-space: pre-wrap;6019 white-space: pre-wrap;
5932}6020}
server.js+133 -18
@@ -4,6 +4,7 @@
4import fs from 'node:fs';4import fs from 'node:fs';
5import http from 'node:http';5import http from 'node:http';
6import https from 'node:https';6import https from 'node:https';
7import os from 'os';
7import path from 'node:path';8import path from 'node:path';
8import util from 'node:util';9import util from 'node:util';
9import net from 'node:net';10import net from 'node:net';
@@ -29,6 +30,7 @@ import bodyParser from 'body-parser';
2930
30// net related library imports31// net related library imports
31import fetch from 'node-fetch';32import fetch from 'node-fetch';
33import ipRegex from 'ip-regex';
3234
33// Unrestrict console logs display limit35// Unrestrict console logs display limit
34util.inspect.defaultOptions.maxArrayLength = null;36util.inspect.defaultOptions.maxArrayLength = null;
@@ -56,8 +58,10 @@ import {
56import getWebpackServeMiddleware from './src/middleware/webpack-serve.js';58import getWebpackServeMiddleware from './src/middleware/webpack-serve.js';
57import basicAuthMiddleware from './src/middleware/basicAuth.js';59import basicAuthMiddleware from './src/middleware/basicAuth.js';
58import whitelistMiddleware from './src/middleware/whitelist.js';60import whitelistMiddleware from './src/middleware/whitelist.js';
61import accessLoggerMiddleware, { getAccessLogPath, migrateAccessLog } from './src/middleware/accessLogWriter.js';
59import multerMonkeyPatch from './src/middleware/multerMonkeyPatch.js';62import multerMonkeyPatch from './src/middleware/multerMonkeyPatch.js';
60import initRequestProxy from './src/request-proxy.js';63import initRequestProxy from './src/request-proxy.js';
64import getCacheBusterMiddleware from './src/middleware/cacheBuster.js';
61import {65import {
62 getVersion,66 getVersion,
63 getConfigValue,67 getConfigValue,
@@ -65,7 +69,11 @@ import {
65 forwardFetchResponse,69 forwardFetchResponse,
66 removeColorFormatting,70 removeColorFormatting,
67 getSeparator,71 getSeparator,
72 stringToBool,
73 urlHostnameToIPv6,
74 canResolve,
68 safeReadFileSync,75 safeReadFileSync,
76 setupLogLevel,
69} from './src/util.js';77} from './src/util.js';
70import { UPLOADS_DIRECTORY } from './src/constants.js';78import { UPLOADS_DIRECTORY } from './src/constants.js';
71import { ensureThumbnailCache } from './src/endpoints/thumbnails.js';79import { ensureThumbnailCache } from './src/endpoints/thumbnails.js';
@@ -125,6 +133,8 @@ if (process.versions && process.versions.node && process.versions.node.match(/20
125const DEFAULT_PORT = 8000;133const DEFAULT_PORT = 8000;
126const DEFAULT_AUTORUN = false;134const DEFAULT_AUTORUN = false;
127const DEFAULT_LISTEN = false;135const DEFAULT_LISTEN = false;
136const DEFAULT_LISTEN_ADDRESS_IPV6 = '[::]';
137const DEFAULT_LISTEN_ADDRESS_IPV4 = '0.0.0.0';
128const DEFAULT_CORS_PROXY = false;138const DEFAULT_CORS_PROXY = false;
129const DEFAULT_WHITELIST = true;139const DEFAULT_WHITELIST = true;
130const DEFAULT_ACCOUNTS = false;140const DEFAULT_ACCOUNTS = false;
@@ -149,11 +159,11 @@ const DEFAULT_PROXY_BYPASS = [];
149const cliArguments = yargs(hideBin(process.argv))159const cliArguments = yargs(hideBin(process.argv))
150 .usage('Usage: <your-start-script> <command> [options]')160 .usage('Usage: <your-start-script> <command> [options]')
151 .option('enableIPv6', {161 .option('enableIPv6', {
152 type: 'boolean',162 type: 'string',
153 default: null,163 default: null,
154 describe: `Enables IPv6.\n[config default: ${DEFAULT_ENABLE_IPV6}]`,164 describe: `Enables IPv6.\n[config default: ${DEFAULT_ENABLE_IPV6}]`,
155 }).option('enableIPv4', {165 }).option('enableIPv4', {
156 type: 'boolean',166 type: 'string',
157 default: null,167 default: null,
158 describe: `Enables IPv4.\n[config default: ${DEFAULT_ENABLE_IPV4}]`,168 describe: `Enables IPv4.\n[config default: ${DEFAULT_ENABLE_IPV4}]`,
159 }).option('port', {169 }).option('port', {
@@ -180,6 +190,14 @@ const cliArguments = yargs(hideBin(process.argv))
180 type: 'boolean',190 type: 'boolean',
181 default: null,191 default: null,
182 describe: `SillyTavern is listening on all network interfaces (Wi-Fi, LAN, localhost). If false, will limit it only to internal localhost (127.0.0.1).\nIf not provided falls back to yaml config 'listen'.\n[config default: ${DEFAULT_LISTEN}]`,192 describe: `SillyTavern is listening on all network interfaces (Wi-Fi, LAN, localhost). If false, will limit it only to internal localhost (127.0.0.1).\nIf not provided falls back to yaml config 'listen'.\n[config default: ${DEFAULT_LISTEN}]`,
193 }).option('listenAddressIPv6', {
194 type: 'string',
195 default: null,
196 describe: 'Set SillyTavern to listen to a specific IPv6 address. If not set, it will fallback to listen to all.\n[config default: [::] ]',
197 }).option('listenAddressIPv4', {
198 type: 'string',
199 default: null,
200 describe: 'Set SillyTavern to listen to a specific IPv4 address. If not set, it will fallback to listen to all.\n[config default: 0.0.0.0 ]',
183 }).option('corsProxy', {201 }).option('corsProxy', {
184 type: 'boolean',202 type: 'boolean',
185 default: null,203 default: null,
@@ -226,7 +244,6 @@ const cliArguments = yargs(hideBin(process.argv))
226 describe: 'Request proxy URL (HTTP or SOCKS protocols)',244 describe: 'Request proxy URL (HTTP or SOCKS protocols)',
227 }).option('requestProxyBypass', {245 }).option('requestProxyBypass', {
228 type: 'array',246 type: 'array',
229 default: null,
230 describe: 'Request proxy bypass list (space separated list of hosts)',247 describe: 'Request proxy bypass list (space separated list of hosts)',
231 }).parseSync();248 }).parseSync();
232249
@@ -242,27 +259,46 @@ app.use(helmet({
242app.use(compression());259app.use(compression());
243app.use(responseTime());260app.use(responseTime());
244261
262
263/** @type {number} */
245const server_port = cliArguments.port ?? process.env.SILLY_TAVERN_PORT ?? getConfigValue('port', DEFAULT_PORT);264const server_port = cliArguments.port ?? process.env.SILLY_TAVERN_PORT ?? getConfigValue('port', DEFAULT_PORT);
265/** @type {boolean} */
246const autorun = (cliArguments.autorun ?? getConfigValue('autorun', DEFAULT_AUTORUN)) && !cliArguments.ssl;266const autorun = (cliArguments.autorun ?? getConfigValue('autorun', DEFAULT_AUTORUN)) && !cliArguments.ssl;
267/** @type {boolean} */
247const listen = cliArguments.listen ?? getConfigValue('listen', DEFAULT_LISTEN);268const listen = cliArguments.listen ?? getConfigValue('listen', DEFAULT_LISTEN);
269/** @type {string} */
270const listenAddressIPv6 = cliArguments.listenAddressIPv6 ?? getConfigValue('listenAddress.ipv6', DEFAULT_LISTEN_ADDRESS_IPV6);
271/** @type {string} */
272const listenAddressIPv4 = cliArguments.listenAddressIPv4 ?? getConfigValue('listenAddress.ipv4', DEFAULT_LISTEN_ADDRESS_IPV4);
273/** @type {boolean} */
248const enableCorsProxy = cliArguments.corsProxy ?? getConfigValue('enableCorsProxy', DEFAULT_CORS_PROXY);274const enableCorsProxy = cliArguments.corsProxy ?? getConfigValue('enableCorsProxy', DEFAULT_CORS_PROXY);
249const enableWhitelist = cliArguments.whitelist ?? getConfigValue('whitelistMode', DEFAULT_WHITELIST);275const enableWhitelist = cliArguments.whitelist ?? getConfigValue('whitelistMode', DEFAULT_WHITELIST);
276/** @type {string} */
250const dataRoot = cliArguments.dataRoot ?? getConfigValue('dataRoot', './data');277const dataRoot = cliArguments.dataRoot ?? getConfigValue('dataRoot', './data');
278/** @type {boolean} */
251const disableCsrf = cliArguments.disableCsrf ?? getConfigValue('disableCsrfProtection', DEFAULT_CSRF_DISABLED);279const disableCsrf = cliArguments.disableCsrf ?? getConfigValue('disableCsrfProtection', DEFAULT_CSRF_DISABLED);
252const basicAuthMode = cliArguments.basicAuthMode ?? getConfigValue('basicAuthMode', DEFAULT_BASIC_AUTH);280const basicAuthMode = cliArguments.basicAuthMode ?? getConfigValue('basicAuthMode', DEFAULT_BASIC_AUTH);
253const perUserBasicAuth = getConfigValue('perUserBasicAuth', DEFAULT_PER_USER_BASIC_AUTH);281const perUserBasicAuth = getConfigValue('perUserBasicAuth', DEFAULT_PER_USER_BASIC_AUTH);
282/** @type {boolean} */
254const enableAccounts = getConfigValue('enableUserAccounts', DEFAULT_ACCOUNTS);283const enableAccounts = getConfigValue('enableUserAccounts', DEFAULT_ACCOUNTS);
255284
256const uploadsPath = path.join(dataRoot, UPLOADS_DIRECTORY);285const uploadsPath = path.join(dataRoot, UPLOADS_DIRECTORY);
257286
258const enableIPv6 = cliArguments.enableIPv6 ?? getConfigValue('protocol.ipv6', DEFAULT_ENABLE_IPV6);
259const enableIPv4 = cliArguments.enableIPv4 ?? getConfigValue('protocol.ipv4', DEFAULT_ENABLE_IPV4);
260287
288/** @type {boolean | "auto"} */
289let enableIPv6 = stringToBool(cliArguments.enableIPv6) ?? getConfigValue('protocol.ipv6', DEFAULT_ENABLE_IPV6);
290/** @type {boolean | "auto"} */
291let enableIPv4 = stringToBool(cliArguments.enableIPv4) ?? getConfigValue('protocol.ipv4', DEFAULT_ENABLE_IPV4);
292
293/** @type {string} */
261const autorunHostname = cliArguments.autorunHostname ?? getConfigValue('autorunHostname', DEFAULT_AUTORUN_HOSTNAME);294const autorunHostname = cliArguments.autorunHostname ?? getConfigValue('autorunHostname', DEFAULT_AUTORUN_HOSTNAME);
295/** @type {number} */
262const autorunPortOverride = cliArguments.autorunPortOverride ?? getConfigValue('autorunPortOverride', DEFAULT_AUTORUN_PORT);296const autorunPortOverride = cliArguments.autorunPortOverride ?? getConfigValue('autorunPortOverride', DEFAULT_AUTORUN_PORT);
263297
298/** @type {boolean} */
264const dnsPreferIPv6 = cliArguments.dnsPreferIPv6 ?? getConfigValue('dnsPreferIPv6', DEFAULT_PREFER_IPV6);299const dnsPreferIPv6 = cliArguments.dnsPreferIPv6 ?? getConfigValue('dnsPreferIPv6', DEFAULT_PREFER_IPV6);
265300
301/** @type {boolean} */
266const avoidLocalhost = cliArguments.avoidLocalhost ?? getConfigValue('avoidLocalhost', DEFAULT_AVOID_LOCALHOST);302const avoidLocalhost = cliArguments.avoidLocalhost ?? getConfigValue('avoidLocalhost', DEFAULT_AVOID_LOCALHOST);
267303
268const proxyEnabled = cliArguments.requestProxyEnabled ?? getConfigValue('requestProxy.enabled', DEFAULT_PROXY_ENABLED);304const proxyEnabled = cliArguments.requestProxyEnabled ?? getConfigValue('requestProxy.enabled', DEFAULT_PROXY_ENABLED);
@@ -279,7 +315,19 @@ if (dnsPreferIPv6) {
279 console.log('Preferring IPv4 for DNS resolution');315 console.log('Preferring IPv4 for DNS resolution');
280}316}
281317
282if (!enableIPv6 && !enableIPv4) {318
319const ipOptions = [true, 'auto', false];
320
321if (!ipOptions.includes(enableIPv6)) {
322 console.warn(color.red('`protocol: ipv6` option invalid'), '\n use:', ipOptions, '\n setting to:', DEFAULT_ENABLE_IPV6);
323 enableIPv6 = DEFAULT_ENABLE_IPV6;
324}
325if (!ipOptions.includes(enableIPv4)) {
326 console.warn(color.red('`protocol: ipv4` option invalid'), '\n use:', ipOptions, '\n setting to:', DEFAULT_ENABLE_IPV4);
327 enableIPv4 = DEFAULT_ENABLE_IPV4;
328}
329
330if (enableIPv6 === false && enableIPv4 === false) {
283 console.error('error: You can\'t disable all internet protocols: at least IPv6 or IPv4 must be enabled.');331 console.error('error: You can\'t disable all internet protocols: at least IPv6 or IPv4 must be enabled.');
284 process.exit(1);332 process.exit(1);
285}333}
@@ -292,9 +340,17 @@ const CORS = cors({
292340
293app.use(CORS);341app.use(CORS);
294342
295if (listen && basicAuthMode) app.use(basicAuthMiddleware);343if (listen && basicAuthMode) {
344 app.use(basicAuthMiddleware);
345}
296346
297app.use(whitelistMiddleware(enableWhitelist, listen));347if (enableWhitelist) {
348 app.use(whitelistMiddleware());
349}
350
351if (listen) {
352 app.use(accessLoggerMiddleware());
353}
298354
299if (enableCorsProxy) {355if (enableCorsProxy) {
300 app.use(bodyParser.json({356 app.use(bodyParser.json({
@@ -364,6 +420,55 @@ function getSessionCookieAge() {
364 return undefined;420 return undefined;
365}421}
366422
423/**
424 * Checks the network interfaces to determine the presence of IPv6 and IPv4 addresses.
425 *
426 * @returns {Promise<[boolean, boolean, boolean, boolean]>} A promise that resolves to an array containing:
427 * - [0]: `hasIPv6` (boolean) - Whether the computer has any IPv6 address, including (`::1`).
428 * - [1]: `hasIPv4` (boolean) - Whether the computer has any IPv4 address, including (`127.0.0.1`).
429 * - [2]: `hasIPv6Local` (boolean) - Whether the computer has local IPv6 address (`::1`).
430 * - [3]: `hasIPv4Local` (boolean) - Whether the computer has local IPv4 address (`127.0.0.1`).
431 */
432async function getHasIP() {
433 let hasIPv6 = false;
434 let hasIPv6Local = false;
435
436 let hasIPv4 = false;
437 let hasIPv4Local = false;
438
439 const interfaces = os.networkInterfaces();
440
441 for (const iface of Object.values(interfaces)) {
442 if (iface === undefined) {
443 continue;
444 }
445
446 for (const info of iface) {
447 if (info.family === 'IPv6') {
448 hasIPv6 = true;
449 if (info.address === '::1') {
450 hasIPv6Local = true;
451 }
452 }
453
454 if (info.family === 'IPv4') {
455 hasIPv4 = true;
456 if (info.address === '127.0.0.1') {
457 hasIPv4Local = true;
458 }
459 }
460 if (hasIPv6 && hasIPv4 && hasIPv6Local && hasIPv4Local) break;
461 }
462 if (hasIPv6 && hasIPv4 && hasIPv6Local && hasIPv4Local) break;
463 }
464 return [
465 hasIPv6,
466 hasIPv4,
467 hasIPv6Local,
468 hasIPv4Local,
469 ];
470}
471
367app.use(cookieSession({472app.use(cookieSession({
368 name: getCookieSessionName(),473 name: getCookieSessionName(),
369 sameSite: 'strict',474 sameSite: 'strict',
@@ -419,7 +524,7 @@ if (!disableCsrf) {
419524
420// Static files525// Static files
421// Host index page526// Host index page
422app.get('/', (request, response) => {527app.get('/', getCacheBusterMiddleware(), (request, response) => {
423 if (shouldRedirectToLogin(request)) {528 if (shouldRedirectToLogin(request)) {
424 const query = request.url.split('?')[1];529 const query = request.url.split('?')[1];
425 const redirectUrl = query ? `/login?${query}` : '/login';530 const redirectUrl = query ? `/login?${query}` : '/login';
@@ -459,7 +564,13 @@ app.use('/api/users', usersPublicRouter);
459564
460// Everything below this line requires authentication565// Everything below this line requires authentication
461app.use(requireLoginMiddleware);566app.use(requireLoginMiddleware);
462app.get('/api/ping', (_, response) => response.sendStatus(204));567app.get('/api/ping', (request, response) => {
568 if (request.query.extend && request.session) {
569 request.session.touch = Date.now();
570 }
571
572 response.sendStatus(204);
573});
463574
464// File uploads575// File uploads
465app.use(multer({ dest: uploadsPath, limits: { fieldSize: 10 * 1024 * 1024 } }).single('avatar'));576app.use(multer({ dest: uploadsPath, limits: { fieldSize: 10 * 1024 * 1024 } }).single('avatar'));
@@ -627,13 +738,13 @@ app.use('/api/azure', azureRouter);
627738
628const tavernUrlV6 = new URL(739const tavernUrlV6 = new URL(
629 (cliArguments.ssl ? 'https://' : 'http://') +740 (cliArguments.ssl ? 'https://' : 'http://') +
630 (listen ? '[::]' : '[::1]') +741 (listen ? (ipRegex.v6({ exact: true }).test(listenAddressIPv6) ? listenAddressIPv6 : '[::]') : '[::1]') +
631 (':' + server_port),742 (':' + server_port),
632);743);
633744
634const tavernUrl = new URL(745const tavernUrl = new URL(
635 (cliArguments.ssl ? 'https://' : 'http://') +746 (cliArguments.ssl ? 'https://' : 'http://') +
636 (listen ? '0.0.0.0' : '127.0.0.1') +747 (listen ? (ipRegex.v4({ exact: true }).test(listenAddressIPv4) ? listenAddressIPv4 : '0.0.0.0') : '127.0.0.1') +
637 (':' + server_port),748 (':' + server_port),
638);749);
639750
@@ -657,6 +768,7 @@ const preSetupTasks = async function () {
657 await checkForNewContent(directories);768 await checkForNewContent(directories);
658 await ensureThumbnailCache();769 await ensureThumbnailCache();
659 cleanUploads();770 cleanUploads();
771 migrateAccessLog();
660772
661 await settingsInit();773 await settingsInit();
662 await statsInit();774 await statsInit();
@@ -693,20 +805,23 @@ const preSetupTasks = async function () {
693805
694/**806/**
695 * Gets the hostname to use for autorun in the browser.807 * Gets the hostname to use for autorun in the browser.
696 * @returns {string} The hostname to use for autorun808 * @param {boolean} useIPv6 If use IPv6
809 * @param {boolean} useIPv4 If use IPv4
810 * @returns Promise<string> The hostname to use for autorun
697 */811 */
698function getAutorunHostname() {812async function getAutorunHostname(useIPv6, useIPv4) {
699 if (autorunHostname === 'auto') {813 if (autorunHostname === 'auto') {
700 if (enableIPv6 && enableIPv4) {814 let localhostResolve = await canResolve('localhost', useIPv6, useIPv4);
701 if (avoidLocalhost) return '[::1]';815
702 return 'localhost';816 if (useIPv6 && useIPv4) {
817 return (avoidLocalhost || !localhostResolve) ? '[::1]' : 'localhost';
703 }818 }
704819
705 if (enableIPv6) {820 if (useIPv6) {
src/constants.js+0 -0
src/endpoints/anthropic.js+0 -0
src/endpoints/assets.js+0 -0
src/endpoints/azure.js+0 -0
src/endpoints/backends/chat-completions.js+0 -0
src/endpoints/backends/kobold.js+0 -0
src/endpoints/backends/scale-alt.js+0 -0
src/endpoints/backends/text-completions.js+0 -0
src/endpoints/backgrounds.js+0 -0
src/endpoints/caption.js+0 -0
src/endpoints/characters.js+0 -0
src/endpoints/chats.js+0 -0
src/endpoints/classify.js+0 -0
src/endpoints/content-manager.js+0 -0
src/endpoints/extensions.js+0 -0
src/endpoints/files.js+0 -0
src/endpoints/google.js+0 -0
src/endpoints/groups.js+0 -0
src/endpoints/horde.js+0 -0
src/endpoints/images.js+0 -0
src/endpoints/novelai.js+0 -0
src/endpoints/openai.js+0 -0
src/endpoints/openrouter.js+0 -0
src/endpoints/presets.js+0 -0
src/endpoints/search.js+0 -0
src/endpoints/secrets.js+0 -0
src/endpoints/settings.js+0 -0
src/endpoints/speech.js+0 -0
src/endpoints/sprites.js+0 -0
src/endpoints/stable-diffusion.js+0 -0
src/endpoints/stats.js+0 -0
src/endpoints/thumbnails.js+0 -0
src/endpoints/tokenizers.js+0 -0
src/endpoints/translate.js+0 -0
src/endpoints/users-admin.js+0 -0
src/endpoints/users-private.js+0 -0
src/endpoints/users-public.js+0 -0
src/endpoints/vectors.js+0 -0
src/endpoints/worldinfo.js+0 -0
src/express-common.js+0 -0
src/middleware/accessLogWriter.js+0 -0
src/middleware/cacheBuster.js+0 -0
src/middleware/webpack-serve.js+0 -0
src/middleware/whitelist.js+0 -0
src/plugin-loader.js+0 -0
src/prompt-converters.js+0 -0
src/request-proxy.js+0 -0
src/users.js+0 -0
src/util.js+0 -0
src/vectors/cohere-vectors.js+0 -0
src/vectors/extras-vectors.js+0 -0
src/vectors/makersuite-vectors.js+0 -0
src/vectors/nomicai-vectors.js+0 -0
src/vectors/openai-vectors.js+0 -0
webpack.config.js+0 -0
Diff truncated