Merge pull request #4925 from SillyTavern/staging Staging

1c0e7ea9556afcc3ae55c888287dff67a6aa42fc

Cohee <18619528+Cohee1207@users.noreply.github.com>

Signed
167 files changed, +4795 -1305Ignore whitespace
.dockerignore+4 -0
@@ -15,3 +15,7 @@ access.log
15/public/scripts/extensions/third-party15/public/scripts/extensions/third-party
16/colab16/colab
17.gemini17.gemini
18/docker/config
19/docker/extensions
20/docker/data
21/docker/plugins
.github/readme.md+1 -0
@@ -79,6 +79,7 @@ GNU Affero General Public License for more details.**
79* Portions of CncAnon's TavernAITurbo mod used with permission79* Portions of CncAnon's TavernAITurbo mod used with permission
80* Visual Novel Mode inspired by the work of PepperTaco (<https://github.com/peppertaco/Tavern/>)80* Visual Novel Mode inspired by the work of PepperTaco (<https://github.com/peppertaco/Tavern/>)
81* Noto Sans font by Google (OFL license)81* Noto Sans font by Google (OFL license)
82* Lexer/Parser by Chevrotain (Apache-2.0 license) <https://github.com/chevrotain/chevrotain>
82* Icon theme by Font Awesome <https://fontawesome.com> (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)83* Icon theme by Font Awesome <https://fontawesome.com> (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
83* Default content by @OtisAlejandro (Seraphina character and lorebook) and @kallmeflocc (10K Discord Users Celebratory Background)84* Default content by @OtisAlejandro (Seraphina character and lorebook) and @kallmeflocc (10K Discord Users Celebratory Background)
84* Docker guide by [@mrguymiah](https://github.com/mrguymiah) and [@Bronya-Rand](https://github.com/Bronya-Rand)85* Docker guide by [@mrguymiah](https://github.com/mrguymiah) and [@Bronya-Rand](https://github.com/Bronya-Rand)
default/config.yaml+10 -0
@@ -287,6 +287,16 @@ claude:
287gemini:287gemini:
288 # API endpoint version ("v1beta" or "v1alpha")288 # API endpoint version ("v1beta" or "v1alpha")
289 apiVersion: 'v1beta'289 apiVersion: 'v1beta'
290 # Enables caching of the system prompt (if supported). Only for OpenRouter.
291 # -- IMPORTANT! --
292 # Use only when the prompt before the chat history is static and doesn't change between requests
293 # (e.g {{random}} macro or lorebooks not as in-chat injections).
294 # Otherwise, you'll just waste money on cache misses.
295 enableSystemPromptCache: false
296 # https://ai.google.dev/gemini-api/docs/imagen#imagen-configuration
297 image:
298 # Leave empty to use the API-default value.
299 personGeneration: 'allow_adult'
290# -- SERVER PLUGIN CONFIGURATION --300# -- SERVER PLUGIN CONFIGURATION --
291enableServerPlugins: false301enableServerPlugins: false
292# Attempt to automatically update server plugins on startup302# Attempt to automatically update server plugins on startup
default/content/presets/instruct/Mistral V7-Tekken.json+1 -1
@@ -1,6 +1,6 @@
1{1{
2 "input_sequence": "[INST]",2 "input_sequence": "[INST]",
3 "output_sequence": " ",3 "output_sequence": "",
4 "last_output_sequence": "",4 "last_output_sequence": "",
5 "system_sequence": "[SYSTEM_PROMPT]",5 "system_sequence": "[SYSTEM_PROMPT]",
6 "stop_sequence": "</s>",6 "stop_sequence": "</s>",
default/content/presets/openai/Default.json+4 -3
@@ -8,6 +8,8 @@
8 "openrouter_sort_models": "alphabetically",8 "openrouter_sort_models": "alphabetically",
9 "ai21_model": "jamba-large",9 "ai21_model": "jamba-large",
10 "mistralai_model": "mistral-large-latest",10 "mistralai_model": "mistral-large-latest",
11 "chutes_model": "deepseek-ai/DeepSeek-V3-0324",
12 "chutes_sort_models": "alphabetically",
11 "electronhub_model": "gpt-4o-mini",13 "electronhub_model": "gpt-4o-mini",
12 "electronhub_sort_models": "alphabetically",14 "electronhub_sort_models": "alphabetically",
13 "electronhub_group_models": false,15 "electronhub_group_models": false,
@@ -28,7 +30,6 @@
28 "repetition_penalty": 1,30 "repetition_penalty": 1,
29 "openai_max_context": 4095,31 "openai_max_context": 4095,
30 "openai_max_tokens": 300,32 "openai_max_tokens": 300,
31 "wrap_in_quotes": false,
32 "names_behavior": 0,33 "names_behavior": 0,
33 "send_if_empty": "",34 "send_if_empty": "",
34 "impersonation_prompt": "[Write your next reply from the point of view of {{user}}, using the chat history so far as a guideline for the writing style of {{user}}. Don't write as {{char}} or system. Don't describe actions of {{char}}.]",35 "impersonation_prompt": "[Write your next reply from the point of view of {{user}}, using the chat history so far as a guideline for the writing style of {{user}}. Don't write as {{char}} or system. Don't describe actions of {{char}}.]",
@@ -231,9 +232,9 @@
231 "show_external_models": false,232 "show_external_models": false,
232 "assistant_prefill": "",233 "assistant_prefill": "",
233 "assistant_impersonation": "",234 "assistant_impersonation": "",
234 "claude_use_sysprompt": false,235 "use_sysprompt": false,
235 "squash_system_messages": false,236 "squash_system_messages": false,
236 "image_inlining": false,237 "media_inlining": true,
237 "bypass_status_check": false,238 "bypass_status_check": false,
238 "continue_prefill": false,239 "continue_prefill": false,
239 "continue_postfix": " ",240 "continue_postfix": " ",
default/content/settings.json+4 -2
@@ -191,6 +191,7 @@
191 "custom_stopping_strings_macro": true,191 "custom_stopping_strings_macro": true,
192 "fuzzy_search": true,192 "fuzzy_search": true,
193 "encode_tags": false,193 "encode_tags": false,
194 "experimental_macro_engine": false,
194 "enableLabMode": false,195 "enableLabMode": false,
195 "enableZenSliders": false,196 "enableZenSliders": false,
196 "ui_mode": 1,197 "ui_mode": 1,
@@ -463,7 +464,6 @@
463 "stream_openai": true,464 "stream_openai": true,
464 "openai_max_context": 4095,465 "openai_max_context": 4095,
465 "openai_max_tokens": 300,466 "openai_max_tokens": 300,
466 "wrap_in_quotes": false,
467 "prompts": [467 "prompts": [
468 {468 {
469 "name": "Main Prompt",469 "name": "Main Prompt",
@@ -632,6 +632,8 @@
632 "show_external_models": false,632 "show_external_models": false,
633 "proxy_password": "",633 "proxy_password": "",
634 "assistant_prefill": "",634 "assistant_prefill": "",
635 "assistant_impersonation": ""635 "assistant_impersonation": "",
636 "use_sysprompt": false,
637 "squash_system_messages": false
636 }638 }
637}639}
package-lock.json+103 -15
@@ -1,12 +1,12 @@
1{1{
2 "name": "sillytavern",2 "name": "sillytavern",
3 "version": "1.14.0",3 "version": "1.15.0",
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.14.0",9 "version": "1.15.0",
10 "hasInstallScript": true,10 "hasInstallScript": true,
11 "license": "AGPL-3.0",11 "license": "AGPL-3.0",
12 "dependencies": {12 "dependencies": {
@@ -45,6 +45,7 @@
45 "bowser": "^2.12.1",45 "bowser": "^2.12.1",
46 "bytes": "^3.1.2",46 "bytes": "^3.1.2",
47 "chalk": "^5.6.0",47 "chalk": "^5.6.0",
48 "chevrotain": "^11.0.3",
48 "command-exists": "^1.2.9",49 "command-exists": "^1.2.9",
49 "compression": "^1.8.1",50 "compression": "^1.8.1",
50 "cookie-parser": "^1.4.6",51 "cookie-parser": "^1.4.6",
@@ -59,7 +60,6 @@
59 "express": "^4.21.0",60 "express": "^4.21.0",
60 "form-data": "^4.0.4",61 "form-data": "^4.0.4",
61 "fuse.js": "^7.1.0",62 "fuse.js": "^7.1.0",
62 "google-translate-api-browser": "^3.0.1",
63 "google-translate-api-x": "^10.7.2",63 "google-translate-api-x": "^10.7.2",
64 "handlebars": "^4.7.8",64 "handlebars": "^4.7.8",
65 "helmet": "^8.1.0",65 "helmet": "^8.1.0",
@@ -73,7 +73,7 @@
73 "is-docker": "^3.0.0",73 "is-docker": "^3.0.0",
74 "localforage": "^1.10.0",74 "localforage": "^1.10.0",
75 "lodash": "^4.17.21",75 "lodash": "^4.17.21",
76 "mime-types": "^3.0.1",76 "mime-types": "^3.0.2",
77 "moment": "^2.30.1",77 "moment": "^2.30.1",
78 "morphdom": "^2.7.7",78 "morphdom": "^2.7.7",
79 "multer": "^2.0.2",79 "multer": "^2.0.2",
@@ -106,6 +106,7 @@
106 "sillytavern": "src/server-global.js"106 "sillytavern": "src/server-global.js"
107 },107 },
108 "devDependencies": {108 "devDependencies": {
109 "@chevrotain/types": "^11.0.3",
109 "@types/archiver": "^6.0.3",110 "@types/archiver": "^6.0.3",
110 "@types/bytes": "^3.1.5",111 "@types/bytes": "^3.1.5",
111 "@types/command-exists": "^1.2.3",112 "@types/command-exists": "^1.2.3",
@@ -134,7 +135,8 @@
134 "@types/yauzl": "^2.10.3",135 "@types/yauzl": "^2.10.3",
135 "eslint": "^8.57.1",136 "eslint": "^8.57.1",
136 "eslint-plugin-jest": "^27.9.0",137 "eslint-plugin-jest": "^27.9.0",
137 "eslint-plugin-jsdoc": "^48.10.0"138 "eslint-plugin-jsdoc": "^48.10.0",
139 "eslint-plugin-playwright": "^2.3.0"
138 },140 },
139 "engines": {141 "engines": {
140 "node": ">= 18"142 "node": ">= 18"
@@ -171,6 +173,45 @@
171 "integrity": "sha512-KlmTftToTtmb6aLVdne4NluS+POWputPF5J8v25UN/EQS+K9vahWEIe1NPRSFqBQclObkqHaj7JOnFrmnSm5MA==",173 "integrity": "sha512-KlmTftToTtmb6aLVdne4NluS+POWputPF5J8v25UN/EQS+K9vahWEIe1NPRSFqBQclObkqHaj7JOnFrmnSm5MA==",
172 "license": "Apache-2.0"174 "license": "Apache-2.0"
173 },175 },
176 "node_modules/@chevrotain/cst-dts-gen": {
177 "version": "11.0.3",
178 "resolved": "https://registry.npmjs.org/@chevrotain/cst-dts-gen/-/cst-dts-gen-11.0.3.tgz",
179 "integrity": "sha512-BvIKpRLeS/8UbfxXxgC33xOumsacaeCKAjAeLyOn7Pcp95HiRbrpl14S+9vaZLolnbssPIUuiUd8IvgkRyt6NQ==",
180 "license": "Apache-2.0",
181 "dependencies": {
182 "@chevrotain/gast": "11.0.3",
183 "@chevrotain/types": "11.0.3",
184 "lodash-es": "4.17.21"
185 }
186 },
187 "node_modules/@chevrotain/gast": {
188 "version": "11.0.3",
189 "resolved": "https://registry.npmjs.org/@chevrotain/gast/-/gast-11.0.3.tgz",
190 "integrity": "sha512-+qNfcoNk70PyS/uxmj3li5NiECO+2YKZZQMbmjTqRI3Qchu8Hig/Q9vgkHpI3alNjr7M+a2St5pw5w5F6NL5/Q==",
191 "license": "Apache-2.0",
192 "dependencies": {
193 "@chevrotain/types": "11.0.3",
194 "lodash-es": "4.17.21"
195 }
196 },
197 "node_modules/@chevrotain/regexp-to-ast": {
198 "version": "11.0.3",
199 "resolved": "https://registry.npmjs.org/@chevrotain/regexp-to-ast/-/regexp-to-ast-11.0.3.tgz",
200 "integrity": "sha512-1fMHaBZxLFvWI067AVbGJav1eRY7N8DDvYCTwGBiE/ytKBgP8azTdgyrKyWZ9Mfh09eHWb5PgTSO8wi7U824RA==",
201 "license": "Apache-2.0"
202 },
203 "node_modules/@chevrotain/types": {
204 "version": "11.0.3",
205 "resolved": "https://registry.npmjs.org/@chevrotain/types/-/types-11.0.3.tgz",
206 "integrity": "sha512-gsiM3G8b58kZC2HaWR50gu6Y1440cHiJ+i3JUvcp/35JchYejb2+5MVeJK0iKThYpAa/P2PYFV4hoi44HD+aHQ==",
207 "license": "Apache-2.0"
208 },
209 "node_modules/@chevrotain/utils": {
210 "version": "11.0.3",
211 "resolved": "https://registry.npmjs.org/@chevrotain/utils/-/utils-11.0.3.tgz",
212 "integrity": "sha512-YslZMgtJUyuMbZ+aKvfF3x1f5liK4mWNxghFRv7jqRR9C3R3fAOGTTKvxXDa2Y1s9zSbcpuO0cAxDYsc9SrXoQ==",
213 "license": "Apache-2.0"
214 },
174 "node_modules/@es-joy/jsdoccomment": {215 "node_modules/@es-joy/jsdoccomment": {
175 "version": "0.46.0",216 "version": "0.46.0",
176 "resolved": "https://registry.npmjs.org/@es-joy/jsdoccomment/-/jsdoccomment-0.46.0.tgz",217 "resolved": "https://registry.npmjs.org/@es-joy/jsdoccomment/-/jsdoccomment-0.46.0.tgz",
@@ -3381,6 +3422,20 @@
3381 "url": "https://github.com/sponsors/fb55"3422 "url": "https://github.com/sponsors/fb55"
3382 }3423 }
3383 },3424 },
3425 "node_modules/chevrotain": {
3426 "version": "11.0.3",
3427 "resolved": "https://registry.npmjs.org/chevrotain/-/chevrotain-11.0.3.tgz",
3428 "integrity": "sha512-ci2iJH6LeIkvP9eJW6gpueU8cnZhv85ELY8w8WiFtNjMHA5ad6pQLaJo9mEly/9qUyCpvqX8/POVUTf18/HFdw==",
3429 "license": "Apache-2.0",
3430 "dependencies": {
3431 "@chevrotain/cst-dts-gen": "11.0.3",
3432 "@chevrotain/gast": "11.0.3",
3433 "@chevrotain/regexp-to-ast": "11.0.3",
3434 "@chevrotain/types": "11.0.3",
3435 "@chevrotain/utils": "11.0.3",
3436 "lodash-es": "4.17.21"
3437 }
3438 },
3384 "node_modules/chrome-trace-event": {3439 "node_modules/chrome-trace-event": {
3385 "version": "1.0.4",3440 "version": "1.0.4",
3386 "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz",3441 "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz",
@@ -4539,6 +4594,35 @@
4539 "dev": true,4594 "dev": true,
4540 "license": "MIT"4595 "license": "MIT"
4541 },4596 },
4597 "node_modules/eslint-plugin-playwright": {
4598 "version": "2.3.0",
4599 "resolved": "https://registry.npmjs.org/eslint-plugin-playwright/-/eslint-plugin-playwright-2.3.0.tgz",
4600 "integrity": "sha512-7UeUuIb5SZrNkrUGb2F+iwHM97kn33/huajcVtAaQFCSMUYGNFvjzRPil5C0OIppslPfuOV68M/zsisXx+/ZvQ==",
4601 "dev": true,
4602 "license": "MIT",
4603 "dependencies": {
4604 "globals": "^16.4.0"
4605 },
4606 "engines": {
4607 "node": ">=16.9.0"
4608 },
4609 "peerDependencies": {
4610 "eslint": ">=8.40.0"
4611 }
4612 },
4613 "node_modules/eslint-plugin-playwright/node_modules/globals": {
4614 "version": "16.5.0",
4615 "resolved": "https://registry.npmjs.org/globals/-/globals-16.5.0.tgz",
4616 "integrity": "sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==",
4617 "dev": true,
4618 "license": "MIT",
4619 "engines": {
4620 "node": ">=18"
4621 },
4622 "funding": {
4623 "url": "https://github.com/sponsors/sindresorhus"
4624 }
4625 },
4542 "node_modules/eslint-scope": {4626 "node_modules/eslint-scope": {
4543 "version": "7.2.2",4627 "version": "7.2.2",
4544 "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz",4628 "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz",
@@ -5360,12 +5444,6 @@
5360 "url": "https://github.com/sponsors/sindresorhus"5444 "url": "https://github.com/sponsors/sindresorhus"
5361 }5445 }
5362 },5446 },
5363 "node_modules/google-translate-api-browser": {
5364 "version": "3.0.1",
5365 "resolved": "https://registry.npmjs.org/google-translate-api-browser/-/google-translate-api-browser-3.0.1.tgz",
5366 "integrity": "sha512-KTLodkyGBWMK9IW6QIeJ2zCuju4Z0CLpbkADKo+yLhbSTD4l+CXXpQ/xaynGVAzeBezzJG6qn8MLeqOq3SmW0A==",
5367 "license": "MIT"
5368 },
5369 "node_modules/google-translate-api-x": {5447 "node_modules/google-translate-api-x": {
5370 "version": "10.7.2",5448 "version": "10.7.2",
5371 "resolved": "https://registry.npmjs.org/google-translate-api-x/-/google-translate-api-x-10.7.2.tgz",5449 "resolved": "https://registry.npmjs.org/google-translate-api-x/-/google-translate-api-x-10.7.2.tgz",
@@ -6285,6 +6363,12 @@
6285 "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",6363 "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
6286 "license": "MIT"6364 "license": "MIT"
6287 },6365 },
6366 "node_modules/lodash-es": {
6367 "version": "4.17.21",
6368 "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz",
6369 "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==",
6370 "license": "MIT"
6371 },
6288 "node_modules/lodash.get": {6372 "node_modules/lodash.get": {
6289 "version": "4.4.2",6373 "version": "4.4.2",
6290 "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz",6374 "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz",
@@ -6418,15 +6502,19 @@
6418 }6502 }
6419 },6503 },
6420 "node_modules/mime-types": {6504 "node_modules/mime-types": {
6421 "version": "3.0.1",6505 "version": "3.0.2",
6422 "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz",6506 "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz",
6423 "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==",6507 "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==",
6424 "license": "MIT",6508 "license": "MIT",
6425 "dependencies": {6509 "dependencies": {
6426 "mime-db": "^1.54.0"6510 "mime-db": "^1.54.0"
6427 },6511 },
6428 "engines": {6512 "engines": {
6429 "node": ">= 0.6"6513 "node": ">=18"
6514 },
6515 "funding": {
6516 "type": "opencollective",
6517 "url": "https://opencollective.com/express"
6430 }6518 }
6431 },6519 },
6432 "node_modules/mime-types/node_modules/mime-db": {6520 "node_modules/mime-types/node_modules/mime-db": {
package.json+6 -4
@@ -35,6 +35,7 @@
35 "bowser": "^2.12.1",35 "bowser": "^2.12.1",
36 "bytes": "^3.1.2",36 "bytes": "^3.1.2",
37 "chalk": "^5.6.0",37 "chalk": "^5.6.0",
38 "chevrotain": "^11.0.3",
38 "command-exists": "^1.2.9",39 "command-exists": "^1.2.9",
39 "compression": "^1.8.1",40 "compression": "^1.8.1",
40 "cookie-parser": "^1.4.6",41 "cookie-parser": "^1.4.6",
@@ -49,7 +50,6 @@
49 "express": "^4.21.0",50 "express": "^4.21.0",
50 "form-data": "^4.0.4",51 "form-data": "^4.0.4",
51 "fuse.js": "^7.1.0",52 "fuse.js": "^7.1.0",
52 "google-translate-api-browser": "^3.0.1",
53 "google-translate-api-x": "^10.7.2",53 "google-translate-api-x": "^10.7.2",
54 "handlebars": "^4.7.8",54 "handlebars": "^4.7.8",
55 "helmet": "^8.1.0",55 "helmet": "^8.1.0",
@@ -63,7 +63,7 @@
63 "is-docker": "^3.0.0",63 "is-docker": "^3.0.0",
64 "localforage": "^1.10.0",64 "localforage": "^1.10.0",
65 "lodash": "^4.17.21",65 "lodash": "^4.17.21",
66 "mime-types": "^3.0.1",66 "mime-types": "^3.0.2",
67 "moment": "^2.30.1",67 "moment": "^2.30.1",
68 "morphdom": "^2.7.7",68 "morphdom": "^2.7.7",
69 "multer": "^2.0.2",69 "multer": "^2.0.2",
@@ -113,7 +113,7 @@
113 "type": "git",113 "type": "git",
114 "url": "https://github.com/SillyTavern/SillyTavern.git"114 "url": "https://github.com/SillyTavern/SillyTavern.git"
115 },115 },
116 "version": "1.14.0",116 "version": "1.15.0",
117 "scripts": {117 "scripts": {
118 "start": "node server.js",118 "start": "node server.js",
119 "debug": "node --inspect server.js",119 "debug": "node --inspect server.js",
@@ -137,6 +137,7 @@
137 },137 },
138 "main": "server.js",138 "main": "server.js",
139 "devDependencies": {139 "devDependencies": {
140 "@chevrotain/types": "^11.0.3",
140 "@types/archiver": "^6.0.3",141 "@types/archiver": "^6.0.3",
141 "@types/bytes": "^3.1.5",142 "@types/bytes": "^3.1.5",
142 "@types/command-exists": "^1.2.3",143 "@types/command-exists": "^1.2.3",
@@ -164,7 +165,8 @@
164 "@types/yargs": "^17.0.33",165 "@types/yargs": "^17.0.33",
165 "@types/yauzl": "^2.10.3",166 "@types/yauzl": "^2.10.3",
166 "eslint": "^8.57.1",167 "eslint": "^8.57.1",
168 "eslint-plugin-jest": "^27.9.0",
167 "eslint-plugin-jsdoc": "^48.10.0",169 "eslint-plugin-jsdoc": "^48.10.0",
168 "eslint-plugin-jest": "^27.9.0"170 "eslint-plugin-playwright": "^2.3.0"
169 }171 }
170}172}
public/css/animations.css+20 -0
@@ -132,3 +132,23 @@
132 overflow-y: hidden;132 overflow-y: hidden;
133 }133 }
134}134}
135
136@keyframes slide {
137 0% {
138 transform: translateX(var(--slide-mes-x-start, 0px));
139 }
140
141 100% {
142 transform: translateX(var(--slide-mes-x-end, 0px));
143 }
144}
145
146@-webkit-keyframes slide {
147 0% {
148 transform: translateX(var(--slide-mes-x-start, 0px));
149 }
150
151 100% {
152 transform: translateX(var(--slide-mes-x-end, 0px));
153 }
154}
public/css/backgrounds.css+78 -8
@@ -63,6 +63,7 @@
63 display: flex;63 display: flex;
64 gap: 5px;64 gap: 5px;
65 width: 100%;65 width: 100%;
66 align-items: baseline;
66}67}
6768
68/* Control buttons in header */69/* Control buttons in header */
@@ -83,14 +84,6 @@
83 gap: 5px;84 gap: 5px;
84}85}
8586
86#bg-scrollable-content {
87 flex-grow: 1;
88 overflow-y: auto;
89 overflow-x: hidden;
90 padding: 0 5px 15px;
91 position: relative;
92}
93
94#bg_menu_content,87#bg_menu_content,
95#bg_custom_content {88#bg_custom_content {
96 display: grid;89 display: grid;
@@ -300,3 +293,80 @@
300.bg_example:focus-within .BGSampleTitle {293.bg_example:focus-within .BGSampleTitle {
301 opacity: 1;294 opacity: 1;
302}295}
296
297#bg_tabs {
298 display: flex;
299 flex-direction: column;
300 flex-grow: 1;
301 height: 100%;
302 overflow-y: hidden;
303 gap: 5px;
304}
305
306#bg_tabs .bg_tabs_list {
307 display: flex;
308 flex-direction: row;
309 gap: 5px;
310 background: none;
311 border: none;
312 margin: 5px 0;
313 padding: 0;
314 padding-right: 4em;
315 position: relative;
316}
317
318#bg_tabs .bg_tabs_list .bg_tab_button {
319 display: flex;
320 align-items: center;
321 justify-content: center;
322 text-align: center;
323 min-width: 8em;
324 list-style: none;
325 color: var(--SmartThemeBodyColor);
326 background-color: var(--SmartThemeBlurTintColor);
327 border: 1px solid var(--SmartThemeBorderColor);
328 border-radius: 5px;
329 padding: 0;
330 cursor: pointer;
331 font-weight: 600;
332 margin: 0 !important;
333 filter: none;
334 opacity: 0.8;
335 transition: opacity var(--animation-duration-2x);
336}
337
338#bg_tabs .bg_tabs_list .bg_tab_button:hover {
339 opacity: 1;
340}
341
342#bg_tabs .bg_tabs_list .bg_tab_button.ui-tabs-active {
343 background-color: color-mix(in srgb, var(--SmartThemeQuoteColor) 33%, var(--SmartThemeBlurTintColor) 66%);
344}
345
346#bg_tabs.ui-widget-content {
347 border: none !important;
348}
349
350#bg_tabs .bg_tabs_list::before,
351#bg_tabs .bg_tabs_list::after {
352 display: none;
353}
354
355#bg_tabs .bg_tabs_list .bg_tab_button a {
356 width: 100%;
357 height: 100%;
358 padding: 3px 5px;
359 cursor: pointer;
360 color: var(--SmartThemeBodyColor);
361}
362
363#bg_tabs .bg_tab_panel {
364 flex-grow: 1;
365 height: 100%;
366 overflow-y: auto;
367 padding: 10px;
368}
369
370#bg_custom_content:not(:empty)~#bg_chat_hint {
371 display: none;
372}
public/css/chat-backups.css+54 -0
@@ -0,0 +1,54 @@
1.chatBackupsList {
2 display: flex;
3 flex-direction: column;
4 gap: 5px;
5 transition: height var(--animation-duration) ease;
6 max-height: min(25dvh, 250px);
7 overflow-y: auto;
8 border-radius: 10px;
9 border: 1px solid var(--SmartThemeBorderColor);
10 padding: 5px;
11 width: 100%;
12}
13
14.chatBackupsList:not(.open),
15.chatBackupsList:empty {
16 display: none;
17}
18
19.chatBackupsListItem {
20 display: flex;
21 flex-wrap: wrap;
22 flex-direction: row;
23 align-items: center;
24 justify-content: space-between;
25 gap: 10px;
26 width: 100%;
27 border: 1px solid var(--SmartThemeBorderColor);
28 border-radius: 5px;
29 padding: 5px 7px;
30 font-size: smaller;
31 background: var(--black30a);
32}
33
34.chatBackupsListItemName {
35 opacity: 0.75;
36 flex-grow: 1;
37 overflow: hidden;
38 text-overflow: ellipsis;
39 white-space: nowrap;
40}
41
42.chatBackupsListItemInfo {
43 opacity: 0.75;
44}
45
46.chatBackupsListItemActions {
47 display: flex;
48 align-items: center;
49 gap: 5px;
50}
51
52.chatBackupsListItemActions .right_menu_button {
53 font-size: var(--mainFontSize);
54}
public/css/file-form.css+1 -0
@@ -33,6 +33,7 @@
33 display: flex;33 display: flex;
34 flex-direction: column;34 flex-direction: column;
35 gap: 0.5em;35 gap: 0.5em;
36 padding-right: var(--mes-right-spacing);
36}37}
3738
38.mes_file_container .right_menu_button {39.mes_file_container .right_menu_button {
public/css/macros.css+515 -0
@@ -0,0 +1,515 @@
1/**
2 * Macro System Styles
3 * Styles for the macro browser, documentation, and related UI components.
4 */
5
6/* =============================================================================
7 MACRO BROWSER
8 Dynamic documentation browser for macros, similar to SlashCommandBrowser.
9 ============================================================================= */
10
11.macroBrowser {
12 display: flex;
13 flex-direction: column;
14 gap: 0.5em;
15}
16
17/* -----------------------------------------------------------------------------
18 Toolbar (Search + Sort)
19 ----------------------------------------------------------------------------- */
20
21.macroBrowser .macro-toolbar {
22 display: flex;
23 gap: 1em;
24 align-items: center;
25 flex-wrap: wrap;
26}
27
28.macroBrowser .macro-search-label {
29 flex: 1 1 auto;
30 display: flex;
31 gap: 0.5em;
32 align-items: center;
33 min-width: 200px;
34 white-space: nowrap;
35}
36
37.macroBrowser .macro-search-input {
38 flex: 1 1 auto;
39 min-width: 100px;
40}
41
42.macroBrowser .macro-sort-btn {
43 flex: 0 0 auto;
44 white-space: nowrap;
45}
46
47.macroBrowser .macro-sort-btn.active {
48 background-color: var(--SmartThemeQuoteColor);
49}
50
51/* -----------------------------------------------------------------------------
52 Container (List + Details panels)
53 ----------------------------------------------------------------------------- */
54
55.macroBrowser .macro-container {
56 display: flex;
57 gap: 1em;
58 align-items: flex-start;
59 container-type: inline-size;
60}
61
62.macroBrowser .macro-list-panel {
63 flex: 1 1 60%;
64 display: flex;
65 flex-direction: column;
66 gap: 2px;
67 max-height: 60vh;
68 overflow-y: auto;
69}
70
71.macroBrowser .macro-details-panel {
72 flex: 0 0 40%;
73 position: sticky;
74 top: 0;
75 max-height: 60vh;
76 overflow-y: auto;
77 background: var(--SmartThemeBlurTintColor);
78 border-radius: 10px;
79 padding: 1em;
80}
81
82.macroBrowser .macro-details-placeholder {
83 opacity: 0.6;
84 text-align: center;
85 padding: 2em;
86}
87
88/* -----------------------------------------------------------------------------
89 Category Headers
90 ----------------------------------------------------------------------------- */
91
92.macroBrowser .macro-category-header {
93 font-weight: bold;
94 padding: 0.75em 0.5em 0.25em;
95 margin-top: 0.5em;
96 border-bottom: 1px solid var(--SmartThemeBorderColor);
97 border-radius: 10px 10px 0 0;
98 color: var(--SmartThemeQuoteColor);
99 position: sticky;
100 top: 0;
101 background: var(--SmartThemeChatTintColor);
102 z-index: 1;
103}
104
105.macroBrowser .macro-category-header:first-child {
106 margin-top: 0;
107}
108
109.macroBrowser .macro-category-header.isFiltered {
110 display: none;
111}
112
113/* -----------------------------------------------------------------------------
114 Macro List Items
115 Layout: [signature] [description (shrinks)] [source icon]
116 ----------------------------------------------------------------------------- */
117
118.macroBrowser .macro-item {
119 display: flex;
120 align-items: center;
121 gap: 0.5em;
122 padding: 0.5em;
123 border-radius: 4px;
124 cursor: pointer;
125 transition: background-color 0.15s;
126 min-width: 0; /* Allow children to shrink */
127}
128
129.macroBrowser .macro-item:hover {
130 background: var(--black30a);
131}
132
133.macroBrowser .macro-item.selected {
134 background: var(--SmartThemeQuoteColor);
135}
136
137.macroBrowser .macro-item.isFiltered {
138 display: none;
139}
140
141.macroBrowser .macro-signature {
142 padding: 0.2em 0;
143 font-family: var(--monoFontFamily);
144 font-size: 0.9em;
145 color: var(--SmartThemeQuoteColor);
146 flex: 0 1 auto; /* Can shrink, but prefers not to */
147 overflow: hidden;
148 text-overflow: ellipsis;
149 white-space: nowrap;
150 min-width: 3em; /* Minimum before fully hidden */
151}
152
153.macroBrowser .macro-desc-preview {
154 flex: 1 1 0; /* Shrinks first, starts at 0 basis */
155 opacity: 0.7;
156 font-size: 0.9em;
157 overflow: hidden;
158 text-overflow: ellipsis;
159 white-space: nowrap;
160 min-width: 0; /* Allow shrinking to nothing */
161}
162
163.macroBrowser .macro-source {
164 flex: 0 0 auto; /* Never shrinks */
165 font-size: 0.8em;
166 opacity: 0.7;
167}
168
169.macroBrowser .macro-source.isExtension.isThirdParty {
170 color: #f0a030;
171}
172
173.macroBrowser .macro-source.isExtension.isCore {
174 color: #30a0f0;
175}
176
177.macroBrowser .macro-source.isCore:not(.isExtension) {
178 color: #50c050;
179}
180
181/* -----------------------------------------------------------------------------
182 Macro Details Panel
183 ----------------------------------------------------------------------------- */
184
185.macro-details .macro-details-header {
186 display: flex;
187 align-items: flex-start;
188 justify-content: space-between;
189 gap: 0.5em;
190 margin-bottom: 0.75em;
191}
192
193.macro-details .macro-details-name {
194 font-family: var(--monoFontFamily);
195 font-size: 1.1em;
196 color: var(--SmartThemeQuoteColor);
197 background: var(--black30a);
198 padding: 0.15em 0.4em 0.3em;
199 border-radius: 4px;
200 word-break: break-all;
201 display: inline-block; /* Shrink-wrap to content */
202}
203
204.macro-details .macro-details-header > .macro-source {
205 flex: 0 0 auto;
206 font-size: 1em;
207 margin-top: 0.15em; /* Align with name top padding */
208}
209
210.macro-details .macro-category-badge {
211 display: inline-block;
212 font-size: 0.75em;
213 padding: 0.2em 0.6em;
214 border-radius: 10px;
215 background: color-mix(in srgb, var(--SmartThemeQuoteColor) 50%, transparent);
216 color: var(--SmartThemeBodyColor);
217 margin-bottom: 0.75em;
218}
219
220.macro-details .macro-details-section {
221 margin-bottom: 1em;
222}
223
224.macro-details .macro-details-label {
225 font-weight: bold;
226 font-size: 0.85em;
227 opacity: 0.8;
228 margin-bottom: 0.25em;
229 text-transform: uppercase;
230 letter-spacing: 0.05em;
231}
232
233.macro-details .macro-details-text {
234 line-height: 1.4;
235}
236
237.macro-details .macro-returns-content {
238 display: flex;
239 align-items: baseline;
240 gap: 0.5em;
241 flex-wrap: wrap;
242}
243
244/* -----------------------------------------------------------------------------
245 Arguments List (in details panel)
246 ----------------------------------------------------------------------------- */
247
248.macro-details .macro-args-list {
249 list-style: none;
250 padding: 0;
251 margin: 0;
252}
253
254.macro-details .macro-arg-item {
255 padding: 0.4em 0;
256 border-bottom: 1px solid var(--black30a);
257 display: flex;
258 flex-wrap: wrap;
259 align-items: baseline;
260 gap: 0.5em;
261}
262
263.macro-details .macro-arg-item:last-child {
264 border-bottom: none;
265}
266
267.macro-details .macro-arg-name {
268 font-family: var(--monoFontFamily);
269 font-weight: bold;
270 color: var(--SmartThemeQuoteColor);
271}
272
273.macro-details .macro-arg-type {
274 font-size: 0.75em;
275 padding: 0.1em 0.4em;
276 border-radius: 3px;
277 background: var(--black30a);
278 color: var(--SmartThemeEmColor);
279}
280
281.macro-details .macro-arg-required {
282 font-size: 0.8em;
283 color: var(--warning-color, #e8a97f);
284}
285
286.macro-details .macro-arg-desc {
287 flex: 1 1 100%;
288 font-size: 0.9em;
289 opacity: 0.8;
290}
291
292.macro-details .macro-arg-sample {
293 font-size: 0.85em;
294 opacity: 0.6;
295 font-style: italic;
296}
297
298.macro-details .macro-arg-list-info {
299 font-size: 0.85em;
300 opacity: 0.8;
301}
302
303.macro-details .macro-example-list {
304 list-style: disc;
305 padding-left: 1.5em;
306 margin: 0.25em 0;
307}
308
309.macro-details .macro-example-list li {
310 padding: 0.2em 0;
311}
312
313.macro-details .macro-example-list code {
314 font-family: var(--monoFontFamily);
315 background: var(--black30a);
316 padding: 0.1em 0.3em;
317 border-radius: 3px;
318}
319
320/* Alias indicator icon in list items */
321.macro-details .macro-alias-indicator {
322 flex: 0 0 auto;
323 font-size: 0.8em;
324 opacity: 0.6;
325 margin-left: 0.5em;
326 transform: rotate(90deg);
327}
328
329.macro-details .macro-item.isAlias .macro-signature {
330 opacity: 0.85;
331}
332
333/* Alias of indicator in details panel */
334.macro-details .macro-alias-of {
335 display: flex;
336 align-items: center;
337 gap: 0.5em;
338 margin: 0.5em 0;
339 padding: 0.4em 0.6em;
340 background: var(--black30a);
341 border-radius: 4px;
342 font-size: 0.9em;
343 opacity: 0.9;
344}
345
346.macro-details .macro-alias-of i {
347 transform: rotate(90deg);
348 font-size: 0.85em;
349 opacity: 0.7;
350}
351
352.macro-details .macro-alias-of code {
353 font-family: var(--monoFontFamily);
354 background: var(--black30a);
355 padding: 0.1em 0.4em;
356 border-radius: 3px;
357}
358
359/* Aliases list in details panel */
360.macro-details .macro-alias-list {
361 list-style: none;
362 padding: 0;
363 margin: 0.25em 0;
364 display: flex;
365 flex-wrap: wrap;
366 gap: 0.5em;
367}
368
369.macro-details .macro-alias-item {
370 display: inline-flex;
371 align-items: center;
372 gap: 0.3em;
373}
374
375.macro-details .macro-alias-item code {
376 font-family: var(--monoFontFamily);
377 background: var(--black30a);
378 padding: 0.2em 0.5em;
379 border-radius: 3px;
380}
381
382.macro-details .macro-alias-item.isHidden {
383 opacity: 0.5;
384}
385
386.macro-details .macro-alias-item.isHidden code {
387 text-decoration: line-through;
388 text-decoration-style: dashed;
389}
390
391.macro-details .macro-alias-hidden-badge {
392 font-size: 0.75em;
393 opacity: 0.7;
394 font-style: italic;
395}
396
397/* ============================================
398 Enhanced Macro Autocomplete Styles
399 ============================================ */
400
401/* Fix macro items in autocomplete list - override the grid display: contents */
402.autoComplete > .item.macro-ac-item {
403 display: flex !important;
404 flex-wrap: nowrap;
405 align-items: center;
406 gap: 0.5em;
407 grid-column: 1 / -1;
408 padding: 0.25em 0.5em;
409 min-height: 1.8em;
410}
411
412.autoComplete > .item.macro-ac-item > .type {
413 flex: 0 0 auto;
414 width: 2em;
415 text-align: center;
416}
417
418.autoComplete > .item.macro-ac-item > .specs {
419 flex: 0 0 auto;
420 max-width: 40%;
421}
422
423.autoComplete > .item.macro-ac-item > .specs > .name {
424 white-space: nowrap;
425 overflow: hidden;
426 text-overflow: ellipsis;
427}
428
429.autoComplete > .item.macro-ac-item > .stopgap {
430 flex: 0 0 0.5em;
431 display: block !important;
432}
433
434.autoComplete > .item.macro-ac-item > .help {
435 flex: 1 1 auto;
436 min-width: 0;
437 overflow: hidden;
438}
439
440.autoComplete > .item.macro-ac-item > .help > .helpContent {
441 white-space: nowrap;
442 overflow: hidden;
443 text-overflow: ellipsis;
444 display: block;
445}
446
447/* Indicator icons (alias and source) at the end */
448.autoComplete > .item.macro-ac-item > .macro-ac-indicator {
449 flex: 0 0 auto;
450 font-size: 0.85em;
451 opacity: 0.6;
452 margin-left: auto;
453}
454
455.autoComplete > .item.macro-ac-item > .macro-ac-indicator:first-of-type {
456 margin-left: auto;
457}
458
459.autoComplete > .item.macro-ac-item > .macro-ac-indicator + .macro-ac-indicator {
460 margin-left: 0.25em;
461}
462
463/* Third-party source indicator */
464.autoComplete > .item.macro-ac-item > .macro-source.isThirdParty {
465 color: #F89406;
466}
467
468/* Current argument hint banner in details */
469.macro-ac-arg-hint {
470 display: flex;
471 align-items: baseline;
472 gap: 0.5em;
473 padding: 0.5em 0.75em;
474 background: linear-gradient(90deg, var(--ac-color-selectedBackground, var(--SmartThemeQuoteColor)), transparent);
475 border-left: 3px solid var(--ac-color-matchedText, var(--SmartThemeBorderColor));
476 border-radius: 0 4px 4px 0;
477 margin-bottom: 0.5em;
478 font-size: 0.9em;
479}
480
481.macro-ac-arg-hint i {
482 color: var(--ac-color-matchedText, var(--SmartThemeBorderColor));
483 font-size: 0.8em;
484}
485
486.macro-ac-hint-type {
487 font-family: var(--monoFontFamily);
488 font-size: 0.85em;
489 padding: 0.1em 0.3em;
490 background: var(--ac-color-hoveredBackground, var(--black30a));
491 border-radius: 3px;
492 margin-left: 0.25em;
493}
494
495.macro-ac-hint-desc {
496 opacity: 0.8;
497}
498
499.macro-ac-hint-sample {
500 opacity: 0.6;
501 font-style: italic;
502}
503
504/* Details panel using MacroBrowser styles - just add autocomplete context adjustments */
505.autoComplete-details .macro-ac-details {
506 padding: 0.5em;
507}
508
509/* Highlight current argument in arguments list */
510.autoComplete-details .macro-arg-item.current {
511 background: var(--ac-color-selectedBackground, var(--SmartThemeQuoteColor));
512 padding: 0.25em 0.5em;
513 margin: 0 -0.5em;
514 border-radius: 4px;
515}
public/css/mobile-styles.css+9 -0
@@ -97,6 +97,15 @@
97 max-width: 100dvw;97 max-width: 100dvw;
98 }98 }
9999
100 #bg_tabs .bg_tabs_list .bg_tab_button {
101 min-width: 6em;
102 }
103
104 #add_background_button_top>span,
105 #auto_background>span {
106 display: none;
107 }
108
100 #extensions_settings,109 #extensions_settings,
101 #extensions_settings2 {110 #extensions_settings2 {
102 width: 100% !important;111 width: 100% !important;
public/css/rm-groups.css+8 -0
@@ -212,6 +212,14 @@
212 background-color: var(--white30a);212 background-color: var(--white30a);
213}213}
214214
215.group_select.avatar {
216 padding: 0;
217}
218
219.group_select.missing-avatar.inline_avatar {
220 justify-content: center;
221}
222
215.group_select .avatar {223.group_select .avatar {
216 flex: 0;224 flex: 0;
217}225}
public/css/select2-overrides.css+5 -0
@@ -16,6 +16,11 @@
16 user-select: none;16 user-select: none;
17}17}
1818
19.select2-container .select2-selection.select2-selection--single.select2-selection--clearable .select2-selection__clear {
20 top: 0;
21 right: 25px;
22}
23
19.select2-container .select2-selection .select2-selection__clear {24.select2-container .select2-selection .select2-selection__clear {
20 color: var(--SmartThemeBodyColor);25 color: var(--SmartThemeBodyColor);
21 font-size: 20px;26 font-size: 20px;
public/css/toggle-dependent.css+7 -5
@@ -477,14 +477,11 @@ body.expandMessageActions .mes .mes_buttons .extraMesButtonsHint {
477 display: none !important;477 display: none !important;
478}478}
479479
480#smooth_streaming:not(:checked)~#smooth_streaming_speed_control {480#smooth_streaming_control:has(#smooth_streaming:not(:checked))~#smooth_streaming_speed_control,
481#smooth_streaming_control:has(#smooth_streaming:not(:checked))~#smooth_streaming_no_think_control {
481 display: none;482 display: none;
482}483}
483484
484#smooth_streaming:checked~#smooth_streaming_speed_control {
485 display: block;
486}
487
488.mdhotkey_icon {485.mdhotkey_icon {
489 opacity: 0.6;486 opacity: 0.6;
490}487}
@@ -532,6 +529,7 @@ label[for="bind_preset_to_connection"]:has(input:checked) {
532}529}
533530
534#openai_settings input[type="checkbox"]:not(:checked)~[data-cc-toggle],531#openai_settings input[type="checkbox"]:not(:checked)~[data-cc-toggle],
532#openai_settings input[type="checkbox"]:not(:checked)~*:has([data-cc-toggle]),
535#openai_settings [data-cc-toggle="false"]>.icon-supported,533#openai_settings [data-cc-toggle="false"]>.icon-supported,
536#openai_settings [data-cc-toggle="true"]>.icon-unsupported {534#openai_settings [data-cc-toggle="true"]>.icon-unsupported {
537 display: none;535 display: none;
@@ -562,3 +560,7 @@ label[for="bind_preset_to_connection"]:has(input:checked) {
562 filter: grayscale(0.5);560 filter: grayscale(0.5);
563 cursor: not-allowed;561 cursor: not-allowed;
564}562}
563
564#request_images_block:has(#openai_request_images:not(:checked)) #request_images_settings {
565 display: none;
566}
public/global.d.ts+73 -4
@@ -6,6 +6,9 @@ import { oai_settings } from './scripts/openai';
6import { textgenerationwebui_settings } from './scripts/textgen-settings';6import { textgenerationwebui_settings } from './scripts/textgen-settings';
7import { FileAttachment } from './scripts/chats';7import { FileAttachment } from './scripts/chats';
8import { ReasoningMessageExtra } from './scripts/reasoning';8import { ReasoningMessageExtra } from './scripts/reasoning';
9import { IGNORE_SYMBOL, OVERSWIPE_BEHAVIOR } from './scripts/constants';
10import { ToolInvocation } from './scripts/tool-calling';
11import { getWorldInfoSettings } from './scripts/world-info';
912
10declare global {13declare global {
11 // Custom types14 // Custom types
@@ -14,7 +17,49 @@ declare global {
14 type ReasoningSettings = typeof power_user.reasoning;17 type ReasoningSettings = typeof power_user.reasoning;
15 type ChatCompletionSettings = typeof oai_settings;18 type ChatCompletionSettings = typeof oai_settings;
16 type TextCompletionSettings = typeof textgenerationwebui_settings;19 type TextCompletionSettings = typeof textgenerationwebui_settings;
20 type WorldInfoSettings = ReturnType<typeof getWorldInfoSettings>;
17 type MessageTimestamp = string | number | Date;21 type MessageTimestamp = string | number | Date;
22 type Character = import('./scripts/char-data').v1CharData;
23 type ChatMessageExtra = BaseMessageExtra & Partial<ReasoningMessageExtra> & Record<string, any>;
24
25 interface Group {
26 id: string;
27 name: string;
28 members: string[];
29 disabled_members: string[];
30 chat_id: string;
31 chats: string[];
32 generation_mode?: number;
33 generation_mode_join_prefix?: string;
34 generation_mode_join_suffix?: string;
35 activation_strategy?: number;
36 auto_mode_delay?: number;
37 allow_self_responses?: boolean;
38 avatar_url?: string;
39 hideMutedSprites?: boolean;
40 fav?: boolean;
41 }
42
43 interface ChatFile extends Array<ChatMessage> {
44 [index: number]: ChatMessage;
45 0?: ChatHeader;
46 }
47
48 interface ChatHeader {
49 chat_metadata: ChatMetadata;
50 /** @deprecated For backward compatibility ONLY */
51 user_name: 'unused';
52 /** @deprecated For backward compatibility ONLY */
53 character_name: 'unused';
54 }
55
56 interface ChatMetadata {
57 tainted?: boolean;
58 integrity?: string;
59 scenario?: string;
60 persona?: string;
61 [key: string]: any;
62 }
1863
19 interface ChatMessage {64 interface ChatMessage {
20 name?: string;65 name?: string;
@@ -28,21 +73,35 @@ declare global {
28 force_avatar?: string;73 force_avatar?: string;
29 original_avatar?: string;74 original_avatar?: string;
30 swipes?: string[];75 swipes?: string[];
31 swipe_info?: Record<string, any>;76 swipe_info?: SwipeInfo[];
32 swipe_id?: number;77 swipe_id?: number;
33 extra?: ChatMessageExtra & Partial<ReasoningMessageExtra> & Record<string, any>;78 extra?: ChatMessageExtra;
34 };79 };
3580
36 interface ChatMessageExtra {81 interface SwipeInfo {
82 send_date?: MessageTimestamp;
83 gen_started?: MessageTimestamp;
84 gen_finished?: MessageTimestamp;
85 extra?: ChatMessageExtra;
86 }
87
88 interface BaseMessageExtra {
89 api?: string;
90 model?: string;
91 type?: string;
92 gen_id?: number;
37 bias?: string;93 bias?: string;
38 uses_system_ui?: boolean;94 uses_system_ui?: boolean;
39 memory?: string;95 memory?: string;
40 display_text?: string;96 display_text?: string;
41 reasoning_display_text?: string;97 reasoning_display_text?: string;
42 tool_invocations?: any[];98 tool_invocations?: ToolInvocation[];
43 title?: string;99 title?: string;
44 isSmallSys?: boolean;100 isSmallSys?: boolean;
45 token_count?: number;101 token_count?: number;
102 /** When false, the message cannot be swiped. */
103 swipeable?: boolean;
104 overswipe_behavior?: OVERSWIPE_BEHAVIOR;
46 files?: FileAttachment[];105 files?: FileAttachment[];
47 inline_image?: boolean;106 inline_image?: boolean;
48 media_display?: string;107 media_display?: string;
@@ -62,6 +121,8 @@ declare global {
62 generationType?: number;121 generationType?: number;
63 /** @deprecated Use `MediaAttachment.negative` instead */122 /** @deprecated Use `MediaAttachment.negative` instead */
64 negative?: string;123 negative?: string;
124 /** Will exclude this message from prompt processing */
125 [IGNORE_SYMBOL]?: boolean;
65 }126 }
66127
67 type MediaAttachment = MediaAttachmentProps & ImageGenerationAttachmentProps & ImageCaptionAttachmentProps;128 type MediaAttachment = MediaAttachmentProps & ImageGenerationAttachmentProps & ImageCaptionAttachmentProps;
@@ -93,6 +154,12 @@ declare global {
93 paused: boolean;154 paused: boolean;
94 }155 }
95156
157 interface ChatCompletionMessage {
158 name?: string;
159 role: string;
160 content: string;
161 }
162
96 // Global namespace modules163 // Global namespace modules
97 interface Window {164 interface Window {
98 ai: any;165 ai: any;
@@ -165,4 +232,6 @@ declare global {
165 rgba: string;232 rgba: string;
166 }233 }
167 };234 };
235
236 type SwipeEvent = JQuery.TriggeredEvent<any, any, HTMLElement, HTMLElement>;
168}237}
public/img/chutes.svg+4 -0
@@ -0,0 +1,4 @@
1<svg width="246" height="153" viewBox="0 0 246 153" fill="none" xmlns="http://www.w3.org/2000/svg">
2<path d="M153.334 146.457C149.769 151.894 142.21 152.909 137.336 148.605L115.671 129.479C110.797 125.176 110.872 117.554 115.83 113.348L137.665 94.8248C161.939 74.232 193.912 66.2053 224.344 73.0645L240.772 76.7673C241.359 76.8995 241.902 77.1773 242.353 77.575C244.73 79.6736 243.399 83.5966 240.235 83.8175L239.884 83.842C209.831 85.9395 182.262 102.339 165.298 128.211L153.334 146.457Z" />
3<path d="M61.7046 134.759C57.1727 139.455 49.5229 139.022 45.5496 133.846L2.20718 77.3823C-1.74588 72.2325 -0.220772 64.7778 5.43762 61.5919L91.8585 12.9343C119.419 -2.58324 152.212 -4.22605 180.759 8.48062L243.451 36.385C244.421 36.8168 245.268 37.4842 245.914 38.3264C249.363 42.8196 245.669 49.2399 240.048 48.5205L202.701 43.7404C172.88 39.9236 142.862 50.6695 121.491 72.8128L61.7046 134.759Z" />
4</svg>
public/index.html+230 -132
@@ -42,6 +42,7 @@
42 <link rel="stylesheet" type="text/css" href="css/extensions-panel.css">42 <link rel="stylesheet" type="text/css" href="css/extensions-panel.css">
43 <link rel="stylesheet" type="text/css" href="css/select2-overrides.css">43 <link rel="stylesheet" type="text/css" href="css/select2-overrides.css">
44 <link rel="stylesheet" type="text/css" href="css/mobile-styles.css">44 <link rel="stylesheet" type="text/css" href="css/mobile-styles.css">
45 <link rel="stylesheet" type="text/css" href="css/macros.css">
45 <link rel="stylesheet" type="text/css" href="css/user.css">46 <link rel="stylesheet" type="text/css" href="css/user.css">
46 <link rel="icon" type="image/x-icon" href="favicon.ico">47 <link rel="icon" type="image/x-icon" href="favicon.ico">
47 <!-- Scripts are loaded at the end of the body to improve page load speed -->48 <!-- Scripts are loaded at the end of the body to improve page load speed -->
@@ -678,6 +679,9 @@
678 <div data-source="electronhub">679 <div data-source="electronhub">
679 <span data-i18n="Max prompt cost:">Max prompt cost:</span> <span id="electronhub_max_prompt_cost" data-i18n="Unknown">Unknown</span>680 <span data-i18n="Max prompt cost:">Max prompt cost:</span> <span id="electronhub_max_prompt_cost" data-i18n="Unknown">Unknown</span>
680 </div>681 </div>
682 <div data-source="chutes">
683 <span data-i18n="Max prompt cost:">Max prompt cost:</span> <span id="chutes_max_prompt_cost" data-i18n="Unknown">Unknown</span>
684 </div>
681 <hr>685 <hr>
682 <div class="range-block">686 <div class="range-block">
683 <label for="stream_toggle" title="Enable OpenAI completion streaming" data-i18n="[title]Enable OpenAI completion streaming" class="checkbox_label widthFreeExpand">687 <label for="stream_toggle" title="Enable OpenAI completion streaming" data-i18n="[title]Enable OpenAI completion streaming" class="checkbox_label widthFreeExpand">
@@ -693,7 +697,7 @@
693 </span>697 </span>
694 </div>698 </div>
695 </div>699 </div>
696 <div class="range-block" data-source="openai,claude,aimlapi,openrouter,ai21,makersuite,vertexai,mistralai,custom,cohere,perplexity,groq,siliconflow,electronhub,nanogpt,deepseek,xai,pollinations,moonshot,fireworks,cometapi,azure_openai,zai">700 <div class="range-block" data-source="openai,claude,aimlapi,openrouter,ai21,makersuite,vertexai,mistralai,custom,cohere,perplexity,groq,siliconflow,electronhub,chutes,nanogpt,deepseek,xai,pollinations,moonshot,fireworks,cometapi,azure_openai,zai">
697 <div class="range-block-title" data-i18n="Temperature">701 <div class="range-block-title" data-i18n="Temperature">
698 Temperature702 Temperature
699 </div>703 </div>
@@ -706,7 +710,7 @@
706 </div>710 </div>
707 </div>711 </div>
708 </div>712 </div>
709 <div class="range-block" data-source="openai,aimlapi,openrouter,custom,cohere,perplexity,groq,siliconflow,mistralai,electronhub,nanogpt,deepseek,xai,pollinations,moonshot,fireworks,cometapi,azure_openai">713 <div class="range-block" data-source="openai,aimlapi,openrouter,custom,cohere,perplexity,groq,siliconflow,mistralai,electronhub,nanogpt,deepseek,xai,pollinations,moonshot,fireworks,cometapi,azure_openai,chutes">
710 <div class="range-block-title" data-i18n="Frequency Penalty">714 <div class="range-block-title" data-i18n="Frequency Penalty">
711 Frequency Penalty715 Frequency Penalty
712 </div>716 </div>
@@ -719,7 +723,7 @@
719 </div>723 </div>
720 </div>724 </div>
721 </div>725 </div>
722 <div class="range-block" data-source="openai,aimlapi,openrouter,custom,cohere,perplexity,groq,siliconflow,mistralai,electronhub,nanogpt,deepseek,xai,pollinations,moonshot,fireworks,cometapi,azure_openai">726 <div class="range-block" data-source="openai,aimlapi,openrouter,custom,cohere,perplexity,groq,siliconflow,mistralai,electronhub,nanogpt,deepseek,xai,pollinations,moonshot,fireworks,cometapi,azure_openai,chutes">
723 <div class="range-block-title" data-i18n="Presence Penalty">727 <div class="range-block-title" data-i18n="Presence Penalty">
724 Presence Penalty728 Presence Penalty
725 </div>729 </div>
@@ -732,7 +736,7 @@
732 </div>736 </div>
733 </div>737 </div>
734 </div>738 </div>
735 <div class="range-block" data-source="claude,aimlapi,openrouter,makersuite,vertexai,cohere,perplexity,electronhub">739 <div class="range-block" data-source="claude,aimlapi,openrouter,makersuite,vertexai,cohere,perplexity,electronhub,chutes,nanogpt">
736 <div class="range-block-title" data-i18n="Top K">740 <div class="range-block-title" data-i18n="Top K">
737 Top K741 Top K
738 </div>742 </div>
@@ -745,7 +749,7 @@
745 </div>749 </div>
746 </div>750 </div>
747 </div>751 </div>
748 <div class="range-block" data-source="openai,claude,aimlapi,openrouter,ai21,makersuite,vertexai,mistralai,custom,cohere,perplexity,groq,siliconflow,electronhub,nanogpt,deepseek,xai,pollinations,moonshot,fireworks,cometapi,azure_openai,zai">752 <div class="range-block" data-source="openai,claude,aimlapi,openrouter,ai21,makersuite,vertexai,mistralai,custom,cohere,perplexity,groq,siliconflow,electronhub,chutes,nanogpt,deepseek,xai,pollinations,moonshot,fireworks,cometapi,azure_openai,zai">
749 <div class="range-block-title" data-i18n="Top P">753 <div class="range-block-title" data-i18n="Top P">
750 Top P754 Top P
751 </div>755 </div>
@@ -758,7 +762,7 @@
758 </div>762 </div>
759 </div>763 </div>
760 </div>764 </div>
761 <div class="range-block" data-source="openrouter">765 <div class="range-block" data-source="openrouter,nanogpt,chutes">
762 <div class="range-block-title" data-i18n="Repetition Penalty">766 <div class="range-block-title" data-i18n="Repetition Penalty">
763 Repetition Penalty767 Repetition Penalty
764 </div>768 </div>
@@ -771,7 +775,7 @@
771 </div>775 </div>
772 </div>776 </div>
773 </div>777 </div>
774 <div class="range-block" data-source="openrouter">778 <div class="range-block" data-source="openrouter,nanogpt,chutes">
775 <div class="range-block-title" data-i18n="Min P">779 <div class="range-block-title" data-i18n="Min P">
776 Min P780 Min P
777 </div>781 </div>
@@ -784,7 +788,7 @@
784 </div>788 </div>
785 </div>789 </div>
786 </div>790 </div>
787 <div class="range-block" data-source="openrouter">791 <div class="range-block" data-source="openrouter,nanogpt">
788 <div class="range-block-title" data-i18n="Top A">792 <div class="range-block-title" data-i18n="Top A">
789 Top A793 Top A
790 </div>794 </div>
@@ -982,7 +986,7 @@
982 </div>986 </div>
983 </div>987 </div>
984 </div>988 </div>
985 <div class="range-block" data-source="openai,openrouter,mistralai,custom,cohere,groq,electronhub,nanogpt,xai,pollinations,aimlapi,makersuite,vertexai,azure_openai">989 <div class="range-block" data-source="openai,openrouter,mistralai,custom,cohere,groq,electronhub,chutes,nanogpt,xai,pollinations,aimlapi,makersuite,vertexai,azure_openai">
986 <div class="range-block-title justifyLeft" data-i18n="Seed">990 <div class="range-block-title justifyLeft" data-i18n="Seed">
987 Seed991 Seed
988 </div>992 </div>
@@ -1123,7 +1127,7 @@
1123 <div id="grammar_block" class="wide100p">1127 <div id="grammar_block" class="wide100p">
1124 <hr class="wide100p">1128 <hr class="wide100p">
1125 <h4 class="wide100p textAlignCenter"><span data-i18n="GBNF Grammar">GBNF Grammar</span>1129 <h4 class="wide100p textAlignCenter"><span data-i18n="GBNF Grammar">GBNF Grammar</span>
1126 <a href="https://github.com/ggerganov/llama.cpp/blob/master/grammars/README.md" target="_blank">1130 <a href="https://github.com/ggml-org/llama.cpp/blob/master/grammars/README.md" target="_blank">
1127 <small>1131 <small>
1128 <div class="fa-solid fa-circle-question note-link-span"></div>1132 <div class="fa-solid fa-circle-question note-link-span"></div>
1129 </small>1133 </small>
@@ -1246,7 +1250,7 @@
1246 <div class="fa-solid fa-circle-info opacity50p" title="Customize displayed samplers or add custom samplers." data-i18n="[title]Customize displayed samplers or add custom samplers."></div>1250 <div class="fa-solid fa-circle-info opacity50p" title="Customize displayed samplers or add custom samplers." data-i18n="[title]Customize displayed samplers or add custom samplers."></div>
1247 </small>1251 </small>
1248 </div>1252 </div>
1249 <div data-tg-type="mancer, vllm, aphrodite, tabby, infermaticai" data-tg-samplers="n" class="flex-container flexFlowColumn alignitemscenter flexBasis100p flexGrow flexShrink gap0">1253 <div data-tg-type="mancer, vllm, aphrodite, tabby, infermaticai, llamacpp" data-tg-samplers="n" class="flex-container flexFlowColumn alignitemscenter flexBasis100p flexGrow flexShrink gap0">
1250 <small data-i18n="Multiple swipes per generation">Multiple swipes per generation</small>1254 <small data-i18n="Multiple swipes per generation">Multiple swipes per generation</small>
1251 <input type="number" id="n_textgenerationwebui" class="text_pole textAlignCenter" min="1" value="1" step="1" />1255 <input type="number" id="n_textgenerationwebui" class="text_pole textAlignCenter" min="1" value="1" step="1" />
1252 </div>1256 </div>
@@ -1690,7 +1694,7 @@
1690 <div class="flex-container justifyCenter alignitemscenter">1694 <div class="flex-container justifyCenter alignitemscenter">
1691 <span data-i18n="Grammar String">Grammar String</span>1695 <span data-i18n="Grammar String">Grammar String</span>
1692 <div class="margin5 fa-solid fa-circle-info opacity50p " data-i18n="[title]GBNF or EBNF, depends on the backend in use. If you're using this you should know which." title="GBNF or EBNF, depends on the backend in use. If you're using this you should know which."></div>1696 <div class="margin5 fa-solid fa-circle-info opacity50p " data-i18n="[title]GBNF or EBNF, depends on the backend in use. If you're using this you should know which." title="GBNF or EBNF, depends on the backend in use. If you're using this you should know which."></div>
1693 <a href="https://github.com/ggerganov/llama.cpp/blob/master/grammars/README.md" target="_blank">1697 <a href="https://github.com/ggml-org/llama.cpp/blob/master/grammars/README.md" target="_blank">
1694 <div class="fa-solid fa-up-right-from-square note-link-span"></div>1698 <div class="fa-solid fa-up-right-from-square note-link-span"></div>
1695 </a>1699 </a>
1696 </div>1700 </div>
@@ -1920,18 +1924,6 @@
1920 </div>1924 </div>
1921 </div>1925 </div>
1922 <div class="range-block">1926 <div class="range-block">
1923 <label for="wrap_in_quotes" title="Wrap user messages in quotes before sending" data-i18n="[title]Wrap user messages in quotes before sending" class="checkbox_label widthFreeExpand">
1924 <input id="wrap_in_quotes" type="checkbox" /><span data-i18n="Wrap in Quotes">
1925 Wrap in Quotes</span>
1926 </label>
1927 <div class="toggle-description justifyLeft">
1928 <span data-i18n="Wrap entire user message in quotes before sending.">Wrap
1929 entire user message in quotes before sending.</span><br>
1930 <span data-i18n="Leave off if you use quotes manually for speech.">Leave off
1931 if you use quotes manually for speech.</span>
1932 </div>
1933 </div>
1934 <div class="range-block">
1935 <label for="continue_prefill" class="checkbox_label widthFreeExpand">1927 <label for="continue_prefill" class="checkbox_label widthFreeExpand">
1936 <input id="continue_prefill" type="checkbox" />1928 <input id="continue_prefill" type="checkbox" />
1937 <span data-i18n="Continue prefill">Continue prefill</span>1929 <span data-i18n="Continue prefill">Continue prefill</span>
@@ -1955,7 +1947,20 @@
1955 </span>1947 </span>
1956 </div>1948 </div>
1957 </div>1949 </div>
1958 <div class="range-block" data-source="makersuite,vertexai,aimlapi,openrouter,claude,xai,electronhub,nanogpt">1950 <div class="range-block" data-source="claude,makersuite,vertexai">
1951 <label for="use_sysprompt" class="checkbox_label widthFreeExpand">
1952 <input id="use_sysprompt" type="checkbox" />
1953 <span>
1954 <span data-i18n="Use system prompt">Use system prompt</span>
1955 </span>
1956 </label>
1957 <div class="toggle-description justifyLeft marginBot5">
1958 <span data-i18n="Send the system prompt for supported models. If disabled, the user message is added to the beginning of the prompt.">
1959 Send the system prompt for supported models. If disabled, the user message is added to the beginning of the prompt.
1960 </span>
1961 </div>
1962 </div>
1963 <div class="range-block" data-source="makersuite,vertexai,aimlapi,openrouter,claude,xai,electronhub,chutes,nanogpt">
1959 <label for="openai_enable_web_search" class="checkbox_label flexWrap widthFreeExpand">1964 <label for="openai_enable_web_search" class="checkbox_label flexWrap widthFreeExpand">
1960 <input id="openai_enable_web_search" type="checkbox" />1965 <input id="openai_enable_web_search" type="checkbox" />
1961 <span data-i18n="Enable web search">Enable web search</span>1966 <span data-i18n="Enable web search">Enable web search</span>
@@ -1969,7 +1974,7 @@
1969 </b>1974 </b>
1970 </div>1975 </div>
1971 </div>1976 </div>
1972 <div class="range-block" data-source="openai,cohere,mistralai,custom,claude,aimlapi,openrouter,groq,siliconflow,deepseek,makersuite,vertexai,ai21,xai,pollinations,moonshot,fireworks,cometapi,electronhub,azure_openai,zai">1977 <div class="range-block" data-source="openai,cohere,mistralai,custom,claude,aimlapi,openrouter,groq,siliconflow,deepseek,makersuite,vertexai,ai21,xai,pollinations,moonshot,fireworks,cometapi,electronhub,chutes,azure_openai,zai">
1973 <label for="openai_function_calling" class="checkbox_label flexWrap widthFreeExpand">1978 <label for="openai_function_calling" class="checkbox_label flexWrap widthFreeExpand">
1974 <input id="openai_function_calling" type="checkbox" />1979 <input id="openai_function_calling" type="checkbox" />
1975 <span data-i18n="Enable function calling">Enable function calling</span>1980 <span data-i18n="Enable function calling">Enable function calling</span>
@@ -1984,19 +1989,31 @@
1984 <strong data-i18n="enable_functions_desc_4">Not supported when Prompt Post-Processing with "no tools" is used!</strong>1989 <strong data-i18n="enable_functions_desc_4">Not supported when Prompt Post-Processing with "no tools" is used!</strong>
1985 </div>1990 </div>
1986 </div>1991 </div>
1987 <div class="range-block" data-source="openai,aimlapi,openrouter,mistralai,makersuite,vertexai,claude,custom,xai,pollinations,moonshot,cohere,cometapi,nanogpt,electronhub,azure_openai,zai,siliconflow">1992 <div class="range-block" data-source="openai,aimlapi,openrouter,mistralai,makersuite,vertexai,claude,custom,xai,pollinations,moonshot,cohere,cometapi,nanogpt,electronhub,azure_openai,zai,siliconflow,chutes">
1988 <label for="openai_image_inlining" class="checkbox_label flexWrap widthFreeExpand">1993 <label for="openai_media_inlining" class="checkbox_label flexWrap widthFreeExpand">
1989 <input id="openai_image_inlining" type="checkbox" />1994 <input id="openai_media_inlining" type="checkbox" />
1990 <span data-i18n="Send inline images">Send inline images</span>1995 <span data-i18n="Send inline media">Send inline media</span>
1991 <div id="openai_image_inlining_supported" data-cc-toggle="false">1996 <div class="flex-container">
1992 <i class="icon-supported fa-solid fa-circle-check" title="Supported by the current model" data-i18n="[title]Supported by the current model"></i>1997 <div id="openai_image_inlining_supported">
1993 <i class="icon-unsupported fa-solid fa-circle-exclamation" title="Unsupported by the current model" data-i18n="[title]Unsupported by the current model"></i>1998 <i class="icon-supported fa-solid fa-image" title="Supported by the current model" data-i18n="[title]Supported by the current model"></i>
1999 <i class="icon-unsupported fa-solid fa-image" title="Unsupported by the current model" data-i18n="[title]Unsupported by the current model"></i>
2000 </div>
2001 <div id="openai_video_inlining_supported" data-source="makersuite,vertexai,openrouter,zai">
2002 <i class="icon-supported fa-solid fa-film" title="Supported by the current model" data-i18n="[title]Supported by the current model"></i>
2003 <i class="icon-unsupported fa-solid fa-film" title="Unsupported by the current model" data-i18n="[title]Unsupported by the current model"></i>
2004 </div>
2005 <div id="openai_audio_inlining_supported" data-source="makersuite,vertexai,openrouter">
2006 <i class="icon-supported fa-solid fa-music" title="Supported by the current model" data-i18n="[title]Supported by the current model"></i>
2007 <i class="icon-unsupported fa-solid fa-music" title="Unsupported by the current model" data-i18n="[title]Unsupported by the current model"></i>
2008 </div>
1994 </div>2009 </div>
1995 </label>2010 </label>
1996 <div id="image_inlining_hint" class="flexBasis100p toggle-description justifyLeft">2011 <div id="image_inlining_hint" class="flexBasis100p toggle-description justifyLeft">
1997 <span data-i18n="image_inlining_hint_1">Sends images in prompts if the model supports it.</span>2012 <span data-i18n="image_inlining_hint_1">Sends attached media in prompts if supported by the model.</span>
2013 <strong data-source="makersuite,vertexai" data-i18n="video_inlining_hint_4">Videos must be less than 20 MB and under 1 minute long.</strong>
2014 <strong data-source="makersuite,vertexai" data-i18n="audio_inlining_hint_2">Audio must be less than 20 MB.</strong>
1998 </div>2015 </div>
1999 <div class="flex-container flexFlowColumn wide100p textAlignCenter marginTop10" data-source="openai,custom,xai,pollinations,cohere,cometapi,nanogpt,moonshot,aimlapi,openrouter,mistralai,electronhub,azure_openai,zai,siliconflow">2016 <div class="flex-container flexFlowColumn wide100p textAlignCenter marginTop10" data-source="openai,custom,xai,pollinations,cohere,cometapi,nanogpt,moonshot,aimlapi,openrouter,mistralai,electronhub,azure_openai,zai,siliconflow,chutes,makersuite,vertexai">
2000 <div class="flex-container oneline-dropdown">2017 <div class="flex-container oneline-dropdown">
2001 <label for="openai_inline_image_quality" data-i18n="Inline Image Quality">2018 <label for="openai_inline_image_quality" data-i18n="Inline Image Quality">
2002 Inline Image Quality2019 Inline Image Quality
@@ -2009,40 +2026,12 @@
2009 </div>2026 </div>
2010 </div>2027 </div>
2011 </div>2028 </div>
2012 <div class="range-block" data-source="makersuite,vertexai,openrouter">2029 <div id="request_images_block" class="range-block" data-source="makersuite,vertexai">
2013 <label for="openai_video_inlining" class="checkbox_label flexWrap widthFreeExpand">
2014 <input id="openai_video_inlining" type="checkbox" />
2015 <span data-i18n="Send inline videos">Send inline videos</span>
2016 <div id="openai_video_inlining_supported" data-cc-toggle="false">
2017 <i class="icon-supported fa-solid fa-circle-check" title="Supported by the current model" data-i18n="[title]Supported by the current model"></i>
2018 <i class="icon-unsupported fa-solid fa-circle-exclamation" title="Unsupported by the current model" data-i18n="[title]Unsupported by the current model"></i>
2019 </div>
2020 </label>
2021 <div id="video_inlining_hint" class="flexBasis100p toggle-description justifyLeft">
2022 <span data-i18n="video_inlining_hint_1">Sends videos in prompts if the model supports it.</span>
2023 <strong data-source="makersuite,vertexai" data-i18n="video_inlining_hint_4">Videos must be less than 20 MB and under 1 minute long</strong>
2024 </div>
2025 </div>
2026 <div class="range-block" data-source="makersuite,vertexai,openrouter">
2027 <label for="openai_audio_inlining" class="checkbox_label flexWrap widthFreeExpand">
2028 <input id="openai_audio_inlining" type="checkbox" />
2029 <span data-i18n="Send inline audio">Send inline audio</span>
2030 <div id="openai_audio_inlining_supported" data-cc-toggle="false">
2031 <i class="icon-supported fa-solid fa-circle-check" title="Supported by the current model" data-i18n="[title]Supported by the current model"></i>
2032 <i class="icon-unsupported fa-solid fa-circle-exclamation" title="Unsupported by the current model" data-i18n="[title]Unsupported by the current model"></i>
2033 </div>
2034 </label>
2035 <div id="audio_inlining_hint" class="flexBasis100p toggle-description justifyLeft">
2036 <span data-i18n="audio_inlining_hint_1">Sends audio in prompts if the model supports it.</span>
2037 <strong data-source="makersuite,vertexai" data-i18n="audio_inlining_hint_2">Audio must be less than 20 MB</strong>
2038 </div>
2039 </div>
2040 <div class="range-block" data-source="makersuite,vertexai">
2041 <label for="openai_request_images" class="checkbox_label widthFreeExpand">2030 <label for="openai_request_images" class="checkbox_label widthFreeExpand">
2042 <input id="openai_request_images" type="checkbox" />2031 <input id="openai_request_images" type="checkbox" />
2043 <span>2032 <span>
2044 <span data-i18n="Request inline images">Request inline images</span>2033 <span data-i18n="Request inline images">Request inline images</span>
2045 <i class="opacity50p fa-solid fa-circle-info" title="Gemini 2.0 Flash Experimental"></i>2034 <i class="opacity50p fa-solid fa-circle-info" title="Gemini 2.5 Flash Image / Gemini 3 Pro Image"></i>
2046 </span>2035 </span>
2047 </label>2036 </label>
2048 <div class="toggle-description justifyLeft marginBot5">2037 <div class="toggle-description justifyLeft marginBot5">
@@ -2053,22 +2042,39 @@
2053 Incompatible with the following features: function calling, web search, system prompt.2042 Incompatible with the following features: function calling, web search, system prompt.
2054 </em>2043 </em>
2055 </div>2044 </div>
2056 </div>2045 <div id="request_images_settings" class="flex-container wide100p textAlignCenter">
2057 <div class="range-block" data-source="makersuite,vertexai">2046 <div class="flex1">
2058 <label for="use_makersuite_sysprompt" class="checkbox_label widthFreeExpand">2047 <label for="request_image_resolution">
2059 <input id="use_makersuite_sysprompt" type="checkbox" />2048 <small data-i18n="Resolution">Resolution</small>
2060 <span>2049 </label>
2061 <span data-i18n="Use system prompt">Use system prompt</span>2050 <select id="request_image_resolution" class="text_pole">
2062 <i class="opacity50p fa-solid fa-circle-info" title="Gemini 1.5+, LearnLM"></i>2051 <option value="">Auto</option>
2063 </span>2052 <option value="1K">1K</option>
2064 </label>2053 <option value="2K">2K</option>
2065 <div class="toggle-description justifyLeft marginBot5">2054 <option value="4K">4K</option>
2066 <span data-i18n="Merges_all_system_messages_desc_1">2055 </select>
2067 Merges all system messages up until the first message with a non-system role, and sends them in a</span> <code>system_instruction</code> <span data-i18n="Merges_all_system_messages_desc_2">field.2056 </div>
2068 </span>2057 <div class="flex1">
2058 <label for="request_image_aspect_ratio">
2059 <small data-i18n="Aspect Ratio">Aspect Ratio</small>
2060 </label>
2061 <select id="request_image_aspect_ratio" class="text_pole">
2062 <option value="">Auto</option>
2063 <option value="1:1">1:1</option>
2064 <option value="9:16">9:16</option>
2065 <option value="16:9">16:9</option>
2066 <option value="3:4">3:4</option>
2067 <option value="4:3">4:3</option>
2068 <option value="3:2">3:2</option>
2069 <option value="2:3">2:3</option>
2070 <option value="5:4">5:4</option>
2071 <option value="4:5">4:5</option>
2072 <option value="21:9">21:9</option>
2073 </select>
2074 </div>
2069 </div>2075 </div>
2070 </div>2076 </div>
2071 <div class="range-block" data-source="deepseek,aimlapi,openrouter,custom,claude,xai,makersuite,vertexai,pollinations,moonshot,mistralai,fireworks,cometapi,electronhub,azure_openai,nanogpt,zai">2077 <div class="range-block" data-source="deepseek,aimlapi,openrouter,custom,claude,xai,makersuite,vertexai,pollinations,moonshot,mistralai,fireworks,cometapi,electronhub,chutes,azure_openai,nanogpt,zai">
2072 <label for="openai_show_thoughts" class="checkbox_label widthFreeExpand">2078 <label for="openai_show_thoughts" class="checkbox_label widthFreeExpand">
2073 <input id="openai_show_thoughts" type="checkbox" />2079 <input id="openai_show_thoughts" type="checkbox" />
2074 <span data-i18n="Request model reasoning">Request model reasoning</span>2080 <span data-i18n="Request model reasoning">Request model reasoning</span>
@@ -2082,7 +2088,7 @@
2082 </strong>2088 </strong>
2083 </div>2089 </div>
2084 </div>2090 </div>
2085 <div class="flex-container flexFlowColumn wide100p textAlignCenter marginTop10" data-source="openai,custom,claude,xai,makersuite,vertexai,aimlapi,openrouter,pollinations,perplexity,cometapi,electronhub,azure_openai">2091 <div class="flex-container flexFlowColumn wide100p textAlignCenter marginTop10" data-source="openai,custom,claude,xai,makersuite,vertexai,aimlapi,openrouter,pollinations,perplexity,cometapi,electronhub,azure_openai,chutes">
2086 <div class="flex-container oneline-dropdown" title="Constrains effort on reasoning for reasoning models.&#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.">2092 <div class="flex-container oneline-dropdown" title="Constrains effort on reasoning for reasoning models.&#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.">
2087 <label for="openai_reasoning_effort">2093 <label for="openai_reasoning_effort">
2088 <span data-i18n="Reasoning Effort">Reasoning Effort</span>2094 <span data-i18n="Reasoning Effort">Reasoning Effort</span>
@@ -2096,14 +2102,36 @@
2096 <option data-i18n="openai_reasoning_effort_high" value="high">High</option>2102 <option data-i18n="openai_reasoning_effort_high" value="high">High</option>
2097 <option data-i18n="openai_reasoning_effort_maximum" value="max">Maximum</option>2103 <option data-i18n="openai_reasoning_effort_maximum" value="max">Maximum</option>
2098 </select>2104 </select>
2099 <div class="toggle-description justifyLeft marginBot5" data-source="openai,custom,xai,aimlapi,openrouter,perplexity,electronhub,azure_openai" data-i18n="OpenAI-style options: low, medium, high. Minimum and maximum are aliased to low and high. Auto does not send an effort level.">2105 <div class="toggle-description justifyLeft marginBot5" data-source="openai,custom,xai,aimlapi,openrouter,pollinations,perplexity,cometapi,electronhub,azure_openai,chutes" data-i18n="OpenAI-style options: low, medium, high. Minimum and maximum are aliased to low and high. Auto does not send an effort level.">
2100 OpenAI-style options: low, medium, high. Minimum and maximum are aliased to low and high. Auto does not send an effort level.2106 OpenAI-style options: low, medium, high. Minimum and maximum are aliased to low and high. Auto does not send an effort level.
2101 </div>2107 </div>
2102 <div class="toggle-description justifyLeft marginBot5" data-source="claude" data-i18n="Allocates a portion of the response length for thinking (min: 1024 tokens, low: 10%, medium: 25%, high: 50%, max: 95%), but minimum 1024 tokens. Auto does not request thinking.">2108 <div class="toggle-description justifyLeft marginBot5" data-source="claude" data-i18n="Allocates a portion of the response length for thinking (min: 1024 tokens, low: 10%, medium: 25%, high: 50%, max: 95%), but minimum 1024 tokens. Auto does not request thinking.">
2103 Allocates a portion of the response length for thinking (min: 1024 tokens, low: 10%, medium: 25%, high: 50%, max: 95%), but minimum 1024 tokens. Auto does not request thinking.2109 Allocates a portion of the response length for thinking (min: 1024 tokens, low: 10%, medium: 25%, high: 50%, max: 95%), but minimum 1024 tokens. Auto does not request thinking.
2104 </div>2110 </div>
2105 <div class="toggle-description justifyLeft marginBot5" data-source="makersuite,vertexai" data-i18n="Allocates a portion of the response length for thinking (Flash 2.5/Pro 2.5) (min: 0/128 tokens, low: 10%, medium: 25%, high: 50%, max: 24576/32768 tokens). Auto lets the model decide.">2111 <div class="toggle-description justifyLeft marginBot5" data-source="makersuite,vertexai">
2106 Allocates a portion of the response length for thinking (Flash 2.5/Pro 2.5) (min: 0/128 tokens, low: 10%, medium: 25%, high: 50%, max: 24576/32768 tokens). Auto lets the model decide.2112 <span data-i18n="Sets a dynamic reasoning depth level for thinking (Flash 3/Pro 3). High and low are supported by both, minimal and medium are Flash 3 only. Auto lets the model decide.">
2113 Sets a dynamic reasoning depth level for thinking (Flash 3/Pro 3). High and low are supported by both, minimal and medium are Flash 3 only. Auto lets the model decide.
2114 </span>
2115 <br>
2116 <span data-i18n="Allocates a portion of the response length for thinking (Flash 2.5/Pro 2.5) (min: 0/128 tokens, low: 10%, medium: 25%, high: 50%, max: 24576/32768 tokens). Auto lets the model decide.">
2117 Allocates a portion of the response length for thinking (Flash 2.5/Pro 2.5) (min: 0/128 tokens, low: 10%, medium: 25%, high: 50%, max: 24576/32768 tokens). Auto lets the model decide.
2118 </span>
2119 </div>
2120 </div>
2121 </div>
2122 <div class="flex-container flexFlowColumn wide100p textAlignCenter marginTop10" data-source="openai,custom,openrouter,claude">
2123 <div class="flex-container oneline-dropdown">
2124 <label for="openai_verbosity">
2125 <span data-i18n="Verbosity">Verbosity</span>
2126 </label>
2127 <select id="openai_verbosity">
2128 <option data-i18n="openai_verbosity_auto" value="auto">Auto</option>
2129 <option data-i18n="openai_verbosity_low" value="low">Low</option>
2130 <option data-i18n="openai_verbosity_medium" value="medium">Medium</option>
2131 <option data-i18n="openai_verbosity_high" value="high">High</option>
2132 </select>
2133 <div class="toggle-description justifyLeft marginBot5" data-i18n="Constrains the verbosity of the model's response.">
2134 Constrains the verbosity of the model's response.
2107 </div>2135 </div>
2108 </div>2136 </div>
2109 </div>2137 </div>
@@ -2124,18 +2152,9 @@
2124 <i class="fa-solid fa-circle-info"></i>2152 <i class="fa-solid fa-circle-info"></i>
2125 <span>Prefills won't work when function calling is enabled and any tools are registered.</span>2153 <span>Prefills won't work when function calling is enabled and any tools are registered.</span>
2126 </div>2154 </div>
2127 <label for="claude_use_sysprompt" class="checkbox_label widthFreeExpand">
2128 <input id="claude_use_sysprompt" type="checkbox" />
2129 <span data-i18n="Use system prompt">Use system prompt</span>
2130 </label>
2131 <div class="toggle-description justifyLeft marginBot5">
2132 <span data-i18n="Send the system prompt for supported models. If disabled, the user message is added to the beginning of the prompt.">
2133 Send the system prompt for supported models. If disabled, the user message is added to the beginning of the prompt.
2134 </span>
2135 </div>
2136 </div>2155 </div>
2137 </div>2156 </div>
2138 <div class="range-block m-t-1" data-source="openai,aimlapi,openrouter,custom,electronhub,azure_openai">2157 <div class="range-block m-t-1" data-source="openai,aimlapi,openrouter,custom,electronhub,azure_openai,chutes">
2139 <div id="logit_bias_openai" class="range-block-title openai_restorable" data-i18n="Logit Bias">2158 <div id="logit_bias_openai" class="range-block-title openai_restorable" data-i18n="Logit Bias">
2140 Logit Bias2159 Logit Bias
2141 </div>2160 </div>
@@ -2642,8 +2661,8 @@
2642 </div>2661 </div>
2643 <div data-tg-type="llamacpp">2662 <div data-tg-type="llamacpp">
2644 <div class="flex-container flexFlowColumn">2663 <div class="flex-container flexFlowColumn">
2645 <a href="https://github.com/ggerganov/llama.cpp" target="_blank" data-i18n="ggerganov/llama.cpp">2664 <a href="https://github.com/ggml-org/llama.cpp/blob/master/README.md#llama-server" target="_blank" data-i18n="ggml-org/llama.cpp">
2646 ggerganov/llama.cpp (inference server)2665 ggml-org/llama.cpp (inference server)
2647 </a>2666 </a>
2648 </div>2667 </div>
2649 <h4 data-i18n="API key (optional)">API key (optional)</h4>2668 <h4 data-i18n="API key (optional)">API key (optional)</h4>
@@ -2660,6 +2679,16 @@
2660 <small data-i18n="Example: http://127.0.0.1:8080">Example: http://127.0.0.1:8080</small>2679 <small data-i18n="Example: http://127.0.0.1:8080">Example: http://127.0.0.1:8080</small>
2661 <input id="llamacpp_api_url_text" class="text_pole wide100p" value="" autocomplete="off" data-server-history="llamacpp">2680 <input id="llamacpp_api_url_text" class="text_pole wide100p" value="" autocomplete="off" data-server-history="llamacpp">
2662 </div>2681 </div>
2682 <div>
2683 <h4>
2684 <span data-i18n="llama.cpp Model">llama.cpp Model</span>
2685 </h4>
2686 <select id="llamacpp_model">
2687 <option value="" data-i18n="-- Connect to the API --">
2688 -- Connect to the API --
2689 </option>
2690 </select>
2691 </div>
2663 </div>2692 </div>
2664 <div data-tg-type="ollama">2693 <div data-tg-type="ollama">
2665 <div class="flex-container flexFlowColumn">2694 <div class="flex-container flexFlowColumn">
@@ -2794,6 +2823,7 @@
2794 <option value="ai21">AI21</option>2823 <option value="ai21">AI21</option>
2795 <option value="aimlapi">AI/ML API</option>2824 <option value="aimlapi">AI/ML API</option>
2796 <option value="azure_openai">Azure OpenAI</option>2825 <option value="azure_openai">Azure OpenAI</option>
2826 <option value="chutes">Chutes</option>
2797 <option value="claude">Claude</option>2827 <option value="claude">Claude</option>
2798 <option value="cohere">Cohere</option>2828 <option value="cohere">Cohere</option>
2799 <!-- Temporarily disabled. -->2829 <!-- Temporarily disabled. -->
@@ -2929,6 +2959,11 @@
2929 <div>2959 <div>
2930 <h4 data-i18n="OpenAI Model">OpenAI Model</h4>2960 <h4 data-i18n="OpenAI Model">OpenAI Model</h4>
2931 <select id="model_openai_select">2961 <select id="model_openai_select">
2962 <optgroup label="GPT-5.2">
2963 <option value="gpt-5.2">gpt-5.2</option>
2964 <option value="gpt-5.2-2025-12-11">gpt-5.2-2025-12-11</option>
2965 <option value="gpt-5.2-chat-latest">gpt-5.2-chat-latest</option>
2966 </optgroup>
2932 <optgroup label="GPT-5.1">2967 <optgroup label="GPT-5.1">
2933 <option value="gpt-5.1">gpt-5.1</option>2968 <option value="gpt-5.1">gpt-5.1</option>
2934 <option value="gpt-5.1-2025-11-13">gpt-5.1-2025-11-13</option>2969 <option value="gpt-5.1-2025-11-13">gpt-5.1-2025-11-13</option>
@@ -3033,6 +3068,8 @@
3033 <h4 data-i18n="Claude Model">Claude Model</h4>3068 <h4 data-i18n="Claude Model">Claude Model</h4>
3034 <select id="model_claude_select">3069 <select id="model_claude_select">
3035 <optgroup label="Versions">3070 <optgroup label="Versions">
3071 <option value="claude-opus-4-5">claude-opus-4-5</option>
3072 <option value="claude-opus-4-5-20251101">claude-opus-4-5-20251101</option>
3036 <option value="claude-sonnet-4-5">claude-sonnet-4-5</option>3073 <option value="claude-sonnet-4-5">claude-sonnet-4-5</option>
3037 <option value="claude-sonnet-4-5-20250929">claude-sonnet-4-5-20250929</option>3074 <option value="claude-sonnet-4-5-20250929">claude-sonnet-4-5-20250929</option>
3038 <option value="claude-haiku-4-5">claude-haiku-4-5</option>3075 <option value="claude-haiku-4-5">claude-haiku-4-5</option>
@@ -3174,6 +3211,7 @@
3174 <optgroup label="Gemini 3.0">3211 <optgroup label="Gemini 3.0">
3175 <option value="gemini-3-pro-preview">gemini-3-pro-preview</option>3212 <option value="gemini-3-pro-preview">gemini-3-pro-preview</option>
3176 <option value="gemini-3-pro-image-preview">gemini-3-pro-image-preview</option>3213 <option value="gemini-3-pro-image-preview">gemini-3-pro-image-preview</option>
3214 <option value="gemini-3-flash-preview">gemini-3-flash-preview</option>
3177 </optgroup>3215 </optgroup>
3178 <optgroup label="Gemini 2.5">3216 <optgroup label="Gemini 2.5">
3179 <option value="gemini-2.5-pro">gemini-2.5-pro</option>3217 <option value="gemini-2.5-pro">gemini-2.5-pro</option>
@@ -3346,6 +3384,7 @@
3346 <optgroup label="Gemini 3.0">3384 <optgroup label="Gemini 3.0">
3347 <option value="gemini-3-pro-preview">gemini-3-pro-preview</option>3385 <option value="gemini-3-pro-preview">gemini-3-pro-preview</option>
3348 <option value="gemini-3-pro-image-preview">gemini-3-pro-image-preview</option>3386 <option value="gemini-3-pro-image-preview">gemini-3-pro-image-preview</option>
3387 <option value="gemini-3-flash-preview">gemini-3-flash-preview</option>
3349 </optgroup>3388 </optgroup>
3350 <optgroup label="Gemini 2.5">3389 <optgroup label="Gemini 2.5">
3351 <option value="gemini-2.5-pro">gemini-2.5-pro</option>3390 <option value="gemini-2.5-pro">gemini-2.5-pro</option>
@@ -3468,6 +3507,45 @@
3468 </div>3507 </div>
3469 </div>3508 </div>
3470 </div>3509 </div>
3510 <div id="chutes_form" data-source="chutes">
3511 <h4 data-i18n="Chutes API Key">Chutes API Key</h4>
3512 <div id="chutes_credits_display">
3513 <a href="https://chutes.ai/app/api/billing-balance" target="_blank" data-i18n="View Billing/Balance">View Billing/Balance</a>
3514 </div>
3515 <div class="flex-container">
3516 <input id="api_key_chutes" name="api_key_chutes" class="text_pole flex1" value="" type="text" autocomplete="off">
3517 <div title="Manage API keys" data-i18n="[title]Manage API keys" class="menu_button fa-solid fa-key fa-fw manage-api-keys" data-key="api_key_chutes"></div>
3518 </div>
3519 <div data-for="api_key_chutes" class="neutral_warning" data-i18n="For privacy reasons, your API key will be hidden after you click 'Connect'.">
3520 For privacy reasons, your API key will be hidden after you click 'Connect'.
3521 </div>
3522 <div>
3523 <h4 data-i18n="Chutes Model">Chutes Model</h4>
3524 <select id="model_chutes_select">
3525 <option value="" data-i18n="-- Connect to the API --">-- Connect to the API --</option>
3526 </select>
3527 </div>
3528 <div class="marginTopBot5">
3529 <div class="inline-drawer wide100p">
3530 <div class="inline-drawer-toggle inline-drawer-header">
3531 <b data-i18n="Chutes Model Sorting">Chutes Model Sorting</b>
3532 <div class="fa-solid fa-circle-chevron-down inline-drawer-icon down"></div>
3533 </div>
3534 <div class="inline-drawer-content m-b-1">
3535 <div class="marginTopBot5">
3536 <label for="chutes_sort_models" class="checkbox_label">
3537 <select id="chutes_sort_models">
3538 <option data-i18n="Alphabetically" value="alphabetically">Alphabetically</option>
3539 <option data-i18n="Input Price" value="pricing.input">Input Price (cheapest)</option>
3540 <option data-i18n="Output Price" value="pricing.output">Output Price (cheapest)</option>
3541 <option data-i18n="Context Size" value="context_length">Context Size</option>
3542 </select>
3543 </label>
3544 </div>
3545 </div>
3546 </div>
3547 </div>
3548 </div>
3471 <div id="nanogpt_form" data-source="nanogpt">3549 <div id="nanogpt_form" data-source="nanogpt">
3472 <h4 data-i18n="NanoGPT API Key">NanoGPT API Key</h4>3550 <h4 data-i18n="NanoGPT API Key">NanoGPT API Key</h4>
3473 <div class="flex-container">3551 <div class="flex-container">
@@ -3733,7 +3811,11 @@
3733 </select>3811 </select>
3734 <h4 data-i18n="Z.AI Model">Z.AI Model</h4>3812 <h4 data-i18n="Z.AI Model">Z.AI Model</h4>
3735 <select id="model_zai_select">3813 <select id="model_zai_select">
3814 <option value="glm-4.7">glm-4.7</option>
3736 <option value="glm-4.6">glm-4.6</option>3815 <option value="glm-4.6">glm-4.6</option>
3816 <option value="glm-4.6v">glm-4.6v</option>
3817 <option value="glm-4.6v-flash">glm-4.6v-flash</option>
3818 <option value="glm-4.6v-flashx">glm-4.6v-flashx</option>
3737 <option value="glm-4.5v">glm-4.5v</option>3819 <option value="glm-4.5v">glm-4.5v</option>
3738 <option value="glm-4.5">glm-4.5</option>3820 <option value="glm-4.5">glm-4.5</option>
3739 <option value="glm-4.5-air">glm-4.5-air</option>3821 <option value="glm-4.5-air">glm-4.5-air</option>
@@ -3741,6 +3823,7 @@
3741 <option value="glm-4.5-airx">glm-4.5-airx</option>3823 <option value="glm-4.5-airx">glm-4.5-airx</option>
3742 <option value="glm-4.5-flash">glm-4.5-flash</option>3824 <option value="glm-4.5-flash">glm-4.5-flash</option>
3743 <option value="glm-4-32b-0414-128k">glm-4-32b-0414-128k</option>3825 <option value="glm-4-32b-0414-128k">glm-4-32b-0414-128k</option>
3826 <option value="autoglm-phone-multilingual">autoglm-phone-multilingual</option>
3744 </select>3827 </select>
3745 </div>3828 </div>
3746 <div id="azure_openai_settings" data-source="azure_openai">3829 <div id="azure_openai_settings" data-source="azure_openai">
@@ -4697,7 +4780,7 @@
4697 <div name="themeElements" class="flex-container flexFlowColumn flexNoGap">4780 <div name="themeElements" class="flex-container flexFlowColumn flexNoGap">
4698 <!-- <h4><span data-i18n="UI Colors">Theme Settings</span></h4> -->4781 <!-- <h4><span data-i18n="UI Colors">Theme Settings</span></h4> -->
4699 <div name="AvatarAndChatDisplay" class="flex-container flexFlowColumn">4782 <div name="AvatarAndChatDisplay" class="flex-container flexFlowColumn">
4700 <div class="flex-container alignItemsBaseline">4783 <div class="flex-container alignItemsBaseline" title="This style applies to all avatars globaly, including your Persona, Character Managment, Account selection, etc." data-i18n="[title]This style applies to all avatars globaly, including your Persona, Character Managment, Account selection, etc.">
4701 <span data-i18n="Avatar Style:">Avatars:</span>4784 <span data-i18n="Avatar Style:">Avatars:</span>
4702 <select id="avatar_style" class="widthNatural flex1 margin0 text_pole">4785 <select id="avatar_style" class="widthNatural flex1 margin0 text_pole">
4703 <option value="0" data-i18n="Circle">Circle</option>4786 <option value="0" data-i18n="Circle">Circle</option>
@@ -4873,7 +4956,7 @@
4873 <input id="mesIDDisplayEnabled" type="checkbox" />4956 <input id="mesIDDisplayEnabled" type="checkbox" />
4874 <small data-i18n="Message IDs">Message IDs</small>4957 <small data-i18n="Message IDs">Message IDs</small>
4875 </label>4958 </label>
4876 <label for="hideChatAvatarsEnabled" class="checkbox_label" title="Hide avatars in chat messages." data-i18n="[title]Hide avatars in chat messages.">4959 <label for="hideChatAvatarsEnabled" class="checkbox_label" title="Hide avatars, only in chat messages." data-i18n="[title]Hide avatars, only in chat messages.">
4877 <input id="hideChatAvatarsEnabled" type="checkbox" />4960 <input id="hideChatAvatarsEnabled" type="checkbox" />
4878 <small data-i18n="Hide Chat Avatars">Hide Chat Avatars</small>4961 <small data-i18n="Hide Chat Avatars">Hide Chat Avatars</small>
4879 </label>4962 </label>
@@ -4975,22 +5058,28 @@
4975 <small data-i18n="Clean-Up">Clean-Up</small>5058 <small data-i18n="Clean-Up">Clean-Up</small>
4976 </div>5059 </div>
4977 </div>5060 </div>
4978 <label class="checkbox_label flexWrap" for="smooth_streaming">5061 <label id="smooth_streaming_control" class="checkbox_label" for="smooth_streaming">
4979 <input id="smooth_streaming" type="checkbox" />5062 <input id="smooth_streaming" type="checkbox" />
4980 <div class="flex-container alignItemsBaseline">5063 <div class="flex-container alignItemsBaseline">
4981 <small data-i18n="Smooth Streaming">5064 <small data-i18n="Smooth Streaming">
4982 Smooth Streaming5065 Smooth Streaming
4983 </small>5066 </small>
4984 </div>5067 </div>
4985 <div id="smooth_streaming_speed_control" class="flexBasis100p wide100p">
4986 <input type="range" id="smooth_streaming_speed" name="smooth_streaming_speed" min="0" max="100" step="10" value="50">
4987 <div class="slider_hint">
4988 <span data-i18n="Slow">Slow</span>
4989 <span></span>
4990 <span data-i18n="Fast">Fast</span>
4991 </div>
4992 </div>
4993 </label>5068 </label>
5069 <label id="smooth_streaming_no_think_control" class="checkbox_label" for="smooth_streaming_no_think" title="Bypass smooth streaming in reasoning blocks." data-i18n="[title]Bypass smooth streaming in reasoning blocks.">
5070 <input id="smooth_streaming_no_think" type="checkbox" />
5071 <small data-i18n="Exclude 'Thinking...'">
5072 Exclude 'Thinking...'
5073 </small>
5074 </label>
5075 <div id="smooth_streaming_speed_control" class="wide100p">
5076 <input type="range" id="smooth_streaming_speed" name="smooth_streaming_speed" min="0" max="100" step="10" value="50">
5077 <div class="slider_hint">
5078 <span data-i18n="Slow">Slow</span>
5079 <span></span>
5080 <span data-i18n="Fast">Fast</span>
5081 </div>
5082 </div>
4994 <label class="checkbox_label" for="stream_fade_in" title="Fade in streamed text when it appears, instead of it just popping in." data-i18n="[title]Fade in streamed text when it appears, instead of it just popping in">5083 <label class="checkbox_label" for="stream_fade_in" title="Fade in streamed text when it appears, instead of it just popping in." data-i18n="[title]Fade in streamed text when it appears, instead of it just popping in">
4995 <input id="stream_fade_in" type="checkbox" />5084 <input id="stream_fade_in" type="checkbox" />
4996 <small data-i18n="Stream Fade-In">Stream Fade-In</small>5085 <small data-i18n="Stream Fade-In">Stream Fade-In</small>
@@ -5191,6 +5280,11 @@
5191 <input id="encode_tags" type="checkbox" />5280 <input id="encode_tags" type="checkbox" />
5192 <small data-i18n="Show tags in responses">Show &lt;tags&gt; in responses</small>5281 <small data-i18n="Show tags in responses">Show &lt;tags&gt; in responses</small>
5193 </label>5282 </label>
5283 <label class="checkbox_label" for="experimental_macro_engine" title="Experimental new Macro Engine.&NewLine;&NewLine;Allows nested macros to be resolved correctly and has a dedicated, logical replacement order.&NewLine;The new engine is designed to cleanly replace the old regex-based macro system.">
5284 <input id="experimental_macro_engine" type="checkbox" />
5285 <small data-i18n="Experimental Macro Engine">Experimental Macro Engine</small>
5286 <i class="fa-solid fa-flask" title="Experimental feature. Currently in development to test." data-i18n="[title]Experimental feature. Currently in development to test."></i>
5287 </label>
5194 <label class="checkbox_label" for="disable_group_trimming" title="Allow AI messages in groups to contain lines spoken by other group members." data-i18n="[title]Allow AI messages in groups to contain lines spoken by other group members">5288 <label class="checkbox_label" for="disable_group_trimming" title="Allow AI messages in groups to contain lines spoken by other group members." data-i18n="[title]Allow AI messages in groups to contain lines spoken by other group members">
5195 <input id="disable_group_trimming" type="checkbox" />5289 <input id="disable_group_trimming" type="checkbox" />
5196 <small data-i18n="Relax message trim in Groups">Relax message trim in Groups</small>5290 <small data-i18n="Relax message trim in Groups">Relax message trim in Groups</small>
@@ -5383,10 +5477,9 @@
5383 <div id="Backgrounds" class="drawer-content closedDrawer bg-drawer-layout">5477 <div id="Backgrounds" class="drawer-content closedDrawer bg-drawer-layout">
5384 <div id="bg-header-fixed">5478 <div id="bg-header-fixed">
5385 <div class="bg-header-row-1">5479 <div class="bg-header-row-1">
5386 <label for="add_bg_button" id="add_background_button_top" class="menu_button menu_button_icon" data-i18n="[title]Add a new background" title="Add a new background">5480 <h3 class="margin0">
5387 <i class="fa-solid fa-plus"></i>5481 <span data-i18n="Backgrounds">Backgrounds</span>
5388 <span data-i18n="Add Background">Add Background</span>5482 </h3>
5389 </label>
5390 <span class="expander"></span>5483 <span class="expander"></span>
5391 <select id="background_fitting" class="text_pole" data-i18n="[title]Background Fitting" title="Background Fitting">5484 <select id="background_fitting" class="text_pole" data-i18n="[title]Background Fitting" title="Background Fitting">
5392 <option value="classic" data-i18n="Classic">Classic</option>5485 <option value="classic" data-i18n="Classic">Classic</option>
@@ -5399,16 +5492,23 @@
5399 <i class="fa-solid fa-wand-magic"></i>5492 <i class="fa-solid fa-wand-magic"></i>
5400 <span data-i18n="Auto-select">Auto-select</span>5493 <span data-i18n="Auto-select">Auto-select</span>
5401 </div>5494 </div>
5495 <label for="add_bg_button" id="add_background_button_top" class="menu_button menu_button_icon" data-i18n="[title]Add a new background" title="Add a new background">
5496 <i class="fa-solid fa-upload"></i>
5497 <span data-i18n="Add Background">Add Background</span>
5498 </label>
5402 </div>5499 </div>
5403 <div class="bg-header-row-2">5500 <div class="bg-header-row-2">
5404 <input id="bg-filter" class="text_pole" type="search" data-i18n="[placeholder]Search..." placeholder="Search..." />5501 <input id="bg-filter" class="text_pole" type="search" data-i18n="[placeholder]Search..." placeholder="Search..." />
5405 </div>5502 </div>
5406 </div>5503 </div>
5407 <div id="bg-scrollable-content">5504 <div id="bg_tabs" class="heading-container-with-controls">
5408 <div class="heading-container-with-controls">5505 <ul class="bg_tabs_list">
5409 <h3 data-i18n="System Backgrounds" class="wide100p textAlignCenter heading-text">5506 <li class="bg_tab_button">
5410 System Backgrounds5507 <a href="#bg_global_tab" data-i18n="Global">Global</a>
5411 </h3>5508 </li>
5509 <li class="bg_tab_button">
5510 <a href="#bg_chat_tab" data-i18n="Chat">Chat</a>
5511 </li>
5412 <div class="heading-controls">5512 <div class="heading-controls">
5413 <button id="bg_thumb_zoom_out" class="menu_button menu_button_icon" title="Make thumbnails smaller" data-i18n="[title]Make thumbnails smaller">5513 <button id="bg_thumb_zoom_out" class="menu_button menu_button_icon" title="Make thumbnails smaller" data-i18n="[title]Make thumbnails smaller">
5414 <i class="fa-solid fa-minus"></i>5514 <i class="fa-solid fa-minus"></i>
@@ -5417,20 +5517,19 @@
5417 <i class="fa-solid fa-plus"></i>5517 <i class="fa-solid fa-plus"></i>
5418 </button>5518 </button>
5419 </div>5519 </div>
5520 </ul>
5521 <div id="bg_global_tab" class="bg_tab_panel">
5522 <div id="bg_menu_content" class="bg_list"></div>
5420 </div>5523 </div>
5421 <div id="bg_menu_content" class="bg_list">5524 <div id="bg_chat_tab" class="bg_tab_panel">
5422 </div>5525 <div id="bg_custom_content" class="bg_list"></div>
5423 <h3 data-i18n="Chat Backgrounds" class="wide100p textAlignCenter">5526 <div id="bg_chat_hint" class="wide100p textAlignCenter">
5424 Chat Backgrounds5527 <span data-i18n="bg_chat_hint_1">Chat backgrounds generated with the</span> <code><i class="fa-solid fa-paintbrush"></i>&nbsp;Image Generation</code> <span data-i18n="bg_chat_hint_2">extension will appear here.</span>
5425 </h3>5528 </div>
5426 <div id="bg_chat_hint" class="wide100p textAlignCenter">
5427 <span data-i18n="bg_chat_hint_1">Chat backgrounds generated with the</span> <code><i class="fa-solid fa-paintbrush"></i>&nbsp;Image Generation</code> <span data-i18n="bg_chat_hint_2">extension will appear here.</span>
5428 </div>
5429 <div id="bg_custom_content" class="bg_list">
5430 </div>5529 </div>
5431 </div>5530 </div>
5432 <form id="form_bg_upload" style="display: none;">5531 <form id="form_bg_upload" hidden>
5433 <input type="file" id="add_bg_button" name="avatar" accept="image/jpeg,image/png,image/gif,image/bmp,image/svg+xml,video/*">5532 <input type="file" id="add_bg_button" name="avatar" accept="image/*,video/*" multiple hidden>
5434 </form>5533 </form>
5435 <button id="bg-scroll-top" type="button" class="menu_button menu_button_icon" title="Scroll backgrounds to top" data-i18n="[title]Scroll backgrounds to top">5534 <button id="bg-scroll-top" type="button" class="menu_button menu_button_icon" title="Scroll backgrounds to top" data-i18n="[title]Scroll backgrounds to top">
5436 <i class="fa-solid fa-chevron-up" aria-hidden="true"></i>5535 <i class="fa-solid fa-chevron-up" aria-hidden="true"></i>
@@ -6379,7 +6478,7 @@
6379 <div class="jg-menu">6478 <div class="jg-menu">
6380 <div data-action="lock" class="jg-button jg-lock fa-solid fa-lock fa-fw pointer" data-i18n="[title]Lock" title="Lock"></div>6479 <div data-action="lock" class="jg-button jg-lock fa-solid fa-lock fa-fw pointer" data-i18n="[title]Lock" title="Lock"></div>
6381 <div data-action="unlock" class="jg-button jg-unlock fa-solid fa-lock-open fa-fw pointer" data-i18n="[title]Unlock" title="Unlock"></div>6480 <div data-action="unlock" class="jg-button jg-unlock fa-solid fa-lock-open fa-fw pointer" data-i18n="[title]Unlock" title="Unlock"></div>
6382 <div data-action="copy" class="jg-button jg-copy fa-solid fa-file-arrow-up" data-i18n="[title]Copy to system backgrounds" title="Copy to system backgrounds"></div>6481 <div data-action="copy" class="jg-button jg-copy fa-solid fa-file-arrow-up" data-i18n="[title]Copy to global backgrounds" title="Copy to global backgrounds"></div>
6383 <div data-action="edit" class="jg-button jg-edit fa-solid fa-pen-to-square fa-fw pointer" data-i18n="[title]Rename Background" title="Rename Background"></div>6482 <div data-action="edit" class="jg-button jg-edit fa-solid fa-pen-to-square fa-fw pointer" data-i18n="[title]Rename Background" title="Rename Background"></div>
6384 <div data-action="delete" class="jg-button jg-delete fa-solid fa-trash-can fa-fw pointer" data-i18n="[title]Delete Background" title="Delete Background"></div>6483 <div data-action="delete" class="jg-button jg-delete fa-solid fa-trash-can fa-fw pointer" data-i18n="[title]Delete Background" title="Delete Background"></div>
6385 </div>6484 </div>
@@ -7047,7 +7146,7 @@
7047 <div class="mes_timer"></div>7146 <div class="mes_timer"></div>
7048 <div class="tokenCounterDisplay"></div>7147 <div class="tokenCounterDisplay"></div>
7049 </div>7148 </div>
7050 <div class="swipe_left fa-solid fa-chevron-left" style="display: none;"></div>7149 <div class="swipe_left fa-solid fa-chevron-left"></div>
7051 <div class="mes_block">7150 <div class="mes_block">
7052 <div class="ch_name flex-container justifySpaceBetween">7151 <div class="ch_name flex-container justifySpaceBetween">
7053 <div class="flex-container flex1 alignitemscenter">7152 <div class="flex-container flex1 alignitemscenter">
@@ -7111,7 +7210,7 @@
7111 <div class="mes_bias"></div>7210 <div class="mes_bias"></div>
7112 </div>7211 </div>
7113 <div class="flex-container swipeRightBlock flexFlowColumn flexNoGap">7212 <div class="flex-container swipeRightBlock flexFlowColumn flexNoGap">
7114 <div class="swipe_right fa-solid fa-chevron-right" style="display: none;"></div>7213 <div class="swipe_right fa-solid fa-chevron-right"></div>
7115 <div class="swipes-counter"></div>7214 <div class="swipes-counter"></div>
7116 </div>7215 </div>
7117 </div>7216 </div>
@@ -7210,9 +7309,8 @@
7210 <div title="Move down" data-action="down" class="right_menu_button fa-solid fa-chevron-down" data-i18n="[title]Move down"></div>7309 <div title="Move down" data-action="down" class="right_menu_button fa-solid fa-chevron-down" data-i18n="[title]Move down"></div>
7211 </div>7310 </div>
7212 <div title="View character card" data-action="view" class="right_menu_button fa-solid fa-xl fa-image-portrait" data-i18n="[title]View character card"></div>7311 <div title="View character card" data-action="view" class="right_menu_button fa-solid fa-xl fa-image-portrait" data-i18n="[title]View character card"></div>
7213 <div title="Remove from group" data-action="remove" class="right_menu_button fa-solid fa-2xl fa-xmark" data-i18n="[title]Remove from group">7312 <div title="Remove from group" data-action="remove" class="right_menu_button fa-solid fa-xl fa-xmark" data-i18n="[title]Remove from group"></div>
7214 </div>7313 <div title="Add to group" data-action="add" class="right_menu_button fa-solid fa-xl fa-plus" data-i18n="[title]Add to group"></div>
7215 <div title="Add to group" data-action="add" class="right_menu_button fa-solid fa-2xl fa-plus" data-i18n="[title]Add to group"></div>
7216 </div>7314 </div>
7217 </div>7315 </div>
7218 </div>7316 </div>
public/jsconfig.json+7 -1
@@ -6,7 +6,13 @@
6 "moduleResolution": "node",6 "moduleResolution": "node",
7 "allowUmdGlobalAccess": true,7 "allowUmdGlobalAccess": true,
8 "allowSyntheticDefaultImports": true,8 "allowSyntheticDefaultImports": true,
9 "strictBindCallApply": true9 "strictBindCallApply": true,
10 "baseUrl": ".",
11 "paths": {
12 "/*": [
13 "./*"
14 ]
15 }
10 },16 },
11 "exclude": [17 "exclude": [
12 "**/node_modules/**",18 "**/node_modules/**",
public/lib.js+3 -0
@@ -22,6 +22,7 @@ import morphdom from 'morphdom';
22import { toggle as slideToggle } from 'slidetoggle';22import { toggle as slideToggle } from 'slidetoggle';
23import chalk from 'chalk';23import chalk from 'chalk';
24import yaml from 'yaml';24import yaml from 'yaml';
25import * as chevrotain from 'chevrotain';
2526
26/**27/**
27 * Expose the libraries to the 'window' object.28 * Expose the libraries to the 'window' object.
@@ -100,6 +101,7 @@ export default {
100 slideToggle,101 slideToggle,
101 chalk,102 chalk,
102 yaml,103 yaml,
104 chevrotain,
103};105};
104106
105export {107export {
@@ -124,4 +126,5 @@ export {
124 slideToggle,126 slideToggle,
125 chalk,127 chalk,
126 yaml,128 yaml,
129 chevrotain,
127};130};
public/locales/ar-sa.json+6 -6
@@ -235,7 +235,7 @@
235 "Combines consecutive system messages into one (excluding example dialogues). May improve coherence for some models.": "يجمع الرسائل المتتالية للنظام في رسالة واحدة (باستثناء الحوارات المثالية). قد يحسن التتابع لبعض النماذج.",235 "Combines consecutive system messages into one (excluding example dialogues). May improve coherence for some models.": "يجمع الرسائل المتتالية للنظام في رسالة واحدة (باستثناء الحوارات المثالية). قد يحسن التتابع لبعض النماذج.",
236 "Enable function calling": "تمكين استدعاء الوظيفة",236 "Enable function calling": "تمكين استدعاء الوظيفة",
237 "Send inline images": "إرسال الصور المضمنة",237 "Send inline images": "إرسال الصور المضمنة",
238 "image_inlining_hint_1": "يرسل الصور في المطالبات إذا كان النموذج يدعمها .",238 "image_inlining_hint_1": "يرسل ملفات الوسائط في المطالبات إذا كان النموذج يدعمها .",
239 "image_inlining_hint_2": "الإجراء على أي رسالة أو",239 "image_inlining_hint_2": "الإجراء على أي رسالة أو",
240 "image_inlining_hint_3": "القائمة لإرفاق ملف صورة للدردشة.",240 "image_inlining_hint_3": "القائمة لإرفاق ملف صورة للدردشة.",
241 "Inline Image Quality": "جودة الصورة المضمنة",241 "Inline Image Quality": "جودة الصورة المضمنة",
@@ -328,7 +328,7 @@
328 "PygmalionAI/aphrodite-engine": "PygmalionAI/aphrodite-engine (وضع التغليف لواجهة برمجة التطبيقات OpenAI)",328 "PygmalionAI/aphrodite-engine": "PygmalionAI/aphrodite-engine (وضع التغليف لواجهة برمجة التطبيقات OpenAI)",
329 "Aphrodite API key": "مفتاح واجهة برمجة التطبيقات Aphrodite",329 "Aphrodite API key": "مفتاح واجهة برمجة التطبيقات Aphrodite",
330 "Aphrodite Model": "نموذج أفروديت",330 "Aphrodite Model": "نموذج أفروديت",
331 "ggerganov/llama.cpp": "ggerganov/llama.cpp (خادم إخراج)",331 "ggml-org/llama.cpp": "ggml-org/llama.cpp (خادم إخراج)",
332 "Example: http://127.0.0.1:8080": "مثال: http://127.0.0.1:8080",332 "Example: http://127.0.0.1:8080": "مثال: http://127.0.0.1:8080",
333 "Example: http://127.0.0.1:11434": "مثال: http://127.0.0.1:11434",333 "Example: http://127.0.0.1:11434": "مثال: http://127.0.0.1:11434",
334 "Ollama Model": "نموذج Ollama",334 "Ollama Model": "نموذج Ollama",
@@ -605,7 +605,7 @@
605 "Model Icon": "أيقونة النموذج",605 "Model Icon": "أيقونة النموذج",
606 "Show sequential message numbers in the chat log": "إظهار أرقام الرسائل التسلسلية في سجل الدردشة",606 "Show sequential message numbers in the chat log": "إظهار أرقام الرسائل التسلسلية في سجل الدردشة",
607 "Message IDs": "معرفات الرسائل",607 "Message IDs": "معرفات الرسائل",
608 "Hide avatars in chat messages.": "إخفاء الصور الرمزية في رسائل الدردشة.",608 "Hide avatars, only in chat messages.": "إخفاء الصور الرمزية في رسائل الدردشة.",
609 "Hide Chat Avatars": "إخفاء الصور الرمزية للدردشة",609 "Hide Chat Avatars": "إخفاء الصور الرمزية للدردشة",
610 "Show the number of tokens in each message in the chat log": "عرض عدد الرموز في كل رسالة في سجل الدردشة",610 "Show the number of tokens in each message in the chat log": "عرض عدد الرموز في كل رسالة في سجل الدردشة",
611 "Show Message Token Count": "عرض عدد الرموز في الرسالة",611 "Show Message Token Count": "عرض عدد الرموز في الرسالة",
@@ -739,8 +739,8 @@
739 "Filter": "تصفية",739 "Filter": "تصفية",
740 "Automatically select a background based on the chat context": "تحديد خلفية تلقائيًا استنادًا إلى سياق الدردشة",740 "Automatically select a background based on the chat context": "تحديد خلفية تلقائيًا استنادًا إلى سياق الدردشة",
741 "Auto-select": "التحديد التلقائي",741 "Auto-select": "التحديد التلقائي",
742 "System Backgrounds": "خلفيات النظام",742 "Global": "عالمي",
743 "Chat Backgrounds": "خلفيات الدردشة",743 "Chat": "الدردشة",
744 "bg_chat_hint_1": "خلفيات الدردشة التي تم إنشاؤها باستخدام",744 "bg_chat_hint_1": "خلفيات الدردشة التي تم إنشاؤها باستخدام",
745 "bg_chat_hint_2": "سوف يظهر الامتداد هنا",745 "bg_chat_hint_2": "سوف يظهر الامتداد هنا",
746 "Extensions": "امتدادات",746 "Extensions": "امتدادات",
@@ -913,7 +913,7 @@
913 "Save": "حفظ",913 "Save": "حفظ",
914 "Chat History": "تاريخ الدردشة",914 "Chat History": "تاريخ الدردشة",
915 "Import Chat": "استيراد الدردشة",915 "Import Chat": "استيراد الدردشة",
916 "Copy to system backgrounds": "نسخ إلى خلفيات النظام",916 "Copy to global backgrounds": "نسخ إلى الخلفيات العالمية",
917 "Rename background": "إعادة تسمية الخلفية",917 "Rename background": "إعادة تسمية الخلفية",
918 "Lock": "قفل",918 "Lock": "قفل",
919 "Unlock": "الغاء القفل",919 "Unlock": "الغاء القفل",
public/locales/de-de.json+6 -6
@@ -235,7 +235,7 @@
235 "Combines consecutive system messages into one (excluding example dialogues). May improve coherence for some models.": "Kombiniert aufeinanderfolgende Systemnachrichten zu einer (ausschließlich Beispiel-Dialoge ausgeschlossen). Kann die Kohärenz für einige Modelle verbessern.",235 "Combines consecutive system messages into one (excluding example dialogues). May improve coherence for some models.": "Kombiniert aufeinanderfolgende Systemnachrichten zu einer (ausschließlich Beispiel-Dialoge ausgeschlossen). Kann die Kohärenz für einige Modelle verbessern.",
236 "Enable function calling": "Funktionsaufruf aktivieren",236 "Enable function calling": "Funktionsaufruf aktivieren",
237 "Send inline images": "Inline-Bilder senden",237 "Send inline images": "Inline-Bilder senden",
238 "image_inlining_hint_1": "Sendet Bilder in Eingabeaufforderungen, wenn das Modell dies unterstützt.",238 "image_inlining_hint_1": "Sendet Mediendateien in Eingabeaufforderungen, wenn das Modell dies unterstützt.",
239 "image_inlining_hint_2": "Aktion auf eine Nachricht oder die",239 "image_inlining_hint_2": "Aktion auf eine Nachricht oder die",
240 "image_inlining_hint_3": "Menü, um eine Bilddatei an den Chat anzuhängen.",240 "image_inlining_hint_3": "Menü, um eine Bilddatei an den Chat anzuhängen.",
241 "Inline Image Quality": "Inline-Bildqualität",241 "Inline Image Quality": "Inline-Bildqualität",
@@ -328,7 +328,7 @@
328 "PygmalionAI/aphrodite-engine": "PygmalionAI/aphrodite-engine (Wrappermodus für OpenAI API)",328 "PygmalionAI/aphrodite-engine": "PygmalionAI/aphrodite-engine (Wrappermodus für OpenAI API)",
329 "Aphrodite API key": "Aphrodite API-Schlüssel",329 "Aphrodite API key": "Aphrodite API-Schlüssel",
330 "Aphrodite Model": "Aphrodite-Modell",330 "Aphrodite Model": "Aphrodite-Modell",
331 "ggerganov/llama.cpp": "ggerganov/llama.cpp (Output-Server)",331 "ggml-org/llama.cpp": "ggml-org/llama.cpp (Output-Server)",
332 "Example: http://127.0.0.1:8080": "Beispiel: http://127.0.0.1:8080",332 "Example: http://127.0.0.1:8080": "Beispiel: http://127.0.0.1:8080",
333 "Example: http://127.0.0.1:11434": "Beispiel: http://127.0.0.1:11434",333 "Example: http://127.0.0.1:11434": "Beispiel: http://127.0.0.1:11434",
334 "Ollama Model": "Ollama-Modell",334 "Ollama Model": "Ollama-Modell",
@@ -605,7 +605,7 @@
605 "Model Icon": "Modell-Icon",605 "Model Icon": "Modell-Icon",
606 "Show sequential message numbers in the chat log": "Zeige aufeinanderfolgende Nachrichtennummern im Chat-Log",606 "Show sequential message numbers in the chat log": "Zeige aufeinanderfolgende Nachrichtennummern im Chat-Log",
607 "Message IDs": "Nachrichten-IDs",607 "Message IDs": "Nachrichten-IDs",
608 "Hide avatars in chat messages.": "Avatare in Chatnachrichten verbergen.",608 "Hide avatars, only in chat messages.": "Avatare in Chatnachrichten verbergen.",
609 "Hide Chat Avatars": "Chat-Avatare ausblenden",609 "Hide Chat Avatars": "Chat-Avatare ausblenden",
610 "Show the number of tokens in each message in the chat log": "Zeige die Anzahl der Tokens in jeder Nachricht im Chat-Log",610 "Show the number of tokens in each message in the chat log": "Zeige die Anzahl der Tokens in jeder Nachricht im Chat-Log",
611 "Show Message Token Count": "Anzahl der Nachrichten-Token anzeigen",611 "Show Message Token Count": "Anzahl der Nachrichten-Token anzeigen",
@@ -739,8 +739,8 @@
739 "Filter": "Filter",739 "Filter": "Filter",
740 "Automatically select a background based on the chat context": "Automatisch einen Hintergrund basierend auf dem Chatkontext auswählen",740 "Automatically select a background based on the chat context": "Automatisch einen Hintergrund basierend auf dem Chatkontext auswählen",
741 "Auto-select": "Automatische Auswahl",741 "Auto-select": "Automatische Auswahl",
742 "System Backgrounds": "Systemhintergründe",742 "Global": "Global",
743 "Chat Backgrounds": "Chat-Hintergründe",743 "Chat": "Chat",
744 "bg_chat_hint_1": "Chat-Hintergründe generiert mit dem",744 "bg_chat_hint_1": "Chat-Hintergründe generiert mit dem",
745 "bg_chat_hint_2": "Die Erweiterung wird hier angezeigt.",745 "bg_chat_hint_2": "Die Erweiterung wird hier angezeigt.",
746 "Extensions": "Erweiterungen",746 "Extensions": "Erweiterungen",
@@ -913,7 +913,7 @@
913 "Save": "Speichern",913 "Save": "Speichern",
914 "Chat History": "Chat-Verlauf",914 "Chat History": "Chat-Verlauf",
915 "Import Chat": "Chat importieren",915 "Import Chat": "Chat importieren",
916 "Copy to system backgrounds": "In Systemhintergründe kopieren",916 "Copy to global backgrounds": "In globale Hintergründe kopieren",
917 "Rename background": "Hintergrund umbenennen",917 "Rename background": "Hintergrund umbenennen",
918 "Lock": "Sperren",918 "Lock": "Sperren",
919 "Unlock": "Freischalten",919 "Unlock": "Freischalten",
public/locales/es-es.json+6 -6
@@ -235,7 +235,7 @@
235 "Combines consecutive system messages into one (excluding example dialogues). May improve coherence for some models.": "Combina mensajes del sistema consecutivos en uno solo (excluyendo diálogos de ejemplo). Puede mejorar la coherencia para algunos modelos.",235 "Combines consecutive system messages into one (excluding example dialogues). May improve coherence for some models.": "Combina mensajes del sistema consecutivos en uno solo (excluyendo diálogos de ejemplo). Puede mejorar la coherencia para algunos modelos.",
236 "Enable function calling": "Habilitar llamada a función",236 "Enable function calling": "Habilitar llamada a función",
237 "Send inline images": "Enviar imágenes en línea",237 "Send inline images": "Enviar imágenes en línea",
238 "image_inlining_hint_1": "Envía imágenes en mensajes si el modelo lo admite.",238 "image_inlining_hint_1": "Envía archivos multimedia en mensajes si el modelo lo admite.",
239 "image_inlining_hint_2": "acción sobre cualquier mensaje o el",239 "image_inlining_hint_2": "acción sobre cualquier mensaje o el",
240 "image_inlining_hint_3": "menú para adjuntar un archivo de imagen al chat.",240 "image_inlining_hint_3": "menú para adjuntar un archivo de imagen al chat.",
241 "Inline Image Quality": "Calidad de imagen en línea",241 "Inline Image Quality": "Calidad de imagen en línea",
@@ -328,7 +328,7 @@
328 "PygmalionAI/aphrodite-engine": "PygmalionAI/aphrodite-engine (Modo envolvente para API de OpenAI)",328 "PygmalionAI/aphrodite-engine": "PygmalionAI/aphrodite-engine (Modo envolvente para API de OpenAI)",
329 "Aphrodite API key": "Clave de API de Aphrodite",329 "Aphrodite API key": "Clave de API de Aphrodite",
330 "Aphrodite Model": "Modelo Afrodita",330 "Aphrodite Model": "Modelo Afrodita",
331 "ggerganov/llama.cpp": "ggerganov/llama.cpp (Servidor de salida)",331 "ggml-org/llama.cpp": "ggml-org/llama.cpp (Servidor de salida)",
332 "Example: http://127.0.0.1:8080": "Ejemplo: http://127.0.0.1:8080",332 "Example: http://127.0.0.1:8080": "Ejemplo: http://127.0.0.1:8080",
333 "Example: http://127.0.0.1:11434": "Ejemplo: http://127.0.0.1:11434",333 "Example: http://127.0.0.1:11434": "Ejemplo: http://127.0.0.1:11434",
334 "Ollama Model": "Modelo Ollama",334 "Ollama Model": "Modelo Ollama",
@@ -605,7 +605,7 @@
605 "Model Icon": "Ícono del Modelo",605 "Model Icon": "Ícono del Modelo",
606 "Show sequential message numbers in the chat log": "Mostrar números de mensaje secuenciales en el registro de chat",606 "Show sequential message numbers in the chat log": "Mostrar números de mensaje secuenciales en el registro de chat",
607 "Message IDs": "IDs de Mensaje",607 "Message IDs": "IDs de Mensaje",
608 "Hide avatars in chat messages.": "Ocultar avatares en los mensajes de chat.",608 "Hide avatars, only in chat messages.": "Ocultar avatares en los mensajes de chat.",
609 "Hide Chat Avatars": "Ocultar avatares de chat",609 "Hide Chat Avatars": "Ocultar avatares de chat",
610 "Show the number of tokens in each message in the chat log": "Mostrar el número de tokens en cada mensaje en el registro de chat",610 "Show the number of tokens in each message in the chat log": "Mostrar el número de tokens en cada mensaje en el registro de chat",
611 "Show Message Token Count": "Mostrar Conteo de Tokens en Mensaje",611 "Show Message Token Count": "Mostrar Conteo de Tokens en Mensaje",
@@ -739,8 +739,8 @@
739 "Filter": "Filtro",739 "Filter": "Filtro",
740 "Automatically select a background based on the chat context": "Seleccionar automáticamente un fondo basado en el contexto del chat",740 "Automatically select a background based on the chat context": "Seleccionar automáticamente un fondo basado en el contexto del chat",
741 "Auto-select": "Auto-seleccionar",741 "Auto-select": "Auto-seleccionar",
742 "System Backgrounds": "Fondos del Sistema",742 "Global": "Global",
743 "Chat Backgrounds": "Fondos de Chat",743 "Chat": "Chat",
744 "bg_chat_hint_1": "Fondos de chat generados con el",744 "bg_chat_hint_1": "Fondos de chat generados con el",
745 "bg_chat_hint_2": "La extensión aparecerá aquí.",745 "bg_chat_hint_2": "La extensión aparecerá aquí.",
746 "Extensions": "Extensiones",746 "Extensions": "Extensiones",
@@ -913,7 +913,7 @@
913 "Save": "Guardar",913 "Save": "Guardar",
914 "Chat History": "Historial de chat",914 "Chat History": "Historial de chat",
915 "Import Chat": "Importar chat",915 "Import Chat": "Importar chat",
916 "Copy to system backgrounds": "Copiar a fondos del sistema",916 "Copy to global backgrounds": "Copiar a fondos globales",
917 "Rename background": "Cambiar nombre de fondo",917 "Rename background": "Cambiar nombre de fondo",
918 "Lock": "Cerrar",918 "Lock": "Cerrar",
919 "Unlock": "desbloquear",919 "Unlock": "desbloquear",
public/locales/fr-fr.json+6 -6
@@ -227,7 +227,7 @@
227 "Combines consecutive system messages into one (excluding example dialogues). May improve coherence for some models.": "Combine les messages système consécutifs en un seul (à l'exclusion des dialogues d'exemple). Peut améliorer la cohérence pour certains modèles.",227 "Combines consecutive system messages into one (excluding example dialogues). May improve coherence for some models.": "Combine les messages système consécutifs en un seul (à l'exclusion des dialogues d'exemple). Peut améliorer la cohérence pour certains modèles.",
228 "Enable function calling": "Activer l'appel de fonction",228 "Enable function calling": "Activer l'appel de fonction",
229 "Send inline images": "Envoyer des images en ligne",229 "Send inline images": "Envoyer des images en ligne",
230 "image_inlining_hint_1": "Envoie des images dans les prompts si le modèle le prend en charge.",230 "image_inlining_hint_1": "Envoie des fichiers multimédias dans les prompts si le modèle le prend en charge.",
231 "image_inlining_hint_2": "action sur n'importe quel message ou le",231 "image_inlining_hint_2": "action sur n'importe quel message ou le",
232 "image_inlining_hint_3": "menu pour joindre un fichier image au chat.",232 "image_inlining_hint_3": "menu pour joindre un fichier image au chat.",
233 "Inline Image Quality": "Qualité d'image en ligne",233 "Inline Image Quality": "Qualité d'image en ligne",
@@ -309,7 +309,7 @@
309 "PygmalionAI/aphrodite-engine": "PygmalionAI/aphrodite-engine (mode wrapper pour l'API OpenAI)",309 "PygmalionAI/aphrodite-engine": "PygmalionAI/aphrodite-engine (mode wrapper pour l'API OpenAI)",
310 "Aphrodite API key": "Clé API Aphrodite",310 "Aphrodite API key": "Clé API Aphrodite",
311 "Aphrodite Model": "Modèle Aphrodite",311 "Aphrodite Model": "Modèle Aphrodite",
312 "ggerganov/llama.cpp": "ggerganov/llama.cpp",312 "ggml-org/llama.cpp": "ggml-org/llama.cpp",
313 "Example: http://127.0.0.1:8080": "Exemple : http://127.0.0.1:8080",313 "Example: http://127.0.0.1:8080": "Exemple : http://127.0.0.1:8080",
314 "Example: http://127.0.0.1:11434": "Exemple : http://127.0.0.1:11434",314 "Example: http://127.0.0.1:11434": "Exemple : http://127.0.0.1:11434",
315 "Ollama Model": "Modèle Ollama",315 "Ollama Model": "Modèle Ollama",
@@ -554,7 +554,7 @@
554 "Model Icon": "Icône du modèle",554 "Model Icon": "Icône du modèle",
555 "Show sequential message numbers in the chat log": "Afficher les numéros de message séquentiels dans le journal de discussion",555 "Show sequential message numbers in the chat log": "Afficher les numéros de message séquentiels dans le journal de discussion",
556 "Message IDs": "Identifiants du message",556 "Message IDs": "Identifiants du message",
557 "Hide avatars in chat messages.": "Masquez les avatars dans les messages de discussion.",557 "Hide avatars, only in chat messages.": "Masquez les avatars dans les messages de discussion.",
558 "Hide Chat Avatars": "Masquer les avatars du chat",558 "Hide Chat Avatars": "Masquer les avatars du chat",
559 "Show the number of tokens in each message in the chat log": "Afficher le nombre de tokens dans chaque message dans le journal de discussion",559 "Show the number of tokens in each message in the chat log": "Afficher le nombre de tokens dans chaque message dans le journal de discussion",
560 "Show Message Token Count": "Afficher le nombre de tokens du message",560 "Show Message Token Count": "Afficher le nombre de tokens du message",
@@ -684,8 +684,8 @@
684 "Filter": "Filtre",684 "Filter": "Filtre",
685 "Automatically select a background based on the chat context": "Sélectionner automatiquement un arrière-plan en fonction du contexte de la discussion",685 "Automatically select a background based on the chat context": "Sélectionner automatiquement un arrière-plan en fonction du contexte de la discussion",
686 "Auto-select": "Sélection automatique",686 "Auto-select": "Sélection automatique",
687 "System Backgrounds": "Arrière-plans du système",687 "Global": "Global",
688 "Chat Backgrounds": "Arrière-plans de la discussion",688 "Chat": "Discussion",
689 "bg_chat_hint_1": "Les arrière-plans générés avec l'extension ",689 "bg_chat_hint_1": "Les arrière-plans générés avec l'extension ",
690 "bg_chat_hint_2": " apparaîtrons ici.",690 "bg_chat_hint_2": " apparaîtrons ici.",
691 "Extensions": "Extensions",691 "Extensions": "Extensions",
@@ -856,7 +856,7 @@
856 "Save": "Enregistrer",856 "Save": "Enregistrer",
857 "Chat History": "Historique de la conversation",857 "Chat History": "Historique de la conversation",
858 "Import Chat": "Importer une discussion",858 "Import Chat": "Importer une discussion",
859 "Copy to system backgrounds": "Copier vers les arrière-plans du système",859 "Copy to global backgrounds": "Copier vers les arrière-plans globaux",
860 "Rename background": "Renommer l'arrière-plan",860 "Rename background": "Renommer l'arrière-plan",
861 "Lock": "Verrouiller",861 "Lock": "Verrouiller",
862 "Unlock": "Déverrouiller",862 "Unlock": "Déverrouiller",
public/locales/is-is.json+6 -6
@@ -235,7 +235,7 @@
235 "Combines consecutive system messages into one (excluding example dialogues). May improve coherence for some models.": "Sameinar samhliða kerfisskilaboð í eitt (sem er utan umsagna dæmum). Getur bætt samfelldni fyrir sumar módel.",235 "Combines consecutive system messages into one (excluding example dialogues). May improve coherence for some models.": "Sameinar samhliða kerfisskilaboð í eitt (sem er utan umsagna dæmum). Getur bætt samfelldni fyrir sumar módel.",
236 "Enable function calling": "Virkja aðgerðarkall",236 "Enable function calling": "Virkja aðgerðarkall",
237 "Send inline images": "Senda myndir í línu",237 "Send inline images": "Senda myndir í línu",
238 "image_inlining_hint_1": "Sendir myndir í skilaboðum ef líkanið styður það.",238 "image_inlining_hint_1": "Sendir margmiðlunarskrár í skilaboðum ef líkanið styður það.",
239 "image_inlining_hint_2": "aðgerð á hvaða skilaboðum sem er eða",239 "image_inlining_hint_2": "aðgerð á hvaða skilaboðum sem er eða",
240 "image_inlining_hint_3": "valmynd til að hengja myndskrá við spjallið.",240 "image_inlining_hint_3": "valmynd til að hengja myndskrá við spjallið.",
241 "Inline Image Quality": "Innbyggð myndgæði",241 "Inline Image Quality": "Innbyggð myndgæði",
@@ -326,7 +326,7 @@
326 "PygmalionAI/aphrodite-engine": "PygmalionAI/aphrodite-engine (OpenAI forritunargrensl)",326 "PygmalionAI/aphrodite-engine": "PygmalionAI/aphrodite-engine (OpenAI forritunargrensl)",
327 "Aphrodite API key": "Aphrodite API lykill",327 "Aphrodite API key": "Aphrodite API lykill",
328 "Aphrodite Model": "Afródíta fyrirmynd",328 "Aphrodite Model": "Afródíta fyrirmynd",
329 "ggerganov/llama.cpp": "ggerganov/llama.cpp (úttak þjónn)",329 "ggml-org/llama.cpp": "ggml-org/llama.cpp (úttak þjónn)",
330 "Example: http://127.0.0.1:8080": "Dæmi: http://127.0.0.1:8080",330 "Example: http://127.0.0.1:8080": "Dæmi: http://127.0.0.1:8080",
331 "Example: http://127.0.0.1:11434": "Dæmi: http://127.0.0.1:11434",331 "Example: http://127.0.0.1:11434": "Dæmi: http://127.0.0.1:11434",
332 "Ollama Model": "Ollama módel",332 "Ollama Model": "Ollama módel",
@@ -603,7 +603,7 @@
603 "Model Icon": "Módel Tákn",603 "Model Icon": "Módel Tákn",
604 "Show sequential message numbers in the chat log": "Sýna röðuð skilaboðanúmer í spjall skránni",604 "Show sequential message numbers in the chat log": "Sýna röðuð skilaboðanúmer í spjall skránni",
605 "Message IDs": "Skilaboðaauðkenni",605 "Message IDs": "Skilaboðaauðkenni",
606 "Hide avatars in chat messages.": "Fela avatar í spjallskilaboðum.",606 "Hide avatars, only in chat messages.": "Fela avatar í spjallskilaboðum.",
607 "Hide Chat Avatars": "Fela spjallmyndir",607 "Hide Chat Avatars": "Fela spjallmyndir",
608 "Show the number of tokens in each message in the chat log": "Sýna fjölda tákn í hverju skilaboði í spjall skránni",608 "Show the number of tokens in each message in the chat log": "Sýna fjölda tákn í hverju skilaboði í spjall skránni",
609 "Show Message Token Count": "Sýna fjölda tákn í skilaboðum",609 "Show Message Token Count": "Sýna fjölda tákn í skilaboðum",
@@ -737,8 +737,8 @@
737 "Filter": "Sía",737 "Filter": "Sía",
738 "Automatically select a background based on the chat context": "Velja sjálfkrafa bakgrunn út frá samhengi spjallsins",738 "Automatically select a background based on the chat context": "Velja sjálfkrafa bakgrunn út frá samhengi spjallsins",
739 "Auto-select": "Sjálfval",739 "Auto-select": "Sjálfval",
740 "System Backgrounds": "Kerfis bakgrunnsmyndir",740 "Global": "Almennt",
741 "Chat Backgrounds": "Bakgrunnsmyndir spjalls",741 "Chat": "Spjall",
742 "bg_chat_hint_1": "Spjallbakgrunnur myndaður með",742 "bg_chat_hint_1": "Spjallbakgrunnur myndaður með",
743 "bg_chat_hint_2": "viðbót mun birtast hér.",743 "bg_chat_hint_2": "viðbót mun birtast hér.",
744 "Extensions": "Viðbætur",744 "Extensions": "Viðbætur",
@@ -911,7 +911,7 @@
911 "Save": "Vista",911 "Save": "Vista",
912 "Chat History": "Spjall saga",912 "Chat History": "Spjall saga",
913 "Import Chat": "Flytja inn spjall",913 "Import Chat": "Flytja inn spjall",
914 "Copy to system backgrounds": "Afritaðu í kerfisbakgrunn",914 "Copy to global backgrounds": "Afritaðu í alþjóðlegan bakgrunn",
915 "Rename background": "Endurnefna bakgrunn",915 "Rename background": "Endurnefna bakgrunn",
916 "Lock": "Læsa",916 "Lock": "Læsa",
917 "Unlock": "Opnaðu",917 "Unlock": "Opnaðu",
public/locales/it-it.json+6 -6
@@ -235,7 +235,7 @@
235 "Combines consecutive system messages into one (excluding example dialogues). May improve coherence for some models.": "Combina i messaggi di sistema consecutivi in uno solo (escludendo i dialoghi di esempio). Potrebbe migliorare la coerenza per alcuni modelli.",235 "Combines consecutive system messages into one (excluding example dialogues). May improve coherence for some models.": "Combina i messaggi di sistema consecutivi in uno solo (escludendo i dialoghi di esempio). Potrebbe migliorare la coerenza per alcuni modelli.",
236 "Enable function calling": "Abilita la chiamata alla funzione",236 "Enable function calling": "Abilita la chiamata alla funzione",
237 "Send inline images": "Invia immagini inline",237 "Send inline images": "Invia immagini inline",
238 "image_inlining_hint_1": "Invia immagini nei prompt se il modello lo supporta.",238 "image_inlining_hint_1": "Invia file multimediali nei prompt se il modello lo supporta.",
239 "image_inlining_hint_2": "azione su qualsiasi messaggio o il",239 "image_inlining_hint_2": "azione su qualsiasi messaggio o il",
240 "image_inlining_hint_3": "menu per allegare un file immagine alla chat.",240 "image_inlining_hint_3": "menu per allegare un file immagine alla chat.",
241 "Inline Image Quality": "Qualità dell'immagine in linea",241 "Inline Image Quality": "Qualità dell'immagine in linea",
@@ -328,7 +328,7 @@
328 "PygmalionAI/aphrodite-engine": "PygmalionAI/aphrodite-engine (Modalità wrapper per l'API OpenAI)",328 "PygmalionAI/aphrodite-engine": "PygmalionAI/aphrodite-engine (Modalità wrapper per l'API OpenAI)",
329 "Aphrodite API key": "Chiave API di Aphrodite",329 "Aphrodite API key": "Chiave API di Aphrodite",
330 "Aphrodite Model": "Modello di Afrodite",330 "Aphrodite Model": "Modello di Afrodite",
331 "ggerganov/llama.cpp": "ggerganov/llama.cpp (Server di output)",331 "ggml-org/llama.cpp": "ggml-org/llama.cpp (Server di output)",
332 "Example: http://127.0.0.1:8080": "Esempio: http://127.0.0.1:8080",332 "Example: http://127.0.0.1:8080": "Esempio: http://127.0.0.1:8080",
333 "Example: http://127.0.0.1:11434": "Esempio: http://127.0.0.1:11434",333 "Example: http://127.0.0.1:11434": "Esempio: http://127.0.0.1:11434",
334 "Ollama Model": "Modello Ollama",334 "Ollama Model": "Modello Ollama",
@@ -605,7 +605,7 @@
605 "Model Icon": "Icona del modello",605 "Model Icon": "Icona del modello",
606 "Show sequential message numbers in the chat log": "Mostra numeri di messaggi sequenziali nel registro della chat",606 "Show sequential message numbers in the chat log": "Mostra numeri di messaggi sequenziali nel registro della chat",
607 "Message IDs": "ID Messaggio",607 "Message IDs": "ID Messaggio",
608 "Hide avatars in chat messages.": "Nascondi gli avatar nei messaggi di chat.",608 "Hide avatars, only in chat messages.": "Nascondi gli avatar nei messaggi di chat.",
609 "Hide Chat Avatars": "Nascondi avatar di chat",609 "Hide Chat Avatars": "Nascondi avatar di chat",
610 "Show the number of tokens in each message in the chat log": "Mostra il numero di token in ogni messaggio nel registro della chat",610 "Show the number of tokens in each message in the chat log": "Mostra il numero di token in ogni messaggio nel registro della chat",
611 "Show Message Token Count": "Mostra Conteggio Token Messaggio",611 "Show Message Token Count": "Mostra Conteggio Token Messaggio",
@@ -739,8 +739,8 @@
739 "Filter": "Filtro",739 "Filter": "Filtro",
740 "Automatically select a background based on the chat context": "Seleziona automaticamente uno sfondo in base al contesto della chat",740 "Automatically select a background based on the chat context": "Seleziona automaticamente uno sfondo in base al contesto della chat",
741 "Auto-select": "Selezione automatica",741 "Auto-select": "Selezione automatica",
742 "System Backgrounds": "Sfondi di sistema",742 "Global": "Globale",
743 "Chat Backgrounds": "Sfondi Chat",743 "Chat": "Chat",
744 "bg_chat_hint_1": "Sfondi chat generati con",744 "bg_chat_hint_1": "Sfondi chat generati con",
745 "bg_chat_hint_2": "l'estensione apparirà qui.",745 "bg_chat_hint_2": "l'estensione apparirà qui.",
746 "Extensions": "Estensioni",746 "Extensions": "Estensioni",
@@ -913,7 +913,7 @@
913 "Save": "Salva",913 "Save": "Salva",
914 "Chat History": "Cronologia Chat",914 "Chat History": "Cronologia Chat",
915 "Import Chat": "Importa chat",915 "Import Chat": "Importa chat",
916 "Copy to system backgrounds": "Copia negli sfondi di sistema",916 "Copy to global backgrounds": "Copia negli sfondi globali",
917 "Rename background": "Rinominare lo sfondo",917 "Rename background": "Rinominare lo sfondo",
918 "Lock": "Serratura",918 "Lock": "Serratura",
919 "Unlock": "Sbloccare",919 "Unlock": "Sbloccare",
public/locales/ja-jp.json+6 -6
@@ -235,7 +235,7 @@
235 "Combines consecutive system messages into one (excluding example dialogues). May improve coherence for some models.": "連続するシステムメッセージを1つに結合します(例のダイアログを除く)。一部のモデルの一貫性を向上させる可能性があります。",235 "Combines consecutive system messages into one (excluding example dialogues). May improve coherence for some models.": "連続するシステムメッセージを1つに結合します(例のダイアログを除く)。一部のモデルの一貫性を向上させる可能性があります。",
236 "Enable function calling": "関数呼び出しを有効にする",236 "Enable function calling": "関数呼び出しを有効にする",
237 "Send inline images": "インライン画像を送信",237 "Send inline images": "インライン画像を送信",
238 "image_inlining_hint_1": "モデルがサポートしている場合、プロンプトで画像を送信します。",238 "image_inlining_hint_1": "モデルがサポートしている場合、プロンプトでメディアファイルを送信します。",
239 "image_inlining_hint_2": "メッセージに対するアクションまたは",239 "image_inlining_hint_2": "メッセージに対するアクションまたは",
240 "image_inlining_hint_3": "チャットに画像ファイルを添付するためのメニュー。",240 "image_inlining_hint_3": "チャットに画像ファイルを添付するためのメニュー。",
241 "Inline Image Quality": "インライン画像品質",241 "Inline Image Quality": "インライン画像品質",
@@ -328,7 +328,7 @@
328 "PygmalionAI/aphrodite-engine": "PygmalionAI/aphrodite-engine(OpenAI APIエンドポイントのパッケージングモード)",328 "PygmalionAI/aphrodite-engine": "PygmalionAI/aphrodite-engine(OpenAI APIエンドポイントのパッケージングモード)",
329 "Aphrodite API key": "アフロディーテAPIキー",329 "Aphrodite API key": "アフロディーテAPIキー",
330 "Aphrodite Model": "アフロディーテモデル",330 "Aphrodite Model": "アフロディーテモデル",
331 "ggerganov/llama.cpp": "ggerganov/llama.cpp(出力サーバー)",331 "ggml-org/llama.cpp": "ggml-org/llama.cpp(出力サーバー)",
332 "Example: http://127.0.0.1:8080": "例: http://127.0.0.1:8080",332 "Example: http://127.0.0.1:8080": "例: http://127.0.0.1:8080",
333 "Example: http://127.0.0.1:11434": "例: http://127.0.0.1:11434",333 "Example: http://127.0.0.1:11434": "例: http://127.0.0.1:11434",
334 "Ollama Model": "Ollamaモデル",334 "Ollama Model": "Ollamaモデル",
@@ -605,7 +605,7 @@
605 "Model Icon": "モデルアイコン",605 "Model Icon": "モデルアイコン",
606 "Show sequential message numbers in the chat log": "チャットログに連続したメッセージ番号を表示する",606 "Show sequential message numbers in the chat log": "チャットログに連続したメッセージ番号を表示する",
607 "Message IDs": "メッセージID",607 "Message IDs": "メッセージID",
608 "Hide avatars in chat messages.": "チャットメッセージ内のアバターを非表示にします。",608 "Hide avatars, only in chat messages.": "チャットメッセージ内のアバターを非表示にします。",
609 "Hide Chat Avatars": "チャットアバターを非表示",609 "Hide Chat Avatars": "チャットアバターを非表示",
610 "Show the number of tokens in each message in the chat log": "チャットログ内の各メッセージのトークン数を表示する",610 "Show the number of tokens in each message in the chat log": "チャットログ内の各メッセージのトークン数を表示する",
611 "Show Message Token Count": "メッセージトークンの数を表示",611 "Show Message Token Count": "メッセージトークンの数を表示",
@@ -739,8 +739,8 @@
739 "Filter": "フィルター",739 "Filter": "フィルター",
740 "Automatically select a background based on the chat context": "チャットのコンテキストに基づいて背景を自動選択",740 "Automatically select a background based on the chat context": "チャットのコンテキストに基づいて背景を自動選択",
741 "Auto-select": "自動選択",741 "Auto-select": "自動選択",
742 "System Backgrounds": "システムの背景",742 "Global": "グローバル",
743 "Chat Backgrounds": "チャットの背景",743 "Chat": "チャット",
744 "bg_chat_hint_1": "",744 "bg_chat_hint_1": "",
745 "bg_chat_hint_2": "拡張機能で生成したチャットの背景はここに表示されます。",745 "bg_chat_hint_2": "拡張機能で生成したチャットの背景はここに表示されます。",
746 "Extensions": "拡張機能",746 "Extensions": "拡張機能",
@@ -913,7 +913,7 @@
913 "Save": "保存",913 "Save": "保存",
914 "Chat History": "チャット履歴",914 "Chat History": "チャット履歴",
915 "Import Chat": "チャットをインポート",915 "Import Chat": "チャットをインポート",
916 "Copy to system backgrounds": "システム背景にコピー",916 "Copy to global backgrounds": "グローバル背景にコピー",
917 "Rename background": "背景の名前を変更",917 "Rename background": "背景の名前を変更",
918 "Lock": "ロック",918 "Lock": "ロック",
919 "Unlock": "ロック解除",919 "Unlock": "ロック解除",
public/locales/ko-kr.json+6 -6
@@ -237,7 +237,7 @@
237 "Combines consecutive system messages into one (excluding example dialogues). May improve coherence for some models.": "연속된 시스템 메시지를 하나로 결합합니다(예제 대화 제외). 일부 모델의 일관성을 향상시킬 수 있습니다.",237 "Combines consecutive system messages into one (excluding example dialogues). May improve coherence for some models.": "연속된 시스템 메시지를 하나로 결합합니다(예제 대화 제외). 일부 모델의 일관성을 향상시킬 수 있습니다.",
238 "Enable function calling": "함수 호출 활성화",238 "Enable function calling": "함수 호출 활성화",
239 "Send inline images": "인라인 이미지 전송",239 "Send inline images": "인라인 이미지 전송",
240 "image_inlining_hint_1": "모델이 지원하는 경우 메시지로 이미지를 보냅니다.",240 "image_inlining_hint_1": "모델이 지원하는 경우 메시지로 미디어 파일을 보냅니다.",
241 "Inline Image Quality": "인라인 이미지 품질",241 "Inline Image Quality": "인라인 이미지 품질",
242 "openai_inline_image_quality_auto": "자동",242 "openai_inline_image_quality_auto": "자동",
243 "openai_inline_image_quality_low": "낮은",243 "openai_inline_image_quality_low": "낮은",
@@ -328,7 +328,7 @@
328 "PygmalionAI/aphrodite-engine": "PygmalionAI/aphrodite-engine (OpenAI API의 래퍼 모드)",328 "PygmalionAI/aphrodite-engine": "PygmalionAI/aphrodite-engine (OpenAI API의 래퍼 모드)",
329 "Aphrodite API key": "Aphrodite API 키",329 "Aphrodite API key": "Aphrodite API 키",
330 "Aphrodite Model": "Aphrodite 모델",330 "Aphrodite Model": "Aphrodite 모델",
331 "ggerganov/llama.cpp": "ggerganov/llama.cpp (출력 서버)",331 "ggml-org/llama.cpp": "ggml-org/llama.cpp (출력 서버)",
332 "Example: http://127.0.0.1:8080": "예: http://127.0.0.1:8080",332 "Example: http://127.0.0.1:8080": "예: http://127.0.0.1:8080",
333 "Example: http://127.0.0.1:11434": "예: http://127.0.0.1:11434",333 "Example: http://127.0.0.1:11434": "예: http://127.0.0.1:11434",
334 "Ollama Model": "Ollama 모델",334 "Ollama Model": "Ollama 모델",
@@ -613,7 +613,7 @@
613 "Model Icon": "모델 아이콘 표시",613 "Model Icon": "모델 아이콘 표시",
614 "Show sequential message numbers in the chat log": "채팅 로그에 순차적인 메시지 번호 표시",614 "Show sequential message numbers in the chat log": "채팅 로그에 순차적인 메시지 번호 표시",
615 "Message IDs": "메시지 ID",615 "Message IDs": "메시지 ID",
616 "Hide avatars in chat messages.": "채팅 메시지에서 아바타 숨김.",616 "Hide avatars, only in chat messages.": "채팅 메시지에서 아바타 숨김.",
617 "Hide Chat Avatars": "캐릭터 프로필 숨기기",617 "Hide Chat Avatars": "캐릭터 프로필 숨기기",
618 "Show the number of tokens in each message in the chat log": "채팅 로그의 각 메시지에 토큰 수 표시",618 "Show the number of tokens in each message in the chat log": "채팅 로그의 각 메시지에 토큰 수 표시",
619 "Show Message Token Count": "메시지 토큰 개수 표시",619 "Show Message Token Count": "메시지 토큰 개수 표시",
@@ -752,8 +752,8 @@
752 "Filter": "필터",752 "Filter": "필터",
753 "Automatically select a background based on the chat context": "채팅 컨텍스트를 기반으로 자동으로 배경 선택",753 "Automatically select a background based on the chat context": "채팅 컨텍스트를 기반으로 자동으로 배경 선택",
754 "Auto-select": "자동 선택",754 "Auto-select": "자동 선택",
755 "System Backgrounds": "시스템 배경",755 "Global": "글로벌",
756 "Chat Backgrounds": "채팅 배경",756 "Chat": "채팅",
757 "bg_chat_hint_1": "다음으로 생성된 채팅 배경",757 "bg_chat_hint_1": "다음으로 생성된 채팅 배경",
758 "bg_chat_hint_2": "확장 프로그램이 여기에 표시됩니다.",758 "bg_chat_hint_2": "확장 프로그램이 여기에 표시됩니다.",
759 "Extensions": "확장",759 "Extensions": "확장",
@@ -927,7 +927,7 @@
927 "Save": "저장",927 "Save": "저장",
928 "Chat History": "채팅 기록",928 "Chat History": "채팅 기록",
929 "Import Chat": "채팅 가져오기",929 "Import Chat": "채팅 가져오기",
930 "Copy to system backgrounds": "시스템 배경에 복사",930 "Copy to global backgrounds": "글로벌 배경에 복사",
931 "Rename background": "배경 이름 바꾸기",931 "Rename background": "배경 이름 바꾸기",
932 "Lock": "잠금",932 "Lock": "잠금",
933 "Unlock": "잠금 해제",933 "Unlock": "잠금 해제",
public/locales/nl-nl.json+6 -6
@@ -235,7 +235,7 @@
235 "Combines consecutive system messages into one (excluding example dialogues). May improve coherence for some models.": "Combineert opeenvolgende systeemberichten tot één (exclusief voorbeeld dialogen). Kan de coherentie verbeteren voor sommige modellen.",235 "Combines consecutive system messages into one (excluding example dialogues). May improve coherence for some models.": "Combineert opeenvolgende systeemberichten tot één (exclusief voorbeeld dialogen). Kan de coherentie verbeteren voor sommige modellen.",
236 "Enable function calling": "Schakel functieaanroepen in",236 "Enable function calling": "Schakel functieaanroepen in",
237 "Send inline images": "Inline afbeeldingen verzenden",237 "Send inline images": "Inline afbeeldingen verzenden",
238 "image_inlining_hint_1": "Verzendt afbeeldingen in prompts als het model dit ondersteunt.",238 "image_inlining_hint_1": "Verzendt mediabestanden in prompts als het model dit ondersteunt.",
239 "Inline Image Quality": "Inline-beeldkwaliteit",239 "Inline Image Quality": "Inline-beeldkwaliteit",
240 "openai_inline_image_quality_auto": "Auto",240 "openai_inline_image_quality_auto": "Auto",
241 "openai_inline_image_quality_low": "Laag",241 "openai_inline_image_quality_low": "Laag",
@@ -324,7 +324,7 @@
324 "PygmalionAI/aphrodite-engine": "PygmalionAI/aphrodite-engine (Wrappermodus voor OpenAI API)",324 "PygmalionAI/aphrodite-engine": "PygmalionAI/aphrodite-engine (Wrappermodus voor OpenAI API)",
325 "Aphrodite API key": "Aphrodite API-sleutel",325 "Aphrodite API key": "Aphrodite API-sleutel",
326 "Aphrodite Model": "Aphrodite-model",326 "Aphrodite Model": "Aphrodite-model",
327 "ggerganov/llama.cpp": "ggerganov/llama.cpp (Output-server)",327 "ggml-org/llama.cpp": "ggml-org/llama.cpp (Output-server)",
328 "Example: http://127.0.0.1:8080": "Voorbeeld: http://127.0.0.1:8080",328 "Example: http://127.0.0.1:8080": "Voorbeeld: http://127.0.0.1:8080",
329 "Example: http://127.0.0.1:11434": "Voorbeeld: http://127.0.0.1:11434",329 "Example: http://127.0.0.1:11434": "Voorbeeld: http://127.0.0.1:11434",
330 "Ollama Model": "Ollama-model",330 "Ollama Model": "Ollama-model",
@@ -601,7 +601,7 @@
601 "Model Icon": "Modelpictogram",601 "Model Icon": "Modelpictogram",
602 "Show sequential message numbers in the chat log": "Toon opeenvolgende berichtnummers in het chatlogboek",602 "Show sequential message numbers in the chat log": "Toon opeenvolgende berichtnummers in het chatlogboek",
603 "Message IDs": "Bericht-IDs",603 "Message IDs": "Bericht-IDs",
604 "Hide avatars in chat messages.": "Verberg avatars in chatberichten.",604 "Hide avatars, only in chat messages.": "Verberg avatars in chatberichten.",
605 "Hide Chat Avatars": "Chatavatars verbergen",605 "Hide Chat Avatars": "Chatavatars verbergen",
606 "Show the number of tokens in each message in the chat log": "Toon het aantal tokens in elk bericht in het chatlogboek",606 "Show the number of tokens in each message in the chat log": "Toon het aantal tokens in elk bericht in het chatlogboek",
607 "Show Message Token Count": "Toon bericht token tellen",607 "Show Message Token Count": "Toon bericht token tellen",
@@ -735,8 +735,8 @@
735 "Filter": "Filter",735 "Filter": "Filter",
736 "Automatically select a background based on the chat context": "Automatisch een achtergrond selecteren op basis van de chatcontext",736 "Automatically select a background based on the chat context": "Automatisch een achtergrond selecteren op basis van de chatcontext",
737 "Auto-select": "Automatisch selecteren",737 "Auto-select": "Automatisch selecteren",
738 "System Backgrounds": "Systeemachtergronden",738 "Global": "Globaal",
739 "Chat Backgrounds": "Achtergronden chat",739 "Chat": "Chat",
740 "bg_chat_hint_1": "Chatachtergronden gegenereerd met de",740 "bg_chat_hint_1": "Chatachtergronden gegenereerd met de",
741 "bg_chat_hint_2": "extensie zal hier verschijnen.",741 "bg_chat_hint_2": "extensie zal hier verschijnen.",
742 "Extensions": "Uitbreidingen",742 "Extensions": "Uitbreidingen",
@@ -909,7 +909,7 @@
909 "Save": "Opslaan",909 "Save": "Opslaan",
910 "Chat History": "Chatgeschiedenis",910 "Chat History": "Chatgeschiedenis",
911 "Import Chat": "Chat importeren",911 "Import Chat": "Chat importeren",
912 "Copy to system backgrounds": "Kopiëren naar systeemachtergronden",912 "Copy to global backgrounds": "Kopiëren naar globale achtergronden",
913 "Rename background": "Achtergrond hernoemen",913 "Rename background": "Achtergrond hernoemen",
914 "Lock": "Slot",914 "Lock": "Slot",
915 "Unlock": "Ontgrendelen",915 "Unlock": "Ontgrendelen",
public/locales/pt-pt.json+6 -6
@@ -235,7 +235,7 @@
235 "Combines consecutive system messages into one (excluding example dialogues). May improve coherence for some models.": "Combina mensagens do sistema consecutivas em uma (excluindo diálogos de exemplo). Pode melhorar a coerência para alguns modelos.",235 "Combines consecutive system messages into one (excluding example dialogues). May improve coherence for some models.": "Combina mensagens do sistema consecutivas em uma (excluindo diálogos de exemplo). Pode melhorar a coerência para alguns modelos.",
236 "Enable function calling": "Habilitar chamada de função",236 "Enable function calling": "Habilitar chamada de função",
237 "Send inline images": "Enviar imagens inline",237 "Send inline images": "Enviar imagens inline",
238 "image_inlining_hint_1": "Envia imagens em prompts se o modelo suportar.",238 "image_inlining_hint_1": "Envia ficheiros multimédia em prompts se o modelo suportar.",
239 "Inline Image Quality": "Qualidade de imagem embutida",239 "Inline Image Quality": "Qualidade de imagem embutida",
240 "openai_inline_image_quality_auto": "Auto",240 "openai_inline_image_quality_auto": "Auto",
241 "openai_inline_image_quality_low": "Baixo",241 "openai_inline_image_quality_low": "Baixo",
@@ -326,7 +326,7 @@
326 "PygmalionAI/aphrodite-engine": "PygmalionAI/aphrodite-engine (Modo Wrapper para API OpenAI)",326 "PygmalionAI/aphrodite-engine": "PygmalionAI/aphrodite-engine (Modo Wrapper para API OpenAI)",
327 "Aphrodite API key": "Chave da API Aphrodite",327 "Aphrodite API key": "Chave da API Aphrodite",
328 "Aphrodite Model": "Modelo Afrodite",328 "Aphrodite Model": "Modelo Afrodite",
329 "ggerganov/llama.cpp": "ggerganov/llama.cpp (Servidor de Saída)",329 "ggml-org/llama.cpp": "ggml-org/llama.cpp (Servidor de Saída)",
330 "Example: http://127.0.0.1:8080": "Exemplo: http://127.0.0.1:8080",330 "Example: http://127.0.0.1:8080": "Exemplo: http://127.0.0.1:8080",
331 "Example: http://127.0.0.1:11434": "Exemplo: http://127.0.0.1:11434",331 "Example: http://127.0.0.1:11434": "Exemplo: http://127.0.0.1:11434",
332 "Ollama Model": "Modelo Ollama",332 "Ollama Model": "Modelo Ollama",
@@ -603,7 +603,7 @@
603 "Model Icon": "Ícone do Modelo",603 "Model Icon": "Ícone do Modelo",
604 "Show sequential message numbers in the chat log": "Mostrar números sequenciais de mensagem no registro de chat",604 "Show sequential message numbers in the chat log": "Mostrar números sequenciais de mensagem no registro de chat",
605 "Message IDs": "IDs de Mensagem",605 "Message IDs": "IDs de Mensagem",
606 "Hide avatars in chat messages.": "Oculte avatares em mensagens de bate-papo.",606 "Hide avatars, only in chat messages.": "Oculte avatares em mensagens de bate-papo.",
607 "Hide Chat Avatars": "Ocultar avatares de bate-papo",607 "Hide Chat Avatars": "Ocultar avatares de bate-papo",
608 "Show the number of tokens in each message in the chat log": "Mostrar o número de tokens em cada mensagem no registro de chat",608 "Show the number of tokens in each message in the chat log": "Mostrar o número de tokens em cada mensagem no registro de chat",
609 "Show Message Token Count": "Mostrar Contagem de Tokens da Mensagem",609 "Show Message Token Count": "Mostrar Contagem de Tokens da Mensagem",
@@ -737,8 +737,8 @@
737 "Filter": "Filtro",737 "Filter": "Filtro",
738 "Automatically select a background based on the chat context": "Selecionar automaticamente um plano de fundo com base no contexto do chat",738 "Automatically select a background based on the chat context": "Selecionar automaticamente um plano de fundo com base no contexto do chat",
739 "Auto-select": "Seleção automática",739 "Auto-select": "Seleção automática",
740 "System Backgrounds": "Fundos do Sistema",740 "Global": "Global",
741 "Chat Backgrounds": "Fundos de Chat",741 "Chat": "Chat",
742 "bg_chat_hint_1": "Planos de fundo de bate-papo gerados com o",742 "bg_chat_hint_1": "Planos de fundo de bate-papo gerados com o",
743 "bg_chat_hint_2": "a extensão aparecerá aqui.",743 "bg_chat_hint_2": "a extensão aparecerá aqui.",
744 "Extensions": "Extensões",744 "Extensions": "Extensões",
@@ -911,7 +911,7 @@
911 "Save": "Salvar",911 "Save": "Salvar",
912 "Chat History": "Histórico de Chat",912 "Chat History": "Histórico de Chat",
913 "Import Chat": "Importar bate-papo",913 "Import Chat": "Importar bate-papo",
914 "Copy to system backgrounds": "Copiar para planos de fundo do sistema",914 "Copy to global backgrounds": "Copiar para planos de fundo globais",
915 "Rename background": "Renomear plano de fundo",915 "Rename background": "Renomear plano de fundo",
916 "Lock": "Trancar",916 "Lock": "Trancar",
917 "Unlock": "Desbloquear",917 "Unlock": "Desbloquear",
public/locales/ru-ru.json+5 -7
@@ -108,7 +108,7 @@
108 "Register": "Зарегистрироваться",108 "Register": "Зарегистрироваться",
109 "TogetherAI Model": "Модель TogetherAI",109 "TogetherAI Model": "Модель TogetherAI",
110 "Example: http://127.0.0.1:5001": "Пример: http://127.0.0.1:5001",110 "Example: http://127.0.0.1:5001": "Пример: http://127.0.0.1:5001",
111 "ggerganov/llama.cpp": "ggerganov/llama.cpp (сервер вывода)",111 "ggml-org/llama.cpp": "ggml-org/llama.cpp (сервер вывода)",
112 "Example: http://127.0.0.1:8080": "Пример: http://127.0.0.1:8080",112 "Example: http://127.0.0.1:8080": "Пример: http://127.0.0.1:8080",
113 "Example: http://127.0.0.1:11434": "Пример: http://127.0.0.1:11434",113 "Example: http://127.0.0.1:11434": "Пример: http://127.0.0.1:11434",
114 "Ollama Model": "Модель Ollama",114 "Ollama Model": "Модель Ollama",
@@ -244,7 +244,6 @@
244 "Always include examples": "Всегда применять примеры",244 "Always include examples": "Всегда применять примеры",
245 "Never include examples": "Никогда не применять примеры",245 "Never include examples": "Никогда не применять примеры",
246 "Forbid External Media": "Запрет внешних медиа",246 "Forbid External Media": "Запрет внешних медиа",
247 "System Backgrounds": "Системные фоны",
248 "Name": "Имя",247 "Name": "Имя",
249 "Auto-connect": "Подключаться автоматически",248 "Auto-connect": "Подключаться автоматически",
250 "First message": "Первое сообщение",249 "First message": "Первое сообщение",
@@ -350,7 +349,7 @@
350 "Minimum number of blacklisted words detected to trigger an auto-swipe": "Минимальное количество обнаруженных запрещённых слов, при котором срабатывает авто-свайп.",349 "Minimum number of blacklisted words detected to trigger an auto-swipe": "Минимальное количество обнаруженных запрещённых слов, при котором срабатывает авто-свайп.",
351 "User Message Blur Tint": "Ваши сообщения",350 "User Message Blur Tint": "Ваши сообщения",
352 "AI Message Blur Tint": "Сообщения ИИ",351 "AI Message Blur Tint": "Сообщения ИИ",
353 "Chat Backgrounds": "Фоны чата",352 "Chat": "Чат",
354 "Chat Background": "Фон чата",353 "Chat Background": "Фон чата",
355 "UI Background": "Фон UI",354 "UI Background": "Фон UI",
356 "Mad Lab Mode": "Режим безумца",355 "Mad Lab Mode": "Режим безумца",
@@ -1020,7 +1019,7 @@
1020 "The prompt to be sent.": "Текст промпта.",1019 "The prompt to be sent.": "Текст промпта.",
1021 "prompt_manager_forbid_overrides": "Запретить перезапись",1020 "prompt_manager_forbid_overrides": "Запретить перезапись",
1022 "This prompt cannot be overridden by character cards, even if overrides are preferred.": "Карточка персонажа не сможет перезаписать этот промпт, даже если настройки отдают приоритет именно ей.",1021 "This prompt cannot be overridden by character cards, even if overrides are preferred.": "Карточка персонажа не сможет перезаписать этот промпт, даже если настройки отдают приоритет именно ей.",
1023 "image_inlining_hint_1": "Отправлять картинки как часть промпта, если позволяет модель.",1022 "image_inlining_hint_1": "Отправлять медиафайлы как часть промпта, если позволяет модель.",
1024 "Contest Winners": "Победители конкурса",1023 "Contest Winners": "Победители конкурса",
1025 "Rename Background": "Переименовать фон",1024 "Rename Background": "Переименовать фон",
1026 "Lock": "Закрепить",1025 "Lock": "Закрепить",
@@ -1046,7 +1045,7 @@
1046 "openai_inline_image_quality_high": "Высокое",1045 "openai_inline_image_quality_high": "Высокое",
1047 "Assistant Impersonation Prefill": "Префилл для ассистента при перевоплощении",1046 "Assistant Impersonation Prefill": "Префилл для ассистента при перевоплощении",
1048 "Hide Chat Avatars": "Не показывать аватарки в чате",1047 "Hide Chat Avatars": "Не показывать аватарки в чате",
1049 "Hide avatars in chat messages.": "Скрыть аватарки сбоку от сообщений в чате",1048 "Hide avatars, only in chat messages.": "Скрыть аватарки сбоку от сообщений в чате",
1050 "Flat": "Стандартный",1049 "Flat": "Стандартный",
1051 "Toggle character info panel": "Показать / скрыть инфо-панель",1050 "Toggle character info panel": "Показать / скрыть инфо-панель",
1052 "(For Chat Completion and Instruct Mode)": "(для Chat Completion и режима Instruct)",1051 "(For Chat Completion and Instruct Mode)": "(для Chat Completion и режима Instruct)",
@@ -1326,7 +1325,7 @@
1326 "Auto Mode delay": "Auto Mode delay",1325 "Auto Mode delay": "Auto Mode delay",
1327 "Bulk_edit_characters": "Bulk edit characters\r\rClick to toggle characters\rShift + Click to select/deselect a range of characters\rRight-click for actions",1326 "Bulk_edit_characters": "Bulk edit characters\r\rClick to toggle characters\rShift + Click to select/deselect a range of characters\rRight-click for actions",
1328 "Bulk select all characters": "Bulk select all characters",1327 "Bulk select all characters": "Bulk select all characters",
1329 "Copy to system backgrounds": "Copy to system backgrounds",1328 "Copy to global backgrounds": "Скопировать в глобальные фоны",
1330 "Chat Scenario Override": "Chat Scenario Override",1329 "Chat Scenario Override": "Chat Scenario Override",
1331 "Chat Lorebook": "Chat Lorebook for",1330 "Chat Lorebook": "Chat Lorebook for",
1332 "chat_world_template_txt": "A selected World Info will be bound to this chat. When generating an AI reply,\n it will be combined with the entries from global and character lorebooks.",1331 "chat_world_template_txt": "A selected World Info will be bound to this chat. When generating an AI reply,\n it will be combined with the entries from global and character lorebooks.",
@@ -2443,7 +2442,6 @@
2443 "Allocates a portion of the response length for thinking (min: 1024 tokens, low: 10%, medium: 25%, high: 50%, max: 95%), but minimum 1024 tokens. Auto does not request thinking.": "Отводит часть ответа под рассуждения (Минимальные: 10%, Обычные: 25%, Подробные: 50%, Максимальные: 95%), но минимум 1024 токена. При выборе значение Авто рассуждения не запрашиваются.",2442 "Allocates a portion of the response length for thinking (min: 1024 tokens, low: 10%, medium: 25%, high: 50%, max: 95%), but minimum 1024 tokens. Auto does not request thinking.": "Отводит часть ответа под рассуждения (Минимальные: 10%, Обычные: 25%, Подробные: 50%, Максимальные: 95%), но минимум 1024 токена. При выборе значение Авто рассуждения не запрашиваются.",
2444 "Use system prompt": "Включить системный промпт",2443 "Use system prompt": "Включить системный промпт",
2445 "Send inline videos": "Отправлять inline-видео",2444 "Send inline videos": "Отправлять inline-видео",
2446 "video_inlining_hint_1": "Отправляет модели видео, если она поддерживает такую возможность.",
2447 "video_inlining_hint_4": "Видео должно весить не более 20 Мб и длиться не дольше 1 минуты.",2445 "video_inlining_hint_4": "Видео должно весить не более 20 Мб и длиться не дольше 1 минуты.",
2448 "Allocates a portion of the response length for thinking (Flash 2.5/Pro 2.5) (min: 0/128 tokens, low: 10%, medium: 25%, high: 50%, max: 24576/32768 tokens). Auto lets the model decide.": "Отводит часть ответа под рассуждения (Flash 2.5/Pro 2.5) (Минимальные: 0/128 токенов, Поверхностные: 10%, Обычные: 25%, Подробные: 50%, Максимальные: 24576/32768 токенов). При выборе значения Авто модель определяет объём самостоятельно.",2446 "Allocates a portion of the response length for thinking (Flash 2.5/Pro 2.5) (min: 0/128 tokens, low: 10%, medium: 25%, high: 50%, max: 24576/32768 tokens). Auto lets the model decide.": "Отводит часть ответа под рассуждения (Flash 2.5/Pro 2.5) (Минимальные: 0/128 токенов, Поверхностные: 10%, Обычные: 25%, Подробные: 50%, Максимальные: 24576/32768 токенов). При выборе значения Авто модель определяет объём самостоятельно.",
2449 "Google Vertex AI Configuration": "Настройки Google Vertex AI",2447 "Google Vertex AI Configuration": "Настройки Google Vertex AI",
public/locales/th-th.json+6 -6
@@ -241,7 +241,7 @@
241 "Allows the model to return its thinking process.": "อนุญาตให้โมเดลส่งคืนกระบวนการคิดของตัวเอง",241 "Allows the model to return its thinking process.": "อนุญาตให้โมเดลส่งคืนกระบวนการคิดของตัวเอง",
242 "This setting affects visibility only.": "การตั้งค่านี้มีผลต่อการมองเห็นเท่านั้น",242 "This setting affects visibility only.": "การตั้งค่านี้มีผลต่อการมองเห็นเท่านั้น",
243 "Send inline images": "ส่งรูปภาพแบบ inline",243 "Send inline images": "ส่งรูปภาพแบบ inline",
244 "image_inlining_hint_1": "ส่งรูปภาพในพรอมต์หากโมเดลรองรับ",244 "image_inlining_hint_1": "ส่งไฟล์สื่อในพรอมต์หากโมเดลรองรับ",
245 "Inline Image Quality": "คุณภาพรูปภาพ Inline",245 "Inline Image Quality": "คุณภาพรูปภาพ Inline",
246 "openai_inline_image_quality_auto": "อัตโนมัติ",246 "openai_inline_image_quality_auto": "อัตโนมัติ",
247 "openai_inline_image_quality_low": "ต่ำ",247 "openai_inline_image_quality_low": "ต่ำ",
@@ -330,7 +330,7 @@
330 "PygmalionAI/aphrodite-engine": "PygmalionAI/aphrodite-engine",330 "PygmalionAI/aphrodite-engine": "PygmalionAI/aphrodite-engine",
331 "Aphrodite API key": "คีย์ API ของ Aphrodite",331 "Aphrodite API key": "คีย์ API ของ Aphrodite",
332 "Aphrodite Model": "โมเดลของ Aphrodite",332 "Aphrodite Model": "โมเดลของ Aphrodite",
333 "ggerganov/llama.cpp": "ggerganov/llama.cpp",333 "ggml-org/llama.cpp": "ggml-org/llama.cpp",
334 "Example: http://127.0.0.1:8080": "ตัวอย่าง: http://127.0.0.1:8080",334 "Example: http://127.0.0.1:8080": "ตัวอย่าง: http://127.0.0.1:8080",
335 "Example: http://127.0.0.1:11434": "ตัวอย่าง: http://127.0.0.1:11434",335 "Example: http://127.0.0.1:11434": "ตัวอย่าง: http://127.0.0.1:11434",
336 "Ollama Model": "โมเดลของ Ollama",336 "Ollama Model": "โมเดลของ Ollama",
@@ -611,7 +611,7 @@
611 "Model Icon": "แสดงไอคอน Model AI ที่ตอบแชท",611 "Model Icon": "แสดงไอคอน Model AI ที่ตอบแชท",
612 "Show sequential message numbers in the chat log": "แสดงหมายเลขลำดับข้อความในประวัติแชท",612 "Show sequential message numbers in the chat log": "แสดงหมายเลขลำดับข้อความในประวัติแชท",
613 "Message IDs": "แสดงเลขจำนวนข้อความ",613 "Message IDs": "แสดงเลขจำนวนข้อความ",
614 "Hide avatars in chat messages.": "ซ่อนภาพ avatar ในแชทข้อความ",614 "Hide avatars, only in chat messages.": "ซ่อนภาพ avatar ในแชทข้อความ",
615 "Hide Chat Avatars": "ซ่อนรูปโปรไฟล์แชท",615 "Hide Chat Avatars": "ซ่อนรูปโปรไฟล์แชท",
616 "Show the number of tokens in each message in the chat log": "แสดงจำนวนโทเค็นในแต่ละข้อความในบันทึกแชท",616 "Show the number of tokens in each message in the chat log": "แสดงจำนวนโทเค็นในแต่ละข้อความในบันทึกแชท",
617 "Show Message Token Count": "แสดงจำนวนโทเค็นที่ Ai ตอบแชท",617 "Show Message Token Count": "แสดงจำนวนโทเค็นที่ Ai ตอบแชท",
@@ -745,8 +745,8 @@
745 "Filter": "ตัวกรอง",745 "Filter": "ตัวกรอง",
746 "Automatically select a background based on the chat context": "เลือกพื้นหลังโดยอัตโนมัติตามบริบทของแชท",746 "Automatically select a background based on the chat context": "เลือกพื้นหลังโดยอัตโนมัติตามบริบทของแชท",
747 "Auto-select": "เลือกอัตโนมัติ",747 "Auto-select": "เลือกอัตโนมัติ",
748 "System Backgrounds": "พื้นหลังของระบบ",748 "Global": "ทั่วโลก",
749 "Chat Backgrounds": "พื้นหลังแชท",749 "Chat": "แชท",
750 "bg_chat_hint_1": "พื้นหลังของแชทที่ถูกสร้างด้วย",750 "bg_chat_hint_1": "พื้นหลังของแชทที่ถูกสร้างด้วย",
751 "bg_chat_hint_2": "จะปรากฏที่นี่",751 "bg_chat_hint_2": "จะปรากฏที่นี่",
752 "Extensions": "โปรแกรมส่วนขยาย",752 "Extensions": "โปรแกรมส่วนขยาย",
@@ -921,7 +921,7 @@
921 "Save": "บันทึก",921 "Save": "บันทึก",
922 "Chat History": "ประวัติการแชท",922 "Chat History": "ประวัติการแชท",
923 "Import Chat": "นำเข้าไฟล์แชท",923 "Import Chat": "นำเข้าไฟล์แชท",
924 "Copy to system backgrounds": "คัดลอกไปยังพื้นหลัง",924 "Copy to global backgrounds": "คัดลอกไปยังพื้นหลังทั่วโลก",
925 "Rename background": "เปลี่ยนชื่อพื้นหลัง",925 "Rename background": "เปลี่ยนชื่อพื้นหลัง",
926 "Lock": "ล็อค",926 "Lock": "ล็อค",
927 "Unlock": "ปลดล็อค",927 "Unlock": "ปลดล็อค",
public/locales/uk-ua.json+6 -6
@@ -235,7 +235,7 @@
235 "Combines consecutive system messages into one (excluding example dialogues). May improve coherence for some models.": "Об'єднує послідовні системні повідомлення в одне (крім прикладів діалогів). Може покращити співпрацю для деяких моделей.",235 "Combines consecutive system messages into one (excluding example dialogues). May improve coherence for some models.": "Об'єднує послідовні системні повідомлення в одне (крім прикладів діалогів). Може покращити співпрацю для деяких моделей.",
236 "Enable function calling": "Увімкнути виклик функцій",236 "Enable function calling": "Увімкнути виклик функцій",
237 "Send inline images": "Надсилати вбудовані зображення",237 "Send inline images": "Надсилати вбудовані зображення",
238 "image_inlining_hint_1": "Надсилає зображення у підказках, якщо модель це підтримує.",238 "image_inlining_hint_1": "Надсилає медіафайли у підказках, якщо модель це підтримує.",
239 "Inline Image Quality": "Якість вбудованого зображення",239 "Inline Image Quality": "Якість вбудованого зображення",
240 "openai_inline_image_quality_auto": "Авто",240 "openai_inline_image_quality_auto": "Авто",
241 "openai_inline_image_quality_low": "Низький",241 "openai_inline_image_quality_low": "Низький",
@@ -326,7 +326,7 @@
326 "PygmalionAI/aphrodite-engine": "PygmalionAI/aphrodite-engine (режим OpenAI API)",326 "PygmalionAI/aphrodite-engine": "PygmalionAI/aphrodite-engine (режим OpenAI API)",
327 "Aphrodite API key": "Ключ API для Aphrodite",327 "Aphrodite API key": "Ключ API для Aphrodite",
328 "Aphrodite Model": "Модель Афродіта",328 "Aphrodite Model": "Модель Афродіта",
329 "ggerganov/llama.cpp": "ggerganov/llama.cpp (сервер виведення)",329 "ggml-org/llama.cpp": "ggml-org/llama.cpp (сервер виведення)",
330 "Example: http://127.0.0.1:8080": "Приклад: http://127.0.0.1:8080",330 "Example: http://127.0.0.1:8080": "Приклад: http://127.0.0.1:8080",
331 "Example: http://127.0.0.1:11434": "Приклад: http://127.0.0.1:11434",331 "Example: http://127.0.0.1:11434": "Приклад: http://127.0.0.1:11434",
332 "Ollama Model": "Модель Ollama",332 "Ollama Model": "Модель Ollama",
@@ -603,7 +603,7 @@
603 "Model Icon": "Іконка моделі",603 "Model Icon": "Іконка моделі",
604 "Show sequential message numbers in the chat log": "Показувати послідовні номери повідомлень у журналі чату",604 "Show sequential message numbers in the chat log": "Показувати послідовні номери повідомлень у журналі чату",
605 "Message IDs": "Ідентифікатори повідомлень",605 "Message IDs": "Ідентифікатори повідомлень",
606 "Hide avatars in chat messages.": "Приховати аватари в повідомленнях чату.",606 "Hide avatars, only in chat messages.": "Приховати аватари в повідомленнях чату.",
607 "Hide Chat Avatars": "Приховати аватари чату",607 "Hide Chat Avatars": "Приховати аватари чату",
608 "Show the number of tokens in each message in the chat log": "Показувати кількість токенів у кожному повідомленні у журналі чату",608 "Show the number of tokens in each message in the chat log": "Показувати кількість токенів у кожному повідомленні у журналі чату",
609 "Show Message Token Count": "Кількість токенів у повідомленні",609 "Show Message Token Count": "Кількість токенів у повідомленні",
@@ -737,8 +737,8 @@
737 "Filter": "Фільтр",737 "Filter": "Фільтр",
738 "Automatically select a background based on the chat context": "Автоматичний вибір фону на основі контексту чату",738 "Automatically select a background based on the chat context": "Автоматичний вибір фону на основі контексту чату",
739 "Auto-select": "Автовибір",739 "Auto-select": "Автовибір",
740 "System Backgrounds": "Системні фони",740 "Global": "Глобальні",
741 "Chat Backgrounds": "Фони чату",741 "Chat": "Чат",
742 "bg_chat_hint_1": "Фонове зображення чату, створене за допомогою",742 "bg_chat_hint_1": "Фонове зображення чату, створене за допомогою",
743 "bg_chat_hint_2": "тут з’явиться розширення.",743 "bg_chat_hint_2": "тут з’явиться розширення.",
744 "Extensions": "Розширення",744 "Extensions": "Розширення",
@@ -911,7 +911,7 @@
911 "Save": "Зберегти",911 "Save": "Зберегти",
912 "Chat History": "Історія чату",912 "Chat History": "Історія чату",
913 "Import Chat": "Імпорт чату",913 "Import Chat": "Імпорт чату",
914 "Copy to system backgrounds": "Скопіювати в системні фони",914 "Copy to global backgrounds": "Скопіювати в глобальні фони",
915 "Rename background": "Перейменувати фон",915 "Rename background": "Перейменувати фон",
916 "Lock": "Замок",916 "Lock": "Замок",
917 "Unlock": "Розблокувати",917 "Unlock": "Розблокувати",
public/locales/vi-vn.json+6 -6
@@ -235,7 +235,7 @@
235 "Combines consecutive system messages into one (excluding example dialogues). May improve coherence for some models.": "Kết hợp các tin nhắn hệ thống liên tiếp thành một (loại bỏ các đoạn hội thoại mẫu). Có thể cải thiện tính nhất quán cho một số model.",235 "Combines consecutive system messages into one (excluding example dialogues). May improve coherence for some models.": "Kết hợp các tin nhắn hệ thống liên tiếp thành một (loại bỏ các đoạn hội thoại mẫu). Có thể cải thiện tính nhất quán cho một số model.",
236 "Enable function calling": "Sử dụng tính năng gọi hàm (function calling)",236 "Enable function calling": "Sử dụng tính năng gọi hàm (function calling)",
237 "Send inline images": "Gửi hình ảnh nội bộ",237 "Send inline images": "Gửi hình ảnh nội bộ",
238 "image_inlining_hint_1": "Gửi hình ảnh theo Prompt nếu kiểu máy hỗ trợ.",238 "image_inlining_hint_1": "Gửi tệp phương tiện theo Prompt nếu kiểu máy hỗ trợ.",
239 "Inline Image Quality": "Chất lượng hình ảnh nội tuyến",239 "Inline Image Quality": "Chất lượng hình ảnh nội tuyến",
240 "openai_inline_image_quality_auto": "Tự động",240 "openai_inline_image_quality_auto": "Tự động",
241 "openai_inline_image_quality_low": "Thấp",241 "openai_inline_image_quality_low": "Thấp",
@@ -326,7 +326,7 @@
326 "PygmalionAI/aphrodite-engine": "PygmalionAI/aphrodite-engine (Chế độ đóng gói cho Giao diện lập trình ứng dụng OpenAI)",326 "PygmalionAI/aphrodite-engine": "PygmalionAI/aphrodite-engine (Chế độ đóng gói cho Giao diện lập trình ứng dụng OpenAI)",
327 "Aphrodite API key": "Key API Aphrodite",327 "Aphrodite API key": "Key API Aphrodite",
328 "Aphrodite Model": "Moddel cho Aphrodite",328 "Aphrodite Model": "Moddel cho Aphrodite",
329 "ggerganov/llama.cpp": "ggerganov/llama.cpp",329 "ggml-org/llama.cpp": "ggml-org/llama.cpp",
330 "Example: http://127.0.0.1:8080": "Ví dụ: http://127.0.0.1:8080",330 "Example: http://127.0.0.1:8080": "Ví dụ: http://127.0.0.1:8080",
331 "Example: http://127.0.0.1:11434": "Ví dụ: http://127.0.0.1:11434",331 "Example: http://127.0.0.1:11434": "Ví dụ: http://127.0.0.1:11434",
332 "Ollama Model": "Model Ollama",332 "Ollama Model": "Model Ollama",
@@ -603,7 +603,7 @@
603 "Model Icon": "Biểu tượng Model",603 "Model Icon": "Biểu tượng Model",
604 "Show sequential message numbers in the chat log": "Hiển thị số tin nhắn tuần tự trong nhật ký trò chuyện",604 "Show sequential message numbers in the chat log": "Hiển thị số tin nhắn tuần tự trong nhật ký trò chuyện",
605 "Message IDs": "ID Tin nhắn",605 "Message IDs": "ID Tin nhắn",
606 "Hide avatars in chat messages.": "Ẩn hình đại diện trong tin nhắn trò chuyện.",606 "Hide avatars, only in chat messages.": "Ẩn hình đại diện trong tin nhắn trò chuyện.",
607 "Hide Chat Avatars": "Ẩn hình đại diện trò chuyện",607 "Hide Chat Avatars": "Ẩn hình đại diện trò chuyện",
608 "Show the number of tokens in each message in the chat log": "Hiển thị số lượng token trong mỗi tin nhắn trong nhật ký trò chuyện",608 "Show the number of tokens in each message in the chat log": "Hiển thị số lượng token trong mỗi tin nhắn trong nhật ký trò chuyện",
609 "Show Message Token Count": "Hiển thị Số lượng Token trong Tin nhắn",609 "Show Message Token Count": "Hiển thị Số lượng Token trong Tin nhắn",
@@ -737,8 +737,8 @@
737 "Filter": "Bộ lọc",737 "Filter": "Bộ lọc",
738 "Automatically select a background based on the chat context": "Tự động chọn một nền dựa trên context trò chuyện",738 "Automatically select a background based on the chat context": "Tự động chọn một nền dựa trên context trò chuyện",
739 "Auto-select": "Tự động chọn",739 "Auto-select": "Tự động chọn",
740 "System Backgrounds": "Nền Hệ thống",740 "Global": "Toàn cầu",
741 "Chat Backgrounds": "Hình nền Chat",741 "Chat": "Trò chuyện",
742 "bg_chat_hint_1": "Hình nền trò chuyện được tạo bằng",742 "bg_chat_hint_1": "Hình nền trò chuyện được tạo bằng",
743 "bg_chat_hint_2": "tiện ích sẽ xuất hiện ở đây.",743 "bg_chat_hint_2": "tiện ích sẽ xuất hiện ở đây.",
744 "Extensions": "Tiện ích",744 "Extensions": "Tiện ích",
@@ -911,7 +911,7 @@
911 "Save": "Lưu",911 "Save": "Lưu",
912 "Chat History": "Lịch sử Trò chuyện",912 "Chat History": "Lịch sử Trò chuyện",
913 "Import Chat": "Nhập trò chuyện",913 "Import Chat": "Nhập trò chuyện",
914 "Copy to system backgrounds": "Sao chép vào nền hệ thống",914 "Copy to global backgrounds": "Sao chép vào nền toàn cầu",
915 "Rename background": "Đổi tên nền background",915 "Rename background": "Đổi tên nền background",
916 "Lock": "Khóa",916 "Lock": "Khóa",
917 "Unlock": "Mở khóa",917 "Unlock": "Mở khóa",
public/locales/zh-cn.json+5 -7
@@ -269,13 +269,12 @@
269 "enable_functions_desc_3": "可以被各种扩展利用来提供附加功能。",269 "enable_functions_desc_3": "可以被各种扩展利用来提供附加功能。",
270 "enable_functions_desc_4": "当提示词后处理没有选择工具时不支持。",270 "enable_functions_desc_4": "当提示词后处理没有选择工具时不支持。",
271 "Send inline images": "发送图片",271 "Send inline images": "发送图片",
272 "image_inlining_hint_1": "如果模型支持,就可以在提示词中发送图片。",272 "image_inlining_hint_1": "如果模型支持,就可以在提示词中发送媒体文件。",
273 "Inline Image Quality": "图片画质",273 "Inline Image Quality": "图片画质",
274 "openai_inline_image_quality_auto": "自动",274 "openai_inline_image_quality_auto": "自动",
275 "openai_inline_image_quality_low": "低",275 "openai_inline_image_quality_low": "低",
276 "openai_inline_image_quality_high": "高",276 "openai_inline_image_quality_high": "高",
277 "Send inline videos": "发送视频",277 "Send inline videos": "发送视频",
278 "video_inlining_hint_1": "当模型支持时,将视频发送给模型。",
279 "video_inlining_hint_4": "视频必须在 20MB 以下且时长不超过1分钟。",278 "video_inlining_hint_4": "视频必须在 20MB 以下且时长不超过1分钟。",
280 "Request inline images": "请求图片返回",279 "Request inline images": "请求图片返回",
281 "Allows the model to return image attachments.": "允许模型返回图片附件。",280 "Allows the model to return image attachments.": "允许模型返回图片附件。",
@@ -397,7 +396,7 @@
397 "PygmalionAI/aphrodite-engine": "PygmalionAI/aphrodite-engine(用于OpenAI API的包装器)",396 "PygmalionAI/aphrodite-engine": "PygmalionAI/aphrodite-engine(用于OpenAI API的包装器)",
398 "Aphrodite API key": "Aphrodite API 密钥",397 "Aphrodite API key": "Aphrodite API 密钥",
399 "Aphrodite Model": "Aphrodite 模型",398 "Aphrodite Model": "Aphrodite 模型",
400 "ggerganov/llama.cpp": "ggerganov/llama.cpp",399 "ggml-org/llama.cpp": "ggml-org/llama.cpp",
401 "Example: http://127.0.0.1:8080": "示例:http://127.0.0.1:8080",400 "Example: http://127.0.0.1:8080": "示例:http://127.0.0.1:8080",
402 "Example: http://127.0.0.1:11434": "示例:http://127.0.0.1:11434",401 "Example: http://127.0.0.1:11434": "示例:http://127.0.0.1:11434",
403 "Ollama Model": "Ollama 模型",402 "Ollama Model": "Ollama 模型",
@@ -787,7 +786,7 @@
787 "Model Icon": "模型图标",786 "Model Icon": "模型图标",
788 "Show sequential message numbers in the chat log": "在聊天记录中显示消息楼层",787 "Show sequential message numbers in the chat log": "在聊天记录中显示消息楼层",
789 "Message IDs": "显示消息楼层",788 "Message IDs": "显示消息楼层",
790 "Hide avatars in chat messages.": "在聊天记录中隐藏头像。",789 "Hide avatars, only in chat messages.": "在聊天记录中隐藏头像。",
791 "Hide Chat Avatars": "隐藏头像",790 "Hide Chat Avatars": "隐藏头像",
792 "Show the number of tokens in each message in the chat log": "在聊天记录中显示每条消息的词符数",791 "Show the number of tokens in each message in the chat log": "在聊天记录中显示每条消息的词符数",
793 "Show Message Token Count": "显示消息词符数",792 "Show Message Token Count": "显示消息词符数",
@@ -962,8 +961,7 @@
962 "Automatically select a background based on the chat context": "根据聊天上下文自动选择背景",961 "Automatically select a background based on the chat context": "根据聊天上下文自动选择背景",
963 "Auto-select": "自动选择",962 "Auto-select": "自动选择",
964 "Add Background": "添加背景",963 "Add Background": "添加背景",
965 "System Backgrounds": "系统背景",964 "Global": "全局",
966 "Chat Backgrounds": "聊天背景",
967 "bg_chat_hint_1": "使用生成的聊天背景",965 "bg_chat_hint_1": "使用生成的聊天背景",
968 "bg_chat_hint_2": "扩展名将出现在这里。",966 "bg_chat_hint_2": "扩展名将出现在这里。",
969 "Extensions": "扩展",967 "Extensions": "扩展",
@@ -1158,7 +1156,7 @@
1158 "Save": "保存",1156 "Save": "保存",
1159 "Chat History": "聊天记录",1157 "Chat History": "聊天记录",
1160 "Import Chat": "导入聊天",1158 "Import Chat": "导入聊天",
1161 "Copy to system backgrounds": "复制到系统背景",1159 "Copy to global backgrounds": "复制到全局背景",
1162 "Lock": "锁定",1160 "Lock": "锁定",
1163 "Unlock": "解锁",1161 "Unlock": "解锁",
1164 "Rename Background": "重命名背景",1162 "Rename Background": "重命名背景",
public/locales/zh-tw.json+5 -7
@@ -236,7 +236,7 @@
236 "Combines consecutive system messages into one (excluding example dialogues). May improve coherence for some models.": "將連續的系統訊息合併為一個(不包括對話範例)。可能會提高某些模型的一致性。",236 "Combines consecutive system messages into one (excluding example dialogues). May improve coherence for some models.": "將連續的系統訊息合併為一個(不包括對話範例)。可能會提高某些模型的一致性。",
237 "Enable function calling": "啟用函式呼叫",237 "Enable function calling": "啟用函式呼叫",
238 "Send inline images": "傳送內嵌圖片",238 "Send inline images": "傳送內嵌圖片",
239 "image_inlining_hint_1": "如果模型支援,則在提示詞中傳送圖片。",239 "image_inlining_hint_1": "如果模型支援,則在提示詞中傳送媒體檔案。",
240 "Inline Image Quality": "內嵌圖片品質",240 "Inline Image Quality": "內嵌圖片品質",
241 "openai_inline_image_quality_auto": "自動",241 "openai_inline_image_quality_auto": "自動",
242 "openai_inline_image_quality_low": "低",242 "openai_inline_image_quality_low": "低",
@@ -325,7 +325,7 @@
325 "PygmalionAI/aphrodite-engine": "PygmalionAI/aphrodite 引擎",325 "PygmalionAI/aphrodite-engine": "PygmalionAI/aphrodite 引擎",
326 "Aphrodite API key": "Aphrodite API 金鑰",326 "Aphrodite API key": "Aphrodite API 金鑰",
327 "Aphrodite Model": "Aphrodite 模型",327 "Aphrodite Model": "Aphrodite 模型",
328 "ggerganov/llama.cpp": "ggerganov/llama.cpp",328 "ggml-org/llama.cpp": "ggml-org/llama.cpp",
329 "Example: http://127.0.0.1:8080": "範例:http://127.0.0.1:8080",329 "Example: http://127.0.0.1:8080": "範例:http://127.0.0.1:8080",
330 "Example: http://127.0.0.1:11434": "範例:http://127.0.0.1:11434",330 "Example: http://127.0.0.1:11434": "範例:http://127.0.0.1:11434",
331 "Ollama Model": "Ollama 模型",331 "Ollama Model": "Ollama 模型",
@@ -602,7 +602,7 @@
602 "Model Icon": "顯示模型圖示",602 "Model Icon": "顯示模型圖示",
603 "Show sequential message numbers in the chat log": "在聊天記錄中顯示連續的訊息編號。",603 "Show sequential message numbers in the chat log": "在聊天記錄中顯示連續的訊息編號。",
604 "Message IDs": "顯示訊息 ID",604 "Message IDs": "顯示訊息 ID",
605 "Hide avatars in chat messages.": "在聊天訊息中隱藏頭像",605 "Hide avatars, only in chat messages.": "在聊天訊息中隱藏頭像",
606 "Hide Chat Avatars": "隱藏聊天頭像",606 "Hide Chat Avatars": "隱藏聊天頭像",
607 "Show the number of tokens in each message in the chat log": "在聊天記錄中顯示每條訊息中的符元數量",607 "Show the number of tokens in each message in the chat log": "在聊天記錄中顯示每條訊息中的符元數量",
608 "Show Message Token Count": "顯示訊息符元數",608 "Show Message Token Count": "顯示訊息符元數",
@@ -736,8 +736,7 @@
736 "Filter": "篩選",736 "Filter": "篩選",
737 "Automatically select a background based on the chat context": "根據聊天上下文自動選擇背景",737 "Automatically select a background based on the chat context": "根據聊天上下文自動選擇背景",
738 "Auto-select": "自動選擇",738 "Auto-select": "自動選擇",
739 "System Backgrounds": "系統背景圖片",739 "Global": "全域",
740 "Chat Backgrounds": "聊天背景圖片",
741 "bg_chat_hint_1": "使用擴充功能",740 "bg_chat_hint_1": "使用擴充功能",
742 "bg_chat_hint_2": "所產生的背景圖片將在此顯示。",741 "bg_chat_hint_2": "所產生的背景圖片將在此顯示。",
743 "Extensions": "擴充功能",742 "Extensions": "擴充功能",
@@ -909,7 +908,7 @@
909 "(Examples of chat dialog. Begin each example with START on a new line.)": "(聊天對話範例。每個範例以新的行並以「START」開始。)",908 "(Examples of chat dialog. Begin each example with START on a new line.)": "(聊天對話範例。每個範例以新的行並以「START」開始。)",
910 "Chat History": "聊天記錄",909 "Chat History": "聊天記錄",
911 "Import Chat": "匯入聊天",910 "Import Chat": "匯入聊天",
912 "Copy to system backgrounds": "複製到系統背景圖片",911 "Copy to global backgrounds": "複製到全局背景圖片",
913 "Rename background": "重新命名背景圖片",912 "Rename background": "重新命名背景圖片",
914 "Lock": "上鎖",913 "Lock": "上鎖",
915 "Unlock": "解鎖",914 "Unlock": "解鎖",
@@ -2648,7 +2647,6 @@
2648 "Min Keep": "最小保留",2647 "Min Keep": "最小保留",
2649 "enable_functions_desc_4": "當使用「無工具」提示後處理時不支援!",2648 "enable_functions_desc_4": "當使用「無工具」提示後處理時不支援!",
2650 "Send inline videos": "傳送內嵌影片",2649 "Send inline videos": "傳送內嵌影片",
2651 "video_inlining_hint_1": "若模型支援,則在提示中傳送影片。",
2652 "video_inlining_hint_4": "影片大小必須小於 20 MB 且長度低於 1 分鐘",2650 "video_inlining_hint_4": "影片大小必須小於 20 MB 且長度低於 1 分鐘",
2653 "This setting affects visibility only.": "此設定僅影響可見性。",2651 "This setting affects visibility only.": "此設定僅影響可見性。",
2654 "Allocates a portion of the response length for thinking (min: 1024 tokens, low: 10%, medium: 25%, high: 50%, max: 95%), but minimum 1024 tokens. Auto does not request thinking.": "為推理部分分配回應長度(最大回應長度至少需 1024 符元,低:10%、中:25%、高:50%、最高:95%)。選擇「自動」將不請求推理。",2652 "Allocates a portion of the response length for thinking (min: 1024 tokens, low: 10%, medium: 25%, high: 50%, max: 95%), but minimum 1024 tokens. Auto does not request thinking.": "為推理部分分配回應長度(最大回應長度至少需 1024 符元,低:10%、中:25%、高:50%、最高:95%)。選擇「自動」將不請求推理。",
public/script.js+859 -360
@@ -65,12 +65,11 @@ import {
65 renameGroupMember,65 renameGroupMember,
66 createNewGroupChat,66 createNewGroupChat,
67 getGroupAvatar,67 getGroupAvatar,
68 editGroup,
69 deleteGroupChat,68 deleteGroupChat,
70 renameGroupChat,69 renameGroupChat,
71 importGroupChat,70 importGroupChat,
72 getGroupBlock,71 getGroupBlock,
73 getGroupCharacterCards,72 getGroupCharacterCardsLazy,
74 getGroupDepthPrompts,73 getGroupDepthPrompts,
75} from './scripts/group-chats.js';74} from './scripts/group-chats.js';
7675
@@ -182,11 +181,13 @@ import {
182 canUseNegativeLookbehind,181 canUseNegativeLookbehind,
183 trimSpaces,182 trimSpaces,
184 clamp,183 clamp,
184 shakeElement,
185 createTimeout,
185} from './scripts/utils.js';186} from './scripts/utils.js';
186import { debounce_timeout, GENERATION_TYPE_TRIGGERS, IGNORE_SYMBOL, inject_ids, MEDIA_DISPLAY, MEDIA_SOURCE, MEDIA_TYPE, SCROLL_BEHAVIOR, SWIPE_DIRECTION } from './scripts/constants.js';187import { debounce_timeout, GENERATION_TYPE_TRIGGERS, IGNORE_SYMBOL, inject_ids, MEDIA_DISPLAY, MEDIA_SOURCE, MEDIA_TYPE, OVERSWIPE_BEHAVIOR, SCROLL_BEHAVIOR, SWIPE_DIRECTION, SWIPE_SOURCE, SWIPE_STATE } from './scripts/constants.js';
187188
188import { cancelDebouncedMetadataSave, doDailyExtensionUpdatesCheck, extension_settings, initExtensions, loadExtensionSettings, runGenerationInterceptors } from './scripts/extensions.js';189import { cancelDebouncedMetadataSave, doDailyExtensionUpdatesCheck, extension_settings, initExtensions, loadExtensionSettings, runGenerationInterceptors } from './scripts/extensions.js';
189import { COMMENT_NAME_DEFAULT, CONNECT_API_MAP, executeSlashCommandsOnChatInput, initDefaultSlashCommands, isExecutingCommandsFromChatInput, pauseScriptExecution, stopScriptExecution, UNIQUE_APIS } from './scripts/slash-commands.js';190import { COMMENT_NAME_DEFAULT, CONNECT_API_MAP, executeSlashCommandsOnChatInput, initDefaultSlashCommands, initSlashCommandAutoComplete, isExecutingCommandsFromChatInput, pauseScriptExecution, stopScriptExecution, UNIQUE_APIS } from './scripts/slash-commands.js';
190import {191import {
191 tag_map,192 tag_map,
192 tags,193 tags,
@@ -266,7 +267,7 @@ import { initServerHistory } from './scripts/server-history.js';
266import { initSettingsSearch } from './scripts/setting-search.js';267import { initSettingsSearch } from './scripts/setting-search.js';
267import { initBulkEdit } from './scripts/bulk-edit.js';268import { initBulkEdit } from './scripts/bulk-edit.js';
268import { getContext } from './scripts/st-context.js';269import { getContext } from './scripts/st-context.js';
269import { extractReasoningFromData, initReasoning, parseReasoningInSwipes, PromptReasoning, ReasoningHandler, removeReasoningFromString, updateReasoningUI } from './scripts/reasoning.js';270import { extractReasoningFromData, extractReasoningSignatureFromData, initReasoning, parseReasoningInSwipes, PromptReasoning, ReasoningHandler, removeReasoningFromString, updateReasoningUI } from './scripts/reasoning.js';
270import { accountStorage } from './scripts/util/AccountStorage.js';271import { accountStorage } from './scripts/util/AccountStorage.js';
271import { initWelcomeScreen, openPermanentAssistantChat, openPermanentAssistantCard, getPermanentAssistantAvatar } from './scripts/welcome-screen.js';272import { initWelcomeScreen, openPermanentAssistantChat, openPermanentAssistantCard, getPermanentAssistantAvatar } from './scripts/welcome-screen.js';
272import { initDataMaid } from './scripts/data-maid.js';273import { initDataMaid } from './scripts/data-maid.js';
@@ -278,6 +279,9 @@ import { applyStreamFadeIn } from './scripts/util/stream-fadein.js';
278import { initDomHandlers } from './scripts/dom-handlers.js';279import { initDomHandlers } from './scripts/dom-handlers.js';
279import { SimpleMutex } from './scripts/util/SimpleMutex.js';280import { SimpleMutex } from './scripts/util/SimpleMutex.js';
280import { AudioPlayer } from './scripts/audio-player.js';281import { AudioPlayer } from './scripts/audio-player.js';
282import { MacroEnvBuilder } from './scripts/macros/engine/MacroEnvBuilder.js';
283import { MacroEngine } from './scripts/macros/engine/MacroEngine.js';
284import { addChatBackupsBrowser } from './scripts/chat-backups.js';
281285
282// API OBJECT FOR EXTERNAL WIRING286// API OBJECT FOR EXTERNAL WIRING
283globalThis.SillyTavern = {287globalThis.SillyTavern = {
@@ -373,18 +377,21 @@ export let name1 = default_user_name;
373export let name2 = systemUserName;377export let name2 = systemUserName;
374/** @type {ChatMessage[]} */378/** @type {ChatMessage[]} */
375export let chat = [];379export let chat = [];
376export let isSwipingAllowed = true; //false when a swipe is in progress, or swiping is blocked.380
381/**
382 * @type {import('./scripts/constants.js').SWIPE_STATE}
383 */
384export let swipeState = SWIPE_STATE.NONE;
377let chatSaveTimeout;385let chatSaveTimeout;
378let importFlashTimeout;386let importFlashTimeout;
379export let isChatSaving = false;387export let isChatSaving = false;
380let chat_create_date = '';
381let firstRun = false;388let firstRun = false;
382let settingsReady = false;389let settingsReady = false;
383let currentVersion = '0.0.0';390let currentVersion = '0.0.0';
384export let displayVersion = 'SillyTavern';391export let displayVersion = 'SillyTavern';
385392
386let generation_started = new Date();393let generation_started = new Date();
387/** @type {import('./scripts/char-data.js').v1CharData[]} */394/** @type {Character[]} */
388export let characters = [];395export let characters = [];
389/**396/**
390 * Stringified index of a currently chosen entity in the characters array.397 * Stringified index of a currently chosen entity in the characters array.
@@ -411,6 +418,7 @@ export const chatElement = $('#chat');
411418
412let dialogueResolve = null;419let dialogueResolve = null;
413let dialogueCloseStop = false;420let dialogueCloseStop = false;
421/** @type {ChatMetadata} */
414export let chat_metadata = {};422export let chat_metadata = {};
415/** @type {StreamingProcessor} */423/** @type {StreamingProcessor} */
416export let streamingProcessor = null;424export let streamingProcessor = null;
@@ -562,6 +570,7 @@ let chat_file_for_del = '';
562export let online_status = 'no_connection';570export let online_status = 'no_connection';
563571
564export let is_send_press = false; //Send generation572export let is_send_press = false; //Send generation
573export const isGenerating = () => (is_send_press || is_group_generating);
565574
566let this_del_mes = -1;575let this_del_mes = -1;
567576
@@ -575,7 +584,14 @@ export let settings;
575export let amount_gen = 80; //default max length of AI generated responses584export let amount_gen = 80; //default max length of AI generated responses
576export let max_context = 2048;585export let max_context = 2048;
577586
578var swipes = true;587/** User preference for swipeable messages */
588let swipes = true;
589/** Forcefully hide swipes. */
590export let swipesHidden = false;
591/** @type {{ now: number, direction: string }} */
592export let lastSwipeInfo = { now: performance.now(), direction: SWIPE_DIRECTION.RIGHT };
593export let recentSwipes = 0;
594
579export let extension_prompts = {};595export let extension_prompts = {};
580596
581export let main_api;// = "kobold";597export let main_api;// = "kobold";
@@ -629,7 +645,7 @@ export async function pingServer() {
629 try {645 try {
630 const result = await fetch('api/ping', {646 const result = await fetch('api/ping', {
631 method: 'POST',647 method: 'POST',
632 headers: getRequestHeaders(),648 headers: getRequestHeaders({ omitContentType: true }),
633 });649 });
634650
635 if (!result.ok) {651 if (!result.ok) {
@@ -693,6 +709,7 @@ async function firstLoadInit() {
693 initBackgrounds();709 initBackgrounds();
694 initAuthorsNote();710 initAuthorsNote();
695 await initPersonas();711 await initPersonas();
712 await initSlashCommandAutoComplete();
696 initWorldInfo();713 initWorldInfo();
697 initHorde();714 initHorde();
698 initRossMods();715 initRossMods();
@@ -730,7 +747,7 @@ function initStandaloneMode() {
730 }747 }
731}748}
732749
733function cancelStatusCheck(reason = 'Manually cancelled status check') {750export function cancelStatusCheck(reason = 'Manually cancelled status check') {
734 abortStatusCheck?.abort(new AbortReason(reason));751 abortStatusCheck?.abort(new AbortReason(reason));
735 abortStatusCheck = new AbortController();752 abortStatusCheck = new AbortController();
736 setOnlineStatus('no_connection');753 setOnlineStatus('no_connection');
@@ -1029,9 +1046,6 @@ function verifyCharactersSearchSortRule() {
1029 }1046 }
1030}1047}
10311048
1032/** @typedef {object} Character - A character */
1033/** @typedef {object} Group - A group */
1034
1035/**1049/**
1036 * @typedef {object} Entity - Object representing a display entity1050 * @typedef {object} Entity - Object representing a display entity
1037 * @property {Character|Group|import('./scripts/tags.js').Tag|*} item - The item1051 * @property {Character|Group|import('./scripts/tags.js').Tag|*} item - The item
@@ -1295,7 +1309,7 @@ export async function deleteCharacterChatByName(characterId, fileName) {
1295 // Make sure all the data is loaded.1309 // Make sure all the data is loaded.
1296 await unshallowCharacter(characterId);1310 await unshallowCharacter(characterId);
12971311
1298 /** @type {import('./scripts/char-data.js').v1CharData} */1312 /** @type {Character} */
1299 const character = characters[characterId];1313 const character = characters[characterId];
1300 if (!character) {1314 if (!character) {
1301 console.warn(`Character with ID ${characterId} not found.`);1315 console.warn(`Character with ID ${characterId} not found.`);
@@ -1380,10 +1394,11 @@ export async function showMoreMessages(messagesToLoad = null) {
13801394
1381 while (messageId > 0 && count > 0) {1395 while (messageId > 0 && count > 0) {
1382 let newMessageId = messageId - 1;1396 let newMessageId = messageId - 1;
1383 addOneMessage(chat[newMessageId], { insertBefore: messageId >= chat.length ? null : messageId, scroll: false, forceId: newMessageId });1397 addOneMessage(chat[newMessageId], { insertBefore: messageId >= chat.length ? null : messageId, scroll: false, forceId: newMessageId, showSwipes: false });
1384 count--;1398 count--;
1385 messageId--;1399 messageId--;
1386 }1400 }
1401 refreshSwipeButtons();
13871402
1388 if (messageId == 0) {1403 if (messageId == 0) {
1389 $('#show_more_messages').remove();1404 $('#show_more_messages').remove();
@@ -1414,13 +1429,13 @@ export async function printMessages() {
14141429
1415 chatElement.find('.mes').removeClass('last_mes');1430 chatElement.find('.mes').removeClass('last_mes');
1416 chatElement.find('.mes').last().addClass('last_mes');1431 chatElement.find('.mes').last().addClass('last_mes');
1417 refreshSwipeButtons();1432 refreshSwipeButtons(false, false);
1418 applyStylePins();1433 applyStylePins();
1419 scrollChatToBottom();1434 scrollChatToBottom({ waitForFrame: true });
1420 delay(debounce_timeout.short).then(() => scrollOnMediaLoad());1435 delay(debounce_timeout.short).then(() => scrollOnMediaLoad());
1421}1436}
14221437
1423function scrollOnMediaLoad() {1438export function scrollOnMediaLoad() {
1424 const started = Date.now();1439 const started = Date.now();
1425 const media = chatElement.find('.mes_block img, .mes_block video, .mes_block audio').toArray();1440 const media = chatElement.find('.mes_block img, .mes_block video, .mes_block audio').toArray();
1426 let mediaLoaded = 0;1441 let mediaLoaded = 0;
@@ -1475,6 +1490,7 @@ export async function clearChat() {
1475 if (is_delete_mode) {1490 if (is_delete_mode) {
1476 $('#dialogue_del_mes_cancel').trigger('click');1491 $('#dialogue_del_mes_cancel').trigger('click');
1477 }1492 }
1493 //This will also remove non '.mes' elements, e.g. '<div id="show_more_messages">Show more messages</div>'.
1478 chatElement.children().remove();1494 chatElement.children().remove();
1479 if ($('.zoomed_avatar[forChar]').length) {1495 if ($('.zoomed_avatar[forChar]').length) {
1480 console.debug('saw avatars to remove');1496 console.debug('saw avatars to remove');
@@ -1579,9 +1595,17 @@ export async function reloadCurrentChat() {
1579 * Send the message currently typed into the chat box.1595 * Send the message currently typed into the chat box.
1580 */1596 */
1581export async function sendTextareaMessage() {1597export async function sendTextareaMessage() {
1598 // don't proceed during swipeGenerate()
1599 if (swipeState == SWIPE_STATE.EDITING) {
1600 toastr.warning(t`Confirm the edit to start a generation.`, t`You cannot send a message during a swipe-edit.`);
1601 return;
1602 }
1603 if (swipeState !== SWIPE_STATE.NONE) return; // don't proceed if mid-swipe.
1582 if (is_send_press) return;1604 if (is_send_press) return;
1583 if (isExecutingCommandsFromChatInput) return;1605 if (isExecutingCommandsFromChatInput) return;
15841606
1607 hideSwipeButtons(); //Swipe buttons must be hidden now, otherwise concurrent generations are possible.
1608
1585 let generateType = 'normal';1609 let generateType = 'normal';
1586 // "Continue on send" is activated when the user hits "send" (or presses enter) on an empty chat box, and the last1610 // "Continue on send" is activated when the user hits "send" (or presses enter) on an empty chat box, and the last
1587 // message was sent from a character (not the user or the system).1611 // message was sent from a character (not the user or the system).
@@ -1601,7 +1625,9 @@ export async function sendTextareaMessage() {
1601 await newAssistantChat({ temporary: false });1625 await newAssistantChat({ temporary: false });
1602 }1626 }
16031627
1604 return await Generate(generateType);1628 let generation = await Generate(generateType);
1629 showSwipeButtons();
1630 return generation;
1605}1631}
16061632
1607/**1633/**
@@ -2366,7 +2392,7 @@ export function addCopyToCodeBlocks(messageElement) {
2366 * @param {boolean} [options.scroll=true] Whether to scroll to the new message2392 * @param {boolean} [options.scroll=true] Whether to scroll to the new message
2367 * @param {number} [options.insertBefore=null] Message ID to insert the new message before2393 * @param {number} [options.insertBefore=null] Message ID to insert the new message before
2368 * @param {number} [options.forceId=null] Force the message ID2394 * @param {number} [options.forceId=null] Force the message ID
2369 * @param {boolean} [options.showSwipes=true] Whether to show swipe buttons2395 * @param {boolean} [options.showSwipes=true] Whether to refresh the swipe buttons.
2370 * @returns {void}2396 * @returns {void}
2371 */2397 */
2372export function addOneMessage(mes, { type = 'normal', insertAfter = null, scroll = true, insertBefore = null, forceId = null, showSwipes = true } = {}) {2398export function addOneMessage(mes, { type = 'normal', insertAfter = null, scroll = true, insertBefore = null, forceId = null, showSwipes = true } = {}) {
@@ -2490,8 +2516,7 @@ export function addOneMessage(mes, { type = 'normal', insertAfter = null, scroll
2490 });2516 });
24912517
2492 if (type === 'swipe') {2518 if (type === 'swipe') {
2493 const messageId = forceId ?? chat.length - 1;2519 const swipeMessage = chatElement.find(`[mesid="${newMessageId}"]`);
2494 const swipeMessage = chatElement.find(`[mesid="${messageId}"]`);
2495 swipeMessage.attr('swipeid', params.swipeId);2520 swipeMessage.attr('swipeid', params.swipeId);
2496 swipeMessage.find('.mes_text').html(messageText).attr('title', title);2521 swipeMessage.find('.mes_text').html(messageText).attr('title', title);
2497 swipeMessage.find('.timestamp').text(timestamp).attr('title', `${params.extra.api} - ${params.extra.model}`);2522 swipeMessage.find('.timestamp').text(timestamp).attr('title', `${params.extra.api} - ${params.extra.model}`);
@@ -2509,30 +2534,27 @@ export function addOneMessage(mes, { type = 'normal', insertAfter = null, scroll
2509 swipeMessage.find('.tokenCounterDisplay').empty();2534 swipeMessage.find('.tokenCounterDisplay').empty();
2510 }2535 }
2511 } else {2536 } else {
2512 const messageId = forceId ?? chat.length - 1;2537 chatElement.find(`[mesid="${newMessageId}"] .mes_text`).append(messageText);
2513 chatElement.find(`[mesid="${messageId}"] .mes_text`).append(messageText);
2514 appendMediaToMessage(mes, newMessage, scroll ? SCROLL_BEHAVIOR.ADJUST : SCROLL_BEHAVIOR.NONE);2538 appendMediaToMessage(mes, newMessage, scroll ? SCROLL_BEHAVIOR.ADJUST : SCROLL_BEHAVIOR.NONE);
2515 showSwipes && hideSwipeButtons();
2516 }2539 }
25172540
2518 addCopyToCodeBlocks(newMessage);2541 addCopyToCodeBlocks(newMessage);
25192542
2520 // Set the swipes counter for past messages, only visible if 'Show Swipes on All Message' is enabled2543 // Set the swipes counter for all non-user messages.
2521 if (!params.isUser && newMessageId !== 0 && newMessageId !== chat.length - 1) {2544 if (!params.isUser) {
2522 const swipesNum = chat[newMessageId].swipes?.length;2545 updateSwipeCounter(newMessageId);
2523 const swipeId = chat[newMessageId].swipe_id + 1;
2524 newMessage.find('.swipes-counter').text(formatSwipeCounter(swipeId, swipesNum));
2525 }2546 }
25262547
2548 //last_mes should always be updated.
2549 chatElement.find('.mes').removeClass('last_mes');
2550 chatElement.find('.mes').last().addClass('last_mes');
2527 if (showSwipes) {2551 if (showSwipes) {
2528 chatElement.find('.mes').last().addClass('last_mes');
2529 chatElement.find('.mes').eq(-2).removeClass('last_mes');
2530 refreshSwipeButtons();2552 refreshSwipeButtons();
2531 }2553 }
25322554
2533 // Don't scroll if not inserting last2555 // Don't scroll if not inserting last
2534 if (!insertAfter && !insertBefore && scroll) {2556 if (!insertAfter && !insertBefore && scroll) {
2535 scrollChatToBottom();2557 scrollChatToBottom({ waitForFrame: true });
2536 }2558 }
25372559
2538 applyCharacterTagsToMessageDivs({ mesIds: newMessageId });2560 applyCharacterTagsToMessageDivs({ mesIds: newMessageId });
@@ -2642,14 +2664,13 @@ export function scrollChatToBottom({ waitForFrame } = {}) {
2642}2664}
26432665
2644/**2666/**
2667 * @deprecated Function is not needed anymore, as the new signature of substituteParams is more flexible.
2668 *
2645 * Substitutes {{macro}} parameters in a string.2669 * Substitutes {{macro}} parameters in a string.
2646 * @param {string} content - The string to substitute parameters in.
2647 * @param {Record<string,any>} additionalMacro - Additional environment variables for substitution.
2648 * @param {(x: string) => string} [postProcessFn] - Post-processing function for each substituted macro.
2649 * @returns {string} The string with substituted parameters.2670 * @returns {string} The string with substituted parameters.
2650 */2671 */
2651export function substituteParamsExtended(content, additionalMacro = {}, postProcessFn = (x) => x) {2672export function substituteParamsExtended(content, additionalMacro = {}, postProcessFn = (x) => x) {
2652 return substituteParams(content, undefined, undefined, undefined, undefined, true, additionalMacro, postProcessFn);2673 return substituteParams(content, { dynamicMacros: additionalMacro, postProcessFn });
2653}2674}
26542675
2655/**2676/**
@@ -2664,11 +2685,24 @@ export function substituteParamsExtended(content, additionalMacro = {}, postProc
2664 * @param {(x: string) => string} [postProcessFn] - Post-processing function for each substituted macro.2685 * @param {(x: string) => string} [postProcessFn] - Post-processing function for each substituted macro.
2665 * @returns {string} The string with substituted parameters.2686 * @returns {string} The string with substituted parameters.
2666 */2687 */
2667export function substituteParams(content, _name1, _name2, _original, _group, _replaceCharacterCard = true, additionalMacro = {}, postProcessFn = (x) => x) {2688export function substituteParamsLegacy(content, _name1, _name2, _original, _group, _replaceCharacterCard = true, additionalMacro = {}, postProcessFn = (x) => x) {
2668 if (!content) {2689 if (!content) {
2669 return '';2690 return '';
2670 }2691 }
26712692
2693 // If experimental macro engine is enabled, use it. This code will be cleaned up in the future.
2694 if (power_user?.experimental_macro_engine) {
2695 return substituteParams(content, {
2696 name1Override: _name1,
2697 name2Override: _name2,
2698 original: _original,
2699 groupOverride: _group,
2700 replaceCharacterCard: _replaceCharacterCard ?? true,
2701 dynamicMacros: additionalMacro ?? {},
2702 postProcessFn: postProcessFn ?? ((x) => x),
2703 });
2704 }
2705
2672 const environment = {};2706 const environment = {};
26732707
2674 if (typeof _original === 'string') {2708 if (typeof _original === 'string') {
@@ -2768,6 +2802,55 @@ export function substituteParams(content, _name1, _name2, _original, _group, _re
2768 return evaluateMacros(content, environment, postProcessFn);2802 return evaluateMacros(content, environment, postProcessFn);
2769}2803}
27702804
2805/** @typedef {import('./scripts/macros/engine/MacroRegistry.js').MacroHandler} MacroHandler */
2806
2807/**
2808 * Substitutes {{macros}} in a string using the new macro engine.
2809 *
2810 * This will replace all registered macros and dynamic additional macros as environment context.
2811 *
2812 * @param {string} content - The string to substitute parameters in.
2813 * @param {Object} [options={}] - Options for the substitution.
2814 * @param {string} [options.name1Override] - The name of the user. Uses global name1 if not provided.
2815 * @param {string} [options.name2Override] - The name of the character. Uses global name2 if not provided.
2816 * @param {string} [options.original] - The original message for {{original}} substitution.
2817 * @param {string} [options.groupOverride] - The group members list for {{group}} substitution.
2818 * @param {boolean} [options.replaceCharacterCard=true] - Whether to replace character card macros.
2819 * @param {Record<string,string|MacroHandler>} [options.dynamicMacros={}] - Additional environment variables as dynamic macros for substitution. Registered as macro functions.
2820 * @param {(x: string) => string} [options.postProcessFn=(x) => x] - Post-processing function for each substituted macro.
2821 * @returns {string} The string with substituted parameters.
2822 */
2823export function substituteParams(content, options = {}) {
2824 if (!content) return '';
2825
2826 // Handle legacy signature calls to substituteParams
2827 // We'll simply re-route them to a temporary legacy function. In the future, we'll remove this and cleanly build the options object ourselves.
2828 const isOptionsObject = options && typeof options === 'object' && !Array.isArray(options);
2829 if (!isOptionsObject) {
2830 return substituteParamsLegacy.call(this, ...arguments);
2831 }
2832
2833 // Keep the new macro engine behind a feature switch for now
2834 if (!power_user?.experimental_macro_engine) {
2835 return substituteParamsLegacy(content, options.name1Override, options.name2Override, options.original, options.groupOverride, options.replaceCharacterCard, options.dynamicMacros, options.postProcessFn);
2836 }
2837
2838 const ctx = /** @type {import('./scripts/macros/engine/MacroEnvBuilder.js').MacroEnvRawContext} */ ({
2839 content,
2840 name1Override: options.name1Override,
2841 name2Override: options.name2Override,
2842 original: options.original,
2843 groupOverride: options.groupOverride,
2844 replaceCharacterCard: options.replaceCharacterCard ?? true,
2845 dynamicMacros: options.dynamicMacros ?? {},
2846 postProcessFn: options.postProcessFn ?? ((x) => x),
2847 });
2848
2849 const env = MacroEnvBuilder.buildFromRawEnv(ctx);
2850 const result = MacroEngine.evaluate(content, env);
2851 return result;
2852}
2853
27712854
2772/**2855/**
2773 * Gets stopping sequences for the prompt.2856 * Gets stopping sequences for the prompt.
@@ -3076,10 +3159,19 @@ export async function getExtensionPrompt(position = extension_prompt_types.IN_PR
3076 return values;3159 return values;
3077}3160}
30783161
3079export function baseChatReplace(value, name1, name2) {3162/**
3080 if (value !== undefined && value.length > 0) {3163 * Base chat replacement function for character card fields.
3081 const _ = undefined;3164 * 1. Substitutes macros using substituteParams.
3082 value = substituteParams(value, name1, name2, _, _, false);3165 * 2. Collapses newlines if enabled in power user settings.
3166 * 3. Removes carriage return characters.
3167 * @param {string} value Input string
3168 * @param {string?} name1Override Override for name1
3169 * @param {string?} name2Override Override for name2
3170 * @returns {string} Processed string
3171 */
3172export function baseChatReplace(value, name1Override = null, name2Override = null) {
3173 if (typeof value === 'string' && value.length > 0) {
3174 value = substituteParams(value, { name1Override, name2Override, replaceCharacterCard: false });
30833175
3084 if (power_user.collapse_newlines) {3176 if (power_user.collapse_newlines) {
3085 value = collapseNewlines(value);3177 value = collapseNewlines(value);
@@ -3091,11 +3183,7 @@ export function baseChatReplace(value, name1, name2) {
3091}3183}
30923184
3093/**3185/**
3094 * Returns the character card fields for the current character.3186 * @typedef {Object} CharacterCardFields
3095 * @param {object} [options]
3096 * @param {number} [options.chid] Optional character index
3097 *
3098 * @typedef {object} CharacterCardFields
3099 * @property {string} system System prompt3187 * @property {string} system System prompt
3100 * @property {string} mesExamples Message examples3188 * @property {string} mesExamples Message examples
3101 * @property {string} description Description3189 * @property {string} description Description
@@ -3106,57 +3194,119 @@ export function baseChatReplace(value, name1, name2) {
3106 * @property {string} version Character version3194 * @property {string} version Character version
3107 * @property {string} charDepthPrompt Character depth note3195 * @property {string} charDepthPrompt Character depth note
3108 * @property {string} creatorNotes Character creator notes3196 * @property {string} creatorNotes Character creator notes
3109 * @returns {CharacterCardFields} Character card fields
3110 */3197 */
3111export function getCharacterCardFields({ chid = null } = {}) {
3112 const currentChid = chid ?? this_chid;
3113
3114 const result = {
3115 system: '',
3116 mesExamples: '',
3117 description: '',
3118 personality: '',
3119 persona: '',
3120 scenario: '',
3121 jailbreak: '',
3122 version: '',
3123 charDepthPrompt: '',
3124 creatorNotes: '',
3125 };
3126 result.persona = baseChatReplace(power_user.persona_description?.trim(), name1, name2);
3127
3128 const character = characters[currentChid];
31293198
3130 if (!character) {3199/**
3131 return result;3200 * Helper to create an object with lazy, memoized getters from a map of field resolvers.
3201 * @param {Record<string, () => string>} resolvers Map of field names to resolver functions
3202 * @returns {CharacterCardFields} Object with lazy getters
3203 */
3204export function createLazyFields(resolvers) {
3205 const result = /** @type {CharacterCardFields} */ ({});
3206 for (const [key, resolver] of Object.entries(resolvers)) {
3207 let cached;
3208 let resolved = false;
3209 Object.defineProperty(result, key, {
3210 get() {
3211 if (!resolved) {
3212 cached = resolver();
3213 resolved = true;
3214 }
3215 return cached;
3216 },
3217 enumerable: true,
3218 configurable: true,
3219 });
3132 }3220 }
3221 return result;
3222}
31333223
3134 const scenarioText = chat_metadata['scenario'] || character.scenario || '';3224/**
3135 const exampleDialog = chat_metadata['mes_example'] || character.mes_example || '';3225 * Returns the character card fields for the current character as lazy getters.
3136 const systemPrompt = chat_metadata['system_prompt'] || character.data?.system_prompt || '';3226 * Each field is only processed (baseChatReplace) when first accessed.
3227 * @param {Object} [options={}]
3228 * @param {number} [options.chid] Optional character index
3229 * @returns {CharacterCardFields} Character card fields with lazy evaluation
3230 */
3231export function getCharacterCardFieldsLazy({ chid = undefined } = {}) {
3232 const currentChid = chid ?? this_chid;
3233 const character = characters[currentChid];
31373234
3138 result.description = baseChatReplace(character.description?.trim(), name1, name2);3235 // For group chats, we need to check if group cards should be used
3139 result.personality = baseChatReplace(character.personality?.trim(), name1, name2);3236 const useGroupCards = selected_group && character;
3140 result.scenario = baseChatReplace(scenarioText.trim(), name1, name2);3237 const groupCardsLazy = useGroupCards ? getGroupCharacterCardsLazy(selected_group, Number(currentChid)) : null;
3141 result.mesExamples = baseChatReplace(exampleDialog.trim(), name1, name2);3238
3142 result.system = power_user.prefer_character_prompt ? baseChatReplace(systemPrompt.trim(), name1, name2) : '';3239 /** @type {Record<string, () => string>} */
3143 result.jailbreak = power_user.prefer_character_jailbreak ? baseChatReplace(character.data?.post_history_instructions?.trim(), name1, name2) : '';3240 const resolvers = {
3144 result.version = character.data?.character_version ?? '';3241 persona: () => baseChatReplace(power_user.persona_description?.trim()),
3145 result.charDepthPrompt = baseChatReplace(character.data?.extensions?.depth_prompt?.prompt?.trim(), name1, name2);3242 system: () => {
3146 result.creatorNotes = baseChatReplace(character.data?.creator_notes?.trim(), name1, name2);3243 if (!character) return '';
3244 const systemPrompt = chat_metadata['system_prompt'] || character.data?.system_prompt || '';
3245 return power_user.prefer_character_prompt ? baseChatReplace(systemPrompt.trim()) : '';
3246 },
3247 jailbreak: () => {
3248 if (!character) return '';
3249 return power_user.prefer_character_jailbreak ? baseChatReplace(character.data?.post_history_instructions?.trim()) : '';
3250 },
3251 version: () => character?.data?.character_version ?? '',
3252 charDepthPrompt: () => {
3253 if (!character) return '';
3254 return baseChatReplace(character.data?.extensions?.depth_prompt?.prompt?.trim());
3255 },
3256 creatorNotes: () => {
3257 if (!character) return '';
3258 return baseChatReplace(character.data?.creator_notes?.trim());
3259 },
3260 // These four fields may be overridden by group cards
3261 description: () => {
3262 if (groupCardsLazy) return groupCardsLazy.description;
3263 if (!character) return '';
3264 return baseChatReplace(character.description?.trim());
3265 },
3266 personality: () => {
3267 if (groupCardsLazy) return groupCardsLazy.personality;
3268 if (!character) return '';
3269 return baseChatReplace(character.personality?.trim());
3270 },
3271 scenario: () => {
3272 if (groupCardsLazy) return groupCardsLazy.scenario;
3273 if (!character) return '';
3274 const scenarioText = chat_metadata['scenario'] || character.scenario || '';
3275 return baseChatReplace(scenarioText.trim());
3276 },
3277 mesExamples: () => {
3278 if (groupCardsLazy) return groupCardsLazy.mesExamples;
3279 if (!character) return '';
3280 const exampleDialog = chat_metadata['mes_example'] || character.mes_example || '';
3281 return baseChatReplace(exampleDialog.trim());
3282 },
3283 };
31473284
3148 if (selected_group) {3285 return createLazyFields(resolvers);
3149 const groupCards = getGroupCharacterCards(selected_group, Number(currentChid));3286}
31503287
3151 if (groupCards) {3288/**
3152 result.description = groupCards.description;3289 * Returns the character card fields for the current character.
3153 result.personality = groupCards.personality;3290 * @param {Object} [options={}]
3154 result.scenario = groupCards.scenario;3291 * @param {number} [options.chid] Optional character index
3155 result.mesExamples = groupCards.mesExamples;3292 * @returns {CharacterCardFields} Character card fields
3156 }3293 */
3157 }3294export function getCharacterCardFields({ chid = undefined } = {}) {
3295 const lazy = getCharacterCardFieldsLazy({ chid });
31583296
3159 return result;3297 // Resolve all lazy fields into a plain object
3298 return {
3299 system: lazy.system,
3300 mesExamples: lazy.mesExamples,
3301 description: lazy.description,
3302 personality: lazy.personality,
3303 persona: lazy.persona,
3304 scenario: lazy.scenario,
3305 jailbreak: lazy.jailbreak,
3306 version: lazy.version,
3307 charDepthPrompt: lazy.charDepthPrompt,
3308 creatorNotes: lazy.creatorNotes,
3309 };
3160}3310}
31613311
3162/**3312/**
@@ -3247,6 +3397,8 @@ class StreamingProcessor {
3247 this.promptReasoning = promptReasoning;3397 this.promptReasoning = promptReasoning;
3248 /** @type {string[]} */3398 /** @type {string[]} */
3249 this.images = [];3399 this.images = [];
3400 /** @type {string?} */
3401 this.reasoningSignature = null;
3250 }3402 }
32513403
3252 /**3404 /**
@@ -3279,7 +3431,7 @@ class StreamingProcessor {
3279 }3431 }
32803432
3281 markUIGenStopped() {3433 markUIGenStopped() {
3282 activateSendButtons();3434 unblockGeneration();
3283 }3435 }
32843436
3285 async onStartStreaming(text) {3437 async onStartStreaming(text) {
@@ -3408,14 +3560,14 @@ class StreamingProcessor {
3408 }3560 }
34093561
3410 async onFinishStreaming(messageId, text) {3562 async onFinishStreaming(messageId, text) {
3411 this.markUIGenStopped();
3412 await this.onProgressStreaming(messageId, text, true);3563 await this.onProgressStreaming(messageId, text, true);
3413 addCopyToCodeBlocks(chatElement.find(`.mes[mesid="${messageId}"]`));3564 const messageElement = chatElement.find(`.mes[mesid="${messageId}"]`);
3565 const message = chat[messageId];
3566 addCopyToCodeBlocks(messageElement);
34143567
3415 await this.reasoningHandler.finish(messageId);3568 await this.reasoningHandler.finish(messageId);
34163569
3417 if (Array.isArray(this.swipes) && this.swipes.length > 0) {3570 if (Array.isArray(this.swipes) && this.swipes.length > 0) {
3418 const message = chat[messageId];
3419 const swipeInfoExtra = structuredClone(message.extra ?? {});3571 const swipeInfoExtra = structuredClone(message.extra ?? {});
3420 delete swipeInfoExtra.token_count;3572 delete swipeInfoExtra.token_count;
3421 delete swipeInfoExtra.reasoning;3573 delete swipeInfoExtra.reasoning;
@@ -3428,15 +3580,26 @@ class StreamingProcessor {
3428 };3580 };
3429 const swipeInfoArray = Array(this.swipes.length).fill().map(() => structuredClone(swipeInfo));3581 const swipeInfoArray = Array(this.swipes.length).fill().map(() => structuredClone(swipeInfo));
3430 parseReasoningInSwipes(this.swipes, swipeInfoArray, message.extra?.reasoning_duration);3582 parseReasoningInSwipes(this.swipes, swipeInfoArray, message.extra?.reasoning_duration);
3431 chat[messageId].swipes.push(...this.swipes);3583 message.swipes.push(...this.swipes);
3432 chat[messageId].swipe_info.push(...swipeInfoArray);3584 message.swipe_info.push(...swipeInfoArray);
3433 }3585 }
34343586
3587 syncMesToSwipe(messageId);
3588 saveLogprobsForActiveMessage(this.messageLogprobs.filter(Boolean), this.continueMessage);
3589
3435 if (Array.isArray(this.images) && this.images.length > 0) {3590 if (Array.isArray(this.images) && this.images.length > 0) {
3436 await processImageAttachment(chat[messageId], { imageUrls: this.images });3591 await processImageAttachment(message, { imageUrls: this.images });
3437 appendMediaToMessage(chat[messageId], $(this.messageDom));3592 appendMediaToMessage(message, $(this.messageDom));
3593 }
3594
3595 // Store reasoning signature for models that support multi-turn context
3596 if (this.reasoningSignature) {
3597 message.extra = message.extra || {};
3598 message.extra.reasoning_signature = this.reasoningSignature;
3438 }3599 }
34393600
3601 this.markUIGenStopped();
3602
3440 if (this.type !== 'impersonate') {3603 if (this.type !== 'impersonate') {
3441 await eventSource.emit(event_types.MESSAGE_RECEIVED, this.messageId, this.type);3604 await eventSource.emit(event_types.MESSAGE_RECEIVED, this.messageId, this.type);
3442 await eventSource.emit(event_types.CHARACTER_MESSAGE_RENDERED, this.messageId, this.type);3605 await eventSource.emit(event_types.CHARACTER_MESSAGE_RENDERED, this.messageId, this.type);
@@ -3444,15 +3607,13 @@ class StreamingProcessor {
3444 await eventSource.emit(event_types.IMPERSONATE_READY, text);3607 await eventSource.emit(event_types.IMPERSONATE_READY, text);
3445 }3608 }
34463609
3447 syncMesToSwipe(messageId);3610 updateSwipeCounter(messageId, { message, messageElement });
3448 saveLogprobsForActiveMessage(this.messageLogprobs.filter(Boolean), this.continueMessage);
3449 await saveChatConditional();
3450 unblockGeneration();
34513611
3452 const isAborted = this.abortController.signal.aborted;3612 const isAborted = this.abortController.signal.aborted;
3453 if (!isAborted && power_user.auto_swipe && generatedTextFiltered(text)) {3613 if (!isAborted && power_user.auto_swipe && generatedTextFiltered(text)) {
3454 return swipe_right();3614 return await swipe(null, SWIPE_DIRECTION.RIGHT, { source: SWIPE_SOURCE.AUTO_SWIPE, repeated: true, forceMesId: chat.length - 1 });
3455 }3615 }
3616 await saveChatConditional();
34563617
3457 playMessageSound();3618 playMessageSound();
3458 }3619 }
@@ -3462,7 +3623,6 @@ class StreamingProcessor {
3462 this.isStopped = true;3623 this.isStopped = true;
34633624
3464 this.markUIGenStopped();3625 this.markUIGenStopped();
3465 unblockGeneration();
34663626
3467 const noEmitTypes = ['swipe', 'impersonate', 'continue'];3627 const noEmitTypes = ['swipe', 'impersonate', 'continue'];
3468 if (!noEmitTypes.includes(this.type)) {3628 if (!noEmitTypes.includes(this.type)) {
@@ -3532,6 +3692,7 @@ class StreamingProcessor {
3532 // Get the updated reasoning string into the handler3692 // Get the updated reasoning string into the handler
3533 this.reasoningHandler.updateReasoning(this.messageId, state?.reasoning);3693 this.reasoningHandler.updateReasoning(this.messageId, state?.reasoning);
3534 this.images = state?.images ?? [];3694 this.images = state?.images ?? [];
3695 this.reasoningSignature = state?.signature ?? null;
3535 await eventSource.emit(event_types.STREAM_TOKEN_RECEIVED, text);3696 await eventSource.emit(event_types.STREAM_TOKEN_RECEIVED, text);
3536 await sw.tick(async () => await this.onProgressStreaming(this.messageId, this.continueMessage + text));3697 await sw.tick(async () => await this.onProgressStreaming(this.messageId, this.continueMessage + text));
3537 }3698 }
@@ -4252,7 +4413,7 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
4252 continue;4413 continue;
4253 }4414 }
42544415
4255 const formattedExample = baseChatReplace(exampleMessage, name1, name2);4416 const formattedExample = baseChatReplace(exampleMessage);
4256 const cleanedExample = parseMesExamples(formattedExample, isInstruct);4417 const cleanedExample = parseMesExamples(formattedExample, isInstruct);
42574418
4258 // Insert depending on before or after position4419 // Insert depending on before or after position
@@ -4296,9 +4457,9 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
4296 if (main_api !== 'openai') {4457 if (main_api !== 'openai') {
4297 if (power_user.sysprompt.enabled) {4458 if (power_user.sysprompt.enabled) {
4298 system = power_user.prefer_character_prompt && system4459 system = power_user.prefer_character_prompt && system
4299 ? substituteParams(system, name1, name2, (power_user.sysprompt.content ?? ''))4460 ? substituteParams(system, { original: power_user.sysprompt.content ?? '' })
4300 : baseChatReplace(power_user.sysprompt.content, name1, name2);4461 : baseChatReplace(power_user.sysprompt.content);
4301 system = isInstruct ? substituteParams(system, name1, name2, power_user.sysprompt.content) : system;4462 system = isInstruct ? substituteParams(system, { original: power_user.sysprompt.content ?? '' }) : system;
4302 } else {4463 } else {
4303 // Nullify if it's not enabled4464 // Nullify if it's not enabled
4304 system = '';4465 system = '';
@@ -4356,8 +4517,8 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
43564517
4357 if (main_api !== 'openai' && power_user.sysprompt.enabled) {4518 if (main_api !== 'openai' && power_user.sysprompt.enabled) {
4358 jailbreak = power_user.prefer_character_jailbreak && jailbreak4519 jailbreak = power_user.prefer_character_jailbreak && jailbreak
4359 ? substituteParams(jailbreak, name1, name2, (power_user.sysprompt.post_history ?? ''))4520 ? substituteParams(jailbreak, { original: power_user.sysprompt.post_history ?? '' })
4360 : baseChatReplace(power_user.sysprompt.post_history, name1, name2);4521 : baseChatReplace(power_user.sysprompt.post_history);
43614522
4362 // Only inject the jb if there is one4523 // Only inject the jb if there is one
4363 if (jailbreak) {4524 if (jailbreak) {
@@ -5094,6 +5255,7 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
5094 let title = extractTitleFromData(data);5255 let title = extractTitleFromData(data);
5095 let reasoning = extractReasoningFromData(data);5256 let reasoning = extractReasoningFromData(data);
5096 let imageUrls = extractImagesFromData(data);5257 let imageUrls = extractImagesFromData(data);
5258 const reasoningSignature = extractReasoningSignatureFromData(data);
5097 kobold_horde_model = title;5259 kobold_horde_model = title;
50985260
5099 const swipes = extractMultiSwipes(data, type);5261 const swipes = extractMultiSwipes(data, type);
@@ -5137,10 +5299,10 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
5137 else {5299 else {
5138 // Without streaming we'll be having a full message on continuation. Treat it as a last chunk.5300 // Without streaming we'll be having a full message on continuation. Treat it as a last chunk.
5139 if (originalType !== 'continue') {5301 if (originalType !== 'continue') {
5140 ({ type, getMessage } = await saveReply({ type, getMessage, title, swipes, reasoning, imageUrls }));5302 ({ type, getMessage } = await saveReply({ type, getMessage, title, swipes, reasoning, imageUrls, reasoningSignature }));
5141 }5303 }
5142 else {5304 else {
5143 ({ type, getMessage } = await saveReply({ type: 'appendFinal', getMessage, title, swipes, reasoning, imageUrls }));5305 ({ type, getMessage } = await saveReply({ type: 'appendFinal', getMessage, title, swipes, reasoning, imageUrls, reasoningSignature }));
5144 }5306 }
51455307
5146 // This relies on `saveReply` having been called to add the message to the chat, so it must be last.5308 // This relies on `saveReply` having been called to add the message to the chat, so it must be last.
@@ -5175,7 +5337,8 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
5175 const isAborted = abortController && abortController.signal.aborted;5337 const isAborted = abortController && abortController.signal.aborted;
5176 if (!isAborted && power_user.auto_swipe && generatedTextFiltered(getMessage)) {5338 if (!isAborted && power_user.auto_swipe && generatedTextFiltered(getMessage)) {
5177 is_send_press = false;5339 is_send_press = false;
5178 return swipe_right();5340 return await swipe(null, SWIPE_DIRECTION.RIGHT, { source: SWIPE_SOURCE.AUTO_SWIPE, repeated: true, forceMesId: chat.length - 1 });
5341
5179 }5342 }
51805343
5181 console.debug('/api/chats/save called by /Generate');5344 console.debug('/api/chats/save called by /Generate');
@@ -5307,7 +5470,6 @@ function unblockGeneration(type) {
53075470
5308 is_send_press = false;5471 is_send_press = false;
5309 activateSendButtons();5472 activateSendButtons();
5310 showSwipeButtons();
5311 setGenerationProgress(0);5473 setGenerationProgress(0);
5312 flushEphemeralStoppingStrings();5474 flushEphemeralStoppingStrings();
5313 flushWIInjections();5475 flushWIInjections();
@@ -5851,7 +6013,7 @@ export function extractMessageFromData(data, activeApi = null) {
5851 case 'koboldhorde':6013 case 'koboldhorde':
5852 return data.text;6014 return data.text;
5853 case 'textgenerationwebui':6015 case 'textgenerationwebui':
5854 return data.choices?.[0]?.text ?? data.choices?.[0]?.message?.content ?? data.content ?? data.response ?? '';6016 return data.choices?.[0]?.text ?? data.choices?.[0]?.message?.content ?? data.content ?? data.response ?? data[0]?.content ?? '';
5855 case 'novel':6017 case 'novel':
5856 return data.output;6018 return data.output;
5857 case 'openai':6019 case 'openai':
@@ -5908,6 +6070,7 @@ export function extractJsonFromData(data, { mainApi = null, chatCompletionSource
5908 case chat_completion_sources.COHERE:6070 case chat_completion_sources.COHERE:
5909 case chat_completion_sources.XAI:6071 case chat_completion_sources.XAI:
5910 case chat_completion_sources.ELECTRONHUB:6072 case chat_completion_sources.ELECTRONHUB:
6073 case chat_completion_sources.CHUTES:
5911 case chat_completion_sources.AZURE_OPENAI:6074 case chat_completion_sources.AZURE_OPENAI:
5912 case chat_completion_sources.ZAI:6075 case chat_completion_sources.ZAI:
5913 default:6076 default:
@@ -5937,6 +6100,22 @@ function extractMultiSwipes(data, type) {
5937 return swipes;6100 return swipes;
5938 }6101 }
59396102
6103 if (main_api === 'textgenerationwebui' && textgen_settings.type === textgen_types.LLAMACPP) {
6104 if (!Array.isArray(data)) {
6105 return swipes;
6106 }
6107
6108 const multiSwipeCount = data.length - 1;
6109 if (multiSwipeCount <= 0) {
6110 return swipes;
6111 }
6112
6113 for (let i = 1; i < data.length; i++) {
6114 const text = data?.[i]?.content ?? '';
6115 swipes.push(text);
6116 }
6117 }
6118
5940 if (main_api === 'openai' || (main_api === 'textgenerationwebui' && [textgen_types.MANCER, textgen_types.VLLM, textgen_types.APHRODITE, textgen_types.TABBY, textgen_types.INFERMATICAI].includes(textgen_settings.type))) {6119 if (main_api === 'openai' || (main_api === 'textgenerationwebui' && [textgen_types.MANCER, textgen_types.VLLM, textgen_types.APHRODITE, textgen_types.TABBY, textgen_types.INFERMATICAI].includes(textgen_settings.type))) {
5941 if (!Array.isArray(data.choices)) {6120 if (!Array.isArray(data.choices)) {
5942 return swipes;6121 return swipes;
@@ -5950,18 +6129,18 @@ function extractMultiSwipes(data, type) {
59506129
5951 for (let i = 1; i < data.choices.length; i++) {6130 for (let i = 1; i < data.choices.length; i++) {
5952 const text = data?.choices[i]?.message?.content ?? data?.choices[i]?.text ?? '';6131 const text = data?.choices[i]?.message?.content ?? data?.choices[i]?.text ?? '';
5953 const cleanedText = cleanUpMessage({6132 swipes.push(text);
5954 getMessage: text,
5955 isImpersonate: false,
5956 isContinue: false,
5957 displayIncompleteSentences: false,
5958 });
5959
5960 swipes.push(cleanedText);
5961 }6133 }
5962 }6134 }
59636135
5964 return swipes;6136 const cleanedSwipes = swipes.map(text => cleanUpMessage({
6137 getMessage: text,
6138 isImpersonate: false,
6139 isContinue: false,
6140 displayIncompleteSentences: false,
6141 }));
6142
6143 return cleanedSwipes;
5965}6144}
59666145
5967/**6146/**
@@ -6172,16 +6351,17 @@ async function processImageAttachment(message, { imageUrls }) {
6172 * @property {string[]} [swipes] Extra swipes6351 * @property {string[]} [swipes] Extra swipes
6173 * @property {string} [reasoning] Message reasoning6352 * @property {string} [reasoning] Message reasoning
6174 * @property {string[]} [imageUrls] Links to images6353 * @property {string[]} [imageUrls] Links to images
6354 * @property {string?} [reasoningSignature] Encrypted signature of the reasoning text
6175 *6355 *
6176 * @typedef {object} SaveReplyResult6356 * @typedef {object} SaveReplyResult
6177 * @property {string} type Type of generation6357 * @property {string} type Type of generation
6178 * @property {string} getMessage Generated message6358 * @property {string} getMessage Generated message
6179 */6359 */
6180export async function saveReply({ type, getMessage, fromStreaming = false, title = '', swipes = [], reasoning = '', imageUrls = [] }) {6360export async function saveReply({ type, getMessage, fromStreaming = false, title = '', swipes = [], reasoning = '', imageUrls = [], reasoningSignature = null }) {
6181 // Backward compatibility6361 // Backward compatibility
6182 if (arguments.length > 1 && typeof arguments[0] !== 'object') {6362 if (arguments.length > 1 && typeof arguments[0] !== 'object') {
6183 console.trace('saveReply called with positional arguments. Please use an object instead.');6363 console.trace('saveReply called with positional arguments. Please use an object instead.');
6184 [type, getMessage, fromStreaming, title, swipes, reasoning, imageUrls] = arguments;6364 [type, getMessage, fromStreaming, title, swipes, reasoning, imageUrls, reasoningSignature] = arguments;
6185 }6365 }
61866366
6187 if (type != 'append' && type != 'continue' && type != 'appendFinal' && chat.length && (chat[chat.length - 1]['swipe_id'] === undefined ||6367 if (type != 'append' && type != 'continue' && type != 'appendFinal' && chat.length && (chat[chat.length - 1]['swipe_id'] === undefined ||
@@ -6217,6 +6397,7 @@ export async function saveReply({ type, getMessage, fromStreaming = false, title
6217 chat[chat.length - 1]['extra']['model'] = getGeneratingModel();6397 chat[chat.length - 1]['extra']['model'] = getGeneratingModel();
6218 chat[chat.length - 1]['extra']['reasoning'] = reasoning;6398 chat[chat.length - 1]['extra']['reasoning'] = reasoning;
6219 chat[chat.length - 1]['extra']['reasoning_duration'] = null;6399 chat[chat.length - 1]['extra']['reasoning_duration'] = null;
6400 chat[chat.length - 1]['extra']['reasoning_signature'] = reasoningSignature;
6220 await processImageAttachment(chat[chat.length - 1], { imageUrls });6401 await processImageAttachment(chat[chat.length - 1], { imageUrls });
6221 if (power_user.message_token_count_enabled) {6402 if (power_user.message_token_count_enabled) {
6222 const tokenCountText = (reasoning || '') + chat[chat.length - 1]['mes'];6403 const tokenCountText = (reasoning || '') + chat[chat.length - 1]['mes'];
@@ -6241,6 +6422,7 @@ export async function saveReply({ type, getMessage, fromStreaming = false, title
6241 chat[chat.length - 1]['extra']['model'] = getGeneratingModel();6422 chat[chat.length - 1]['extra']['model'] = getGeneratingModel();
6242 chat[chat.length - 1]['extra']['reasoning'] = reasoning;6423 chat[chat.length - 1]['extra']['reasoning'] = reasoning;
6243 chat[chat.length - 1]['extra']['reasoning_duration'] = null;6424 chat[chat.length - 1]['extra']['reasoning_duration'] = null;
6425 chat[chat.length - 1]['extra']['reasoning_signature'] = reasoningSignature;
6244 await processImageAttachment(chat[chat.length - 1], { imageUrls });6426 await processImageAttachment(chat[chat.length - 1], { imageUrls });
6245 if (power_user.message_token_count_enabled) {6427 if (power_user.message_token_count_enabled) {
6246 const tokenCountText = (reasoning || '') + chat[chat.length - 1]['mes'];6428 const tokenCountText = (reasoning || '') + chat[chat.length - 1]['mes'];
@@ -6261,6 +6443,7 @@ export async function saveReply({ type, getMessage, fromStreaming = false, title
6261 chat[chat.length - 1]['extra']['api'] = getGeneratingApi();6443 chat[chat.length - 1]['extra']['api'] = getGeneratingApi();
6262 chat[chat.length - 1]['extra']['model'] = getGeneratingModel();6444 chat[chat.length - 1]['extra']['model'] = getGeneratingModel();
6263 chat[chat.length - 1]['extra']['reasoning'] += reasoning;6445 chat[chat.length - 1]['extra']['reasoning'] += reasoning;
6446 chat[chat.length - 1]['extra']['reasoning_signature'] = reasoningSignature;
6264 await processImageAttachment(chat[chat.length - 1], { imageUrls });6447 await processImageAttachment(chat[chat.length - 1], { imageUrls });
6265 // We don't know if the reasoning duration extended, so we don't update it here on purpose.6448 // We don't know if the reasoning duration extended, so we don't update it here on purpose.
6266 if (power_user.message_token_count_enabled) {6449 if (power_user.message_token_count_enabled) {
@@ -6283,6 +6466,7 @@ export async function saveReply({ type, getMessage, fromStreaming = false, title
6283 chat[chat.length - 1]['extra']['model'] = getGeneratingModel();6466 chat[chat.length - 1]['extra']['model'] = getGeneratingModel();
6284 chat[chat.length - 1]['extra']['reasoning'] = reasoning;6467 chat[chat.length - 1]['extra']['reasoning'] = reasoning;
6285 chat[chat.length - 1]['extra']['reasoning_duration'] = null;6468 chat[chat.length - 1]['extra']['reasoning_duration'] = null;
6469 chat[chat.length - 1]['extra']['reasoning_signature'] = reasoningSignature;
6286 if (power_user.trim_spaces) {6470 if (power_user.trim_spaces) {
6287 getMessage = getMessage.trim();6471 getMessage = getMessage.trim();
6288 }6472 }
@@ -6362,6 +6546,63 @@ export async function saveReply({ type, getMessage, fromStreaming = false, title
6362}6546}
63636547
6364/**6548/**
6549 * Creates a message's `swipes`, `swipe_id` and `swipe_info` if necessary.
6550 * @param {ChatMessage} message
6551 * @returns {boolean} true if the message was updated.
6552 */
6553export function ensureSwipes(message) {
6554 let updated = false;
6555
6556 if (!message || typeof message !== 'object') {
6557 console.trace(`[ensureSwipes] failed. '${message}' is not an object.`);
6558 return updated;
6559 }
6560
6561 //Small system messages and user messages should not have swipes.
6562 if (message?.is_user || message?.extra?.isSmallSys) {
6563 return updated;
6564 }
6565
6566 if (!Array.isArray(message.swipes)) {
6567 message.swipes = [message.mes ?? ''];
6568 updated = true;
6569 }
6570
6571 if (typeof message.swipe_id !== 'number') {
6572 message.swipe_id = 0;
6573 updated = true;
6574 }
6575
6576 /** @type {() => SwipeInfo} */
6577 const createSwipeInfo = () => ({
6578 send_date: message.send_date,
6579 gen_started: message.gen_started,
6580 gen_finished: message.gen_finished,
6581 extra: {},
6582 });
6583
6584 if (!Array.isArray(message.swipe_info)) {
6585 message.swipe_info = message.swipes.map(_ => createSwipeInfo());
6586 updated = true;
6587 }
6588
6589 for (let i = 0; i < message.swipes.length; i++) {
6590 if (typeof message.swipes[i] !== 'string') {
6591 updated = true;
6592 console.warn('The message had a swipe that is not a string. It has has been set to \'\'.', message);
6593 message.swipes[i] = '';
6594 }
6595 if (!message.swipe_info[i] || typeof message.swipe_info[i] !== 'object') {
6596 updated = true;
6597 console.warn('The message had missing or invalid swipe_info for a swipe. It has been backfilled.', message);
6598 message.swipe_info[i] = createSwipeInfo();
6599 }
6600 }
6601
6602 return updated;
6603}
6604
6605/**
6365 * Syncs the current message and all its data into the swipe data at the given message ID (or the last message if no ID is given).6606 * Syncs the current message and all its data into the swipe data at the given message ID (or the last message if no ID is given).
6366 *6607 *
6367 * If the swipe data is invalid in some way, this function will exit out without doing anything.6608 * If the swipe data is invalid in some way, this function will exit out without doing anything.
@@ -6515,7 +6756,7 @@ export function getGeneratingApi() {
6515 }6756 }
6516}6757}
65176758
6518function getGeneratingModel(mes) {6759export function getGeneratingModel(mes) {
6519 let model = '';6760 let model = '';
6520 switch (main_api) {6761 switch (main_api) {
6521 case 'kobold':6762 case 'kobold':
@@ -6543,6 +6784,7 @@ function getGeneratingModel(mes) {
6543export function activateSendButtons() {6784export function activateSendButtons() {
6544 is_send_press = false;6785 is_send_press = false;
6545 hideStopButton();6786 hideStopButton();
6787 showSwipeButtons();
6546 delete document.body.dataset.generating;6788 delete document.body.dataset.generating;
6547}6789}
65486790
@@ -6551,6 +6793,7 @@ export function activateSendButtons() {
6551 */6793 */
6552export function deactivateSendButtons() {6794export function deactivateSendButtons() {
6553 showStopButton();6795 showStopButton();
6796 hideSwipeButtons();
6554 document.body.dataset.generating = 'true';6797 document.body.dataset.generating = 'true';
6555}6798}
65566799
@@ -6731,7 +6974,7 @@ export async function renameCharacter(name = null, { silent = false, renameChats
6731 }6974 }
67326975
6733 // Also rename as a group member6976 // Also rename as a group member
6734 await renameGroupMember(oldAvatar, newAvatar, newValue);6977 await renameGroupMember(oldAvatar, newAvatar, newValue.toString());
6735 const renamePastChatsConfirm = renameChats !== null6978 const renamePastChatsConfirm = renameChats !== null
6736 ? renameChats6979 ? renameChats
6737 : silent6980 : silent
@@ -6860,6 +7103,11 @@ export function saveChatDebounced() {
6860 * @returns {Promise<void>}7103 * @returns {Promise<void>}
6861 */7104 */
6862export async function saveChat({ chatName, withMetadata, mesId, force = false } = {}) {7105export async function saveChat({ chatName, withMetadata, mesId, force = false } = {}) {
7106 if (selected_group) {
7107 toastr.error(t`Operation was aborted to prevent data corruption.`, t`saveChat called for a group chat`);
7108 throw new Error('saveChat called for a group chat');
7109 }
7110
6863 if (arguments.length > 0 && typeof arguments[0] !== 'object') {7111 if (arguments.length > 0 && typeof arguments[0] !== 'object') {
6864 console.trace('saveChat called with positional arguments. Please use an object instead.');7112 console.trace('saveChat called with positional arguments. Please use an object instead.');
6865 [chatName, withMetadata, mesId, force] = arguments;7113 [chatName, withMetadata, mesId, force] = arguments;
@@ -6879,26 +7127,17 @@ export async function saveChat({ chatName, withMetadata, mesId, force = false }
6879 }7127 }
68807128
6881 characters[this_chid]['date_last_chat'] = Date.now();7129 characters[this_chid]['date_last_chat'] = Date.now();
6882 chat.forEach(function (item, i) {
6883 if (item['is_group']) {
6884 toastr.error(t`Trying to save group chat with regular saveChat function. Aborting to prevent corruption.`);
6885 throw new Error('Group chat saved from saveChat');
6886 }
6887 });
68887130
6889 const trimmedChat = (mesId !== undefined && mesId >= 0 && mesId < chat.length)7131 const trimmedChat = (mesId !== undefined && mesId >= 0 && mesId < chat.length)
6890 ? chat.slice(0, Number(mesId) + 1)7132 ? chat.slice(0, Number(mesId) + 1)
6891 : chat.slice();7133 : chat.slice();
68927134
6893 const chatToSave = [7135 /** @type {ChatHeader} */
6894 {7136 const chatHeader = {
6895 user_name: name1,7137 chat_metadata: metadata,
6896 character_name: name2,7138 user_name: 'unused',
6897 create_date: chat_create_date,7139 character_name: 'unused',
6898 chat_metadata: metadata,7140 };
6899 },
6900 ...trimmedChat,
6901 ];
69027141
6903 try {7142 try {
6904 const result = await fetch('/api/chats/save', {7143 const result = await fetch('/api/chats/save', {
@@ -6908,7 +7147,7 @@ export async function saveChat({ chatName, withMetadata, mesId, force = false }
6908 body: JSON.stringify({7147 body: JSON.stringify({
6909 ch_name: characters[this_chid].name,7148 ch_name: characters[this_chid].name,
6910 file_name: fileName,7149 file_name: fileName,
6911 chat: chatToSave,7150 chat: [chatHeader, ...trimmedChat],
6912 avatar_url: characters[this_chid].avatar,7151 avatar_url: characters[this_chid].avatar,
6913 force: force,7152 force: force,
6914 }),7153 }),
@@ -7082,7 +7321,7 @@ export async function unshallowCharacter(characterId) {
7082 return;7321 return;
7083 }7322 }
70847323
7085 /** @type {import('./scripts/char-data.js').v1CharData} */7324 /** @type {Character} */
7086 const character = characters[characterId];7325 const character = characters[characterId];
7087 if (!character) {7326 if (!character) {
7088 console.debug('Character not found:', characterId);7327 console.debug('Character not found:', characterId);
@@ -7121,13 +7360,10 @@ export async function getChat() {
7121 });7360 });
7122 if (response[0] !== undefined) {7361 if (response[0] !== undefined) {
7123 chat.splice(0, chat.length, ...response);7362 chat.splice(0, chat.length, ...response);
7124 chat_create_date = chat[0]['create_date'];
7125 chat_metadata = chat[0]['chat_metadata'] ?? {};7363 chat_metadata = chat[0]['chat_metadata'] ?? {};
71267364
7127 chat.shift();7365 chat.shift();
7128 chat.forEach(ensureMessageMediaIsArray);7366 chat.forEach(ensureMessageMediaIsArray);
7129 } else {
7130 chat_create_date = humanizedDateTime();
7131 }7367 }
7132 if (!chat_metadata['integrity']) {7368 if (!chat_metadata['integrity']) {
7133 chat_metadata['integrity'] = uuidv4();7369 chat_metadata['integrity'] = uuidv4();
@@ -7221,8 +7457,8 @@ export async function openCharacterChat(file_name) {
72217457
7222////////// OPTIMZED MAIN API CHANGE FUNCTION ////////////7458////////// OPTIMZED MAIN API CHANGE FUNCTION ////////////
72237459
7224export function changeMainAPI() {7460export function changeMainAPI(api = null) {
7225 const selectedVal = $('#main_api').val();7461 const selectedVal = api ?? $('#main_api').val();
7226 //console.log(selectedVal);7462 //console.log(selectedVal);
7227 const apiElements = {7463 const apiElements = {
7228 'koboldhorde': {7464 'koboldhorde': {
@@ -7596,8 +7832,11 @@ function updateMessage(div) {
7596 const mesElement = div.closest('.mes');7832 const mesElement = div.closest('.mes');
7597 const mes = chat[mesElement.attr('mesid')];7833 const mes = chat[mesElement.attr('mesid')];
75987834
7835 // editing old messages
7836 mes['extra'] ??= {};
7837
7599 let regexPlacement;7838 let regexPlacement;
7600 if (mes.is_user) {7839 if (mes?.is_user) {
7601 regexPlacement = regex_placement.USER_INPUT;7840 regexPlacement = regex_placement.USER_INPUT;
7602 } else if (mes.extra?.type === 'narrator') {7841 } else if (mes.extra?.type === 'narrator') {
7603 regexPlacement = regex_placement.SLASH_COMMAND;7842 regexPlacement = regex_placement.SLASH_COMMAND;
@@ -7627,15 +7866,11 @@ function updateMessage(div) {
7627 }7866 }
7628 mes['mes'] = text;7867 mes['mes'] = text;
7629 if (mes['swipe_id'] !== undefined) {7868 if (mes['swipe_id'] !== undefined) {
7869 ensureSwipes(mes);
7630 mes['swipes'][mes['swipe_id']] = text;7870 mes['swipes'][mes['swipe_id']] = text;
7631 }7871 }
76327872
7633 // editing old messages7873 if (mes?.is_system || mes?.is_user || mes.extra?.type === system_message_types.NARRATOR) {
7634 if (!mes.extra) {
7635 mes.extra = {};
7636 }
7637
7638 if (mes.is_system || mes.is_user || mes.extra.type === system_message_types.NARRATOR) {
7639 mes.extra.bias = bias ?? null;7874 mes.extra.bias = bias ?? null;
7640 } else {7875 } else {
7641 mes.extra.bias = null;7876 mes.extra.bias = null;
@@ -7706,8 +7941,7 @@ export async function messageEdit(editMessageId) {
7706 this_edit_mes_id = editMessageId;7941 this_edit_mes_id = editMessageId;
7707 this_edit_mes_chname = editMessage.name || (editMessage.is_user ? name1 : name2);7942 this_edit_mes_chname = editMessage.name || (editMessage.is_user ? name1 : name2);
77087943
7709 const hideCounters = editMessageId < chat.length - 1;7944 refreshSwipeButtons();
7710 hideSwipeButtons({ hideCounters });
77117945
7712 const chatScrollPosition = chatElement.scrollTop();7946 const chatScrollPosition = chatElement.scrollTop();
7713 const messageBlock = messageElement.find('.mes_block');7947 const messageBlock = messageElement.find('.mes_block');
@@ -7762,7 +7996,7 @@ async function messageEditCancel(messageId = this_edit_mes_id) {
7762 if (this?.classList?.contains('mes_edit_cancel')) {7996 if (this?.classList?.contains('mes_edit_cancel')) {
7763 thisMesDiv = $(this).closest('.mes');7997 thisMesDiv = $(this).closest('.mes');
7764 } else {7998 } else {
7765 thisMesDiv = chatElement.children().filter(`[mesid="${messageId}"]`);7999 thisMesDiv = chatElement.children('.mes').filter(`[mesid="${messageId}"]`);
7766 }8000 }
77678001
7768 const thisMesBlock = thisMesDiv.find('.mes_block');8002 const thisMesBlock = thisMesDiv.find('.mes_block');
@@ -7843,11 +8077,17 @@ async function messageEditMove(sourceId, targetId) {
7843 }8077 }
78448078
7845 updateViewMessageIds();8079 updateViewMessageIds();
8080 refreshSwipeButtons();
7846 await saveChatConditional();8081 await saveChatConditional();
7847 return true;8082 return true;
7848}8083}
78498084
7850async function messageEditDone(div) {8085async function messageEditDone(div) {
8086 if (!(this_edit_mes_id >= 0)) {
8087 console.trace('this_edit_mes_id cannot be blank when calling messageEditDone.');
8088 return;
8089 }
8090
7851 let { mesBlock, text, mes, bias } = updateMessage(div);8091 let { mesBlock, text, mes, bias } = updateMessage(div);
7852 if (this_edit_mes_id == 0) {8092 if (this_edit_mes_id == 0) {
7853 text = substituteParams(text);8093 text = substituteParams(text);
@@ -8016,7 +8256,7 @@ export async function displayPastChats(hightlightNames = []) {
8016 });8256 });
80178257
8018 // Define the search input listener8258 // Define the search input listener
8019 $('#select_chat_search').on('input', function () {8259 $('#select_chat_search').off('input').on('input', function () {
8020 const searchQuery = $(this).val();8260 const searchQuery = $(this).val();
8021 debouncedDisplay(searchQuery);8261 debouncedDisplay(searchQuery);
8022 });8262 });
@@ -8026,6 +8266,8 @@ export async function displayPastChats(hightlightNames = []) {
8026 const textSearchElement = $('#select_chat_search');8266 const textSearchElement = $('#select_chat_search');
8027 textSearchElement.trigger('click').trigger('focus').trigger('select');8267 textSearchElement.trigger('click').trigger('focus').trigger('select');
8028 }, 200);8268 }, 200);
8269
8270 addChatBackupsBrowser();
8029}8271}
80308272
8031async function displayChats(searchQuery, currentChat, displayName, avatarImg, selected_group, highlightNames) {8273async function displayChats(searchQuery, currentChat, displayName, avatarImg, selected_group, highlightNames) {
@@ -8258,7 +8500,7 @@ export function select_selected_character(chid, { switchMenu = true } = {}) {
8258 $('#talkativeness_slider').val(characters[chid].talkativeness || talkativeness_default);8500 $('#talkativeness_slider').val(characters[chid].talkativeness || talkativeness_default);
8259 $('#mes_example_textarea').val(characters[chid].mes_example);8501 $('#mes_example_textarea').val(characters[chid].mes_example);
8260 $('#selected_chat_pole').val(characters[chid].chat);8502 $('#selected_chat_pole').val(characters[chid].chat);
8261 $('#create_date_pole').val(characters[chid].create_date);8503 $('#create_date_pole').val(timestampToMoment(characters[chid].create_date).toISOString());
8262 $('#avatar_url_pole').val(characters[chid].avatar);8504 $('#avatar_url_pole').val(characters[chid].avatar);
8263 $('#chat_import_avatar_url').val(characters[chid].avatar);8505 $('#chat_import_avatar_url').val(characters[chid].avatar);
8264 $('#chat_import_character_name').val(characters[chid].name);8506 $('#chat_import_character_name').val(characters[chid].name);
@@ -8572,99 +8814,191 @@ export function callPopup(text, type, inputValue = '', { okButton, rows, wide, w
85728814
8573/**8815/**
8574 * Update the swipe counter for mesId.8816 * Update the swipe counter for mesId.
8817 * By default, the swipe counter's opacity will appear greyed out. The opacity is changed with CSS.
8575 * @param {Number} mesId8818 * @param {Number} mesId
8819 * @param {object} [options] Options
8820 * @param {ChatMessage} [options.message=undefined] Swipe numbers from this message will be used instead of mesId.
8821 * @param {JQuery<HTMLElement>} [options.messageElement=undefined] Target Element. Passing in the message's element will save a DOM query.
8576 */8822 */
8577export async function updateSwipeCounter(mesId) {8823export async function updateSwipeCounter(mesId, { message = undefined, messageElement = undefined } = {}) {
8578 const swipeCounterText = formatSwipeCounter((chat[mesId]?.['swipe_id'] + 1), chat[mesId]?.['swipes']?.length);8824 message ??= chat[mesId];
8579 const currentMessage = chatElement.children().filter(`[mesid="${mesId}"]`);8825 messageElement ??= chatElement.children('.mes').filter(`[mesid="${mesId}"]`);
8580 const swipeCounter = currentMessage.find('.swipes-counter');8826
8581 swipeCounter.text(swipeCounterText).show();8827 //If the message does not have swipes, create them.
8828 if (ensureSwipes(message)) {
8829 syncMesToSwipe(mesId);
8830 }
8831
8832 const swipeCounterText = formatSwipeCounter((message?.swipe_id + 1), message?.swipes?.length);
8833 const swipeCounter = messageElement.find('.swipes-counter');
8834 swipeCounter.text(swipeCounterText).prop('hidden', false);
8582}8835}
85838836
8584/**8837/**
8585 * Swipe buttons are often toggled to update their position.8838 * Returns true if messages are generally swipeable.
8586 * This should be replaced with a more efficient function.8839 * @returns {boolean}
8587 */8840 */
8588export function refreshSwipeButtons() {8841export function isSwipingAllowed() {
8589 hideSwipeButtons();8842 return (
8590 showSwipeButtons();8843 //Swipe cannot be called on an empty chat.
8844 chat.length !== 0 &&
8845 //The swipes setting must be enabled, and swipes can't be hidden.
8846 swipes && !swipesHidden &&
8847 //Cannot swipe while generating.
8848 !isGenerating() &&
8849 //If mid-swipe, the message cannot be swiped.
8850 swipeState === SWIPE_STATE.NONE
8851 );
8591}8852}
85928853
8593export function showSwipeButtons(mesId = chat.length - 1) {8854/**
8594 isSwipingAllowed = true;8855 * Returns true if the message is swipeable.
8856 * This does not check if messages are generally swipeable. See isSwipingAllowed().
8857 * This does not check if the swipes exist or are valid.
8858 * @param {number} messageId The message Id to check.
8859 * @param {ChatMessage} [message=undefined] If undefined, then the message checks will be skipped.
8860 * @returns {boolean}
8861 */
8862export function isMessageSwipeable(messageId, message = undefined) {
8863 message ??= chat[messageId];
85958864
8596 if (chat.length === 0) {8865 //If the message does not have swipes, create them.
8597 return;8866 if (ensureSwipes(message)) {
8867 syncMesToSwipe(messageId);
8598 }8868 }
85998869
8600 if (8870 if (
8601 chat[mesId].is_system ||8871 //Only messages below the currently edited message can be swiped, if it's not mid-swipe edit.
8602 !swipes ||8872 ((messageId > (this_edit_mes_id ?? -1)) && (swipeState != SWIPE_STATE.EDITING)) &&
8603 Number($('.mes:last').attr('mesid')) < 0 ||8873
8604 chat[mesId].is_user ||8874 //If the message is the last message, and it exists.
8605 (selected_group && is_group_generating)8875 (messageId == chat.length - 1) &&
8606 ) {8876 (message &&
8607 return;8877 //Small system messages cannot be swiped.
8608 }8878 !(message?.extra?.isSmallSys) &&
8879 //Some messages, like the welcome screen, are not swipeable.
8880 !(message?.extra?.swipeable === false) &&
8881 //User messages are not swipeable.
8882 !message.is_user
8883 )
8884 )
8885 //The message is swipeable.
8886 { return true; }
8887 //The message is not swipeable.
8888 else { return false; }
8889}
8890
8891/**
8892 * Returns the message's behavior when swiped past it's last branch.
8893 * This does not check if the message can currently be swiped. See isMessageSwipeable().
8894 * This does not check if messages are generally swipeable. See isSwipingAllowed().
8895 * This does not check if the swipes exist or are valid.
8896 * @param {number} messageId The message Id to check.
8897 * @param {ChatMessage} [message=undefined] If defined, this will be used instead of chat[messageId].
8898 * @returns {OVERSWIPE_BEHAVIOR}
8899 */
8900export function getOverswipeBehavior(messageId, message = undefined) {
8901 message ??= chat[messageId];
8902
8903 const isPristine = !chat_metadata?.tainted;
8904 const isGreeting = messageId === 0;
8905
8906 //Do not override explicitly set overswipe_behavior.
8907 if (typeof message?.extra?.overswipe_behavior == 'string') return message.extra.overswipe_behavior;
8908 //Some messages, like the welcome screen, are not swipeable.
8909 else if (message?.extra?.swipeable === false) return OVERSWIPE_BEHAVIOR.NONE;
8910 //Small System messages can't be swiped.
8911 else if (message?.extra?.isSmallSys) return OVERSWIPE_BEHAVIOR.NONE;
8912 //The first message in a priistine chat will loop. It's chevrons will always be visible https://github.com/SillyTavern/SillyTavern/pull/4712#issuecomment-3557893373
8913 else if (isGreeting && isPristine) return OVERSWIPE_BEHAVIOR.PRISTINE_GREETING;
8914 //Non-user and non-prompt hidden messages will regenerate.
8915 else if (!message?.is_user && !message?.is_system) return OVERSWIPE_BEHAVIOR.REGENERATE;
8916 //By default, all other messages will loop. Their swipe chevrons will only be shown if there is more than one swipe.
8917 else { return OVERSWIPE_BEHAVIOR.LOOP; }
8918}
8919
8920/**
8921 * Refreshes all swipe buttons and updates their swipe counters.
8922 * This has been optimized for bulk updates by minimizing DOM queries.
8923 * @param {boolean} updateCounters When true, the swipe counters will also be updated. Typically redundant because addOneMessage updates the counters.
8924 * @param {boolean} fade By default, the chevrons fade in and out.
8925 * @returns
8926 */
8927export function refreshSwipeButtons(updateCounters = false, fade = true) {
8928 //Never show swipe buttons on an empty chat.
8929 if (chat?.length === 0) return false;
86098930
8610 // swipe_id should be set if alternate greetings are added8931 //If swipes are disabled or hidden, hide all swipe buttons.
8611 if (chat.length == 1 && chat[0].swipe_id === undefined) {8932 if (!isSwipingAllowed()) {
8933 $('body').addClass('hideAllSwipeButtons');
8612 return;8934 return;
8935 //Don't hide all swipe buttons.
8936 } else {
8937 //CSS will hide all messages.
8938 $('body').removeClass('hideAllSwipeButtons');
8613 }8939 }
8940 //Non-messages can appear in chat. '.mes' is required.
8941 const messageElements = chatElement.children('.mes[mesid]');
86148942
8615 //had to add this to make the swipe counter work8943 const firstDisplayedMesId = Number(messageElements.first().attr('mesid'));
8616 //(copied from the onclick functions for swipe buttons..
8617 //don't know why the array isn't set for non-swipe messages in Generate or addOneMessage..)
8618 if (chat[mesId]['swipe_id'] === undefined) { // if there is no swipe-message in the last spot of the chat array
8619 chat[mesId]['swipe_id'] = 0; // set it to id 0
8620 chat[mesId]['swipes'] = []; // empty the array
8621 chat[mesId]['swipes'][0] = chat[mesId]['mes']; //assign swipe array with last message from chat
8622 chat[mesId]['swipe_info'] = [];
8623 chat[mesId]['swipe_info'][0] = {
8624 'send_date': chat[mesId]['send_date'],
8625 'gen_started': chat[mesId]['gen_started'],
8626 'gen_finished': chat[mesId]['gen_finished'],
8627 'extra': structuredClone(chat[mesId]['extra']),
8628 };
8629 }
86308944
8631 const currentMessage = chatElement.children().filter(`[mesid="${mesId}"]`);8945 //Group each message.
8632 const swipeId = chat[mesId].swipe_id;8946 messageElements.each((index, div) => {
8633 const swipeCounterText = formatSwipeCounter((swipeId + 1), chat[mesId]['swipes'].length);8947 //This assumes the messages are in order and their Id's are accurate.
8634 const swipeRight = currentMessage.find('.swipe_right');8948 const messageId = firstDisplayedMesId + index;
8635 const swipeLeft = currentMessage.find('.swipe_left');8949 //Number($(div).attr('mesid')); Would not misscount due to a missing div, but is much slower.
8636 const swipeCounter = currentMessage.find('.swipes-counter');
86378950
8638 if (swipeId !== undefined && (chat[mesId].swipes.length > 1 || swipeId > 0)) {8951 const message = chat[messageId];
8639 swipeLeft.css('display', 'flex');8952
8640 }8953 //Chevrons should not fade-in during printMessages. //https://github.com/SillyTavern/SillyTavern/pull/4712#issuecomment-3539315919
8641 //only show right when generate is off, or when next right swipe would not make a generate happen8954 div.classList.toggle('fade', fade);
8642 if (is_send_press === false || chat[mesId].swipes.length >= swipeId) {8955
8643 swipeRight.css('display', 'flex').css('opacity', '0.3');8956 if (isMessageSwipeable(messageId, message)) {
8644 swipeCounter.css('opacity', '0.3');8957 //If a right swipe would trigger a generation or loop to the first swipe.
8645 }8958 const isLastSwipe = (message?.swipes?.length ?? 1) - 1 <= (message?.swipe_id ?? 0);
8646 if ((chat[mesId].swipes.length - swipeId) === 1) {8959 const hasSwipes = (message?.swipes?.length > 1);
8647 //chevron was moved out of hardcode in HTML to class toggle dependent on last_mes or not8960 const overswipe = getOverswipeBehavior(messageId, message);
8648 //necessary for 'swipe_right' div in past messages to have no chevron if 'show swipes for all messages' is turned on8961
8649 swipeRight.css('opacity', '0.7');8962 // Chevrons should always be shown on pristine greetings: https://github.com/SillyTavern/SillyTavern/pull/4712#issuecomment-3557893373
8650 swipeCounter.css('opacity', '0.7');8963 const pristineGreeting = overswipe == OVERSWIPE_BEHAVIOR.PRISTINE_GREETING;
8651 }8964
8965 //The swipe button will be shown if an overswipe would trigger REGENERATE or EDIT_GENERATE.
8966 const isOverswipeable = isLastSwipe &&
8967 overswipe == OVERSWIPE_BEHAVIOR.REGENERATE ||
8968 overswipe == OVERSWIPE_BEHAVIOR.EDIT_GENERATE;
86528969
8653 //allows for writing individual swipe counters for past messages8970 div.classList.toggle('last_swipe', isOverswipeable);
8654 const lastSwipeCounter = $('.last_mes .swipes-counter');8971
8655 lastSwipeCounter.text(swipeCounterText).show();8972 //If there's only one swipe, the left arrow should not be shown.
8973 div.classList.toggle('swipes_visible', hasSwipes || pristineGreeting);
8974
8975 //updateSwipeCounter does not need to be awaited, It can run a bit later.
8976 if (updateCounters) updateSwipeCounter(messageId, { message, messageElement: $(div) });
8977 } else {
8978 //Hide all messages that are not swipeable.
8979 div.classList.remove('swipes_visible', 'last_swipe');
8980 }
8981 });
8982}
8983/**
8984 * This function is misleadingly named. It allows generation then refreshes the swipe buttons and counters.
8985 */
8986export function showSwipeButtons() {
8987 swipesHidden = false;
8988 refreshSwipeButtons();
8656}8989}
86578990
8658/**8991/**
8992 * This function is misleadingly named. It blocks generation then refreshes the swipe buttons and counters.
8659 * @param {object} [options] Options8993 * @param {object} [options] Options
8660 * @param {boolean} [options.hideCounters=false] Also hide the swipes counter.8994 * @param {boolean} [options.hideCounters=false] Also hide the swipes counter.
8661 */8995 */
8662export function hideSwipeButtons({ hideCounters = false } = {}) {8996export function hideSwipeButtons({ hideCounters = false } = {}) {
8663 isSwipingAllowed = false;8997 swipesHidden = true;
8664 chatElement.find('.swipe_right').hide();8998 refreshSwipeButtons();
8665 chatElement.find('.swipe_left').hide();8999
8666 if (hideCounters === true) {9000 if (hideCounters === true) {
8667 chatElement.find('.last_mes .swipes-counter').hide();9001 chatElement.find('.last_mes .swipes-counter').prop('hidden', true);
8668 }9002 }
8669}9003}
86709004
@@ -8707,25 +9041,23 @@ export async function deleteSwipe(swipeId = null, messageId = chat.length - 1) {
87079041
8708 // Select the next swipe, or the one before if it was the last one9042 // Select the next swipe, or the one before if it was the last one
8709 const newSwipeId = Math.min(swipeId, message.swipes.length - 1);9043 const newSwipeId = Math.min(swipeId, message.swipes.length - 1);
8710 syncSwipeToMes(messageId, newSwipeId);
87119044
8712 chat_metadata['tainted'] = true;9045 chat_metadata['tainted'] = true;
87139046
9047 messageId = Number(messageId);
9048 swipeId = Number(swipeId);
8714 await eventSource.emit(event_types.MESSAGE_SWIPE_DELETED, { messageId, swipeId, newSwipeId });9049 await eventSource.emit(event_types.MESSAGE_SWIPE_DELETED, { messageId, swipeId, newSwipeId });
9050 let direction = (swipeId <= newSwipeId) ? SWIPE_DIRECTION.RIGHT : SWIPE_DIRECTION.LEFT;
9051 //Animate swipe and swap dispayed message.
9052 await swipe(null, direction, { source: SWIPE_SOURCE.DELETE, repeated: false, forceMesId: messageId, forceSwipeId: newSwipeId });
87159053
8716 await saveChatConditional();9054 await saveChatConditional();
8717 await reloadCurrentChat();
87189055
8719 return newSwipeId;9056 return newSwipeId;
8720}9057}
87219058
8722export async function saveMetadata() {9059export async function saveMetadata() {
8723 if (selected_group) {9060 return await saveChatConditional();
8724 await editGroup(selected_group, true, false);
8725 }
8726 else {
8727 await saveChatConditional();
8728 }
8729}9061}
87309062
8731export async function saveChatConditional() {9063export async function saveChatConditional() {
@@ -8765,7 +9097,7 @@ export async function saveChatConditional() {
8765 * @param {boolean} [options.refresh] Whether to refresh the group chat list after import9097 * @param {boolean} [options.refresh] Whether to refresh the group chat list after import
8766 * @returns {Promise<string[]>} List of imported file names.9098 * @returns {Promise<string[]>} List of imported file names.
8767 */9099 */
8768async function importCharacterChat(formData, { refresh = true } = {}) {9100export async function importCharacterChat(formData, { refresh = true } = {}) {
8769 const fetchResult = await fetch('/api/chats/import', {9101 const fetchResult = await fetch('/api/chats/import', {
8770 method: 'POST',9102 method: 'POST',
8771 body: formData,9103 body: formData,
@@ -8809,7 +9141,7 @@ export function updateEditArrowClasses() {
8809 return;9141 return;
8810 }9142 }
88119143
8812 const message = chatElement.find(`.mes[mesid="${this_edit_mes_id}"]`);9144 const message = chatElement.children('.mes').filter(`.mes[mesid="${this_edit_mes_id}"]`);
88139145
8814 const downButton = message.find('.mes_edit_down');9146 const downButton = message.find('.mes_edit_down');
8815 const upButton = message.find('.mes_edit_up');9147 const upButton = message.find('.mes_edit_up');
@@ -9244,6 +9576,23 @@ export async function createOrEditCharacter(e) {
9244}9576}
92459577
9246/**9578/**
9579 * Visually updates all chat messages including andd after index by removing them, then adding them.
9580 * @param {ChatMessage[]} chat All messages in chat before index will remain unchanged.
9581 * @param {Number} index The last unchanged messageId.
9582 */
9583export async function redisplayChat(chat, index) {
9584 //Remove messages after index.
9585 chatElement.children(`.mes[mesid="${index}"]`).nextAll('.mes').addBack().remove();
9586
9587 //Skip to index, then add extra messages.
9588 for (let i = index; i <= chat.length - 1; i++) {
9589 //addOneMessage will update last_mes.
9590 addOneMessage(chat[i], { scroll: false, showSwipes: false, forceId: i });
9591 }
9592 refreshSwipeButtons();
9593}
9594
9595/**
9247 * Formats a counter for a swipe view.9596 * Formats a counter for a swipe view.
9248 * @param {number} current The current number of items.9597 * @param {number} current The current number of items.
9249 * @param {number} total The total number of items.9598 * @param {number} total The total number of items.
@@ -9258,41 +9607,61 @@ function formatSwipeCounter(current, total) {
92589607
9259/**9608/**
9260 * Handles the swipe event.9609 * Handles the swipe event.
9261 * @param {JQuery.Event} _event Event.9610 * @param {SwipeEvent} event Event.
9262 * @param {'left'|'right'} direction The direction to swipe.9611 * @param {SWIPE_DIRECTION} direction The direction to swipe.
9263 * @param {object} params Additional parameters.9612 * @param {object} params Additional parameters.
9264 * @param {string} [params.source] The source of the swipe event.9613 * @param {import('./scripts/constants.js').SWIPE_SOURCE} [params.source] The source of the swipe event. null, 'keyboard', 'auto_swipe', 'back' or 'delete'.
9265 * @param {boolean} [params.repeated] Is the swipe event repeated.9614 * @param {boolean} [params.repeated] Is the swipe event repeated.
9266 * @param {object} [params.message=chat[chat.length - 1]] The chat message to swipe.9615 * @param {ChatMessage} [params.message=chat[chat.length - 1]] The chat message to swipe.
9616 * @param {number} [params.forceMesId] The message id to swipe.
9617 * @param {number} [params.forceSwipeId] The target swipe_id. When out of range, it will be looped or clamped.
9618 * @param {number} [params.forceDuration] Overwrites the default swipe duration.
9267 */9619 */
9268export async function swipe(_event, direction, { source, repeated, message = chat[chat.length - 1] } = {}) {9620export async function swipe(event, direction, { source, repeated, message = chat[chat.length - 1], forceMesId, forceSwipeId, forceDuration } = {}) {
9269 if (chat.length === 0) {9621 if (chat.length === 0) {
9270 console.warn('Swipe was called on an empty chat.');9622 console.warn('Swipe was called on an empty chat.');
9271 return;9623 return;
9272 }9624 }
92739625
9274 //Only allow one concurrent swipe.
9275 if (!isSwipingAllowed) {
9276 console.info('The swipe has been ignored because another is in progress.');
9277 return;
9278 }
9279 isSwipingAllowed = false;
9280
9281 let generation;
9282 let messageIndex;9626 let messageIndex;
92839627
9284 //Only set messageIndex if message exists because -1 is truthy.9628 //Only set messageIndex if message exists because -1 is truthy.
9285 if (message) {9629 if (message) {
9286 messageIndex = chat.indexOf(message);9630 messageIndex = chat.indexOf(message);
9287 if (messageIndex === -1) {9631 if (messageIndex === -1 && typeof (forceMesId) != 'number') {
9288 console.error(`The message must exist in chat. ${message};`);9632 console.error(`The message must exist in chat. ${message};`);
9289 return;9633 return;
9290 }9634 }
9291 }9635 }
92929636
9293 const mesId = Number($(this).closest('.mes').attr('mesid') ?? messageIndex ?? chat.length - 1);9637 const mesId = Number(forceMesId ?? event?.currentTarget?.closest('.mes')?.getAttribute('mesid') ?? messageIndex ?? chat.length - 1);
92949638
9295 const thisMesDiv = chatElement.children().filter(`.mes[mesid="${mesId}"]`);9639 if (source === SWIPE_SOURCE.DELETE || source === SWIPE_SOURCE.BACK || source === SWIPE_SOURCE.AUTO_SWIPE) {
9640 console.info(`The ${direction} swipe source on message #${mesId} is ${source}, Most checks have been bypassed. `);
9641 } else {
9642 //Only show an error if swipes are not hidden and a message is generating.
9643 if (isGenerating() && (swipes && !swipesHidden && (swipeState === SWIPE_STATE.NONE))) {
9644 toastr.warning(t`Cannot swipe while generating. Stop the request and try again.`, t`Swipe aborted`);
9645 return;
9646 }
9647 //Only allow one concurrent swipe.
9648 if (!isSwipingAllowed()) {
9649 console.info('The swipe has been ignored messages cannot currently be swiped.');
9650 return;
9651 }
9652 if (!isMessageSwipeable(mesId, message)) {
9653 console.info(`Message #${mesId} cannot be swiped. ${message}`);
9654 return;
9655 }
9656 }
9657
9658 // Cancel pending save to prevent accidental swipe_id overwrites.
9659 cancelDebouncedChatSave();
9660
9661 swipeState = SWIPE_STATE.SWIPING;
9662 let generation;
9663
9664 const thisMesDiv = chatElement.children('.mes').filter(`[mesid="${mesId}"]`);
9296 const thisMesText = thisMesDiv.find('.mes_block .mes_text');9665 const thisMesText = thisMesDiv.find('.mes_block .mes_text');
9297 const thisMesDivHeight = thisMesDiv[0]?.scrollHeight;9666 const thisMesDivHeight = thisMesDiv[0]?.scrollHeight;
9298 const thisMesTextHeight = thisMesText[0]?.scrollHeight;9667 const thisMesTextHeight = thisMesText[0]?.scrollHeight;
@@ -9301,43 +9670,108 @@ export async function swipe(_event, direction, { source, repeated, message = cha
9301 return;9670 return;
9302 }9671 }
9303 const originalSwipeId = Number(chat[mesId]?.['swipe_id'] ?? 0);9672 const originalSwipeId = Number(chat[mesId]?.['swipe_id'] ?? 0);
9304 let newSwipeId = Number(originalSwipeId);9673 let newSwipeId = Number(forceSwipeId ?? originalSwipeId);
93059674
9306 const isPristine = !chat_metadata?.tainted;9675 /**
9307 const swipeDuration = Math.round(animation_duration * 1.25);9676 * Calculates the next swipe duration with how many swipes have been repeated.
9308 const swipeRange = direction === SWIPE_DIRECTION.RIGHT ? -700 : 700;9677 * @param {number} animation_duration
9678 * @returns {number} The adjusted swipe duration.
9679 */
9680 function getSwipeDuration(animation_duration) {
9681 const now = performance.now();
9682 const resetTime = animation_duration * 2 + 300;
9683
9684 //Reset the counter if the last swipe was more than half a second ago.
9685 if (now - lastSwipeInfo.now >= resetTime || direction !== lastSwipeInfo.direction) recentSwipes = 0;
9686 recentSwipes++;
9687 lastSwipeInfo = { now, direction };
9688
9689 //At 4 swipes, animation_duration will be halved.
9690 const sigmoid = 1 / (1 + Math.exp(recentSwipes - 4));
9691
9692 return animation_duration * sigmoid;
9693 }
9694
9695 const swipeDuration = forceDuration ?? getSwipeDuration(animation_duration);
9696
9697 //The offscreen messages may be visible if the user resizes the viewport during a swipe.
9698 const thisMesDivWidth = thisMesDiv.width() + 30;
9699 let swipeRange = (direction === SWIPE_DIRECTION.RIGHT) ? -thisMesDivWidth : thisMesDivWidth;
93099700
9310 async function endSwipe() {9701 /**
9702 * Waits for the generation to end, reverts the swipe if swipe_id has not changed.
9703 * @param {boolean} revert Attept to revert the swipe without saving.
9704 */
9705 async function endSwipe(revert = false) {
9311 //Wait for the generation to end.9706 //Wait for the generation to end.
9312 try {9707 try {
9313 await generation;9708 //`mes_buttons` need to be hidden until the animation completes.
9709 if (generation) {
9710 document.body.dataset.swiping = 'true';
9711 await generation;
9712 }
9314 }9713 }
9315 catch (error) {9714 catch (error) {
9316 console.warn(`Swipe failed, Swiping back. ${error}`);9715 console.warn(`Swipe failed, Swiping back. ${error}`);
9317 }9716 }
9318 //Allow for another swipe.
9319 showSwipeButtons();
93209717
9321 //Clamp Id between swipes.9718 //Clamp Id between swipes.
9322 let clampedId = clamp(chat[mesId]['swipe_id'], 0, Math.max(0, chat[mesId]['swipes'].length - 1));9719 let clampedId = clamp(chat[mesId]['swipe_id'], 0, Math.max(0, chat[mesId]['swipes'].length - 1));
93239720
9324 //If the id is not within bounds, Swipe back.
9325 if (chat[mesId]['swipe_id'] !== clampedId) {
9326 chat[mesId]['swipe_id'] = clampedId;
9327 syncSwipeToMes(mesId);
9328 addOneMessage(chat[mesId], { type: 'swipe', forceId: mesId, scroll: false });
9329 }
9330
9331 await updateSwipeCounter(mesId);9721 await updateSwipeCounter(mesId);
9332 //Fallback.9722 //Fallback.
9333 if (mesId != chat.length - 1) {9723 if (mesId != chat.length - 1) {
9334 await updateSwipeCounter(chat.length - 1);9724 await updateSwipeCounter(chat.length - 1);
9335 }9725 }
9726
9727 // If swipe_id has not changed, give the user feedback.
9728 if (clampedId == originalSwipeId && source != SWIPE_SOURCE.DELETE) {
9729 try {
9730 //Shake 700/140=5px
9731 shakeElement(thisMesDiv, -swipeRange / 140, animation_duration, 'ease-in');
9732 //Flash red.
9733 const flashTime = Math.max(animation_duration * 2, 100);
9734 await Promise.race([thisMesDiv.find('.swipes-counter').animate({ color: 'red' }, flashTime).animate({ color: '' }).promise(), createTimeout(flashTime * 4, `The shake animation did not end within ${flashTime * 4}ms`)].filter(Boolean));
9735 } catch (error) {
9736 console.warn(error);
9737 }
9738 }
9739
9740 //If the id is not within bounds, Swipe back.
9741 if (chat[mesId]?.swipe_id !== clampedId || revert) {
9742 // Prevent recursion.
9743 if (source != SWIPE_SOURCE.BACK) {
9744 source = SWIPE_SOURCE.BACK;
9745 chat[mesId].swipe_id = clampedId;
9746
9747 //Update the chat.
9748 await loadFromSwipeId(mesId, chat[mesId].swipe_id);
9749 await redisplayChat(chat, mesId);
9750 }
9751 else {
9752 await Popup.show.confirm(
9753 t`ERROR: <code>syncSwipeToMes</code> has failed to revert the failed ${direction} swipe on message #${mesId}.`,
9754 t`<p>After you click OK, the chat will be reloaded to prevent data corruption.</p>`,
9755 { okButton: 'OK', cancelButton: false },
9756 );
9757 console.trace(`Error! Recursion detected when reverting failed ${direction} swipe on message #${mesId}. Something has broken.`);
9758 await reloadCurrentChat();
9759 }
9760 //Out of bounds swipes should not be saved.
9761 } else if (source != SWIPE_SOURCE.BACK) {
9762 //Save the chat if swipe_id has changed.
9763 saveChatDebounced();
9764 }
9765
9766 //Allow for another swipe.
9767 swipeState = SWIPE_STATE.NONE;
9768 delete document.body.dataset.swiping;
9769 showSwipeButtons();
9336 }9770 }
93379771
9338 async function standardSwipe() {9772 async function standardSwipe(newSwipeId) {
9339 //If swipe_id has changed, or the source is being deleted.9773 //If swipe_id has changed, or the source is being deleted.
9340 if (newSwipeId !== originalSwipeId || source == 'delete') {9774 if (newSwipeId !== originalSwipeId || source == SWIPE_SOURCE.DELETE || source == SWIPE_SOURCE.BACK) {
9341 //Update the chat.9775 //Update the chat.
9342 await loadFromSwipeId(mesId, newSwipeId);9776 await loadFromSwipeId(mesId, newSwipeId);
9343 //Transition to the new chat.9777 //Transition to the new chat.
@@ -9347,6 +9781,27 @@ export async function swipe(_event, direction, { source, repeated, message = cha
9347 }9781 }
93489782
9349 /**9783 /**
9784 * Removes a message's extra and gen times.
9785 * @param {ChatMessage} message
9786 */
9787 function clearMessageData(message) {
9788 if (message.extra && typeof message.extra === 'object') {
9789 delete message.extra.memory;
9790 delete message.extra.display_text;
9791 delete message.extra.media;
9792 delete message.extra.inline_image;
9793 delete message.extra.files;
9794 delete message.extra.fileLength;
9795 delete message.extra.generationType;
9796 delete message.extra.negative;
9797 delete message.extra.title;
9798 delete message.extra.append_title;
9799 }
9800 delete message.gen_started;
9801 delete message.gen_finished;
9802 }
9803
9804 /**
9350 * Sets the message to the newSwipeId and loads it.9805 * Sets the message to the newSwipeId and loads it.
9351 * @param {number} mesId9806 * @param {number} mesId
9352 * @param {number} newSwipeId9807 * @param {number} newSwipeId
@@ -9355,55 +9810,89 @@ export async function swipe(_event, direction, { source, repeated, message = cha
9355 //Update the swipe_id.9810 //Update the swipe_id.
9356 chat[mesId]['swipe_id'] = newSwipeId;9811 chat[mesId]['swipe_id'] = newSwipeId;
93579812
9358 if (chat[mesId].extra && typeof chat[mesId].extra === 'object') {9813 clearMessageData(chat[mesId]);
9359 delete chat[mesId].extra.memory;9814
9360 delete chat[mesId].extra.display_text;9815 //Load from swipes.
9361 delete chat[mesId].extra.media;9816 if (syncSwipeToMes(mesId, newSwipeId) == false) {
9362 delete chat[mesId].extra.inline_image;9817 let errorMessage = t`When swiping ${direction} on message ${mesId}, syncSwipeToMes has returned false. Attempting to swipe back!`;
9363 delete chat[mesId].extra.files;9818 toastr.error(errorMessage);
9364 delete chat[mesId].extra.fileLength;9819
9365 delete chat[mesId].extra.generationType;9820 chat[mesId].swipe_id = originalSwipeId;
9366 delete chat[mesId].extra.negative;9821 await endSwipe(true);
9367 delete chat[mesId].extra.title;9822 }
9368 delete chat[mesId].extra.append_title;9823 return true;
9369 }9824 }
9370 delete chat[mesId].gen_started;
9371 delete chat[mesId].gen_finished;
9372 //load from swipes.
9373 syncSwipeToMes(mesId, chat[mesId]['swipe_id']);
9374 }
9375
9376 // Helper function to convert transition to promise
9377 const transitionPromise = (element, properties) => {
9378 return new Promise((resolve) => {
9379 element.transition({
9380 ...properties,
9381 complete: resolve,
9382 });
9383 });
9384 };
93859825
9386 /**9826 /**
9387 * Animates a swipe for all messages >= mesId.9827 * Animates a swipe for all messages >= mesId.
9388 * @param {number} mesId9828 * @param {number} mesId
9389 * @param {number} x9829 * @param {object} params
9390 * @param {number} duration9830 * @param {string} [params.xStart='opx']
9831 * @param {string} [params.xEnd='0px']
9832 * @param {number} [params.duration=animation_duration]
9833 * @param {string} [params.classes=''] Additional CSS classes to target during the swipe.
9834 * @param {boolean} [params.freeze=true] When true, do not remove the class from the animation, leaving it stuck at xEnd.
9835 * @returns {Promise<boolean|Function>} endSlide unfreezes the messages from xEnd.
9391 */9836 */
9392 async function animateSwipeTransition(mesId, x, duration) {9837 async function animateSwipeTransition(mesId, { xStart = '0px', xEnd = '0px', duration = animation_duration, classes = '', freeze = false } = {}) {
9393 //Selects the swiped message.9838 // If the animation_duration is zero, the 'animationend' promise will never resolve.
9394 const swipedMessagesDiv = chatElement.children().filter((index, div) => {9839 //Skip the animation if it's faster than 50ms.
9395 const $div = $(div);9840 if (duration <= 50) return;
9396 return mesId === Number($div.attr('mesid'));
9397 });
9398 const swipedElementsDiv = swipedMessagesDiv.children('.mes_block, .mesAvatarWrapper');
93999841
9400 //Swipe.9842 //Select MAXIMUM_ANIMATED messages after mesId. Ideally, only visible messages would be animated.
9401 await transitionPromise(swipedElementsDiv, {9843 const MAXIMUM_ANIMATED = 100;
9402 x: x,9844
9403 duration: duration,9845 const messages = chatElement.children('.mes');
9404 easing: animation_easing,9846 const firstDisplayedMesId = Number(messages.first().attr('mesid'));
9405 queue: false,9847
9848 const swipedMessagesDiv = messages.filter((index, div) => {
9849 // const messageId = Number($(div).attr('mesid')); //Slower.
9850 //This assumes the messages are in order and their Id's are accurate.
9851 const divMessageId = firstDisplayedMesId + index;
9852
9853 return (divMessageId < mesId + MAXIMUM_ANIMATED && divMessageId >= mesId);
9406 });9854 });
9855 if (swipedMessagesDiv.length > 0) {
9856 let swipeClasses = '.mes_block, .mesAvatarWrapper';
9857 swipeClasses += classes;
9858
9859 //Select only the target classes.
9860 const swipedElementsDiv = swipedMessagesDiv.children(swipeClasses);
9861 if (swipedElementsDiv.length > 0) {
9862 //This is a global variable, only one swipe transition can occur concurrently.
9863 document.documentElement.style.setProperty('--slide-mes-x-start', xStart);
9864 document.documentElement.style.setProperty('--slide-mes-x-end', xEnd);
9865 document.documentElement.style.setProperty('--slide-mes-x-duration', `${duration}ms`);
9866
9867 //The class must be removed to unfreze previous slides.
9868 swipedElementsDiv.removeClass('slide');
9869 //CSS starts the animation.
9870 void swipedElementsDiv[0].offsetWidth;
9871 swipedElementsDiv.addClass('slide');
9872
9873 const endSlide = () => {
9874 //Remove the style when done.
9875 swipedElementsDiv.removeClass('slide');
9876
9877 document.documentElement.style.setProperty('--slide-mes-x-start', '');
9878 document.documentElement.style.setProperty('--slide-mes-x-end', '');
9879 document.documentElement.style.setProperty('--slide-mes-duration', '');
9880 return true;
9881 };
9882 //Wait for the animation's end. https://developer.mozilla.org/en-US/docs/Web/API/Animation/finished
9883 const animation = swipedElementsDiv[0]?.getAnimations().filter((a) => a['animationName'] == 'slide')[0];
9884 try {
9885 await Promise.race([animation?.finished, createTimeout(duration * 2, `The ${duration}ms swipe animation has not ended after ${duration * 2}ms. It has been skipped.`)].filter(Boolean));
9886 } catch (error) {
9887 console.warn(error);
9888 }
9889
9890 //If not frozen, end the slide now.
9891 return freeze ? endSlide : endSlide();
9892 }
9893 }
9894 console.warn(`No animatable messages were found after message #${mesId}.`);
9895 return false;
9407 }9896 }
94089897
9409 function getMessageBottomHeight(thisMesDiv) {9898 function getMessageBottomHeight(thisMesDiv) {
@@ -9445,11 +9934,15 @@ export async function swipe(_event, direction, { source, repeated, message = cha
9445 /**9934 /**
9446 * Anime a swipe, optionally running a generation.9935 * Anime a swipe, optionally running a generation.
9447 * @param {boolean} run_generate9936 * @param {boolean} run_generate
9937 * @param {boolean} [skipSwipeOut=false]
9448 */9938 */
9449 async function animateSwipe(run_generate = false) {9939 async function animateSwipe(run_generate = false, skipSwipeOut = false) {
9940
9941 if (!skipSwipeOut) {
9942 //Swipe out.
9943 await animateSwipeTransition(mesId, { xEnd: `${swipeRange}px`, duration: swipeDuration });
9944 }
94509945
9451 //Swipe out.
9452 await animateSwipeTransition(mesId, swipeRange, swipeDuration);
94539946
9454 if (run_generate) {9947 if (run_generate) {
9455 await updateSwipeCounter(mesId);9948 await updateSwipeCounter(mesId);
@@ -9465,7 +9958,8 @@ export async function swipe(_event, direction, { source, repeated, message = cha
94659958
9466 //Only scroll when swiping the last message.9959 //Only scroll when swiping the last message.
9467 const scroll = (mesId == chat.length - 1);9960 const scroll = (mesId == chat.length - 1);
9468 addOneMessage(chat[mesId], { type: 'swipe', forceId: mesId, scroll: scroll });9961 //The swipe buttons will be refreshed in endSwipe(), refreshing them now will cause flickering.
9962 addOneMessage(chat[mesId], { type: 'swipe', forceId: mesId, scroll: scroll, showSwipes: false });
94699963
9470 if (power_user.message_token_count_enabled) {9964 if (power_user.message_token_count_enabled) {
9471 if (!chat[mesId].extra) {9965 if (!chat[mesId].extra) {
@@ -9483,10 +9977,6 @@ export async function swipe(_event, direction, { source, repeated, message = cha
9483 thisMesDiv.css('height', thisMesDivHeight);9977 thisMesDiv.css('height', thisMesDivHeight);
9484 expandNewMessage(thisMesDiv);9978 expandNewMessage(thisMesDiv);
94859979
9486
9487 //Jump to the opposite side.
9488 await animateSwipeTransition(mesId, -swipeRange, 0);
9489
9490 appendMediaToMessage(chat[mesId], thisMesDiv);9980 appendMediaToMessage(chat[mesId], thisMesDiv);
94919981
9492 await eventSource.emit(event_types.MESSAGE_SWIPED, (mesId));9982 await eventSource.emit(event_types.MESSAGE_SWIPED, (mesId));
@@ -9494,12 +9984,10 @@ export async function swipe(_event, direction, { source, repeated, message = cha
9494 if (run_generate && !is_send_press) {9984 if (run_generate && !is_send_press) {
9495 is_send_press = true;9985 is_send_press = true;
9496 generation = Generate('swipe');9986 generation = Generate('swipe');
9497 } else if (Number(chat[mesId]['swipe_id']) !== chat[mesId]['swipes'].length) {
9498 saveChatDebounced();
9499 }9987 }
95009988
9501 //Swipe in.9989 //Swipe in from the opposite side.
9502 await animateSwipeTransition(mesId, 0, swipeDuration);9990 await animateSwipeTransition(mesId, { xStart: `${-swipeRange}px`, xEnd: `${0}px`, duration: swipeDuration });
9503 }9991 }
95049992
9505 if (mesId === Number(this_edit_mes_id)) {9993 if (mesId === Number(this_edit_mes_id)) {
@@ -9514,7 +10002,7 @@ export async function swipe(_event, direction, { source, repeated, message = cha
9514 }10002 }
951510003
9516 //If the swipe is not being deleted.10004 //If the swipe is not being deleted.
9517 if (source != 'delete') {10005 if (source != SWIPE_SOURCE.DELETE && source != SWIPE_SOURCE.BACK) {
951810006
9519 // Make sure ad-hoc changes to extras are saved before swiping away10007 // Make sure ad-hoc changes to extras are saved before swiping away
9520 syncMesToSwipe(mesId);10008 syncMesToSwipe(mesId);
@@ -9533,19 +10021,19 @@ export async function swipe(_event, direction, { source, repeated, message = cha
9533 }10021 }
9534 // If the user is holding down the key and we're at the last or first swipe, don't do anything.10022 // If the user is holding down the key and we're at the last or first swipe, don't do anything.
9535 let isLastSwipe = (direction === SWIPE_DIRECTION.RIGHT) ? (chat[mesId].swipe_id === Math.max(0, chat[mesId]['swipes'].length - 1)) : chat[mesId].swipe_id === 0;10023 let isLastSwipe = (direction === SWIPE_DIRECTION.RIGHT) ? (chat[mesId].swipe_id === Math.max(0, chat[mesId]['swipes'].length - 1)) : chat[mesId].swipe_id === 0;
9536 if (source === 'keyboard' && repeated && isLastSwipe) {10024 if (source === SWIPE_SOURCE.KEYBOARD && repeated && isLastSwipe) {
9537 await endSwipe();10025 await endSwipe();
9538 return;10026 return;
9539 }10027 }
9540 } else if (source == 'delete') {10028 } else if (source == SWIPE_SOURCE.DELETE || source == SWIPE_SOURCE.BACK) {
9541 //If the swipe is being deleted.10029 //If the swipe is being deleted or reverted.
9542 await standardSwipe();10030 await standardSwipe(newSwipeId);
9543 return;10031 return;
9544 }10032 }
954510033
9546 //If swiping left.10034 //If swiping left.
9547 if (direction === SWIPE_DIRECTION.LEFT) {10035 if (direction === SWIPE_DIRECTION.LEFT) {
9548 newSwipeId--;10036 if (forceSwipeId == null) newSwipeId--;
9549 //Loop to last swipe if negative.10037 //Loop to last swipe if negative.
9550 if (newSwipeId < 0) {10038 if (newSwipeId < 0) {
9551 newSwipeId = Math.max(0, chat[mesId]['swipes'].length - 1);10039 newSwipeId = Math.max(0, chat[mesId]['swipes'].length - 1);
@@ -9557,13 +10045,13 @@ export async function swipe(_event, direction, { source, repeated, message = cha
9557 await endSwipe();10045 await endSwipe();
9558 return;10046 return;
9559 }10047 }
9560 await standardSwipe();10048 await standardSwipe(newSwipeId);
9561 return;10049 return;
9562 }10050 }
9563 //If swiping right.10051 //If swiping right.
9564 else if (direction === SWIPE_DIRECTION.RIGHT) {10052 else if (direction === SWIPE_DIRECTION.RIGHT) {
9565 // make new slot in array10053 // make new slot in array
9566 newSwipeId++;10054 if (forceSwipeId == null) newSwipeId++;
956710055
9568 //Minimum of zero.10056 //Minimum of zero.
9569 if (newSwipeId < 0) {10057 if (newSwipeId < 0) {
@@ -9573,35 +10061,37 @@ export async function swipe(_event, direction, { source, repeated, message = cha
9573 return;10061 return;
9574 }10062 }
957510063
9576 //if swipe id of last message is the same as the length of the 'swipes' array and not the greeting.10064 //If overswiping.
9577 if (newSwipeId >= chat[mesId]['swipes'].length && ((chat.length !== 1 || !isPristine))) {10065 if (newSwipeId >= chat[mesId]['swipes'].length) {
9578 newSwipeId = chat[mesId]['swipes'].length;10066 newSwipeId = chat[mesId]['swipes'].length;
957910067
9580 //Update the swipe_id.10068 //Update the swipe_id.
9581 chat[mesId]['swipe_id'] = newSwipeId;10069 chat[mesId]['swipe_id'] = newSwipeId;
958210070
9583 //Cancel the generation if it's a user message or the first message in a pristine chat.10071 const overswipe = getOverswipeBehavior(mesId);
9584 if (chat[mesId].is_user || (mesId === 0 && isPristine)) {10072
10073 //Cancel the generation.
10074 if (overswipe == OVERSWIPE_BEHAVIOR.NONE) {
9585 //Cancel swipe.10075 //Cancel swipe.
9586 chat[mesId]['swipe_id'] = originalSwipeId;10076 chat[mesId]['swipe_id'] = originalSwipeId;
9587 await endSwipe();10077 await endSwipe();
9588 return;10078 return;
9589 } else {10079 }
9590 //Generate.10080 //Regenerate the message
9591 await loadFromSwipeId(mesId, newSwipeId);10081 else if (overswipe == OVERSWIPE_BEHAVIOR.REGENERATE) {
10082 clearMessageData(chat[mesId]);
9592 let run_generate = true;10083 let run_generate = true;
10084 //Generate.
9593 await animateSwipe(run_generate);10085 await animateSwipe(run_generate);
9594 await endSwipe();10086 await endSwipe();
9595 return;10087 return;
9596 }10088 }
9597 }10089 // Loop to the first swipe.
9598 else {10090 else if (overswipe == OVERSWIPE_BEHAVIOR.LOOP || overswipe == OVERSWIPE_BEHAVIOR.PRISTINE_GREETING) {
9599 // if swipe_right is called on the last alternate greeting in pristine chats, loop back around
9600 if (chat.length === 1 && newSwipeId !== undefined && newSwipeId === chat[0]['swipes'].length && isPristine) {
9601 newSwipeId = 0;10091 newSwipeId = 0;
9602 }10092 }
9603 }10093 }
9604 await standardSwipe();10094 await standardSwipe(newSwipeId);
9605 return;10095 return;
9606 }10096 }
9607}10097}
@@ -9609,28 +10099,28 @@ export async function swipe(_event, direction, { source, repeated, message = cha
9609/**10099/**
9610 * @deprecated Use `swipe` instead.10100 * @deprecated Use `swipe` instead.
9611 * Handles the swipe to the left event.10101 * Handles the swipe to the left event.
9612 * @param {JQuery.Event} _event Event.10102 * @param {SwipeEvent} [event] Event.
9613 * @param {object} params Additional parameters.10103 * @param {object} params Additional parameters.
9614 * @param {string} [params.source] The source of the swipe event.10104 * @param {import('./scripts/constants.js').SWIPE_SOURCE} [params.source] The source of the swipe event. null, 'keyboard', 'auto_swipe', 'back' or 'delete'.
9615 * @param {boolean} [params.repeated] Is the swipe event repeated.10105 * @param {boolean} [params.repeated] Is the swipe event repeated.
9616 * @param {object} [params.message] The chat message to swipe.10106 * @param {object} [params.message] The chat message to swipe.
9617 */10107 */
9618export async function swipe_left(_event, { source, repeated, message } = {}) {10108export async function swipe_left(event, { source, repeated, message } = {}) {
9619 await swipe.call(this, _event, SWIPE_DIRECTION.LEFT, { source: source, repeated: repeated, message: message });10109 await swipe.call(this, event, SWIPE_DIRECTION.LEFT, { source: source, repeated: repeated, message: message });
9620}10110}
962110111
9622/**10112/**
9623 * @deprecated Use `swipe` instead.10113 * @deprecated Use `swipe` instead.
9624 * Handles the swipe to the right event.10114 * Handles the swipe to the right event.
9625 * @param {JQuery.Event} [_event] Event.10115 * @param {SwipeEvent} [event] Event.
9626 * @param {object} params Additional parameters.10116 * @param {object} params Additional parameters.
9627 * @param {string} [params.source] The source of the swipe event.10117 * @param {import('./scripts/constants.js').SWIPE_SOURCE} [params.source] The source of the swipe event. null, 'keyboard', 'auto_swipe', 'back' or 'delete'.
9628 * @param {boolean} [params.repeated] Is the swipe event repeated.10118 * @param {boolean} [params.repeated] Is the swipe event repeated.
9629 * @param {object} [params.message] The chat message to swipe.10119 * @param {object} [params.message] The chat message to swipe.
9630 */10120 */
9631//MARK: swipe_right10121//MARK: swipe_right
9632export async function swipe_right(_event = null, { source, repeated, message } = {}) {10122export async function swipe_right(event = null, { source, repeated, message } = {}) {
9633 await swipe.call(this, _event, SWIPE_DIRECTION.RIGHT, { source: source, repeated: repeated, message: message });10123 await swipe.call(this, event, SWIPE_DIRECTION.RIGHT, { source: source, repeated: repeated, message: message });
9634}10124}
963510125
9636/**10126/**
@@ -10356,7 +10846,7 @@ jQuery(async function () {
10356 });10846 });
10357 $('#rm_button_selected_ch').on('click', function () {10847 $('#rm_button_selected_ch').on('click', function () {
10358 if (selected_group) {10848 if (selected_group) {
10359 select_group_chats(selected_group);10849 select_group_chats(selected_group, false);
10360 } else {10850 } else {
10361 selected_button = 'character_edit';10851 selected_button = 'character_edit';
10362 select_selected_character(this_chid);10852 select_selected_character(this_chid);
@@ -10800,10 +11290,12 @@ jQuery(async function () {
10800 }11290 }
1080111291
10802 else if (id == 'option_regenerate') {11292 else if (id == 'option_regenerate') {
10803 closeMessageEditor();11293 //Attempting to regenerate a user message will instead generate a new message.
11294 if (chat.length && chat.length - 1 === this_edit_mes_id && chat[this_edit_mes_id]?.is_user == false) {
11295 toastr.warning(t`Finish the edit before starting a generation.`, t`You cannot regenerate the message you are editing.`);
11296 return;
11297 }
10804 if (is_send_press == false) {11298 if (is_send_press == false) {
10805 //hideSwipeButtons();
10806
10807 if (selected_group) {11299 if (selected_group) {
10808 regenerateGroup();11300 regenerateGroup();
10809 }11301 }
@@ -10822,7 +11314,14 @@ jQuery(async function () {
10822 }11314 }
1082311315
10824 else if (id == 'option_continue') {11316 else if (id == 'option_continue') {
10825 if (this_edit_mes_id >= 0) return; // don't proceed if editing a message11317 if (swipeState == SWIPE_STATE.EDITING) {
11318 toastr.warning(t`Confirm the edit to start a generation.`, t`You cannot send a message during a swipe-edit.`);
11319 return;
11320 }
11321 if (chat.length && chat.length - 1 === this_edit_mes_id) {
11322 toastr.warning(t`Finish the edit before starting a generation.`, t`You cannot continue the message you are editing.`);
11323 return;
11324 }
1082611325
10827 if (is_send_press == false || fromSlashCommand) {11326 if (is_send_press == false || fromSlashCommand) {
10828 is_send_press = true;11327 is_send_press = true;
@@ -11289,7 +11788,7 @@ jQuery(async function () {
1128911788
11290 $('#rm_button_group_chats').on('click', function () {11789 $('#rm_button_group_chats').on('click', function () {
11291 selected_button = 'group_chats';11790 selected_button = 'group_chats';
11292 select_group_chats();11791 select_group_chats(null, false);
11293 });11792 });
1129411793
11295 $('#rm_button_back_from_group').on('click', function () {11794 $('#rm_button_back_from_group').on('click', function () {
public/scripts/PromptManager.js+4 -12
@@ -579,10 +579,6 @@ class PromptManager {
579 const sourceName = this.promptSources[promptId];579 const sourceName = this.promptSources[promptId];
580 entrySource.textContent = sourceName;580 entrySource.textContent = sourceName;
581 }581 }
582
583 if (!this.systemPrompts.includes(promptId)) {
584 injectionPositionField.removeAttribute('disabled');
585 }
586 };582 };
587583
588 // Append prompt to selected character584 // Append prompt to selected character
@@ -1284,10 +1280,10 @@ class PromptManager {
1284 const preparedPrompt = new Prompt(prompt);1280 const preparedPrompt = new Prompt(prompt);
12851281
1286 if (typeof original === 'string') {1282 if (typeof original === 'string') {
1287 if (0 < groupMembers.length) preparedPrompt.content = substituteParams(prompt.content ?? '', null, null, original, groupMembers.join(', '));1283 if (0 < groupMembers.length) preparedPrompt.content = substituteParams(prompt.content ?? '', { original, groupOverride: groupMembers.join(', ') });
1288 else preparedPrompt.content = substituteParams(prompt.content, null, null, original);1284 else preparedPrompt.content = substituteParams(prompt.content, { original });
1289 } else {1285 } else {
1290 if (0 < groupMembers.length) preparedPrompt.content = substituteParams(prompt.content ?? '', null, null, null, groupMembers.join(', '));1286 if (0 < groupMembers.length) preparedPrompt.content = substituteParams(prompt.content ?? '', { groupOverride: groupMembers.join(', ') });
1291 else preparedPrompt.content = substituteParams(prompt.content);1287 else preparedPrompt.content = substituteParams(prompt.content);
1292 }1288 }
12931289
@@ -1399,10 +1395,6 @@ class PromptManager {
1399 entrySource.textContent = sourceName;1395 entrySource.textContent = sourceName;
1400 }1396 }
14011397
1402 if (this.systemPrompts.includes(prompt.identifier)) {
1403 injectionPositionField.setAttribute('disabled', 'disabled');
1404 }
1405
1406 const resetPromptButton = document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_reset');1398 const resetPromptButton = document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_reset');
1407 if (true === prompt.system_prompt) {1399 if (true === prompt.system_prompt) {
1408 resetPromptButton.style.display = 'block';1400 resetPromptButton.style.display = 'block';
@@ -1757,7 +1749,7 @@ class PromptManager {
1757 ${isInjectionPrompt ? '<span class="fa-fw fa-solid fa-syringe" title="In-Chat Injection"></span>' : ''}1749 ${isInjectionPrompt ? '<span class="fa-fw fa-solid fa-syringe" title="In-Chat Injection"></span>' : ''}
1758 ${this.isPromptInspectionAllowed(prompt) ? `<a title="${encodedName}" class="prompt-manager-inspect-action">${encodedName}</a>` : `<span title="${encodedName}">${encodedName}</span>`}1750 ${this.isPromptInspectionAllowed(prompt) ? `<a title="${encodedName}" class="prompt-manager-inspect-action">${encodedName}</a>` : `<span title="${encodedName}">${encodedName}</span>`}
1759 ${roleIcon ? `<span data-role="${escapeHtml(prompt.role)}" class="fa-xs fa-solid ${roleIcon}" title="${roleTitle}"></span>` : ''}1751 ${roleIcon ? `<span data-role="${escapeHtml(prompt.role)}" class="fa-xs fa-solid ${roleIcon}" title="${roleTitle}"></span>` : ''}
1760 ${isInjectionPrompt ? `<small class="prompt-manager-injection-depth">@ ${escapeHtml(prompt.injection_depth)}</small>` : ''}1752 ${isInjectionPrompt ? `<small class="prompt-manager-injection-depth">@ ${escapeHtml(prompt.injection_depth.toString())}</small>` : ''}
1761 ${isOverriddenPrompt ? '<small class="fa-solid fa-address-card prompt-manager-overridden" title="Pulled from a character card"></small>' : ''}1753 ${isOverriddenPrompt ? '<small class="fa-solid fa-address-card prompt-manager-overridden" title="Pulled from a character card"></small>' : ''}
1762 </span>1754 </span>
1763 <span>1755 <span>
public/scripts/RossAscends-mods.js+39 -42
@@ -37,7 +37,7 @@ import { debounce, getStringHash, isValidUrl } from './utils.js';
37import { chat_completion_sources, oai_settings } from './openai.js';37import { chat_completion_sources, oai_settings } from './openai.js';
38import { getTokenCountAsync } from './tokenizers.js';38import { getTokenCountAsync } from './tokenizers.js';
39import { textgen_types, textgenerationwebui_settings as textgen_settings, getTextGenServer } from './textgen-settings.js';39import { textgen_types, textgenerationwebui_settings as textgen_settings, getTextGenServer } from './textgen-settings.js';
40import { debounce_timeout } from './constants.js';40import { debounce_timeout, SWIPE_SOURCE } from './constants.js';
4141
42import { Popup } from './popup.js';42import { Popup } from './popup.js';
43import { accountStorage } from './util/AccountStorage.js';43import { accountStorage } from './util/AccountStorage.js';
@@ -162,43 +162,37 @@ export function shouldSendOnEnter() {
162 }162 }
163}163}
164164
165//RossAscends: Added function to format dates used in files and chat timestamps to a humanized format.165/**
166//Mostly I wanted this to be for file names, but couldn't figure out exactly where the filename save code was as everything seemed to be connected.166 * Gets a humanized date time string from a given timestamp.
167//Does not break old characters/chats, as the code just uses whatever timestamp exists in the chat.167 * @param {number} timestamp Timestamp in milliseconds
168//New chats made with characters will use this new formatting.168 * @returns {string} Humanized date time string in the format `YYYY-MM-DD@HHhMMmSSsMSms`
169export function humanizedDateTime() {169 */
170 const now = new Date(Date.now());170export function humanizedDateTime(timestamp = Date.now()) {
171 const date = new Date(timestamp);
171 const dt = {172 const dt = {
172 year: now.getFullYear(), month: now.getMonth() + 1, day: now.getDate(),173 year: date.getFullYear(),
173 hour: now.getHours(), minute: now.getMinutes(), second: now.getSeconds(),174 month: date.getMonth() + 1,
175 day: date.getDate(),
176 hour: date.getHours(),
177 minute: date.getMinutes(),
178 second: date.getSeconds(),
179 millisecond: date.getMilliseconds(),
174 };180 };
175 for (const key in dt) {181 for (const key in dt) {
176 dt[key] = dt[key].toString().padStart(2, '0');182 const padLength = key === 'millisecond' ? 3 : 2;
183 dt[key] = dt[key].toString().padStart(padLength, '0');
177 }184 }
178 return `${dt.year}-${dt.month}-${dt.day}@${dt.hour}h${dt.minute}m${dt.second}s`;185 return `${dt.year}-${dt.month}-${dt.day}@${dt.hour}h${dt.minute}m${dt.second}s${dt.millisecond}ms`;
179}186}
180187
181//this is a common format version to display a timestamp on each chat message188/**
182//returns something like: June 19, 2023 2:20pm189 * Gets a timestamp for messages in ISO 8601 format.
183export function getMessageTimeStamp() {190 * @param {number} timestamp - optional timestamp in milliseconds
184 const date = Date.now();191 * @returns {string} ISO 8601 formatted timestamp
185 const months = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];192 */
186 const d = new Date(date);193export function getMessageTimeStamp(timestamp = Date.now()) {
187 const month = months[d.getMonth()];194 const date = new Date(timestamp);
188 const day = d.getDate();195 return date.toISOString();
189 const year = d.getFullYear();
190 let hours = d.getHours();
191 const minutes = ('0' + d.getMinutes()).slice(-2);
192 let meridiem = 'am';
193 if (hours >= 12) {
194 meridiem = 'pm';
195 hours -= 12;
196 }
197 if (hours === 0) {
198 hours = 12;
199 }
200 const formattedDate = month + ' ' + day + ', ' + year + ' ' + hours + ':' + minutes + meridiem;
201 return formattedDate;
202}196}
203197
204198
@@ -403,6 +397,7 @@ function RA_autoconnect(PrevApi) {
403 || (secret_state[SECRET_KEYS.COHERE] && oai_settings.chat_completion_source == chat_completion_sources.COHERE)397 || (secret_state[SECRET_KEYS.COHERE] && oai_settings.chat_completion_source == chat_completion_sources.COHERE)
404 || (secret_state[SECRET_KEYS.PERPLEXITY] && oai_settings.chat_completion_source == chat_completion_sources.PERPLEXITY)398 || (secret_state[SECRET_KEYS.PERPLEXITY] && oai_settings.chat_completion_source == chat_completion_sources.PERPLEXITY)
405 || (secret_state[SECRET_KEYS.GROQ] && oai_settings.chat_completion_source == chat_completion_sources.GROQ)399 || (secret_state[SECRET_KEYS.GROQ] && oai_settings.chat_completion_source == chat_completion_sources.GROQ)
400 || (secret_state[SECRET_KEYS.CHUTES] && oai_settings.chat_completion_source == chat_completion_sources.CHUTES)
406 || (secret_state[SECRET_KEYS.SILICONFLOW] && oai_settings.chat_completion_source == chat_completion_sources.SILICONFLOW)401 || (secret_state[SECRET_KEYS.SILICONFLOW] && oai_settings.chat_completion_source == chat_completion_sources.SILICONFLOW)
407 || (secret_state[SECRET_KEYS.ELECTRONHUB] && oai_settings.chat_completion_source == chat_completion_sources.ELECTRONHUB)402 || (secret_state[SECRET_KEYS.ELECTRONHUB] && oai_settings.chat_completion_source == chat_completion_sources.ELECTRONHUB)
408 || (secret_state[SECRET_KEYS.NANOGPT] && oai_settings.chat_completion_source == chat_completion_sources.NANOGPT)403 || (secret_state[SECRET_KEYS.NANOGPT] && oai_settings.chat_completion_source == chat_completion_sources.NANOGPT)
@@ -930,7 +925,7 @@ export function initRossMods() {
930 var SwipeButR = $('.swipe_right:last');925 var SwipeButR = $('.swipe_right:last');
931 var SwipeTargetMesClassParent = $(e.target).closest('.last_mes');926 var SwipeTargetMesClassParent = $(e.target).closest('.last_mes');
932 if (SwipeTargetMesClassParent !== null) {927 if (SwipeTargetMesClassParent !== null) {
933 if (SwipeButR.css('display') === 'flex') {928 if (SwipeButR.is(':visible')) {
934 SwipeButR.trigger('click');929 SwipeButR.trigger('click');
935 }930 }
936 }931 }
@@ -954,7 +949,7 @@ export function initRossMods() {
954 var SwipeButL = $('.swipe_left:last');949 var SwipeButL = $('.swipe_left:last');
955 var SwipeTargetMesClassParent = $(e.target).closest('.last_mes');950 var SwipeTargetMesClassParent = $(e.target).closest('.last_mes');
956 if (SwipeTargetMesClassParent !== null) {951 if (SwipeTargetMesClassParent !== null) {
957 if (SwipeButL.css('display') === 'flex') {952 if (SwipeButL.is(':visible')) {
958 SwipeButL.trigger('click');953 SwipeButL.trigger('click');
959 }954 }
960 }955 }
@@ -975,10 +970,10 @@ export function initRossMods() {
975970
976 function isModifiedKeyboardEvent(event) {971 function isModifiedKeyboardEvent(event) {
977 return (event instanceof KeyboardEvent &&972 return (event instanceof KeyboardEvent &&
978 event.shiftKey ||973 (event.shiftKey ||
979 event.ctrlKey ||974 event.ctrlKey ||
980 event.altKey ||975 event.altKey ||
981 event.metaKey);976 event.metaKey));
982 }977 }
983978
984 $(document).on('keydown', async function (event) {979 $(document).on('keydown', async function (event) {
@@ -1112,29 +1107,31 @@ export function initRossMods() {
11121107
1113 if (event.key == 'ArrowLeft') { //swipes left1108 if (event.key == 'ArrowLeft') { //swipes left
1114 if (1109 if (
1115 isSwipingAllowed &&1110 isSwipingAllowed() &&
1116 !isNanogallery2LightboxActive() && // Check if lightbox is NOT active1111 !isNanogallery2LightboxActive() && // Check if lightbox is NOT active
1117 $('#send_textarea').val() === '' &&1112 $('#send_textarea').val() === '' &&
1118 $('#character_popup').css('display') === 'none' &&1113 $('#character_popup').css('display') === 'none' &&
1119 $('#shadow_select_chat_popup').css('display') === 'none' &&1114 $('#shadow_select_chat_popup').css('display') === 'none' &&
1120 !isInputElementInFocus() &&1115 !isInputElementInFocus() &&
1121 !isModifiedKeyboardEvent(event)1116 !isModifiedKeyboardEvent(event) &&
1117 !(document.activeElement instanceof HTMLVideoElement)
1122 ) {1118 ) {
1123 $('.swipe_left:last').trigger('click', { source: 'keyboard', repeated: event.repeat });1119 $('.swipe_left:last').trigger('click', { source: SWIPE_SOURCE.KEYBOARD, repeated: event.repeat });
1124 return;1120 return;
1125 }1121 }
1126 }1122 }
1127 if (event.key == 'ArrowRight') { //swipes right1123 if (event.key == 'ArrowRight') { //swipes right
1128 if (1124 if (
1129 isSwipingAllowed &&1125 isSwipingAllowed() &&
1130 !isNanogallery2LightboxActive() && // Check if lightbox is NOT active1126 !isNanogallery2LightboxActive() && // Check if lightbox is NOT active
1131 $('#send_textarea').val() === '' &&1127 $('#send_textarea').val() === '' &&
1132 $('#character_popup').css('display') === 'none' &&1128 $('#character_popup').css('display') === 'none' &&
1133 $('#shadow_select_chat_popup').css('display') === 'none' &&1129 $('#shadow_select_chat_popup').css('display') === 'none' &&
1134 !isInputElementInFocus() &&1130 !isInputElementInFocus() &&
1135 !isModifiedKeyboardEvent(event)1131 !isModifiedKeyboardEvent(event) &&
1132 !(document.activeElement instanceof HTMLVideoElement)
1136 ) {1133 ) {
1137 $('.swipe_right:last').trigger('click', { source: 'keyboard', repeated: event.repeat });1134 $('.swipe_right:last').trigger('click', { source: SWIPE_SOURCE.KEYBOARD, repeated: event.repeat });
1138 return;1135 return;
1139 }1136 }
1140 }1137 }
public/scripts/a11y.js+14 -0
@@ -50,6 +50,14 @@ const toolbarSelectors = [
50 '.jg-menu',50 '.jg-menu',
51].join(', ');51].join(', ');
5252
53const tabListSelectors = [
54 '#bg_tabs .bg_tabs_list',
55].join(', ');
56
57const tabItemSelectors = [
58 '#bg_tabs .bg_tabs_list .bg_tab_button',
59].join(', ');
60
53/** @type {Record<string, (element: Element) => void>} */61/** @type {Record<string, (element: Element) => void>} */
54const a11yRules = {62const a11yRules = {
55 [buttonSelectors]: (element) => {63 [buttonSelectors]: (element) => {
@@ -64,6 +72,12 @@ const a11yRules = {
64 [toolbarSelectors]: (element) => {72 [toolbarSelectors]: (element) => {
65 element.setAttribute('role', 'toolbar');73 element.setAttribute('role', 'toolbar');
66 },74 },
75 [tabListSelectors]: (element) => {
76 element.setAttribute('role', 'tablist');
77 },
78 [tabItemSelectors]: (element) => {
79 element.setAttribute('role', 'tab');
80 },
67 '#toast-container .toast': (element) => {81 '#toast-container .toast': (element) => {
68 element.setAttribute('role', 'status');82 element.setAttribute('role', 'status');
69 },83 },
public/scripts/authors-note.js+21 -4
@@ -19,7 +19,7 @@ import { SlashCommand } from './slash-commands/SlashCommand.js';
19import { ARGUMENT_TYPE, SlashCommandArgument } from './slash-commands/SlashCommandArgument.js';19import { ARGUMENT_TYPE, SlashCommandArgument } from './slash-commands/SlashCommandArgument.js';
20export { MODULE_NAME as NOTE_MODULE_NAME };20export { MODULE_NAME as NOTE_MODULE_NAME };
21import { t } from './i18n.js';21import { t } from './i18n.js';
22import { MacrosParser } from './macros.js';22import { macros, MacroCategory } from './macros/macro-system.js';
2323
24const MODULE_NAME = '2_floating_prompt'; // <= Deliberate, for sorting lower than memory24const MODULE_NAME = '2_floating_prompt'; // <= Deliberate, for sorting lower than memory
2525
@@ -581,7 +581,24 @@ export function initAuthorsNote() {
581 }));581 }));
582 eventSource.on(event_types.CHAT_CHANGED, onChatChanged);582 eventSource.on(event_types.CHAT_CHANGED, onChatChanged);
583583
584 MacrosParser.registerMacro('authorsNote', () => chat_metadata[metadata_keys.prompt] ?? '', t`The contents of the Author's Note`);584 registerAuthorsNoteMacros();
585 MacrosParser.registerMacro('charAuthorsNote', () => this_chid !== undefined ? (extension_settings.note.chara.find((e) => e.name === getCharaFilename())?.prompt ?? '') : '', t`The contents of the Character Author's Note`);
586 MacrosParser.registerMacro('defaultAuthorsNote', () => extension_settings.note.default ?? '', t`The contents of the Default Author's Note`);
587}585}
586
587function registerAuthorsNoteMacros() {
588 macros.register('authorsNote', {
589 category: MacroCategory.PROMPTS,
590 description: t`The contents of the Author's Note`,
591 handler: () => chat_metadata[metadata_keys.prompt] ?? '',
592 });
593 macros.register('charAuthorsNote', {
594 category: MacroCategory.CHARACTER,
595 description: t`The contents of the Character Author's Note`,
596 handler: () => this_chid !== undefined ? (extension_settings.note.chara.find((e) => e.name === getCharaFilename())?.prompt ?? '') : '',
597 });
598 macros.register('defaultAuthorsNote', {
599 category: MacroCategory.PROMPTS,
600 description: t`The contents of the Default Author's Note`,
601 handler: () => extension_settings.note.default ?? '',
602 });
603}
604
public/scripts/autocomplete/EnhancedMacroAutoCompleteOption.js+260 -0
@@ -0,0 +1,260 @@
1/**
2 * Enhanced macro autocomplete option for the new MacroRegistry-based system.
3 * Reuses rendering logic from MacroBrowser for consistency and DRY.
4 */
5
6import { AutoCompleteOption } from './AutoCompleteOption.js';
7import {
8 formatMacroSignature,
9 createSourceIndicator,
10 createAliasIndicator,
11 renderMacroDetails,
12} from '../macros/MacroBrowser.js';
13import { enumIcons } from '../slash-commands/SlashCommandCommonEnumsProvider.js';
14
15/** @typedef {import('../macros/engine/MacroRegistry.js').MacroDefinition} MacroDefinition */
16
17/**
18 * Macro context passed from the parser to provide cursor position info.
19 * @typedef {Object} MacroAutoCompleteContext
20 * @property {string} fullText - The full macro text being typed (without {{ }}).
21 * @property {number} cursorOffset - Cursor position within the macro text.
22 * @property {string} identifier - The macro identifier (name).
23 * @property {string[]} args - Array of arguments typed so far.
24 * @property {number} currentArgIndex - Index of the argument being typed (-1 if on identifier).
25 */
26
27export class EnhancedMacroAutoCompleteOption extends AutoCompleteOption {
28 /** @type {MacroDefinition} */
29 #macro;
30
31 /** @type {MacroAutoCompleteContext|null} */
32 #context = null;
33
34 /**
35 * @param {MacroDefinition} macro - The macro definition from MacroRegistry.
36 * @param {MacroAutoCompleteContext} [context] - Optional context for argument hints.
37 */
38 constructor(macro, context = null) {
39 // Use the macro name as the autocomplete key
40 super(macro.name, enumIcons.macro);
41 this.#macro = macro;
42 this.#context = context;
43 // nameOffset = 2 to skip the {{ prefix in the display (formatMacroSignature includes braces)
44 this.nameOffset = 2;
45 }
46
47 /** @returns {MacroDefinition} */
48 get macro() {
49 return this.#macro;
50 }
51
52 /**
53 * Renders the list item for the autocomplete dropdown.
54 * Tight display: [icon] [signature] [description] [alias icon?] [source icon]
55 * @returns {HTMLElement}
56 */
57 renderItem() {
58 const li = document.createElement('li');
59 li.classList.add('item', 'macro-ac-item');
60 li.setAttribute('data-name', this.name);
61 li.setAttribute('data-option-type', 'macro');
62
63 // Type icon
64 const type = document.createElement('span');
65 type.classList.add('type', 'monospace');
66 type.textContent = '{}';
67 li.append(type);
68
69 // Specs container (for fuzzy highlight compatibility)
70 const specs = document.createElement('span');
71 specs.classList.add('specs');
72
73 // Name with character spans for fuzzy highlighting
74 const nameEl = document.createElement('span');
75 nameEl.classList.add('name', 'monospace');
76
77 // Build signature with individual character spans (includes {{ }})
78 const sigText = formatMacroSignature(this.#macro);
79 for (const char of sigText) {
80 const span = document.createElement('span');
81 span.textContent = char;
82 nameEl.append(span);
83 }
84 specs.append(nameEl);
85 li.append(specs);
86
87 // Stopgap (spacer for flex layout)
88 const stopgap = document.createElement('span');
89 stopgap.classList.add('stopgap');
90 li.append(stopgap);
91
92 // Help text (description)
93 const help = document.createElement('span');
94 help.classList.add('help');
95 const content = document.createElement('span');
96 content.classList.add('helpContent');
97 content.textContent = this.#macro.description || '';
98 help.append(content);
99 li.append(help);
100
101 // Alias indicator icon (if this is an alias)
102 const aliasIcon = createAliasIndicator(this.#macro);
103 if (aliasIcon) {
104 aliasIcon.classList.add('macro-ac-indicator');
105 li.append(aliasIcon);
106 }
107
108 // Source indicator icon
109 const sourceIcon = createSourceIndicator(this.#macro);
110 sourceIcon.classList.add('macro-ac-indicator');
111 li.append(sourceIcon);
112
113 return li;
114 }
115
116 /**
117 * Renders the details panel content.
118 * Reuses renderMacroDetails from MacroBrowser with autocomplete-specific options.
119 * @returns {DocumentFragment}
120 */
121 renderDetails() {
122 const frag = document.createDocumentFragment();
123
124 // Determine current argument index for highlighting
125 const currentArgIndex = this.#context?.currentArgIndex ?? -1;
126
127 // Render argument hint banner if we're typing an argument
128 if (currentArgIndex >= 0) {
129 const hint = this.#renderArgumentHint();
130 if (hint) frag.append(hint);
131 }
132
133 // Reuse MacroBrowser's renderMacroDetails with options
134 const details = renderMacroDetails(this.#macro, { currentArgIndex });
135
136 // Add class for autocomplete-specific styling overrides
137 details.classList.add('macro-ac-details');
138 frag.append(details);
139
140 return frag;
141 }
142
143 /**
144 * Renders the current argument hint banner.
145 * @returns {HTMLElement|null}
146 */
147 #renderArgumentHint() {
148 if (!this.#context || this.#context.currentArgIndex < 0) return null;
149
150 const argIndex = this.#context.currentArgIndex;
151 const isListArg = argIndex >= this.#macro.maxArgs;
152
153 // If we're beyond unnamed args and there's no list, no hint
154 if (isListArg && !this.#macro.list) return null;
155
156 const hint = document.createElement('div');
157 hint.classList.add('macro-ac-arg-hint');
158
159 const icon = document.createElement('i');
160 icon.classList.add('fa-solid', 'fa-arrow-right');
161 hint.append(icon);
162
163 if (isListArg) {
164 // List argument hint
165 const listIndex = argIndex - this.#macro.maxArgs + 1;
166 const text = document.createElement('span');
167 text.innerHTML = `<strong>List item ${listIndex}</strong>`;
168 hint.append(text);
169 } else {
170 // Unnamed argument hint (required or optional)
171 const argDef = this.#macro.unnamedArgDefs[argIndex];
172 let optionalLabel = '';
173 if (argDef?.optional) {
174 optionalLabel = argDef.defaultValue !== undefined
175 ? ` <em>(optional, default: ${argDef.defaultValue === '' ? '<empty string>' : argDef.defaultValue})</em>`
176 : ' <em>(optional)</em>';
177 }
178 const text = document.createElement('span');
179 text.innerHTML = `<strong>${argDef?.name || `Argument ${argIndex + 1}`}</strong>${optionalLabel}`;
180 if (argDef?.type) {
181 const typeSpan = document.createElement('code');
182 typeSpan.classList.add('macro-ac-hint-type');
183 if (Array.isArray(argDef.type)) {
184 typeSpan.textContent = argDef.type.join(' | ');
185 typeSpan.title = `Accepts: ${argDef.type.join(', ')}`;
186 } else {
187 typeSpan.textContent = argDef.type;
188 }
189 text.append(' ', typeSpan);
190 }
191 hint.append(text);
192
193 if (argDef?.description) {
194 const descSpan = document.createElement('span');
195 descSpan.classList.add('macro-ac-hint-desc');
196 descSpan.textContent = ` — ${argDef.description}`;
197 hint.append(descSpan);
198 }
199
200 if (argDef?.sampleValue) {
201 const sampleSpan = document.createElement('span');
202 sampleSpan.classList.add('macro-ac-hint-sample');
203 sampleSpan.textContent = ` (e.g. ${argDef.sampleValue})`;
204 hint.append(sampleSpan);
205 }
206 }
207
208 return hint;
209 }
210}
211
212/**
213 * Parses the macro text to determine current argument context.
214 * @param {string} macroText - The text inside {{ }}, e.g., "roll::1d20" or "random::a::b".
215 * @param {number} cursorOffset - Cursor position within macroText.
216 * @returns {MacroAutoCompleteContext}
217 */
218export function parseMacroContext(macroText, cursorOffset) {
219 const parts = [];
220 let currentPart = '';
221 let partStart = 0;
222 let i = 0;
223
224 while (i < macroText.length) {
225 if (macroText[i] === ':' && macroText[i + 1] === ':') {
226 parts.push({ text: currentPart, start: partStart, end: i });
227 currentPart = '';
228 i += 2;
229 partStart = i;
230 } else {
231 currentPart += macroText[i];
232 i++;
233 }
234 }
235 // Push the last part
236 parts.push({ text: currentPart, start: partStart, end: macroText.length });
237
238 // Determine which part the cursor is in
239 let currentArgIndex = -1;
240 for (let idx = 0; idx < parts.length; idx++) {
241 const part = parts[idx];
242 if (cursorOffset >= part.start && cursorOffset <= part.end) {
243 currentArgIndex = idx - 1; // -1 because first part is identifier
244 break;
245 }
246 }
247
248 // If cursor is after all parts (at the end), we're in the last arg
249 if (currentArgIndex === -1 && cursorOffset >= parts[parts.length - 1].end) {
250 currentArgIndex = parts.length - 1;
251 }
252
253 return {
254 fullText: macroText,
255 cursorOffset,
256 identifier: parts[0]?.text.trim() || '',
257 args: parts.slice(1).map(p => p.text),
258 currentArgIndex,
259 };
260}
public/scripts/backgrounds.js+153 -27
@@ -1,12 +1,15 @@
1import { Fuse, localforage } from '../lib.js';1import { Fuse, localforage } from '../lib.js';
2import { chat_metadata, eventSource, event_types, generateQuietPrompt, getCurrentChatId, getRequestHeaders, getThumbnailUrl, saveSettingsDebounced } from '../script.js';2import { characters, chat_metadata, eventSource, event_types, generateQuietPrompt, getCurrentChatId, getRequestHeaders, getThumbnailUrl, saveMetadata, saveSettingsDebounced, this_chid } from '../script.js';
3import { openThirdPartyExtensionMenu, saveMetadataDebounced } from './extensions.js';3import { openThirdPartyExtensionMenu, saveMetadataDebounced } from './extensions.js';
4import { SlashCommand } from './slash-commands/SlashCommand.js';4import { SlashCommand } from './slash-commands/SlashCommand.js';
5import { SlashCommandParser } from './slash-commands/SlashCommandParser.js';5import { SlashCommandParser } from './slash-commands/SlashCommandParser.js';
6import { createThumbnail, flashHighlight, getBase64Async, stringFormat, debounce, setupScrollToTop } from './utils.js';6import { createThumbnail, flashHighlight, getBase64Async, stringFormat, debounce, setupScrollToTop, saveBase64AsFile, getFileExtension } from './utils.js';
7import { debounce_timeout } from './constants.js';7import { debounce_timeout } from './constants.js';
8import { t } from './i18n.js';8import { t } from './i18n.js';
9import { Popup } from './popup.js';9import { Popup } from './popup.js';
10import { groups, selected_group } from './group-chats.js';
11import { humanizedDateTime } from './RossAscends-mods.js';
12import { deleteMediaFromServer } from './chats.js';
1013
11const BG_METADATA_KEY = 'custom_background';14const BG_METADATA_KEY = 'custom_background';
12const LIST_METADATA_KEY = 'chat_backgrounds';15const LIST_METADATA_KEY = 'chat_backgrounds';
@@ -39,6 +42,26 @@ const THUMBNAIL_CONFIG = {
39};42};
4043
41/**44/**
45 * Background source types.
46 * @readonly
47 * @enum {number}
48 */
49const BG_SOURCES = {
50 GLOBAL: 0,
51 CHAT: 1,
52};
53
54/**
55 * Mapping of background sources to their corresponding tab IDs.
56 * @readonly
57 * @type {Record<string, string>}
58 */
59const BG_TABS = Object.freeze({
60 [BG_SOURCES.GLOBAL]: 'bg_global_tab',
61 [BG_SOURCES.CHAT]: 'bg_chat_tab',
62});
63
64/**
42 * Global IntersectionObserver instance for lazy loading backgrounds65 * Global IntersectionObserver instance for lazy loading backgrounds
43 * @type {IntersectionObserver|null}66 * @type {IntersectionObserver|null}
44 */67 */
@@ -187,6 +210,7 @@ function onLockBackgroundClick(event = null) {
187210
188 // Update UI states to reflect the new lock.211 // Update UI states to reflect the new lock.
189 highlightLockedBackground();212 highlightLockedBackground();
213 highlightSelectedBackground();
190}214}
191215
192/**216/**
@@ -219,21 +243,27 @@ function removeBackgroundMetadata() {
219 saveMetadataDebounced();243 saveMetadataDebounced();
220}244}
221245
222function onSelectBackgroundClick() {246/**
247 * Handles the click event for selecting a background.
248 * @param {JQuery.Event} e Event
249 */
250function onSelectBackgroundClick(e) {
223 const bgFile = $(this).attr('bgfile');251 const bgFile = $(this).attr('bgfile');
252 const isCustom = $(this).attr('custom') === 'true';
224 const backgroundCssUrl = getUrlParameter(this);253 const backgroundCssUrl = getUrlParameter(this);
254 const bypassGlobalLock = !isCustom && e.shiftKey;
225255
226 if (isChatBackgroundLocked()) {256 if ((isChatBackgroundLocked() || isCustom) && !bypassGlobalLock) {
227 // If a background is locked, update the locked background directly257 // If a background is locked, update the locked background directly
228 saveBackgroundMetadata(backgroundCssUrl);258 saveBackgroundMetadata(backgroundCssUrl);
229 $('#bg1').css('background-image', backgroundCssUrl);259 $('#bg1').css('background-image', backgroundCssUrl);
230 highlightLockedBackground();
231 } else {260 } else {
232 // Otherwise, update the global background setting261 // Otherwise, update the global background setting
233 setBackground(bgFile, backgroundCssUrl);262 setBackground(bgFile, backgroundCssUrl);
234 }263 }
235264
236 // Update UI highlights to reflect the changes.265 // Update UI highlights to reflect the changes.
266 highlightLockedBackground();
237 highlightSelectedBackground();267 highlightSelectedBackground();
238}268}
239269
@@ -375,20 +405,37 @@ async function onDeleteBackgroundClick(e) {
375 const bgToDelete = $(this).closest('.bg_example');405 const bgToDelete = $(this).closest('.bg_example');
376 const url = bgToDelete.data('url');406 const url = bgToDelete.data('url');
377 const isCustom = bgToDelete.attr('custom') === 'true';407 const isCustom = bgToDelete.attr('custom') === 'true';
378 const confirm = await Popup.show.confirm(t`Delete the background?`, null);408 const deleteFromServerId = 'delete_bg_from_server';
409 const customInputs = [
410 {
411 type: 'checkbox',
412 label: t`Also delete file from server`,
413 id: deleteFromServerId,
414 defaultState: true,
415 },
416 ];
417 let deleteFromServer = false;
418 const confirm = await Popup.show.confirm(t`Delete the background?`, null, {
419 customInputs: isCustom ? customInputs : [],
420 onClose: (popup) => {
421 if (isCustom) {
422 deleteFromServer = Boolean(popup?.inputResults?.get(deleteFromServerId) ?? false);
423 }
424 },
425 });
379 const bg = bgToDelete.attr('bgfile');426 const bg = bgToDelete.attr('bgfile');
380427
381 if (confirm) {428 if (confirm) {
382 // If it's not custom, it's a built-in background. Delete it from the server429 // If it's not custom, it's a built-in background. Delete it from the server
383 if (!isCustom) {430 if (!isCustom) {
384 delBackground(bg);431 await delBackground(bg);
385 } else {432 } else {
386 const list = chat_metadata[LIST_METADATA_KEY] || [];433 const list = chat_metadata[LIST_METADATA_KEY] || [];
387 const index = list.indexOf(bg);434 const index = list.indexOf(bg);
388 list.splice(index, 1);435 list.splice(index, 1);
389 }436 }
390437
391 if (bg === background_settings.name) {438 if (bg === background_settings.name || url === chat_metadata[BG_METADATA_KEY]) {
392 const siblingSelector = '.bg_example';439 const siblingSelector = '.bg_example';
393 const nextBg = bgToDelete.next(siblingSelector);440 const nextBg = bgToDelete.next(siblingSelector);
394 const prevBg = bgToDelete.prev(siblingSelector);441 const prevBg = bgToDelete.prev(siblingSelector);
@@ -409,13 +456,18 @@ async function onDeleteBackgroundClick(e) {
409456
410 if (url === chat_metadata[BG_METADATA_KEY]) {457 if (url === chat_metadata[BG_METADATA_KEY]) {
411 removeBackgroundMetadata();458 removeBackgroundMetadata();
412 highlightLockedBackground();
413 }459 }
414460
415 if (isCustom) {461 if (isCustom) {
462 if (deleteFromServer) {
463 await deleteMediaFromServer(bg);
464 }
416 renderChatBackgrounds();465 renderChatBackgrounds();
417 saveMetadataDebounced();466 await saveMetadata();
418 }467 }
468
469 highlightLockedBackground();
470 highlightSelectedBackground();
419 }471 }
420}472}
421473
@@ -607,25 +659,43 @@ async function delBackground(bg) {
607 }659 }
608}660}
609661
610async function onBackgroundUploadSelected() {662/**
611 const form = $('#form_bg_upload').get(0);663 * Background upload handler.
664 * @param {Event} e Event
665 * @returns {Promise<void>}
666 */
667async function onBackgroundUploadSelected(e) {
668 const input = e.currentTarget;
612669
613 if (!(form instanceof HTMLFormElement)) {670 if (!(input instanceof HTMLInputElement)) {
614 console.error('form_bg_upload is not a form');671 console.error('Invalid input element for background upload');
615 return;672 return;
616 }673 }
617674
618 const formData = new FormData(form);675 for (const file of input.files) {
676 if (file.size === 0) {
677 continue;
678 }
619679
620 const file = formData.get('avatar');680 const formData = new FormData();
621 if (!(file instanceof File) || file.size === 0) {681 formData.append('avatar', file);
622 form.reset();682
623 return;683 await convertFileIfVideo(formData);
684 switch (getActiveBackgroundTab()) {
685 case BG_SOURCES.GLOBAL:
686 await uploadBackground(formData);
687 break;
688 case BG_SOURCES.CHAT:
689 await uploadChatBackground(formData);
690 break;
691 default:
692 console.error('Unknown background source type');
693 continue;
694 }
624 }695 }
625696
626 await convertFileIfVideo(formData);697 // Allow re-uploading the same file again by clearing the input value
627 await uploadBackground(formData);698 input.value = '';
628 form.reset();
629}699}
630700
631/**701/**
@@ -699,6 +769,50 @@ async function uploadBackground(formData) {
699}769}
700770
701/**771/**
772 * Upload a chat background using a FormData object.
773 * @param {FormData} formData FormData containing the background file
774 * @returns {Promise<void>}
775 */
776async function uploadChatBackground(formData) {
777 try {
778 if (!getCurrentChatId()) {
779 toastr.warning(t`Select a chat to upload a background for it`);
780 return;
781 }
782 if (!formData.has('avatar')) {
783 console.log('No file provided. Chat background upload cancelled.');
784 return;
785 }
786
787 const file = formData.get('avatar');
788 if (!(file instanceof File)) {
789 console.error('Invalid file type for chat background upload');
790 return;
791 }
792
793 const imageDataUri = await getBase64Async(file);
794 const base64Data = imageDataUri.split(',')[1];
795 const extension = getFileExtension(file);
796 const characterName = selected_group
797 ? groups.find(g => g.id === selected_group)?.id?.toString()
798 : characters[this_chid]?.name;
799 const filename = `${characterName}_${humanizedDateTime()}`;
800 const imagePath = await saveBase64AsFile(base64Data, characterName, filename, extension);
801
802 const list = chat_metadata[LIST_METADATA_KEY] || [];
803 list.push(imagePath);
804 chat_metadata[LIST_METADATA_KEY] = list;
805 await saveMetadata();
806 renderChatBackgrounds();
807 highlightNewBackground(imagePath);
808 highlightLockedBackground();
809 highlightSelectedBackground();
810 } catch (error) {
811 console.error('Error uploading chat background:', error);
812 }
813}
814
815/**
702 * @param {string} bg816 * @param {string} bg
703 */817 */
704function highlightNewBackground(bg) {818function highlightNewBackground(bg) {
@@ -746,6 +860,14 @@ function onBackgroundFilterInput() {
746860
747const debouncedOnBackgroundFilterInput = debounce(onBackgroundFilterInput, debounce_timeout.standard);861const debouncedOnBackgroundFilterInput = debounce(onBackgroundFilterInput, debounce_timeout.standard);
748862
863/**
864 * Gets the active background tab source.
865 * @returns {BG_SOURCES} Active background tab source
866 */
867export function getActiveBackgroundTab() {
868 return $('#bg_tabs').tabs('option', 'active');
869}
870
749export function initBackgrounds() {871export function initBackgrounds() {
750 eventSource.on(event_types.CHAT_CHANGED, onChatChanged);872 eventSource.on(event_types.CHAT_CHANGED, onChatChanged);
751 eventSource.on(event_types.FORCE_SET_BACKGROUND, forceSetBackground);873 eventSource.on(event_types.FORCE_SET_BACKGROUND, forceSetBackground);
@@ -797,7 +919,7 @@ export function initBackgrounds() {
797 applyThumbnailColumns(background_settings.thumbnailColumns + 1);919 applyThumbnailColumns(background_settings.thumbnailColumns + 1);
798 });920 });
799 $('#auto_background').on('click', autoBackgroundCommand);921 $('#auto_background').on('click', autoBackgroundCommand);
800 $('#add_bg_button').on('change', onBackgroundUploadSelected);922 $('#add_bg_button').on('change', (e) => onBackgroundUploadSelected(e.originalEvent));
801 $('#bg-filter').on('input', () => debouncedOnBackgroundFilterInput());923 $('#bg-filter').on('input', () => debouncedOnBackgroundFilterInput());
802 SlashCommandParser.addCommandObject(SlashCommand.fromProps({924 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
803 name: 'lockbg',925 name: 'lockbg',
@@ -839,9 +961,13 @@ export function initBackgrounds() {
839 await onChatChanged();961 await onChatChanged();
840 });962 });
841963
842 setupScrollToTop({964 Object.values(BG_TABS).forEach(tabId => {
843 scrollContainerId: 'bg-scrollable-content',965 setupScrollToTop({
844 buttonId: 'bg-scroll-top',966 scrollContainerId: tabId,
845 drawerId: 'Backgrounds',967 buttonId: 'bg-scroll-top',
968 drawerId: 'Backgrounds',
969 });
846 });970 });
971
972 $('#bg_tabs').tabs();
847}973}
public/scripts/bookmarks.js+33 -28
@@ -11,6 +11,7 @@ import {
11 chat,11 chat,
12 saveChatConditional,12 saveChatConditional,
13 saveItemizedPrompts,13 saveItemizedPrompts,
14 setActiveGroup,
14} from '../script.js';15} from '../script.js';
15import { humanizedDateTime } from './RossAscends-mods.js';16import { humanizedDateTime } from './RossAscends-mods.js';
16import {17import {
@@ -298,28 +299,35 @@ export async function convertSoloToGroupChat() {
298 const chats = [chatName];299 const chats = [chatName];
299 const members = [character.avatar];300 const members = [character.avatar];
300 const favChecked = character.fav || character.fav == 'true';301 const favChecked = character.fav || character.fav == 'true';
301 /** @type {any} */302 /** @type {ChatMetadata} */
302 const metadata = Object.assign({}, chat_metadata);303 const metadata = Object.assign({}, chat_metadata);
303 delete metadata.main_chat;304 delete metadata.main_chat;
305 /** @type {ChatHeader} */
306 const chatHeader = {
307 chat_metadata: metadata,
308 user_name: 'unused',
309 character_name: 'unused',
310 };
311 /** @type {Omit<Group, 'id'>} */
312 const groupCreateModel = {
313 name: name,
314 members: members,
315 avatar_url: avatar,
316 allow_self_responses: false,
317 activation_strategy: group_activation_strategy.NATURAL,
318 disabled_members: [],
319 fav: favChecked,
320 chat_id: chatName,
321 chats: chats,
322 hideMutedSprites: false,
323 generation_mode: group_generation_mode.SWAP,
324 auto_mode_delay: DEFAULT_AUTO_MODE_DELAY,
325 };
304326
305 const createGroupResponse = await fetch('/api/groups/create', {327 const createGroupResponse = await fetch('/api/groups/create', {
306 method: 'POST',328 method: 'POST',
307 headers: getRequestHeaders(),329 headers: getRequestHeaders(),
308 body: JSON.stringify({330 body: JSON.stringify(groupCreateModel),
309 name: name,
310 members: members,
311 avatar_url: avatar,
312 allow_self_responses: false,
313 activation_strategy: group_activation_strategy.NATURAL,
314 disabled_members: [],
315 chat_metadata: metadata,
316 fav: favChecked,
317 chat_id: chatName,
318 chats: chats,
319 hideMutedSprites: false,
320 generation_mode: group_generation_mode.SWAP,
321 auto_mode_delay: DEFAULT_AUTO_MODE_DELAY,
322 }),
323 });331 });
324332
325 if (!createGroupResponse.ok) {333 if (!createGroupResponse.ok) {
@@ -327,6 +335,7 @@ export async function convertSoloToGroupChat() {
327 return;335 return;
328 }336 }
329337
338 /** @type {Group} */
330 const group = await createGroupResponse.json();339 const group = await createGroupResponse.json();
331340
332 // Convert tags list and assign to group341 // Convert tags list and assign to group
@@ -336,39 +345,34 @@ export async function convertSoloToGroupChat() {
336 await getCharacters();345 await getCharacters();
337346
338 // Convert chat to group format347 // Convert chat to group format
339 const groupChat = chat.slice();348 const groupChat = [...chat].map(m => structuredClone(m));
340 const genIdFirst = Date.now();349 const genIdFirst = Date.now();
341350
342 for (let index = 0; index < groupChat.length; index++) {351 for (let index = 0; index < groupChat.length; index++) {
343 const message = groupChat[index];352 const message = groupChat[index];
344353
345 // Save group-chat marker
346 if (index == 0) {
347 // @ts-ignore
348 message.is_group = true;
349 }
350
351 // Skip messages we don't care about354 // Skip messages we don't care about
352 if (message.is_user || message.is_system || message.extra?.type === system_message_types.NARRATOR || message.force_avatar !== undefined) {355 if (message.is_user || message.is_system || message.extra?.type === system_message_types.NARRATOR || message.force_avatar !== undefined) {
353 continue;356 continue;
354 }357 }
355358
359 if (!message.extra || typeof message.extra !== 'object') {
360 message.extra = {};
361 }
362
356 // Set force fields for solo character363 // Set force fields for solo character
357 message.name = character.name;364 message.name = character.name;
358 message.original_avatar = character.avatar;365 message.original_avatar = character.avatar;
359 message.force_avatar = getThumbnailUrl('avatar', character.avatar);366 message.force_avatar = getThumbnailUrl('avatar', character.avatar);
360
361 // Allow regens of a single message in group367 // Allow regens of a single message in group
362 if (typeof message.extra !== 'object') {368 message.extra.gen_id = genIdFirst + index;
363 message.extra = { gen_id: genIdFirst + index };
364 }
365 }369 }
366370
367 // Save group chat371 // Save group chat
368 const createChatResponse = await fetch('/api/chats/group/save', {372 const createChatResponse = await fetch('/api/chats/group/save', {
369 method: 'POST',373 method: 'POST',
370 headers: getRequestHeaders(),374 headers: getRequestHeaders(),
371 body: JSON.stringify({ id: chatName, chat: groupChat }),375 body: JSON.stringify({ id: chatName, chat: [chatHeader, ...groupChat] }),
372 });376 });
373377
374 if (!createChatResponse.ok) {378 if (!createChatResponse.ok) {
@@ -378,6 +382,7 @@ export async function convertSoloToGroupChat() {
378 }382 }
379383
380 // Click on the freshly selected group to open it384 // Click on the freshly selected group to open it
385 setActiveGroup(group.id);
381 await openGroupById(group.id);386 await openGroupById(group.id);
382387
383 toastr.success(t`The chat has been successfully converted!`);388 toastr.success(t`The chat has been successfully converted!`);
public/scripts/chat-backups.js+335 -0
@@ -0,0 +1,335 @@
1import { t } from './i18n.js';
2import { callGenericPopup, Popup, POPUP_TYPE } from './popup.js';
3import { getFileExtension, sortMoments, timestampToMoment } from './utils.js';
4import { displayPastChats, getRequestHeaders, importCharacterChat } from '/script.js';
5import { importGroupChat } from './group-chats.js';
6
7class BackupsBrowser {
8 /** @type {HTMLElement} */
9 #buttonElement;
10 /** @type {HTMLElement} */
11 #buttonChevronIcon;
12 /** @type {HTMLElement} */
13 #backupsListElement;
14 /** @type {AbortController} */
15 #loadingAbortController;
16 /** @type {boolean} */
17 #isOpen = false;
18
19 get isOpen() {
20 return this.#isOpen;
21 }
22
23 /**
24 * View a backup file content.
25 * @param {string} name File name of the backup to view.
26 * @returns {Promise<void>}
27 */
28 async viewBackup(name) {
29 const response = await fetch('/api/backups/chat/download', {
30 method: 'POST',
31 headers: getRequestHeaders(),
32 body: JSON.stringify({ name: name }),
33 });
34
35 if (!response.ok) {
36 toastr.error(t`Failed to download backup, try again later.`);
37 console.error('Failed to download chat backup:', response.statusText);
38 return;
39 }
40
41 try {
42 /** @type {ChatMessage[]} */
43 const parsedLines = [];
44 const fileText = await response.text();
45 fileText.split('\n').forEach(line => {
46 try {
47 /** @type {ChatMessage} */
48 const lineData = JSON.parse(line);
49 if (lineData?.mes) {
50 parsedLines.push(lineData);
51 }
52 } catch (error) {
53 console.error('Failed to parse chat backup line:', error);
54 }
55 });
56 const textArea = document.createElement('textarea');
57 textArea.classList.add('text_pole', 'monospace', 'textarea_compact', 'margin0', 'height100p');
58 textArea.readOnly = true;
59 textArea.value = parsedLines.map(l => `${l.name} [${timestampToMoment(l.send_date).format('lll')}]\n${l.mes}`).join('\n\n\n');
60 await callGenericPopup(textArea, POPUP_TYPE.TEXT, '', { allowVerticalScrolling: true, large: true, wide: true });
61 } catch (error) {
62 console.error('Failed to parse chat backup content:', error);
63 toastr.error(t`Failed to parse backup content.`);
64 return;
65 }
66 }
67
68 /**
69 * Restore a backup by importing it.
70 * @param {string} name File name of the backup to restore.
71 * @returns {Promise<void>}
72 */
73 async restoreBackup(name) {
74 const response = await fetch('/api/backups/chat/download', {
75 method: 'POST',
76 headers: getRequestHeaders(),
77 body: JSON.stringify({ name: name }),
78 });
79
80 if (!response.ok) {
81 toastr.error(t`Failed to download backup, try again later.`);
82 console.error('Failed to download chat backup:', response.statusText);
83 return;
84 }
85
86 const blob = await response.blob();
87 const file = new File([blob], name, { type: 'application/octet-stream' });
88
89 const extension = getFileExtension(file);
90
91 if (extension !== 'jsonl') {
92 toastr.warning(t`Only .jsonl files are supported for chat imports.`);
93 return;
94 }
95
96 const context = SillyTavern.getContext();
97
98 const formData = new FormData();
99 formData.set('file_type', extension);
100 formData.set('avatar', file);
101 formData.set('avatar_url', context.characters[context.characterId]?.avatar || '');
102 formData.set('user_name', context.name1);
103 formData.set('character_name', context.name2);
104
105 const importFn = context.groupId ? importGroupChat : importCharacterChat;
106 const result = await importFn(formData, { refresh: false });
107
108 if (result.length === 0) {
109 toastr.error(t`Failed to import chat backup, try again later.`);
110 return;
111 }
112
113 toastr.success(`Chat imported: ${result.join(', ')}`);
114 await displayPastChats(result);
115 }
116
117 /**
118 * Delete a backup file.
119 * @param {string} name File name of the backup to delete.
120 * @returns {Promise<boolean>} True if deleted, false otherwise.
121 */
122 async deleteBackup(name) {
123 const confirm = await Popup.show.confirm(t`Are you sure?`);
124 if (!confirm) {
125 return false;
126 }
127
128 const response = await fetch('/api/backups/chat/delete', {
129 method: 'POST',
130 headers: getRequestHeaders(),
131 body: JSON.stringify({ name: name }),
132 });
133
134 if (!response.ok) {
135 toastr.error(t`Failed to delete backup, try again later.`);
136 console.error('Failed to delete chat backup:', response.statusText);
137 return false;
138 }
139
140 toastr.success(t`Backup deleted successfully.`);
141 return true;
142 }
143
144 /**
145 * Load backups and populate the list element.
146 * @param {AbortSignal} signal Signal to abort loading.
147 * @returns {Promise<void>}
148 */
149 async loadBackupsIntoList(signal) {
150 if (!this.#backupsListElement) {
151 return;
152 }
153
154 this.#backupsListElement.innerHTML = '';
155
156 const response = await fetch('/api/backups/chat/get', {
157 method: 'POST',
158 headers: getRequestHeaders(),
159 signal,
160 });
161
162 if (!response.ok) {
163 console.error('Failed to load chat backups list:', response.statusText);
164 return;
165 }
166
167 /** @type {import('../../src/endpoints/chats.js').ChatInfo[]} */
168 const backupsList = await response.json();
169
170 for (const backup of backupsList.sort((a, b) => sortMoments(timestampToMoment(a.last_mes), timestampToMoment(b.last_mes)))) {
171 const listItem = document.createElement('div');
172 listItem.classList.add('chatBackupsListItem');
173
174 const backupName = document.createElement('div');
175 backupName.textContent = backup.file_name;
176 backupName.classList.add('chatBackupsListItemName');
177
178 const backupInfo = document.createElement('div');
179 backupInfo.classList.add('chatBackupsListItemInfo');
180 backupInfo.textContent = `${timestampToMoment(backup.last_mes).format('lll')} (${backup.file_size}, ${backup.chat_items} 💬)`;
181
182 const actionsList = document.createElement('div');
183 actionsList.classList.add('chatBackupsListItemActions');
184
185 const viewButton = document.createElement('div');
186 viewButton.classList.add('right_menu_button', 'fa-solid', 'fa-eye');
187 viewButton.title = t`View backup`;
188 viewButton.addEventListener('click', async () => {
189 await this.viewBackup(backup.file_name);
190 });
191
192 const restoreButton = document.createElement('div');
193 restoreButton.classList.add('right_menu_button', 'fa-solid', 'fa-rotate-left');
194 restoreButton.title = t`Restore backup`;
195 restoreButton.addEventListener('click', async () => {
196 await this.restoreBackup(backup.file_name);
197 });
198
199 const deleteButton = document.createElement('div');
200 deleteButton.classList.add('right_menu_button', 'fa-solid', 'fa-trash');
201 deleteButton.title = t`Delete backup`;
202 deleteButton.addEventListener('click',async () => {
203 const isDeleted = await this.deleteBackup(backup.file_name);
204 if (isDeleted) {
205 listItem.remove();
206 }
207 });
208
209 actionsList.appendChild(viewButton);
210 actionsList.appendChild(restoreButton);
211 actionsList.appendChild(deleteButton);
212
213 listItem.appendChild(backupName);
214 listItem.appendChild(backupInfo);
215 listItem.appendChild(actionsList);
216
217 this.#backupsListElement.appendChild(listItem);
218 }
219 }
220
221 closeBackups() {
222 if (!this.#isOpen) {
223 return;
224 }
225
226 this.#isOpen = false;
227 if (this.#buttonChevronIcon) {
228 this.#buttonChevronIcon.classList.remove('fa-chevron-up');
229 this.#buttonChevronIcon.classList.add('fa-chevron-down');
230 }
231 if (this.#backupsListElement) {
232 this.#backupsListElement.classList.remove('open');
233 this.#backupsListElement.innerHTML = '';
234 }
235 if (this.#loadingAbortController) {
236 this.#loadingAbortController.abort();
237 this.#loadingAbortController = null;
238 }
239 }
240
241 openBackups() {
242 if (this.#isOpen) {
243 return;
244 }
245
246 this.#isOpen = true;
247 if (this.#buttonChevronIcon) {
248 this.#buttonChevronIcon.classList.remove('fa-chevron-down');
249 this.#buttonChevronIcon.classList.add('fa-chevron-up');
250 }
251 if (this.#backupsListElement) {
252 this.#backupsListElement.classList.add('open');
253 }
254 if (this.#loadingAbortController) {
255 this.#loadingAbortController.abort();
256 this.#loadingAbortController = null;
257 }
258
259 this.#loadingAbortController = new AbortController();
260 this.loadBackupsIntoList(this.#loadingAbortController.signal);
261 }
262
263 renderButton() {
264 if (this.#buttonElement) {
265 return;
266 }
267
268 const sibling = document.getElementById('select_chat_search');
269 if (!sibling) {
270 console.error('Could not find sibling element for BackupsBrowser button');
271 return;
272 }
273
274 const button = document.createElement('button');
275 button.classList.add('menu_button', 'menu_button_icon');
276
277 const buttonIcon = document.createElement('i');
278 buttonIcon.classList.add('fa-solid', 'fa-box-open');
279
280 const buttonText = document.createElement('span');
281 buttonText.textContent = t`Backups`;
282 buttonText.title = t`Browse chat backups`;
283
284 const chevronIcon = document.createElement('i');
285 chevronIcon.classList.add('fa-solid', 'fa-chevron-down', 'fa-sm');
286
287 button.appendChild(buttonIcon);
288 button.appendChild(buttonText);
289 button.appendChild(chevronIcon);
290
291 button.addEventListener('click', () => {
292 if (this.#isOpen) {
293 this.closeBackups();
294 } else {
295 this.openBackups();
296 }
297 });
298
299 sibling.parentNode.insertBefore(button, sibling);
300
301 this.#buttonElement = button;
302 this.#buttonChevronIcon = chevronIcon;
303 }
304
305 renderBackupsList() {
306 if (this.#backupsListElement) {
307 return;
308 }
309
310 const sibling = document.getElementById('select_chat_div');
311 if (!sibling) {
312 console.error('Could not find sibling element for BackupsBrowser list');
313 return;
314 }
315
316 const list = document.createElement('div');
317 list.classList.add('chatBackupsList');
318
319 sibling.parentNode.insertBefore(list, sibling);
320 this.#backupsListElement = list;
321 }
322}
323
324const backupsBrowser = new BackupsBrowser();
325
326export function addChatBackupsBrowser() {
327 backupsBrowser.renderButton();
328 backupsBrowser.renderBackupsList();
329
330 // Refresh the backups list if it's already open
331 if (backupsBrowser.isOpen) {
332 backupsBrowser.closeBackups();
333 backupsBrowser.openBackups();
334 }
335}
public/scripts/chats.js+35 -19
@@ -10,7 +10,6 @@ import {
10 event_types,10 event_types,
11 getCurrentChatId,11 getCurrentChatId,
12 getRequestHeaders,12 getRequestHeaders,
13 name1,
14 name2,13 name2,
15 reloadCurrentChat,14 reloadCurrentChat,
16 saveSettingsDebounced,15 saveSettingsDebounced,
@@ -349,8 +348,7 @@ async function validateFile(file) {
349export function hasPendingFileAttachment() {348export function hasPendingFileAttachment() {
350 const fileInput = document.getElementById('file_form_input');349 const fileInput = document.getElementById('file_form_input');
351 if (!(fileInput instanceof HTMLInputElement)) return false;350 if (!(fileInput instanceof HTMLInputElement)) return false;
352 const file = fileInput.files[0];351 return fileInput.files.length > 0;
353 return !!file;
354}352}
355353
356/**354/**
@@ -2140,13 +2138,15 @@ export function initChatUtilities() {
2140 await viewMessageFile(messageId, fileIndex);2138 await viewMessageFile(messageId, fileIndex);
2141 });2139 });
21422140
2143 $(document).on('click', '.assistant_note_export', async function () {2141 $(document).on('click', '.assistant_note_export', async function (_e) {
2142 /** @type {ChatHeader} */
2143 const chatHeader = {
2144 chat_metadata: chat_metadata,
2145 user_name: 'unused',
2146 character_name: 'unused',
2147 };
2144 const chatToSave = [2148 const chatToSave = [
2145 {2149 chatHeader,
2146 user_name: name1,
2147 character_name: name2,
2148 chat_metadata: chat_metadata,
2149 },
2150 ...chat.filter(x => x?.extra?.type !== system_message_types.ASSISTANT_NOTE),2150 ...chat.filter(x => x?.extra?.type !== system_message_types.ASSISTANT_NOTE),
2151 ];2151 ];
21522152
@@ -2182,9 +2182,31 @@ export function initChatUtilities() {
2182 fileInput.click();2182 fileInput.click();
2183 });2183 });
21842184
2185 const fileInput = document.getElementById('file_form_input');
2186
2185 // Do not change. #attachFile is added by extension.2187 // Do not change. #attachFile is added by extension.
2186 $(document).on('click', '#attachFile', function () {2188 $(document).on('click', '#attachFile', function () {
2187 $('#file_form_input').trigger('click');2189 if (!(fileInput instanceof HTMLInputElement)) return;
2190 const $fileInput = $(fileInput);
2191
2192 // Preserve existing files in DataTransfer
2193 const dataTransfer = new DataTransfer();
2194 for (const file of fileInput.files) {
2195 dataTransfer.items.add(file);
2196 }
2197
2198 $fileInput.off('change').on('change', async () => {
2199 for (const file of fileInput.files) {
2200 if (!Array.from(dataTransfer.files).some(f => isSameFile(f, file))) {
2201 dataTransfer.items.add(file);
2202 }
2203 }
2204
2205 fileInput.files = dataTransfer.files;
2206 await onFileAttach(fileInput.files);
2207 });
2208
2209 $fileInput.trigger('click');
2188 });2210 });
21892211
2190 // Do not change. #manageAttachments is added by extension.2212 // Do not change. #manageAttachments is added by extension.
@@ -2347,11 +2369,6 @@ export function initChatUtilities() {
2347 await onImageSwiped(messageId, messageBlock, SWIPE_DIRECTION.RIGHT);2369 await onImageSwiped(messageId, messageBlock, SWIPE_DIRECTION.RIGHT);
2348 });2370 });
23492371
2350 $('#file_form_input').on('change', async () => {
2351 const fileInput = document.getElementById('file_form_input');
2352 if (!(fileInput instanceof HTMLInputElement)) return;
2353 await onFileAttach(fileInput.files);
2354 });
2355 $('#file_form').on('reset', function () {2372 $('#file_form').on('reset', function () {
2356 $('#file_form').addClass('displayNone');2373 $('#file_form').addClass('displayNone');
2357 });2374 });
@@ -2377,17 +2394,16 @@ export function initChatUtilities() {
2377 * @returns {Promise<void>}2394 * @returns {Promise<void>}
2378 */2395 */
2379 async function handleFileAttach(files) {2396 async function handleFileAttach(files) {
2380 const fileInput = document.getElementById('file_form_input');
2381 if (!(fileInput instanceof HTMLInputElement)) return;2397 if (!(fileInput instanceof HTMLInputElement)) return;
23822398
2383 // Workaround for Firefox: Use a DataTransfer object to indirectly set fileInput.files2399 // Workaround for Firefox: Use a DataTransfer object to indirectly set fileInput.files
2384 const dataTransfer = new DataTransfer();2400 const dataTransfer = new DataTransfer();
2385 for (let i = 0; i < files.length; i++) {2401 for (const file of fileInput.files) {
2386 dataTransfer.items.add(files[i]);2402 dataTransfer.items.add(file);
2387 }2403 }
23882404
2389 // Preserve existing non-duplicate files in the input2405 // Preserve existing non-duplicate files in the input
2390 for (const file of fileInput.files) {2406 for (const file of files) {
2391 if (!Array.from(dataTransfer.files).some(f => isSameFile(f, file))) {2407 if (!Array.from(dataTransfer.files).some(f => isSameFile(f, file))) {
2392 dataTransfer.items.add(file);2408 dataTransfer.items.add(file);
2393 }2409 }
public/scripts/constants.js+56 -5
@@ -1,5 +1,6 @@
1/**1/**
2 * Common debounce timeout values to use with `debounce` calls.2 * Common debounce timeout values to use with `debounce` calls.
3 * @readonly
3 * @enum {number}4 * @enum {number}
4 */5 */
5export const debounce_timeout = {6export const debounce_timeout = {
@@ -68,8 +69,8 @@ export const COMETAPI_IGNORE_PATTERNS = [
68];69];
6970
70/**71/**
71 * @enum {string}
72 * @readonly72 * @readonly
73 * @enum {string}
73 */74 */
74export const MEDIA_SOURCE = {75export const MEDIA_SOURCE = {
75 API: 'api',76 API: 'api',
@@ -79,8 +80,8 @@ export const MEDIA_SOURCE = {
79};80};
8081
81/**82/**
82 * @enum {string}
83 * @readonly83 * @readonly
84 * @enum {string}
84 */85 */
85export const MEDIA_DISPLAY = {86export const MEDIA_DISPLAY = {
86 LIST: 'list',87 LIST: 'list',
@@ -88,8 +89,8 @@ export const MEDIA_DISPLAY = {
88};89};
8990
90/**91/**
91 * @enum {string}
92 * @readonly92 * @readonly
93 * @enum {string}
93 */94 */
94export const IMAGE_OVERSWIPE = {95export const IMAGE_OVERSWIPE = {
95 GENERATE: 'generate',96 GENERATE: 'generate',
@@ -118,9 +119,20 @@ export const MEDIA_TYPE = {
118};119};
119120
120/**121/**
122 * Bitwise flag-style media request types.
123 * @readonly
124 * @enum {number}
125 */
126export const MEDIA_REQUEST_TYPE = {
127 IMAGE: 0b001,
128 VIDEO: 0b010,
129 AUDIO: 0b100,
130};
131
132/**
121 * Scroll behavior options when appending media to messages.133 * Scroll behavior options when appending media to messages.
122 * @enum {string}
123 * @readonly134 * @readonly
135 * @enum {string}
124 */136 */
125export const SCROLL_BEHAVIOR = {137export const SCROLL_BEHAVIOR = {
126 NONE: 'none',138 NONE: 'none',
@@ -129,9 +141,48 @@ export const SCROLL_BEHAVIOR = {
129};141};
130142
131/**143/**
132 * @type {{readonly LEFT: 'left', readonly RIGHT: 'right'}}144 * @readonly
145 * @enum {string}
146 */
147export const OVERSWIPE_BEHAVIOR = {
148 /** The overswipe right chevron will not be displayed. */
149 NONE: 'none',
150 /** An overswipe will loop to the first swipe. */
151 LOOP: 'loop',
152 /** Pristine greetings will loop, and chevrons will always be shown: https://github.com/SillyTavern/SillyTavern/pull/4712#issuecomment-3557893373 */
153 PRISTINE_GREETING: 'pristine_greeting',
154 /** If chat tree is enabled, then an overswipe will allow the user to edit the message before starting a new generation. */
155 EDIT_GENERATE: 'edit_generate',
156 /** This is the default behavior on character messages. */
157 REGENERATE: 'regenerate',
158};
159
160/**
161 * @readonly
162 * @enum {string}
133 */163 */
134export const SWIPE_DIRECTION = {164export const SWIPE_DIRECTION = {
135 LEFT: 'left',165 LEFT: 'left',
136 RIGHT: 'right',166 RIGHT: 'right',
137};167};
168
169/**
170 * @readonly
171 * @enum {string}
172 */
173export const SWIPE_SOURCE = {
174 DELETE: 'delete',
175 KEYBOARD: 'keyboard',
176 BACK: 'back',
177 AUTO_SWIPE: 'auto_swipe',
178};
179
180/**
181 * @readonly
182 * @enum {string}
183 */
184export const SWIPE_STATE = {
185 NONE: 'none',
186 SWIPING: 'swiping',
187 EDITING: 'editing',
188};
public/scripts/custom-request.js+166 -150
@@ -1,9 +1,9 @@
1import { getPresetManager } from './preset-manager.js';1import { getPresetManager } from './preset-manager.js';
2import { extractJsonFromData, extractMessageFromData, getGenerateUrl, getRequestHeaders } from '../script.js';2import { extractJsonFromData, extractMessageFromData, getGenerateUrl, getRequestHeaders, name1, name2 } from '../script.js';
3import { getTextGenServer } from './textgen-settings.js';3import { getTextGenServer, createTextGenGenerationData, setting_names, textgenerationwebui_settings } from './textgen-settings.js';
4import { extractReasoningFromData } from './reasoning.js';4import { extractReasoningFromData } from './reasoning.js';
5import { formatInstructModeChat, formatInstructModePrompt, getInstructStoppingSequences, names_behavior_types } from './instruct-mode.js';5import { formatInstructModeChat, formatInstructModePrompt, getInstructStoppingSequences } from './instruct-mode.js';
6import { getStreamingReply, tryParseStreamingError } from './openai.js';6import { getStreamingReply, tryParseStreamingError, createGenerationParameters, settingsToUpdate, oai_settings } from './openai.js';
7import EventSourceStream from './sse-stream.js';7import EventSourceStream from './sse-stream.js';
88
9// #region Type Definitions9// #region Type Definitions
@@ -34,6 +34,7 @@ import EventSourceStream from './sse-stream.js';
3434
35/**35/**
36 * @typedef {Object} ChatCompletionMessage36 * @typedef {Object} ChatCompletionMessage
37 * @property {string} [name] - The name of the message author (optional)
37 * @property {string} role - The role of the message author (e.g., "user", "assistant", "system")38 * @property {string} role - The role of the message author (e.g., "user", "assistant", "system")
38 * @property {string} content - The content of the message39 * @property {string} content - The content of the message
39 */40 */
@@ -125,7 +126,7 @@ export class TextCompletionService {
125126
126 const json = await response.json();127 const json = await response.json();
127 if (!response.ok || json.error) {128 if (!response.ok || json.error) {
128 throw json;129 throw new Error(String(json.error?.message || 'Response not OK'));
129 }130 }
130131
131 if (!extractData) {132 if (!extractData) {
@@ -188,8 +189,87 @@ export class TextCompletionService {
188 }189 }
189190
190 /**191 /**
192 * Return a formatted prompt string given an array of messages, a chosen instruct preset, and instruct settings.
193 * @param {(ChatCompletionMessage & {ignoreInstruct?: boolean})[]} prompt An array of messages
194 * @param {InstructSettings|string} instructPreset Either the name of an instruct preset or the instruct preset object itself.
195 * @param {Partial<InstructSettings>} instructSettings Optional instruct settings
196 */
197 static constructPrompt(prompt, instructPreset, instructSettings) {
198 // InstructPreset may either be a name or itself a preset
199 if (typeof instructPreset === 'string') {
200 const instructPresetManager = getPresetManager('instruct');
201 instructPreset = instructPresetManager?.getCompletionPresetByName(instructPreset);
202 }
203
204 // Clone the preset to avoid modifying the original
205 instructPreset = structuredClone(instructPreset);
206 if (instructSettings) { // apply any additional settings
207 Object.assign(instructPreset, instructSettings);
208 }
209
210 // Make the type check shut up. We 100% don't have a string here.
211 if (typeof instructPreset === 'string') {
212 return;
213 }
214
215 // Format messages using instruct formatting
216 const formattedMessages = [];
217 const prefillActive = prompt.length > 0 ? prompt[prompt.length - 1].role === 'assistant' : false;
218 for (const message of prompt) {
219 let messageContent = message.content;
220 if (!message.ignoreInstruct) {
221 const isLastMessage = message === prompt[prompt.length - 1];
222
223 // This complicated logic means:
224 // 1. If prefill is not active, format all messages
225 // 2. If prefill is active, format all messages except the last one
226 if (!isLastMessage || !prefillActive) {
227 messageContent = formatInstructModeChat(
228 message.name ?? message.role,
229 message.content,
230 message.role === 'user',
231 message.role === 'system',
232 undefined,
233 name1, // for macros
234 name2, // for macros
235 undefined,
236 instructPreset,
237 );
238 }
239
240 // Add prompt formatting for the last message.
241 // e.g. "<|im_start|>assistant"
242 if (isLastMessage) {
243 let last_line = formatInstructModePrompt(
244 'assistant', // for sequences using {{name}}
245 false, // not an impersonation
246 prefillActive ? message.content : undefined, // if using prefill, last message is the prefill
247 name1, // for macros
248 name2, // for macros
249 true, // quiet
250 false,
251 instructPreset,
252 );
253
254 if (prefillActive) { // content is the prefilled message
255 if (last_line.endsWith('\n') && !message.content.endsWith('\n')) {
256 last_line = last_line.slice(0, -1); // remove newline after prefill if it's not in the prefill itself
257 }
258 messageContent = last_line;
259 } else { // append last line to content (e.g. "<|im_start|>assistant:")
260 messageContent += last_line;
261 }
262 }
263 }
264 formattedMessages.push(messageContent);
265 }
266 return formattedMessages.join('');
267 }
268
269
270 /**
191 * Process and send a text completion request with optional preset & instruct271 * Process and send a text completion request with optional preset & instruct
192 * @param {Record<string, any> & TextCompletionRequestBase & {prompt: (ChatCompletionMessage & {ignoreInstruct?: boolean})[] |string}} custom272 * @param {TextCompletionPayload} requestData
193 * @param {Object} options - Configuration options273 * @param {Object} options - Configuration options
194 * @param {string?} [options.presetName] - Name of the preset to use for generation settings274 * @param {string?} [options.presetName] - Name of the preset to use for generation settings
195 * @param {string?} [options.instructName] - Name of instruct preset for message formatting275 * @param {string?} [options.instructName] - Name of instruct preset for message formatting
@@ -199,15 +279,35 @@ export class TextCompletionService {
199 * @returns {Promise<ExtractedData | (() => AsyncGenerator<StreamResponse>)>} If not streaming, returns extracted data; if streaming, returns a function that creates an AsyncGenerator279 * @returns {Promise<ExtractedData | (() => AsyncGenerator<StreamResponse>)>} If not streaming, returns extracted data; if streaming, returns a function that creates an AsyncGenerator
200 * @throws {Error}280 * @throws {Error}
201 */281 */
202 static async processRequest(282 static async processRequest(requestData, options = {}, extractData = true, signal = null) {
203 custom,
204 options = {},
205 extractData = true,
206 signal = null,
207 ) {
208 const { presetName, instructName } = options;283 const { presetName, instructName } = options;
209 let requestData = { ...custom };284
210 const prompt = custom.prompt;285 // remove any undefined params in given request data
286 requestData = this.createRequestData(requestData);
287
288 /** @type {InstructSettings | undefined} */
289 let instructPreset;
290 const prompt = requestData.prompt;
291 // Handle instruct formatting if requested
292 if (Array.isArray(prompt)) {
293 if (instructName) {
294 const instructPresetManager = getPresetManager('instruct');
295 instructPreset = instructPresetManager?.getCompletionPresetByName(instructName);
296 if (instructPreset) {
297 requestData.prompt = this.constructPrompt(prompt, instructPreset, options.instructSettings);
298 const stoppingStrings = getInstructStoppingSequences({ customInstruct: instructPreset, useStopStrings: false });
299 requestData.stop = stoppingStrings;
300 requestData.stopping_strings = stoppingStrings;
301 } else {
302 console.warn(`Instruct preset "${instructName}" not found, using basic formatting`);
303 requestData.prompt = prompt.map(x => x.content).join('\n\n');
304 }
305 } else {
306 requestData.prompt = prompt.map(x => x.content).join('\n\n');
307 }
308 } else if (typeof prompt === 'string') {
309 requestData.prompt = prompt;
310 }
211311
212 // Apply generation preset if specified312 // Apply generation preset if specified
213 if (presetName) {313 if (presetName) {
@@ -215,9 +315,8 @@ export class TextCompletionService {
215 if (presetManager) {315 if (presetManager) {
216 const preset = presetManager.getCompletionPresetByName(presetName);316 const preset = presetManager.getCompletionPresetByName(presetName);
217 if (preset) {317 if (preset) {
218 // Convert preset to payload and merge with custom parameters318 // Convert preset to payload and merge with custom data
219 const presetPayload = this.presetToGeneratePayload(preset, {});319 requestData = this.presetToGeneratePayload(preset, {}, requestData);
220 requestData = { ...presetPayload, ...requestData };
221 } else {320 } else {
222 console.warn(`Preset "${presetName}" not found, continuing with default settings`);321 console.warn(`Preset "${presetName}" not found, continuing with default settings`);
223 }322 }
@@ -226,99 +325,10 @@ export class TextCompletionService {
226 }325 }
227 }326 }
228327
328 const response = await this.sendRequest(requestData, extractData, signal);
229329
230 /** @type {InstructSettings | undefined} */
231 let instructPreset;
232 // Handle instruct formatting if requested
233 if (Array.isArray(prompt) && instructName) {
234 const instructPresetManager = getPresetManager('instruct');
235 instructPreset = instructPresetManager?.getCompletionPresetByName(instructName);
236 if (instructPreset) {
237 // Clone the preset to avoid modifying the original
238 instructPreset = structuredClone(instructPreset);
239 instructPreset.names_behavior = names_behavior_types.NONE;
240 if (options.instructSettings) {
241 Object.assign(instructPreset, options.instructSettings);
242 }
243
244 // Format messages using instruct formatting
245 const formattedMessages = [];
246 const prefillActive = prompt.length > 0 ? prompt[prompt.length - 1].role === 'assistant' : false;
247 for (const message of prompt) {
248 let messageContent = message.content;
249 if (!message.ignoreInstruct) {
250 const isLastMessage = message === prompt[prompt.length - 1];
251
252 // This complicated logic means:
253 // 1. If prefill is not active, format all messages
254 // 2. If prefill is active, format all messages except the last one
255 if (!isLastMessage || !prefillActive) {
256 messageContent = formatInstructModeChat(
257 message.role,
258 message.content,
259 message.role === 'user',
260 message.role === 'system',
261 undefined,
262 undefined,
263 undefined,
264 undefined,
265 instructPreset,
266 );
267 }
268
269 // Add prompt formatting for the last message.
270 if (isLastMessage) {
271 if (!prefillActive) { // e.g. "<|im_start|>user:"
272 messageContent += formatInstructModePrompt(
273 undefined,
274 false,
275 undefined,
276 undefined,
277 undefined,
278 false,
279 false,
280 instructPreset,
281 );
282 } else { // e.g. "<|im_start|>assistant: Hello, my name is"
283 const overriddenInstructPreset = structuredClone(instructPreset);
284 overriddenInstructPreset.output_suffix = '';
285 overriddenInstructPreset.wrap = false;
286 messageContent = formatInstructModeChat(
287 message.role,
288 message.content,
289 false, // since it is assistant
290 false,
291 undefined,
292 undefined,
293 undefined,
294 undefined,
295 overriddenInstructPreset,
296 );
297 }
298 }
299 }
300 formattedMessages.push(messageContent);
301 }
302 requestData.prompt = formattedMessages.join('');
303 const stoppingStrings = getInstructStoppingSequences({ customInstruct: instructPreset, useStopStrings: false });
304 requestData.stop = stoppingStrings;
305 requestData.stopping_strings = stoppingStrings;
306 } else {
307 console.warn(`Instruct preset "${instructName}" not found, using basic formatting`);
308 requestData.prompt = prompt.map(x => x.content).join('\n\n');
309 }
310 } else if (typeof prompt === 'string') {
311 requestData.prompt = prompt;
312 } else {
313 requestData.prompt = prompt.map(x => x.content).join('\n\n');
314 }
315
316 // @ts-ignore
317 const data = this.createRequestData(requestData);
318
319 const response = await this.sendRequest(data, extractData, signal);
320 // Remove stopping strings from the end330 // Remove stopping strings from the end
321 if (!data.stream && extractData) {331 if (!requestData.stream && extractData) {
322 /** @type {ExtractedData} */332 /** @type {ExtractedData} */
323 // @ts-ignore333 // @ts-ignore
324 const extractedData = response;334 const extractedData = response;
@@ -377,31 +387,30 @@ export class TextCompletionService {
377 * Converts a preset to a valid text completion payload.387 * Converts a preset to a valid text completion payload.
378 * Only supports temperature.388 * Only supports temperature.
379 * @param {Object} preset - The preset configuration389 * @param {Object} preset - The preset configuration
380 * @param {Object} customPreset - Additional parameters to override preset values390 * @param {Object} overridePreset - Additional parameters to override preset values
391 * @param {Object} overridePayload - Additional parameters to override payload values
381 * @returns {Object} - Formatted payload for text completion API392 * @returns {Object} - Formatted payload for text completion API
382 */393 */
383 static presetToGeneratePayload(preset, customPreset = {}) {394 static presetToGeneratePayload(preset, overridePreset = {}, overridePayload = {}) {
384 if (!preset || typeof preset !== 'object') {395 if (!preset || typeof preset !== 'object') {
385 throw new Error('Invalid preset: must be an object');396 throw new Error('Invalid preset: must be an object');
386 }397 }
387398
388 // Merge preset with custom parameters399 // apply preset overrides
389 const settings = { ...preset, ...customPreset };400 preset = { ...preset, ...overridePreset };
390401
391 // Initialize base payload with common parameters402 // Only take fields from the preset specified in setting_names to use as TextCompletionSettings
392 let payload = {403 const settings = structuredClone(textgenerationwebui_settings);
393 'temperature': settings.temp >= 0 ? Number(settings.temp) : undefined,404 for (const [key, value] of Object.entries(preset)) {
394 'min_p': settings.min_p >= 0 ? Number(settings.min_p) : undefined,405 if (!setting_names.includes(key)) continue;
395 };406 settings[key] = value;
407 }
396408
397 // Remove undefined values to avoid API errors409 // convert to a generation payload
398 Object.keys(payload).forEach(key => {410 const payload = createTextGenGenerationData(settings, overridePayload.model, overridePayload.prompt, preset.genamt);
399 if (payload[key] === undefined) {
400 delete payload[key];
401 }
402 });
403411
404 return payload;412 // apply overrides
413 return this.createRequestData({ ...payload, ...overridePayload });
405 }414 }
406}415}
407416
@@ -427,8 +436,7 @@ export class ChatCompletionService {
427 reverse_proxy,436 reverse_proxy,
428 proxy_password,437 proxy_password,
429 custom_prompt_post_processing,438 custom_prompt_post_processing,
430 use_makersuite_sysprompt: true,439 use_sysprompt: true,
431 claude_use_sysprompt: true,
432 ...props,440 ...props,
433 };441 };
434442
@@ -462,7 +470,7 @@ export class ChatCompletionService {
462 if (!data.stream) {470 if (!data.stream) {
463 const json = await response.json();471 const json = await response.json();
464 if (!response.ok || json.error) {472 if (!response.ok || json.error) {
465 throw json;473 throw new Error(String(json.error?.message || 'Response not OK'));
466 }474 }
467475
468 if (!extractData) {476 if (!extractData) {
@@ -524,7 +532,7 @@ export class ChatCompletionService {
524532
525 /**533 /**
526 * Process and send a chat completion request with optional preset534 * Process and send a chat completion request with optional preset
527 * @param {ChatCompletionPayload} custom535 * @param {ChatCompletionPayload} requestData - payload data, overriding preset if given
528 * @param {Object} options - Configuration options536 * @param {Object} options - Configuration options
529 * @param {string?} [options.presetName] - Name of the preset to use for generation settings537 * @param {string?} [options.presetName] - Name of the preset to use for generation settings
530 * @param {boolean} [extractData=true] - Whether to extract structured data from response538 * @param {boolean} [extractData=true] - Whether to extract structured data from response
@@ -532,9 +540,9 @@ export class ChatCompletionService {
532 * @returns {Promise<ExtractedData | (() => AsyncGenerator<StreamResponse>)>} If not streaming, returns extracted data; if streaming, returns a function that creates an AsyncGenerator540 * @returns {Promise<ExtractedData | (() => AsyncGenerator<StreamResponse>)>} If not streaming, returns extracted data; if streaming, returns a function that creates an AsyncGenerator
533 * @throws {Error}541 * @throws {Error}
534 */542 */
535 static async processRequest(custom, options, extractData = true, signal = null) {543 static async processRequest(requestData, options, extractData = true, signal = null) {
536 const { presetName } = options;544 const { presetName } = options;
537 let requestData = { ...custom };545 requestData = this.createRequestData(requestData);
538546
539 // Apply generation preset if specified547 // Apply generation preset if specified
540 if (presetName) {548 if (presetName) {
@@ -543,8 +551,7 @@ export class ChatCompletionService {
543 const preset = presetManager.getCompletionPresetByName(presetName);551 const preset = presetManager.getCompletionPresetByName(presetName);
544 if (preset) {552 if (preset) {
545 // Convert preset to payload and merge with custom parameters553 // Convert preset to payload and merge with custom parameters
546 const presetPayload = this.presetToGeneratePayload(preset, {});554 requestData = await this.presetToGeneratePayload(preset, {}, requestData);
547 requestData = { ...presetPayload, ...requestData };
548 } else {555 } else {
549 console.warn(`Preset "${presetName}" not found, continuing with default settings`);556 console.warn(`Preset "${presetName}" not found, continuing with default settings`);
550 }557 }
@@ -553,38 +560,47 @@ export class ChatCompletionService {
553 }560 }
554 }561 }
555562
556 const data = this.createRequestData(requestData);563 return await this.sendRequest(requestData, extractData, signal);
557
558 return await this.sendRequest(data, extractData, signal);
559 }564 }
560565
561 /**566 /**
562 * Converts a preset to a valid chat completion payload567 * Converts a preset to a valid chat completion payload
563 * Only supports temperature.568 * Only supports temperature.
564 * @param {Object} preset - The preset configuration569 * @param {Object} preset - The preset configuration
565 * @param {Object} customParams - Additional parameters to override preset values570 * @param {Object} overridePreset - Additional parameters to override preset values
566 * @returns {Object} - Formatted payload for chat completion API571 * @param {Object} overridePayload - Additional parameters to override payload values
572 * @returns {Promise<any>} - Formatted payload for chat completion API
567 */573 */
568 static presetToGeneratePayload(preset, customParams = {}) {574 static async presetToGeneratePayload(preset, overridePreset = {}, overridePayload = {}) {
569 if (!preset || typeof preset !== 'object') {575 if (!preset || typeof preset !== 'object') {
570 throw new Error('Invalid preset: must be an object');576 throw new Error('Invalid preset: must be an object');
571 }577 }
572578
573 // Merge preset with custom parameters579 // apply preset overrides
574 const settings = { ...preset, ...customParams };580 preset = { ...preset, ...overridePreset };
575581
576 // Initialize base payload with common parameters582 // Fix any fields before converting to settings
577 const payload = {583 preset.bias_preset_selected = preset.bias_presets !== undefined ? preset.bias_preset_selected : undefined; // presets might have bias_preset_selected but not bias_presets, but settings need both or neither.
578 temperature: settings.temperature >= 0 ? Number(settings.temperature) : undefined,
579 };
580584
581 // Remove undefined values to avoid API errors585 // Convert from preset to ChatCompletionSettings
582 Object.keys(payload).forEach(key => {586 const settings = structuredClone(oai_settings);
583 if (payload[key] === undefined) {587 for (const [key, value] of Object.entries(preset)) {
584 delete payload[key];588 const settingToUpdate = settingsToUpdate[key];
585 }589 if (!settingToUpdate) continue;
590 settings[settingToUpdate[1]] = value;
591 }
592
593 // Ensure api-url is properly applied for all sources that accept it
594 ['custom_url', 'vertexai_region', 'zai_endpoint'].forEach(field => {
595 // The order is: connection profile => CC preset => CC settings
596 overridePayload[field] = overridePayload[field] || settings[field] || oai_settings[field];
586 });597 });
587598
588 return payload;599 // Convert from settings to generation payload
600 const data = await createGenerationParameters(settings, overridePayload.model, 'quiet', overridePayload.messages);
601 const payload = data.generate_data;
602
603 // apply overrides
604 return this.createRequestData({ ...payload, ...overridePayload });
589 }605 }
590}606}
public/scripts/data-maid.js+1 -1
@@ -68,7 +68,7 @@ class DataMaidDialog {
68 async getReport() {68 async getReport() {
69 const response = await fetch('/api/data-maid/report', {69 const response = await fetch('/api/data-maid/report', {
70 method: 'POST',70 method: 'POST',
71 headers: getRequestHeaders(),71 headers: getRequestHeaders({ omitContentType: true }),
72 });72 });
7373
74 if (!response.ok) {74 if (!response.ok) {
public/scripts/events.js+1 -0
@@ -91,6 +91,7 @@ export const event_types = {
91 PRESET_RENAMED_BEFORE: 'preset_renamed_before',91 PRESET_RENAMED_BEFORE: 'preset_renamed_before',
92 MAIN_API_CHANGED: 'main_api_changed',92 MAIN_API_CHANGED: 'main_api_changed',
93 WORLDINFO_ENTRIES_LOADED: 'worldinfo_entries_loaded',93 WORLDINFO_ENTRIES_LOADED: 'worldinfo_entries_loaded',
94 WORLDINFO_SCAN_DONE: 'worldinfo_scan_done',
94 MEDIA_ATTACHMENT_DELETED: 'media_attachment_deleted',95 MEDIA_ATTACHMENT_DELETED: 'media_attachment_deleted',
95};96};
9697
public/scripts/extensions/assets/index.js+3 -3
@@ -139,8 +139,8 @@ async function downloadAssetsList(url) {
139 assetTypeMenu.append(await renderExtensionTemplateAsync('assets', 'installation'));139 assetTypeMenu.append(await renderExtensionTemplateAsync('assets', 'installation'));
140 }140 }
141141
142 for (const i in availableAssets[assetType].sort((a, b) => a?.name && b?.name && a['name'].localeCompare(b['name']))) {142 for (const asset of availableAssets[assetType].sort((a, b) => a?.name && b?.name && a['name'].localeCompare(b['name']))) {
143 const asset = availableAssets[assetType][i];143 const i = availableAssets[assetType].indexOf(asset);
144 const elemId = `assets_install_${assetType}_${i}`;144 const elemId = `assets_install_${assetType}_${i}`;
145 let element = $('<div />', { id: elemId, class: 'asset-download-button right_menu_button' });145 let element = $('<div />', { id: elemId, class: 'asset-download-button right_menu_button' });
146 const label = $('<i class="fa-fw fa-solid fa-download fa-lg"></i>');146 const label = $('<i class="fa-fw fa-solid fa-download fa-lg"></i>');
@@ -425,7 +425,7 @@ async function updateCurrentAssets() {
425 try {425 try {
426 const result = await fetch('/api/assets/get', {426 const result = await fetch('/api/assets/get', {
427 method: 'POST',427 method: 'POST',
428 headers: getRequestHeaders(),428 headers: getRequestHeaders({ omitContentType: true }),
429 });429 });
430 currentAssets = result.ok ? (await result.json()) : {};430 currentAssets = result.ok ? (await result.json()) : {};
431 }431 }
public/scripts/extensions/attachments/index.js+1 -1
@@ -218,7 +218,7 @@ function cleanUpAttachments() {
218218
219/**219/**
220 * Clean up character attachments when a character is deleted.220 * Clean up character attachments when a character is deleted.
221 * @param {{character: import('../../char-data.js').v1CharData}} data Event data221 * @param {{character: Character}} data Event data
222 */222 */
223function cleanUpCharacterAttachments(data) {223function cleanUpCharacterAttachments(data) {
224 const avatar = data?.character?.avatar;224 const avatar = data?.character?.avatar;
public/scripts/extensions/caption/index.js+7 -4
@@ -10,7 +10,7 @@ import { SlashCommand } from '../../slash-commands/SlashCommand.js';
10import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from '../../slash-commands/SlashCommandArgument.js';10import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from '../../slash-commands/SlashCommandArgument.js';
11import { commonEnumProviders } from '../../slash-commands/SlashCommandCommonEnumsProvider.js';11import { commonEnumProviders } from '../../slash-commands/SlashCommandCommonEnumsProvider.js';
12import { callGenericPopup, Popup, POPUP_TYPE } from '../../popup.js';12import { callGenericPopup, Popup, POPUP_TYPE } from '../../popup.js';
13import { MEDIA_DISPLAY, MEDIA_SOURCE, MEDIA_TYPE, SCROLL_BEHAVIOR } from '../../constants.js';13import { debounce_timeout, MEDIA_DISPLAY, MEDIA_SOURCE, MEDIA_TYPE, SCROLL_BEHAVIOR } from '../../constants.js';
14export { MODULE_NAME };14export { MODULE_NAME };
1515
16const MODULE_NAME = 'caption';16const MODULE_NAME = 'caption';
@@ -108,7 +108,7 @@ async function wrapCaptionTemplate(caption) {
108 'Review and edit the generated caption:',108 'Review and edit the generated caption:',
109 'Press "Cancel" to abort the caption sending.',109 'Press "Cancel" to abort the caption sending.',
110 messageText,110 messageText,
111 { rows: 5, okButton: 'Send' });111 { rows: 8, okButton: 'Send' });
112112
113 if (!messageText) {113 if (!messageText) {
114 throw new Error('User aborted the caption sending.');114 throw new Error('User aborted the caption sending.');
@@ -211,6 +211,7 @@ async function sendCaptionedMessage(caption, image, mimeType) {
211 context.addOneMessage(message);211 context.addOneMessage(message);
212 await eventSource.emit(event_types.USER_MESSAGE_RENDERED, messageId);212 await eventSource.emit(event_types.USER_MESSAGE_RENDERED, messageId);
213 await context.saveChat();213 await context.saveChat();
214 setTimeout(() => context.scrollOnMediaLoad(), debounce_timeout.short);
214}215}
215216
216/**217/**
@@ -454,7 +455,7 @@ function isVideoCaptioningAvailable() {
454 return false;455 return false;
455 }456 }
456457
457 return ['google', 'vertexai'].includes(extension_settings.caption.multimodal_api);458 return ['google', 'vertexai', 'zai'].includes(extension_settings.caption.multimodal_api);
458}459}
459460
460jQuery(async function () {461jQuery(async function () {
@@ -503,6 +504,7 @@ jQuery(async function () {
503 'aimlapi': SECRET_KEYS.AIMLAPI,504 'aimlapi': SECRET_KEYS.AIMLAPI,
504 'moonshot': SECRET_KEYS.MOONSHOT,505 'moonshot': SECRET_KEYS.MOONSHOT,
505 'nanogpt': SECRET_KEYS.NANOGPT,506 'nanogpt': SECRET_KEYS.NANOGPT,
507 'chutes': SECRET_KEYS.CHUTES,
506 'electronhub': SECRET_KEYS.ELECTRONHUB,508 'electronhub': SECRET_KEYS.ELECTRONHUB,
507 'zai': SECRET_KEYS.ZAI,509 'zai': SECRET_KEYS.ZAI,
508 };510 };
@@ -592,7 +594,7 @@ jQuery(async function () {
592 const options = Array.from(dropdown.options);594 const options = Array.from(dropdown.options);
593 const response = await fetch(url, {595 const response = await fetch(url, {
594 method: 'POST',596 method: 'POST',
595 headers: getRequestHeaders(),597 headers: getRequestHeaders({ omitContentType: true }),
596 });598 });
597 if (!response.ok) {599 if (!response.ok) {
598 return;600 return;
@@ -616,6 +618,7 @@ jQuery(async function () {
616 await processEndpoint('aimlapi', '/api/backends/chat-completions/multimodal-models/aimlapi');618 await processEndpoint('aimlapi', '/api/backends/chat-completions/multimodal-models/aimlapi');
617 await processEndpoint('pollinations', '/api/backends/chat-completions/multimodal-models/pollinations');619 await processEndpoint('pollinations', '/api/backends/chat-completions/multimodal-models/pollinations');
618 await processEndpoint('nanogpt', '/api/backends/chat-completions/multimodal-models/nanogpt');620 await processEndpoint('nanogpt', '/api/backends/chat-completions/multimodal-models/nanogpt');
621 await processEndpoint('chutes', '/api/backends/chat-completions/multimodal-models/chutes');
619 await processEndpoint('electronhub', '/api/backends/chat-completions/multimodal-models/electronhub');622 await processEndpoint('electronhub', '/api/backends/chat-completions/multimodal-models/electronhub');
620 await processEndpoint('mistral', '/api/backends/chat-completions/multimodal-models/mistral');623 await processEndpoint('mistral', '/api/backends/chat-completions/multimodal-models/mistral');
621 await processEndpoint('xai', '/api/backends/chat-completions/multimodal-models/xai');624 await processEndpoint('xai', '/api/backends/chat-completions/multimodal-models/xai');
public/scripts/extensions/caption/settings.html+12 -3
@@ -18,6 +18,7 @@
18 <label for="caption_multimodal_api" data-i18n="API">API</label>18 <label for="caption_multimodal_api" data-i18n="API">API</label>
19 <select id="caption_multimodal_api" class="flex1 text_pole">19 <select id="caption_multimodal_api" class="flex1 text_pole">
20 <option value="aimlapi">AI/ML API</option>20 <option value="aimlapi">AI/ML API</option>
21 <option value="chutes">Chutes</option>
21 <option value="anthropic">Claude</option>22 <option value="anthropic">Claude</option>
22 <option value="cohere">Cohere</option>23 <option value="cohere">Cohere</option>
23 <option value="custom" data-i18n="Custom (OpenAI-compatible)">Custom (OpenAI-compatible)</option>24 <option value="custom" data-i18n="Custom (OpenAI-compatible)">Custom (OpenAI-compatible)</option>
@@ -55,6 +56,9 @@
55 <option data-type="moonshot" value="moonshot-v1-8k-vision-preview">moonshot-v1-8k-vision-preview</option>56 <option data-type="moonshot" value="moonshot-v1-8k-vision-preview">moonshot-v1-8k-vision-preview</option>
56 <option data-type="moonshot" value="moonshot-v1-32k-vision-preview">moonshot-v1-32k-vision-preview</option>57 <option data-type="moonshot" value="moonshot-v1-32k-vision-preview">moonshot-v1-32k-vision-preview</option>
57 <option data-type="moonshot" value="moonshot-v1-128k-vision-preview">moonshot-v1-128k-vision-preview</option>58 <option data-type="moonshot" value="moonshot-v1-128k-vision-preview">moonshot-v1-128k-vision-preview</option>
59 <option data-type="openai" value="gpt-5.2">gpt-5.2</option>
60 <option data-type="openai" value="gpt-5.2-2025-12-11">gpt-5.2-2025-12-11</option>
61 <option data-type="openai" value="gpt-5.2-chat-latest">gpt-5.2-chat-latest</option>
58 <option data-type="openai" value="gpt-5.1">gpt-5.1</option>62 <option data-type="openai" value="gpt-5.1">gpt-5.1</option>
59 <option data-type="openai" value="gpt-5.1-2025-11-13">gpt-5.1-2025-11-13</option>63 <option data-type="openai" value="gpt-5.1-2025-11-13">gpt-5.1-2025-11-13</option>
60 <option data-type="openai" value="gpt-5.1-chat-latest">gpt-5.1-chat-latest</option>64 <option data-type="openai" value="gpt-5.1-chat-latest">gpt-5.1-chat-latest</option>
@@ -85,6 +89,8 @@
85 <option data-type="openai" value="o4-mini-2025-04-16">o4-mini-2025-04-16</option>89 <option data-type="openai" value="o4-mini-2025-04-16">o4-mini-2025-04-16</option>
86 <option data-type="openai" value="gpt-4.5-preview">gpt-4.5-preview</option>90 <option data-type="openai" value="gpt-4.5-preview">gpt-4.5-preview</option>
87 <option data-type="openai" value="gpt-4.5-preview-2025-02-27">gpt-4.5-preview-2025-02-27</option>91 <option data-type="openai" value="gpt-4.5-preview-2025-02-27">gpt-4.5-preview-2025-02-27</option>
92 <option data-type="anthropic" value="claude-opus-4-5">claude-opus-4-5</option>
93 <option data-type="anthropic" value="claude-opus-4-5-20251101">claude-opus-4-5-20251101</option>
88 <option data-type="anthropic" value="claude-sonnet-4-5">claude-sonnet-4-5</option>94 <option data-type="anthropic" value="claude-sonnet-4-5">claude-sonnet-4-5</option>
89 <option data-type="anthropic" value="claude-sonnet-4-5-20250929">claude-sonnet-4-5-20250929</option>95 <option data-type="anthropic" value="claude-sonnet-4-5-20250929">claude-sonnet-4-5-20250929</option>
90 <option data-type="anthropic" value="claude-haiku-4-5">claude-haiku-4-5</option>96 <option data-type="anthropic" value="claude-haiku-4-5">claude-haiku-4-5</option>
@@ -106,6 +112,7 @@
106 <option data-type="anthropic" value="claude-3-haiku-20240307">claude-3-haiku-20240307</option>112 <option data-type="anthropic" value="claude-3-haiku-20240307">claude-3-haiku-20240307</option>
107 <option data-type="google" value="gemini-3-pro-preview">gemini-3-pro-preview</option>113 <option data-type="google" value="gemini-3-pro-preview">gemini-3-pro-preview</option>
108 <option data-type="google" value="gemini-3-pro-image-preview">gemini-3-pro-image-preview</option>114 <option data-type="google" value="gemini-3-pro-image-preview">gemini-3-pro-image-preview</option>
115 <option data-type="google" value="gemini-3-flash-preview">gemini-3-flash-preview</option>
109 <option data-type="google" value="gemini-2.5-pro">gemini-2.5-pro</option>116 <option data-type="google" value="gemini-2.5-pro">gemini-2.5-pro</option>
110 <option data-type="google" value="gemini-2.5-pro-preview-06-05">gemini-2.5-pro-preview-06-05</option>117 <option data-type="google" value="gemini-2.5-pro-preview-06-05">gemini-2.5-pro-preview-06-05</option>
111 <option data-type="google" value="gemini-2.5-pro-preview-05-06">gemini-2.5-pro-preview-05-06</option>118 <option data-type="google" value="gemini-2.5-pro-preview-05-06">gemini-2.5-pro-preview-05-06</option>
@@ -135,6 +142,7 @@
135 <option data-type="google" value="gemini-robotics-er-1.5-preview">gemini-robotics-er-1.5-preview</option>142 <option data-type="google" value="gemini-robotics-er-1.5-preview">gemini-robotics-er-1.5-preview</option>
136 <option data-type="vertexai" value="gemini-3-pro-preview">gemini-3-pro-preview</option>143 <option data-type="vertexai" value="gemini-3-pro-preview">gemini-3-pro-preview</option>
137 <option data-type="vertexai" value="gemini-3-pro-image-preview">gemini-3-pro-image-preview</option>144 <option data-type="vertexai" value="gemini-3-pro-image-preview">gemini-3-pro-image-preview</option>
145 <option data-type="vertexai" value="gemini-3-flash-preview">gemini-3-flash-preview</option>
138 <option data-type="vertexai" value="gemini-2.5-pro">gemini-2.5-pro</option>146 <option data-type="vertexai" value="gemini-2.5-pro">gemini-2.5-pro</option>
139 <option data-type="vertexai" value="gemini-2.5-flash">gemini-2.5-flash</option>147 <option data-type="vertexai" value="gemini-2.5-flash">gemini-2.5-flash</option>
140 <option data-type="vertexai" value="gemini-2.5-flash-lite">gemini-2.5-flash-lite</option>148 <option data-type="vertexai" value="gemini-2.5-flash-lite">gemini-2.5-flash-lite</option>
@@ -159,7 +167,11 @@
159 <option data-type="ollama" value="mistral-small3.2">mistral-small3.2</option>167 <option data-type="ollama" value="mistral-small3.2">mistral-small3.2</option>
160 <option data-type="ollama" value="llama3.2-vision">llama3.2-vision</option>168 <option data-type="ollama" value="llama3.2-vision">llama3.2-vision</option>
161 <option data-type="ollama" value="llama4">llama4</option>169 <option data-type="ollama" value="llama4">llama4</option>
170 <option data-type="zai" value="glm-4.6v">glm-4.6v</option>
171 <option data-type="zai" value="glm-4.6v-flashx">glm-4.6v-flashx</option>
172 <option data-type="zai" value="glm-4.6v-flash">glm-4.6v-flash</option>
162 <option data-type="zai" value="glm-4.5v">glm-4.5v</option>173 <option data-type="zai" value="glm-4.5v">glm-4.5v</option>
174 <option data-type="zai" value="autoglm-phone-multilingual">autoglm-phone-multilingual</option>
163 <option data-type="llamacpp" value="llamacpp_current" data-i18n="currently_loaded">[Currently loaded]</option>175 <option data-type="llamacpp" value="llamacpp_current" data-i18n="currently_loaded">[Currently loaded]</option>
164 <option data-type="ooba" value="ooba_current" data-i18n="currently_loaded">[Currently loaded]</option>176 <option data-type="ooba" value="ooba_current" data-i18n="currently_loaded">[Currently loaded]</option>
165 <option data-type="koboldcpp" value="koboldcpp_current" data-i18n="currently_loaded">[Currently loaded]</option>177 <option data-type="koboldcpp" value="koboldcpp_current" data-i18n="currently_loaded">[Currently loaded]</option>
@@ -167,9 +179,6 @@
167 <option data-type="custom" value="custom_current" data-i18n="currently_selected">[Currently selected]</option>179 <option data-type="custom" value="custom_current" data-i18n="currently_selected">[Currently selected]</option>
168 </select>180 </select>
169 </div>181 </div>
170 <div data-type="zai">
171 <b>Will use Common API. Coding API is not supported!</b>
172 </div>
173 <div data-type="ollama">182 <div data-type="ollama">
174 <div>183 <div>
175 The model must be downloaded first! Do it with the <code>ollama pull</code> command or <a href="#" id="caption_ollama_pull">click here</a>.184 The model must be downloaded first! Do it with the <code>ollama pull</code> command or <a href="#" id="caption_ollama_pull">click here</a>.
public/scripts/extensions/expressions/index.js+24 -3
@@ -667,7 +667,14 @@ export async function sendExpressionCall(spriteFolderName, expression, { force =
667 }667 }
668}668}
669669
670async function setSpriteFolderCommand(_, folder) {670/**
671 * Slash command callback for /setspritefolder
672 * @param {object} param Command parameters
673 * @param {string} param.name Character name override
674 * @param {string} folder Folder path, can be full or partial with leading slash
675 * @returns {Promise<string>} Empty string
676 */
677async function setSpriteFolderCommand({ name }, folder) {
671 if (!folder) {678 if (!folder) {
672 console.log('Clearing sprite set');679 console.log('Clearing sprite set');
673 folder = '';680 folder = '';
@@ -675,8 +682,12 @@ async function setSpriteFolderCommand(_, folder) {
675682
676 if (folder.startsWith('/') || folder.startsWith('\\')) {683 if (folder.startsWith('/') || folder.startsWith('\\')) {
677 const currentLastMessage = getLastCharacterMessage();684 const currentLastMessage = getLastCharacterMessage();
685 if (currentLastMessage.name === null && !name) {
686 toastr.error('At least one character message is required to set a sprites subfolder.', 'Provide the name with "name=" argument.');
687 return '';
688 }
678 folder = folder.slice(1);689 folder = folder.slice(1);
679 folder = `${currentLastMessage.name}/${folder}`;690 folder = `${name || currentLastMessage.name}/${folder}`;
680 }691 }
681692
682 $('#expression_override').val(folder.trim());693 $('#expression_override').val(folder.trim());
@@ -1414,7 +1425,7 @@ export async function getExpressionsList({ filterAvailable = false } = {}) {
1414 if (extension_settings.expressions.api == EXPRESSION_API.local) {1425 if (extension_settings.expressions.api == EXPRESSION_API.local) {
1415 const apiResult = await fetch('/api/extra/classify/labels', {1426 const apiResult = await fetch('/api/extra/classify/labels', {
1416 method: 'POST',1427 method: 'POST',
1417 headers: getRequestHeaders(),1428 headers: getRequestHeaders({ omitContentType: true }),
1418 });1429 });
14191430
1420 if (apiResult.ok) {1431 if (apiResult.ok) {
@@ -2322,6 +2333,16 @@ function migrateSettings() {
2322 name: 'expression-folder-override',2333 name: 'expression-folder-override',
2323 aliases: ['spriteoverride', 'costume'],2334 aliases: ['spriteoverride', 'costume'],
2324 callback: setSpriteFolderCommand,2335 callback: setSpriteFolderCommand,
2336 namedArgumentList:[
2337 SlashCommandNamedArgument.fromProps({
2338 name: 'name',
2339 description: 'Character name to set a subfolder for. If not provided, the character who last sent a message will be used.',
2340 typeList: [ARGUMENT_TYPE.STRING],
2341 enumProvider: commonEnumProviders.characters('character'),
2342 isRequired: false,
2343 acceptsMultiple: false,
2344 }),
2345 ],
2325 unnamedArgumentList: [2346 unnamedArgumentList: [
2326 new SlashCommandArgument(2347 new SlashCommandArgument(
2327 'optional folder', [ARGUMENT_TYPE.STRING], false,2348 'optional folder', [ARGUMENT_TYPE.STRING], false,
public/scripts/extensions/gallery/index.js+46 -18
@@ -8,7 +8,7 @@ import {
8 animation_easing,8 animation_easing,
9} from '../../../script.js';9} from '../../../script.js';
10import { groups, selected_group } from '../../group-chats.js';10import { groups, selected_group } from '../../group-chats.js';
11import { loadFileToDocument, delay, getBase64Async, getSanitizedFilename, saveBase64AsFile, getFileExtension } from '../../utils.js';11import { loadFileToDocument, delay, getBase64Async, getSanitizedFilename, saveBase64AsFile, getFileExtension, getVideoThumbnail, clamp } from '../../utils.js';
12import { loadMovingUIState } from '../../power-user.js';12import { loadMovingUIState } from '../../power-user.js';
13import { dragElement } from '../../RossAscends-mods.js';13import { dragElement } from '../../RossAscends-mods.js';
14import { SlashCommandParser } from '../../slash-commands/SlashCommandParser.js';14import { SlashCommandParser } from '../../slash-commands/SlashCommandParser.js';
@@ -19,17 +19,14 @@ import { commonEnumProviders } from '../../slash-commands/SlashCommandCommonEnum
19import { t, translate } from '../../i18n.js';19import { t, translate } from '../../i18n.js';
20import { Popup } from '../../popup.js';20import { Popup } from '../../popup.js';
21import { deleteMediaFromServer } from '../../chats.js';21import { deleteMediaFromServer } from '../../chats.js';
22import { MEDIA_REQUEST_TYPE, VIDEO_EXTENSIONS } from '../../constants.js';
2223
24const isVideo = (/** @type {string} */ url) => VIDEO_EXTENSIONS.some(ext => new RegExp(`.${ext}$`, 'i').test(url));
23const extensionName = 'gallery';25const extensionName = 'gallery';
24const extensionFolderPath = `scripts/extensions/${extensionName}/`;26const extensionFolderPath = `scripts/extensions/${extensionName}/`;
25let firstTime = true;27let firstTime = true;
26let deleteModeActive = false;28let deleteModeActive = false;
2729
28// Exposed defaults for future tweaking
29let thumbnailHeight = 150;
30let paginationVisiblePages = 10;
31let paginationMaxLinesPerPage = 2;
32let galleryMaxRows = 3;
3330
34// Remove all draggables associated with the gallery31// Remove all draggables associated with the gallery
35$('#movingDivs').on('click', '.dragClose', function () {32$('#movingDivs').on('click', '.dragClose', function () {
@@ -98,7 +95,7 @@ function initSettings() {
9895
99/**96/**
100 * Retrieves the gallery folder for a given character.97 * Retrieves the gallery folder for a given character.
101 * @param {import('../../char-data.js').v1CharData} char Character data98 * @param {Character} char Character data
102 * @returns {string} The gallery folder for the character99 * @returns {string} The gallery folder for the character
103 */100 */
104function getGalleryFolder(char) {101function getGalleryFolder(char) {
@@ -122,17 +119,34 @@ async function getGalleryItems(url) {
122 folder: url,119 folder: url,
123 sortField: sortObj.field,120 sortField: sortObj.field,
124 sortOrder: sortObj.order,121 sortOrder: sortObj.order,
122 type: MEDIA_REQUEST_TYPE.IMAGE | MEDIA_REQUEST_TYPE.VIDEO,
125 }),123 }),
126 });124 });
127125
128 url = await getSanitizedFilename(url);126 url = await getSanitizedFilename(url);
129127
130 const data = await response.json();128 const data = await response.json();
131 const items = data.map((file) => ({129 const items = [];
132 src: `user/images/${url}/${file}`,130
133 srct: `user/images/${url}/${file}`,131 for (const file of data) {
134 title: '', // Optional title for each item132 const item = {
135 }));133 src: `user/images/${url}/${file}`,
134 srct: `user/images/${url}/${file}`,
135 title: '', // Optional title for each item
136 };
137
138 if (isVideo(file)) {
139 try {
140 // 150px of max height with some allowance for various aspect ratios
141 const maxSide = Math.round(150 * 1.5);
142 item.srct = await getVideoThumbnail(item.src, maxSide, maxSide);
143 } catch (error) {
144 console.error('Failed to generate video thumbnail for gallery:', error);
145 }
146 }
147
148 items.push(item);
149 }
136150
137 return items;151 return items;
138}152}
@@ -145,7 +159,7 @@ async function getGalleryFolders() {
145 try {159 try {
146 const response = await fetch('/api/images/folders', {160 const response = await fetch('/api/images/folders', {
147 method: 'POST',161 method: 'POST',
148 headers: getRequestHeaders(),162 headers: getRequestHeaders({ omitContentType: true }),
149 });163 });
150164
151 if (!response.ok) {165 if (!response.ok) {
@@ -165,7 +179,7 @@ async function getGalleryFolders() {
165 */179 */
166async function deleteGalleryItem(url) {180async function deleteGalleryItem(url) {
167 const isDeleted = await deleteMediaFromServer(url, false);181 const isDeleted = await deleteMediaFromServer(url, false);
168 if (isDeleted){182 if (isDeleted) {
169 toastr.success(t`Image deleted successfully.`);183 toastr.success(t`Image deleted successfully.`);
170 }184 }
171}185}
@@ -198,6 +212,12 @@ function getSortOrder() {
198 * @returns {Promise<void>} - Promise representing the completion of the gallery initialization.212 * @returns {Promise<void>} - Promise representing the completion of the gallery initialization.
199 */213 */
200async function initGallery(items, url) {214async function initGallery(items, url) {
215 // Exposed defaults for future tweaking
216 const thumbnailHeight = 150;
217 const paginationVisiblePages = 5;
218 const paginationMaxLinesPerPage = 2;
219 const galleryMaxRows = clamp(Math.floor((window.innerHeight * 0.9 - 75) / thumbnailHeight), 1, 10);
220
201 const nonce = `nonce-${Math.random().toString(36).substring(2, 15)}`;221 const nonce = `nonce-${Math.random().toString(36).substring(2, 15)}`;
202 const gallery = $('#dragGallery');222 const gallery = $('#dragGallery');
203 gallery.addClass(nonce);223 gallery.addClass(nonce);
@@ -210,6 +230,7 @@ async function initGallery(items, url) {
210 galleryMaxRows: galleryMaxRows,230 galleryMaxRows: galleryMaxRows,
211 galleryPaginationTopButtons: false,231 galleryPaginationTopButtons: false,
212 galleryNavigationOverlayButtons: true,232 galleryNavigationOverlayButtons: true,
233 galleryPaginationMode: 'rectangles',
213 galleryTheme: {234 galleryTheme: {
214 navigationBar: { background: 'none', borderTop: '', borderBottom: '', borderRight: '', borderLeft: '' },235 navigationBar: { background: 'none', borderTop: '', borderBottom: '', borderRight: '', borderLeft: '' },
215 navigationBreadcrumb: { background: '#111', color: '#fff', colorHover: '#ccc', borderRadius: '4px' },236 navigationBreadcrumb: { background: '#111', color: '#fff', colorHover: '#ccc', borderRadius: '4px' },
@@ -399,7 +420,7 @@ async function makeMovable(url) {
399 // Create a hidden file input420 // Create a hidden file input
400 const fileInput = document.createElement('input');421 const fileInput = document.createElement('input');
401 fileInput.type = 'file';422 fileInput.type = 'file';
402 fileInput.accept = 'image/*';423 fileInput.accept = 'image/*,video/*';
403 fileInput.multiple = true;424 fileInput.multiple = true;
404 fileInput.style.display = 'none';425 fileInput.style.display = 'none';
405426
@@ -617,12 +638,19 @@ function makeDragImg(id, url) {
617 const newElement = document.importNode(template.content, true);638 const newElement = document.importNode(template.content, true);
618639
619 // Step 2: Append the given image640 // Step 2: Append the given image
620 const imgElem = document.createElement('img');641 const mediaElement = isVideo(url)
621 imgElem.src = url;642 ? document.createElement('video')
643 : document.createElement('img');
644 mediaElement.src = url;
645 if (mediaElement instanceof HTMLVideoElement) {
646 mediaElement.controls = true;
647 mediaElement.autoplay = true;
648 }
649
622 let uniqueId = `draggable_${id}`;650 let uniqueId = `draggable_${id}`;
623 const draggableElem = /** @type {HTMLElement} */ (newElement.querySelector('.draggable'));651 const draggableElem = /** @type {HTMLElement} */ (newElement.querySelector('.draggable'));
624 if (draggableElem) {652 if (draggableElem) {
625 draggableElem.appendChild(imgElem);653 draggableElem.appendChild(mediaElement);
626654
627 // Find a unique id for the draggable element655 // Find a unique id for the draggable element
628656
public/scripts/extensions/gallery/style.css+4 -0
@@ -49,3 +49,7 @@
49 opacity: 1;49 opacity: 1;
50 filter: unset;50 filter: unset;
51}51}
52
53.galleryImageDraggable video {
54 width: 100%;
55}
public/scripts/extensions/memory/index.js+15 -3
@@ -19,17 +19,18 @@ import {
19 animation_easing,19 animation_easing,
20} from '../../../script.js';20} from '../../../script.js';
21import { is_group_generating, selected_group } from '../../group-chats.js';21import { is_group_generating, selected_group } from '../../group-chats.js';
22import { loadMovingUIState } from '../../power-user.js';22import { loadMovingUIState, power_user } from '../../power-user.js';
23import { dragElement } from '../../RossAscends-mods.js';23import { dragElement } from '../../RossAscends-mods.js';
24import { getTextTokens, getTokenCountAsync, tokenizers } from '../../tokenizers.js';24import { getTextTokens, getTokenCountAsync, tokenizers } from '../../tokenizers.js';
25import { debounce_timeout } from '../../constants.js';25import { debounce_timeout } from '../../constants.js';
26import { SlashCommandParser } from '../../slash-commands/SlashCommandParser.js';26import { SlashCommandParser } from '../../slash-commands/SlashCommandParser.js';
27import { SlashCommand } from '../../slash-commands/SlashCommand.js';27import { SlashCommand } from '../../slash-commands/SlashCommand.js';
28import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from '../../slash-commands/SlashCommandArgument.js';28import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from '../../slash-commands/SlashCommandArgument.js';
29import { MacrosParser } from '../../macros.js';29import { macros, MacroCategory } from '../../macros/macro-system.js';
30import { countWebLlmTokens, generateWebLlmChatPrompt, getWebLlmContextSize, isWebLlmSupported } from '../shared.js';30import { countWebLlmTokens, generateWebLlmChatPrompt, getWebLlmContextSize, isWebLlmSupported } from '../shared.js';
31import { commonEnumProviders } from '../../slash-commands/SlashCommandCommonEnumsProvider.js';31import { commonEnumProviders } from '../../slash-commands/SlashCommandCommonEnumsProvider.js';
32import { removeReasoningFromString } from '../../reasoning.js';32import { removeReasoningFromString } from '../../reasoning.js';
33import { MacrosParser } from '/scripts/macros.js';
33export { MODULE_NAME };34export { MODULE_NAME };
3435
35const MODULE_NAME = '1_memory';36const MODULE_NAME = '1_memory';
@@ -1094,5 +1095,16 @@ jQuery(async function () {
1094 returns: ARGUMENT_TYPE.STRING,1095 returns: ARGUMENT_TYPE.STRING,
1095 }));1096 }));
10961097
1097 MacrosParser.registerMacro('summary', () => getLatestMemoryFromChat(getContext().chat));1098 if (power_user.experimental_macro_engine) {
1099 macros.register('summary', {
1100 category: MacroCategory.CHAT,
1101 description: 'Returns the latest memory/summary from the current chat.',
1102 handler: () => getLatestMemoryFromChat(getContext().chat),
1103 });
1104 } else {
1105 // TODO: Remove this when the experimental macro engine is replacing the old macro engine
1106 MacrosParser.registerMacro('summary',
1107 () => getLatestMemoryFromChat(getContext().chat),
1108 'Returns the latest memory/summary from the current chat.');
1109 }
1098});1110});
public/scripts/extensions/quick-reply/index.js+1 -1
@@ -152,7 +152,7 @@ const handleCharChange = () => {
152 lastCharId = this_chid;152 lastCharId = this_chid;
153153
154 // If no character is loaded, there's nothing more to do.154 // If no character is loaded, there's nothing more to do.
155 /** @type {import('../../char-data.js').v1CharData} */155 /** @type {Character} */
156 const character = characters[this_chid];156 const character = characters[this_chid];
157 if (!character || selected_group) {157 if (!character || selected_group) {
158 return;158 return;
public/scripts/extensions/regex/engine.js+63 -8
@@ -5,8 +5,8 @@ import { regexFromString } from '../../utils.js';
5import { lodash } from '../../../lib.js';5import { lodash } from '../../../lib.js';
66
7/**7/**
8 * @enum {number} Regex scripts types
9 * @readonly8 * @readonly
9 * @enum {number} Regex scripts types
10 */10 */
11export const SCRIPT_TYPES = {11export const SCRIPT_TYPES = {
12 // ORDER MATTERS: defines the regex script priority12 // ORDER MATTERS: defines the regex script priority
@@ -35,6 +35,61 @@ export const SCRIPT_TYPE_UNKNOWN = -1;
35const DEFAULT_GET_REGEX_SCRIPTS_OPTIONS = Object.freeze({ allowedOnly: false });35const DEFAULT_GET_REGEX_SCRIPTS_OPTIONS = Object.freeze({ allowedOnly: false });
3636
37/**37/**
38 * Manages the compiled regex cache with LRU eviction.
39 */
40export class RegexProvider {
41 /** @type {Map<string, RegExp>} */
42 #cache = new Map();
43 /** @type {number} */
44 #maxSize = 1000;
45
46 static instance = new RegexProvider();
47
48 /**
49 * Gets a regex instance by its string representation.
50 * @param {string} regexString The regex string to retrieve
51 * @returns {RegExp?} Compiled regex or null if invalid
52 */
53 get(regexString) {
54 const isCached = this.#cache.has(regexString);
55 const regex = isCached
56 ? this.#cache.get(regexString)
57 : regexFromString(regexString);
58
59 if (!regex) {
60 return null;
61 }
62
63 if (isCached) {
64 // LRU: Move to end by re-inserting
65 this.#cache.delete(regexString);
66 this.#cache.set(regexString, regex);
67 } else {
68 // Evict oldest if at capacity
69 if (this.#cache.size >= this.#maxSize) {
70 const firstKey = this.#cache.keys().next().value;
71 this.#cache.delete(firstKey);
72 }
73 this.#cache.set(regexString, regex);
74 }
75
76 // Reset lastIndex for global/sticky regexes
77 if (regex.global || regex.sticky) {
78 regex.lastIndex = 0;
79 }
80
81 return regex;
82 }
83
84 /**
85 * Clears the entire cache.
86 */
87 clear() {
88 this.#cache.clear();
89 }
90}
91
92/**
38 * Retrieves the list of regex scripts by combining the scripts from the extension settings and the character data93 * Retrieves the list of regex scripts by combining the scripts from the extension settings and the character data
39 *94 *
40 * @param {GetRegexScriptsOptions} options Options for retrieving the regex scripts95 * @param {GetRegexScriptsOptions} options Options for retrieving the regex scripts
@@ -105,7 +160,7 @@ export async function saveScriptsByType(scripts, scriptType) {
105160
106/**161/**
107 * Check if character's regexes are allowed to be used; if character is undefined, returns false162 * Check if character's regexes are allowed to be used; if character is undefined, returns false
108 * @param {import('../../char-data.js').v1CharData|undefined} character163 * @param {Character|undefined} character
109 * @returns {boolean}164 * @returns {boolean}
110 */165 */
111export function isScopedScriptsAllowed(character) {166export function isScopedScriptsAllowed(character) {
@@ -114,7 +169,7 @@ export function isScopedScriptsAllowed(character) {
114169
115/**170/**
116 * Allow character's regexes to be used; if character is undefined, do nothing171 * Allow character's regexes to be used; if character is undefined, do nothing
117 * @param {import('../../char-data.js').v1CharData|undefined} character172 * @param {Character|undefined} character
118 * @returns {void}173 * @returns {void}
119 */174 */
120export function allowScopedScripts(character) {175export function allowScopedScripts(character) {
@@ -133,7 +188,7 @@ export function allowScopedScripts(character) {
133188
134/**189/**
135 * Disallow character's regexes to be used; if character is undefined, do nothing190 * Disallow character's regexes to be used; if character is undefined, do nothing
136 * @param {import('../../char-data.js').v1CharData|undefined} character191 * @param {Character|undefined} character
137 * @returns {void}192 * @returns {void}
138 */193 */
139export function disallowScopedScripts(character) {194export function disallowScopedScripts(character) {
@@ -220,8 +275,8 @@ export function getCurrentPresetName() {
220}275}
221276
222/**277/**
223 * @enum {number} Where the regex script should be applied
224 * @readonly278 * @readonly
279 * @enum {number} Where the regex script should be applied
225 */280 */
226export const regex_placement = {281export const regex_placement = {
227 /**282 /**
@@ -237,8 +292,8 @@ export const regex_placement = {
237};292};
238293
239/**294/**
240 * @enum {number} How to substitute parameters in the find regex
241 * @readonly295 * @readonly
296 * @enum {number} How to substitute parameters in the find regex
242 */297 */
243export const substitute_find_regex = {298export const substitute_find_regex = {
244 NONE: 0,299 NONE: 0,
@@ -353,7 +408,7 @@ export function runRegexScript(regexScript, rawString, { characterOverride } = {
353 }408 }
354 };409 };
355 const regexString = getRegexString();410 const regexString = getRegexString();
356 const findRegex = regexFromString(regexString);411 const findRegex = RegexProvider.instance.get(regexString);
357412
358 // The user skill issued. Return with nothing.413 // The user skill issued. Return with nothing.
359 if (!findRegex) {414 if (!findRegex) {
@@ -402,7 +457,7 @@ export function runRegexScript(regexScript, rawString, { characterOverride } = {
402function filterString(rawString, trimStrings, { characterOverride } = {}) {457function filterString(rawString, trimStrings, { characterOverride } = {}) {
403 let finalString = rawString;458 let finalString = rawString;
404 trimStrings.forEach((trimString) => {459 trimStrings.forEach((trimString) => {
405 const subTrimString = substituteParams(trimString, undefined, characterOverride);460 const subTrimString = substituteParams(trimString, { name2Override: characterOverride });
406 finalString = finalString.replaceAll(subTrimString, '');461 finalString = finalString.replaceAll(subTrimString, '');
407 });462 });
408463
public/scripts/extensions/regex/index.js+3 -1
@@ -8,7 +8,7 @@ import { commonEnumProviders, enumIcons } from '../../slash-commands/SlashComman
8import { SlashCommandEnumValue, enumTypes } from '../../slash-commands/SlashCommandEnumValue.js';8import { SlashCommandEnumValue, enumTypes } from '../../slash-commands/SlashCommandEnumValue.js';
9import { SlashCommandParser } from '../../slash-commands/SlashCommandParser.js';9import { SlashCommandParser } from '../../slash-commands/SlashCommandParser.js';
10import { download, equalsIgnoreCaseAndAccents, escapeHtml, getFileText, getSortableDelay, isFalseBoolean, isTrueBoolean, regexFromString, setInfoBlock, uuidv4 } from '../../utils.js';10import { download, equalsIgnoreCaseAndAccents, escapeHtml, getFileText, getSortableDelay, isFalseBoolean, isTrueBoolean, regexFromString, setInfoBlock, uuidv4 } from '../../utils.js';
11import { allowPresetScripts, allowScopedScripts, disallowPresetScripts, disallowScopedScripts, getCurrentPresetAPI, getCurrentPresetName, getRegexScripts, getScriptsByType, isPresetScriptsAllowed, isScopedScriptsAllowed, regex_placement, runRegexScript, saveScriptsByType, SCRIPT_TYPE_UNKNOWN, SCRIPT_TYPES, substitute_find_regex } from './engine.js';11import { allowPresetScripts, allowScopedScripts, disallowPresetScripts, disallowScopedScripts, getCurrentPresetAPI, getCurrentPresetName, getRegexScripts, getScriptsByType, isPresetScriptsAllowed, isScopedScriptsAllowed, regex_placement, RegexProvider, runRegexScript, saveScriptsByType, SCRIPT_TYPE_UNKNOWN, SCRIPT_TYPES, substitute_find_regex } from './engine.js';
12import { t } from '../../i18n.js';12import { t } from '../../i18n.js';
13import { accountStorage } from '../../util/AccountStorage.js';13import { accountStorage } from '../../util/AccountStorage.js';
14import { getPresetManager } from '../../preset-manager.js';14import { getPresetManager } from '../../preset-manager.js';
@@ -1628,6 +1628,8 @@ async function checkCharEmbeddedRegexScripts() {
1628 }1628 }
1629 }1629 }
16301630
1631 // Clear cache and reload scripts
1632 RegexProvider.instance.clear();
1631 await loadRegexScripts();1633 await loadRegexScripts();
1632}1634}
16331635
public/scripts/extensions/shared.js+56 -2
@@ -1,7 +1,7 @@
1import { CONNECT_API_MAP, getRequestHeaders } from '../../script.js';1import { CONNECT_API_MAP, getRequestHeaders } from '../../script.js';
2import { extension_settings, openThirdPartyExtensionMenu } from '../extensions.js';2import { extension_settings, openThirdPartyExtensionMenu } from '../extensions.js';
3import { t } from '../i18n.js';3import { t } from '../i18n.js';
4import { oai_settings, proxies } from '../openai.js';4import { oai_settings, proxies, ZAI_ENDPOINT } from '../openai.js';
5import { SECRET_KEYS, secret_state } from '../secrets.js';5import { SECRET_KEYS, secret_state } from '../secrets.js';
6import { textgen_types, textgenerationwebui_settings } from '../textgen-settings.js';6import { textgen_types, textgenerationwebui_settings } from '../textgen-settings.js';
7import { getTokenCountAsync } from '../tokenizers.js';7import { getTokenCountAsync } from '../tokenizers.js';
@@ -115,6 +115,10 @@ export async function getMultimodalCaption(base64Img, prompt) {
115 requestBody.custom_exclude_body = oai_settings.custom_exclude_body;115 requestBody.custom_exclude_body = oai_settings.custom_exclude_body;
116 }116 }
117117
118 if (extension_settings.caption.multimodal_api === 'zai') {
119 requestBody.zai_endpoint = oai_settings.zai_endpoint || ZAI_ENDPOINT.COMMON;
120 }
121
118 function getEndpointUrl() {122 function getEndpointUrl() {
119 switch (extension_settings.caption.multimodal_api) {123 switch (extension_settings.caption.multimodal_api) {
120 case 'google':124 case 'google':
@@ -257,6 +261,10 @@ function throwIfInvalidModel(useReverseProxy) {
257 throw new Error('Electron Hub API key is not set.');261 throw new Error('Electron Hub API key is not set.');
258 }262 }
259263
264 if (multimodalApi === 'chutes' && !secret_state[SECRET_KEYS.CHUTES]) {
265 throw new Error('Chutes API key is not set.');
266 }
267
260 if (multimodalApi === 'zai' && !secret_state[SECRET_KEYS.ZAI]) {268 if (multimodalApi === 'zai' && !secret_state[SECRET_KEYS.ZAI]) {
261 throw new Error('Z.AI API key is not set.');269 throw new Error('Z.AI API key is not set.');
262 }270 }
@@ -393,7 +401,7 @@ export class ConnectionManagerRequestService {
393 throw new Error('Connection Manager is not available');401 throw new Error('Connection Manager is not available');
394 }402 }
395403
396 const profile = context.extensionSettings.connectionManager.profiles.find((p) => p.id === profileId);404 const profile = this.getProfile(profileId);
397 const selectedApiMap = this.validateProfile(profile);405 const selectedApiMap = this.validateProfile(profile);
398406
399 try {407 try {
@@ -413,6 +421,8 @@ export class ConnectionManagerRequestService {
413 model: profile.model,421 model: profile.model,
414 chat_completion_source: selectedApiMap.source,422 chat_completion_source: selectedApiMap.source,
415 custom_url: profile['api-url'],423 custom_url: profile['api-url'],
424 vertexai_region: profile['api-url'],
425 zai_endpoint: profile['api-url'],
416 reverse_proxy: proxyPreset?.url,426 reverse_proxy: proxyPreset?.url,
417 proxy_password: proxyPreset?.password,427 proxy_password: proxyPreset?.password,
418 custom_prompt_post_processing: profile['prompt-post-processing'],428 custom_prompt_post_processing: profile['prompt-post-processing'],
@@ -450,6 +460,38 @@ export class ConnectionManagerRequestService {
450 }460 }
451461
452 /**462 /**
463 * If using text completion, return a formatted prompt string given an array of messages, a given profile ID, and optional instruct settings.
464 * If using chat completion, simply return the given prompt as-is.
465 * @param {ChatCompletionMessage[]} prompt An array of prompt messages.
466 * @param {string} profileId ID of a given connection profile (from which to infer a completion preset).
467 * @param {InstructSettings} instructSettings optional instruct settings
468 */
469 static constructPrompt(prompt, profileId, instructSettings = null) {
470 const context = SillyTavern.getContext();
471 const profile = this.getProfile(profileId);
472 const selectedApiMap = this.validateProfile(profile);
473 const instructName = profile.instruct;
474
475 switch (selectedApiMap.selected) {
476 case 'openai': {
477 if (!selectedApiMap.source) {
478 throw new Error(`API type ${selectedApiMap.selected} does not support chat completions`);
479 }
480 return prompt;
481 }
482 case 'textgenerationwebui': {
483 if (!selectedApiMap.type) {
484 throw new Error(`API type ${selectedApiMap.selected} does not support text completions`);
485 }
486 return context.TextCompletionService.constructPrompt(prompt, instructName, instructSettings);
487 }
488 default: {
489 throw new Error(`Unknown API type ${selectedApiMap.selected}`);
490 }
491 }
492 }
493
494 /**
453 * Respects allowed types.495 * Respects allowed types.
454 * @returns {import('./connection-manager/index.js').ConnectionProfile[]}496 * @returns {import('./connection-manager/index.js').ConnectionProfile[]}
455 */497 */
@@ -464,6 +506,18 @@ export class ConnectionManagerRequestService {
464 }506 }
465507
466 /**508 /**
509 * Return profile data given the profile ID
510 * @param {string} profileId
511 * @returns {import('./connection-manager/index.js').ConnectionProfile?} [profile]
512 * @throws {Error}
513 */
514 static getProfile(profileId) {
515 const profile = SillyTavern.getContext().extensionSettings.connectionManager.profiles.find((p) => p.id === profileId);
516 if (!profile) throw new Error(`Profile not found (ID: ${profileId})`);
517 return profile;
518 }
519
520 /**
467 * @param {import('./connection-manager/index.js').ConnectionProfile?} [profile]521 * @param {import('./connection-manager/index.js').ConnectionProfile?} [profile]
468 * @returns {boolean}522 * @returns {boolean}
469 */523 */
public/scripts/extensions/stable-diffusion/index.js+384 -55
@@ -57,9 +57,11 @@ import { SlashCommandEnumValue } from '../../slash-commands/SlashCommandEnumValu
57import { callGenericPopup, Popup, POPUP_TYPE } from '../../popup.js';57import { callGenericPopup, Popup, POPUP_TYPE } from '../../popup.js';
58import { commonEnumProviders } from '../../slash-commands/SlashCommandCommonEnumsProvider.js';58import { commonEnumProviders } from '../../slash-commands/SlashCommandCommonEnumsProvider.js';
59import { ToolManager } from '../../tool-calling.js';59import { ToolManager } from '../../tool-calling.js';
60import { MacrosParser } from '../../macros.js';60import { macros, MacroCategory } from '../../macros/macro-system.js';
61import { t, translate } from '../../i18n.js';61import { t, translate } from '../../i18n.js';
62import { oai_settings } from '../../openai.js';62import { oai_settings } from '../../openai.js';
63import { power_user } from '/scripts/power-user.js';
64import { MacrosParser } from '/scripts/macros.js';
6365
64export { MODULE_NAME };66export { MODULE_NAME };
6567
@@ -81,12 +83,19 @@ const sources = {
81 pollinations: 'pollinations',83 pollinations: 'pollinations',
82 stability: 'stability',84 stability: 'stability',
83 huggingface: 'huggingface',85 huggingface: 'huggingface',
86 chutes: 'chutes',
84 electronhub: 'electronhub',87 electronhub: 'electronhub',
85 nanogpt: 'nanogpt',88 nanogpt: 'nanogpt',
86 bfl: 'bfl',89 bfl: 'bfl',
87 falai: 'falai',90 falai: 'falai',
88 xai: 'xai',91 xai: 'xai',
89 google: 'google',92 google: 'google',
93 zai: 'zai',
94 openrouter: 'openrouter',
95};
96const comfyTypes = {
97 standard: 'standard',
98 runpod_serverless: 'runpod_serverless',
90};99};
91100
92const initiators = {101const initiators = {
@@ -317,9 +326,13 @@ const defaultSettings = {
317 styles: defaultStyles,326 styles: defaultStyles,
318327
319 // ComyUI settings328 // ComyUI settings
329 comfy_type: 'standard',
330
320 comfy_url: 'http://127.0.0.1:8188',331 comfy_url: 'http://127.0.0.1:8188',
321 comfy_workflow: 'Default_Comfy_Workflow.json',332 comfy_workflow: 'Default_Comfy_Workflow.json',
322333
334 comfy_runpod_url: '',
335
323 // Pollinations settings336 // Pollinations settings
324 pollinations_enhance: false,337 pollinations_enhance: false,
325338
@@ -432,6 +445,10 @@ function toggleSourceControls() {
432 const source = $(this).data('sd-source').split(',');445 const source = $(this).data('sd-source').split(',');
433 $(this).toggle(source.includes(extension_settings.sd.source));446 $(this).toggle(source.includes(extension_settings.sd.source));
434 });447 });
448 $('.sd_settings [data-sd-comfy-type]').each(function () {
449 const source = $(this).data('sd-comfy-type').split(',');
450 $(this).toggle(source.includes(extension_settings.sd.comfy_type));
451 });
435}452}
436453
437async function loadSettings() {454async function loadSettings() {
@@ -512,8 +529,10 @@ async function loadSettings() {
512 $('#sd_openai_style').val(extension_settings.sd.openai_style);529 $('#sd_openai_style').val(extension_settings.sd.openai_style);
513 $('#sd_openai_quality').val(extension_settings.sd.openai_quality);530 $('#sd_openai_quality').val(extension_settings.sd.openai_quality);
514 $('#sd_openai_duration').val(extension_settings.sd.openai_duration);531 $('#sd_openai_duration').val(extension_settings.sd.openai_duration);
532 $('#sd_comfy_type').val(extension_settings.sd.comfy_type);
515 $('#sd_comfy_url').val(extension_settings.sd.comfy_url);533 $('#sd_comfy_url').val(extension_settings.sd.comfy_url);
516 $('#sd_comfy_prompt').val(extension_settings.sd.comfy_prompt);534 $('#sd_comfy_prompt').val(extension_settings.sd.comfy_prompt);
535 $('#sd_comfy_runpod_url').val(extension_settings.sd.comfy_runpod_url);
517 $('#sd_snap').prop('checked', extension_settings.sd.snap);536 $('#sd_snap').prop('checked', extension_settings.sd.snap);
518 $('#sd_clip_skip').val(extension_settings.sd.clip_skip);537 $('#sd_clip_skip').val(extension_settings.sd.clip_skip);
519 $('#sd_clip_skip_value').val(extension_settings.sd.clip_skip);538 $('#sd_clip_skip_value').val(extension_settings.sd.clip_skip);
@@ -734,7 +753,7 @@ async function onSaveStyleClick() {
734async function refinePrompt(prompt, isNegative) {753async function refinePrompt(prompt, isNegative) {
735 if (extension_settings.sd.refine_mode) {754 if (extension_settings.sd.refine_mode) {
736 const text = isNegative ? '<h3>Review and edit the <i>negative</i> prompt:</h3>' : '<h3>Review and edit the prompt:</h3>';755 const text = isNegative ? '<h3>Review and edit the <i>negative</i> prompt:</h3>' : '<h3>Review and edit the prompt:</h3>';
737 const refinedPrompt = await callGenericPopup(text + 'Press "Cancel" to abort the image generation.', POPUP_TYPE.INPUT, prompt.trim(), { rows: 5, okButton: 'Continue' });756 const refinedPrompt = await callGenericPopup(text + 'Press "Cancel" to abort the image generation.', POPUP_TYPE.INPUT, prompt.trim(), { rows: 8, okButton: 'Continue' });
738757
739 if (refinedPrompt) {758 if (refinedPrompt) {
740 return String(refinedPrompt);759 return String(refinedPrompt);
@@ -1011,6 +1030,11 @@ async function onSourceChange() {
1011 await loadSettingOptions();1030 await loadSettingOptions();
1012}1031}
10131032
1033async function onComfyTypeChange() {
1034 extension_settings.sd.comfy_type = $('#sd_comfy_type').find(':selected').val();
1035 await onSourceChange();
1036}
1037
1014function onFunctionToolInput() {1038function onFunctionToolInput() {
1015 extension_settings.sd.function_tool = !!$(this).prop('checked');1039 extension_settings.sd.function_tool = !!$(this).prop('checked');
1016 saveSettingsDebounced();1040 saveSettingsDebounced();
@@ -1161,11 +1185,15 @@ function onHrSecondPassStepsInput() {
1161}1185}
11621186
1163function onComfyUrlInput() {1187function onComfyUrlInput() {
1164 // Remove trailing slashes
1165 extension_settings.sd.comfy_url = String($('#sd_comfy_url').val());1188 extension_settings.sd.comfy_url = String($('#sd_comfy_url').val());
1166 saveSettingsDebounced();1189 saveSettingsDebounced();
1167}1190}
11681191
1192function onComfyRunPodUrlInput() {
1193 extension_settings.sd.comfy_runpod_url = String($('#sd_comfy_runpod_url').val());
1194 saveSettingsDebounced();
1195}
1196
1169function onHFModelInput() {1197function onHFModelInput() {
1170 extension_settings.sd.huggingface_model_id = $('#sd_huggingface_model_id').val();1198 extension_settings.sd.huggingface_model_id = $('#sd_huggingface_model_id').val();
1171 saveSettingsDebounced();1199 saveSettingsDebounced();
@@ -1291,6 +1319,30 @@ async function validateComfyUrl() {
1291 }1319 }
1292}1320}
12931321
1322async function validateComfyRunPodUrl() {
1323 try {
1324 if (!extension_settings.sd.comfy_runpod_url) {
1325 throw new Error('URL is not set.');
1326 }
1327
1328 const result = await fetch('/api/sd/comfyrunpod/ping', {
1329 method: 'POST',
1330 headers: getRequestHeaders(),
1331 body: JSON.stringify({
1332 url: extension_settings.sd.comfy_runpod_url,
1333 }),
1334 });
1335 if (!result.ok) {
1336 throw new Error('ComfyUI RunPod returned an error.');
1337 }
1338
1339 await loadSettingOptions();
1340 toastr.success('ComfyUI RunPod API connected.');
1341 } catch (error) {
1342 toastr.error(`Could not validate ComfyUI RunPod API: ${error.message}`);
1343 }
1344}
1345
1294async function onModelChange() {1346async function onModelChange() {
1295 const selectedModel = $('#sd_model').find(':selected');1347 const selectedModel = $('#sd_model').find(':selected');
1296 extension_settings.sd.model = selectedModel.val();1348 extension_settings.sd.model = selectedModel.val();
@@ -1304,24 +1356,13 @@ async function onModelChange() {
13041356
1305 switchModelSpecificControls(extension_settings.sd.model);1357 switchModelSpecificControls(extension_settings.sd.model);
13061358
1307 const cloudSources = [1359 const updateRemoteModelSources = [
1308 sources.horde,1360 sources.auto,
1309 sources.novel,1361 sources.vlad,
1310 sources.openai,1362 sources.extras,
1311 sources.aimlapi,
1312 sources.togetherai,
1313 sources.pollinations,
1314 sources.stability,
1315 sources.huggingface,
1316 sources.electronhub,
1317 sources.nanogpt,
1318 sources.bfl,
1319 sources.falai,
1320 sources.xai,
1321 sources.google,
1322 ];1363 ];
13231364
1324 if (cloudSources.includes(extension_settings.sd.source)) {1365 if (!updateRemoteModelSources.includes(extension_settings.sd.source)) {
1325 return;1366 return;
1326 }1367 }
13271368
@@ -1531,6 +1572,9 @@ async function loadSamplers() {
1531 case sources.huggingface:1572 case sources.huggingface:
1532 samplers = ['N/A'];1573 samplers = ['N/A'];
1533 break;1574 break;
1575 case sources.chutes:
1576 samplers = ['N/A'];
1577 break;
1534 case sources.electronhub:1578 case sources.electronhub:
1535 samplers = ['N/A'];1579 samplers = ['N/A'];
1536 break;1580 break;
@@ -1540,12 +1584,21 @@ async function loadSamplers() {
1540 case sources.bfl:1584 case sources.bfl:
1541 samplers = ['N/A'];1585 samplers = ['N/A'];
1542 break;1586 break;
1587 case sources.falai:
1588 samplers = ['N/A'];
1589 break;
1543 case sources.xai:1590 case sources.xai:
1544 samplers = ['N/A'];1591 samplers = ['N/A'];
1545 break;1592 break;
1546 case sources.google:1593 case sources.google:
1547 samplers = ['N/A'];1594 samplers = ['N/A'];
1548 break;1595 break;
1596 case sources.zai:
1597 samplers = ['N/A'];
1598 break;
1599 case sources.openrouter:
1600 samplers = ['N/A'];
1601 break;
1549 }1602 }
15501603
1551 for (const sampler of samplers) {1604 for (const sampler of samplers) {
@@ -1565,7 +1618,7 @@ async function loadSamplers() {
1565async function loadHordeSamplers() {1618async function loadHordeSamplers() {
1566 const result = await fetch('/api/horde/sd-samplers', {1619 const result = await fetch('/api/horde/sd-samplers', {
1567 method: 'POST',1620 method: 'POST',
1568 headers: getRequestHeaders(),1621 headers: getRequestHeaders({ omitContentType: true }),
1569 });1622 });
15701623
1571 if (result.ok) {1624 if (result.ok) {
@@ -1665,6 +1718,9 @@ async function loadNovelSamplers() {
1665}1718}
16661719
1667async function loadComfySamplers() {1720async function loadComfySamplers() {
1721 if (extension_settings.sd.comfy_type === comfyTypes.runpod_serverless) {
1722 return ['N/A'];
1723 }
1668 if (!extension_settings.sd.comfy_url) {1724 if (!extension_settings.sd.comfy_url) {
1669 return [];1725 return [];
1670 }1726 }
@@ -1730,6 +1786,9 @@ async function loadModels() {
1730 case sources.huggingface:1786 case sources.huggingface:
1731 models = [{ value: '', text: t`<Enter Model ID above>` }];1787 models = [{ value: '', text: t`<Enter Model ID above>` }];
1732 break;1788 break;
1789 case sources.chutes:
1790 models = await loadChutesModels();
1791 break;
1733 case sources.electronhub:1792 case sources.electronhub:
1734 models = await loadElectronHubModels();1793 models = await loadElectronHubModels();
1735 break;1794 break;
@@ -1748,6 +1807,12 @@ async function loadModels() {
1748 case sources.google:1807 case sources.google:
1749 models = await loadGoogleModels();1808 models = await loadGoogleModels();
1750 break;1809 break;
1810 case sources.zai:
1811 models = await loadZaiModels();
1812 break;
1813 case sources.openrouter:
1814 models = await loadOpenRouterModels();
1815 break;
1751 }1816 }
17521817
1753 if (extension_settings.sd.source === sources.electronhub) {1818 if (extension_settings.sd.source === sources.electronhub) {
@@ -1858,7 +1923,7 @@ async function loadFalaiModels() {
18581923
1859 const result = await fetch('/api/sd/falai/models', {1924 const result = await fetch('/api/sd/falai/models', {
1860 method: 'POST',1925 method: 'POST',
1861 headers: getRequestHeaders(),1926 headers: getRequestHeaders({ omitContentType: true }),
1862 });1927 });
18631928
1864 if (result.ok) {1929 if (result.ok) {
@@ -1877,7 +1942,7 @@ async function loadXAIModels() {
1877async function loadPollinationsModels() {1942async function loadPollinationsModels() {
1878 const result = await fetch('/api/sd/pollinations/models', {1943 const result = await fetch('/api/sd/pollinations/models', {
1879 method: 'POST',1944 method: 'POST',
1880 headers: getRequestHeaders(),1945 headers: getRequestHeaders({ omitContentType: true }),
1881 });1946 });
18821947
1883 if (result.ok) {1948 if (result.ok) {
@@ -1895,7 +1960,7 @@ async function loadTogetherAIModels() {
18951960
1896 const result = await fetch('/api/sd/together/models', {1961 const result = await fetch('/api/sd/together/models', {
1897 method: 'POST',1962 method: 'POST',
1898 headers: getRequestHeaders(),1963 headers: getRequestHeaders({ omitContentType: true }),
1899 });1964 });
19001965
1901 if (result.ok) {1966 if (result.ok) {
@@ -1905,6 +1970,27 @@ async function loadTogetherAIModels() {
1905 return [];1970 return [];
1906}1971}
19071972
1973async function loadChutesModels() {
1974 if (!secret_state[SECRET_KEYS.CHUTES]) {
1975 console.debug('Chutes API key is not set.');
1976 return [];
1977 }
1978
1979 const result = await fetch('/api/sd/chutes/models', {
1980 method: 'POST',
1981 headers: getRequestHeaders({ omitContentType: true }),
1982 });
1983
1984 if (result.ok) {
1985 const models = await result.json();
1986 console.debug('Loaded Chutes image models:', models);
1987 return models;
1988 }
1989
1990 console.warn('Failed to load Chutes models:', result.status);
1991 return [];
1992}
1993
1908async function loadElectronHubModels() {1994async function loadElectronHubModels() {
1909 if (!secret_state[SECRET_KEYS.ELECTRONHUB]) {1995 if (!secret_state[SECRET_KEYS.ELECTRONHUB]) {
1910 console.debug('Electron Hub API key is not set.');1996 console.debug('Electron Hub API key is not set.');
@@ -1913,7 +1999,7 @@ async function loadElectronHubModels() {
19131999
1914 const result = await fetch('/api/sd/electronhub/models', {2000 const result = await fetch('/api/sd/electronhub/models', {
1915 method: 'POST',2001 method: 'POST',
1916 headers: getRequestHeaders(),2002 headers: getRequestHeaders({ omitContentType: true }),
1917 });2003 });
19182004
1919 function getModelName(model) {2005 function getModelName(model) {
@@ -1946,7 +2032,7 @@ async function loadNanoGPTModels() {
19462032
1947 const result = await fetch('/api/sd/nanogpt/models', {2033 const result = await fetch('/api/sd/nanogpt/models', {
1948 method: 'POST',2034 method: 'POST',
1949 headers: getRequestHeaders(),2035 headers: getRequestHeaders({ omitContentType: true }),
1950 });2036 });
19512037
1952 if (result.ok) {2038 if (result.ok) {
@@ -1959,7 +2045,7 @@ async function loadNanoGPTModels() {
1959async function loadHordeModels() {2045async function loadHordeModels() {
1960 const result = await fetch('/api/horde/sd-models', {2046 const result = await fetch('/api/horde/sd-models', {
1961 method: 'POST',2047 method: 'POST',
1962 headers: getRequestHeaders(),2048 headers: getRequestHeaders({ omitContentType: true }),
1963 });2049 });
19642050
19652051
@@ -2080,6 +2166,8 @@ async function loadDrawthingsModels() {
20802166
2081async function loadOpenAiModels() {2167async function loadOpenAiModels() {
2082 return [2168 return [
2169 { value: 'gpt-image-1.5', text: 'gpt-image-1.5' },
2170 { value: 'gpt-image-1-mini', text: 'gpt-image-1-mini' },
2083 { value: 'gpt-image-1', text: 'gpt-image-1' },2171 { value: 'gpt-image-1', text: 'gpt-image-1' },
2084 { value: 'dall-e-3', text: 'dall-e-3' },2172 { value: 'dall-e-3', text: 'dall-e-3' },
2085 { value: 'dall-e-2', text: 'dall-e-2' },2173 { value: 'dall-e-2', text: 'dall-e-2' },
@@ -2093,7 +2181,7 @@ async function loadAimlapiModels() {
20932181
2094 const result = await fetch('/api/sd/aimlapi/models', {2182 const result = await fetch('/api/sd/aimlapi/models', {
2095 method: 'POST',2183 method: 'POST',
2096 headers: getRequestHeaders(),2184 headers: getRequestHeaders({ omitContentType: true }),
2097 });2185 });
20982186
2099 if (!result.ok) {2187 if (!result.ok) {
@@ -2205,11 +2293,34 @@ async function loadGoogleModels() {
2205 ].map(name => ({ value: name, text: name }));2293 ].map(name => ({ value: name, text: name }));
2206}2294}
22072295
2296async function loadZaiModels() {
2297 return ['cogview-4-250304'].map(name => ({ value: name, text: name }));
2298}
2299
2300async function loadOpenRouterModels() {
2301 const result = await fetch('/api/openrouter/models/image', {
2302 method: 'POST',
2303 headers: getRequestHeaders({ omitContentType: true }),
2304 });
2305
2306 if (result.ok) {
2307 return await result.json();
2308 }
2309
2310 return [];
2311}
2312
2208function loadNovelSchedulers() {2313function loadNovelSchedulers() {
2209 return ['karras', 'native', 'exponential', 'polyexponential'];2314 return ['karras', 'native', 'exponential', 'polyexponential'];
2210}2315}
22112316
2212async function loadComfyModels() {2317async function loadComfyModels() {
2318 if (extension_settings.sd.comfy_type === comfyTypes.runpod_serverless) {
2319 $('#sd_runpod_key').toggleClass('success', !!secret_state[SECRET_KEYS.COMFY_RUNPOD]);
2320 return [
2321 { value: '', text: 'N/A' },
2322 ];
2323 }
2213 if (!extension_settings.sd.comfy_url) {2324 if (!extension_settings.sd.comfy_url) {
2214 return [];2325 return [];
2215 }2326 }
@@ -2275,6 +2386,9 @@ async function loadSchedulers() {
2275 case sources.huggingface:2386 case sources.huggingface:
2276 schedulers = ['N/A'];2387 schedulers = ['N/A'];
2277 break;2388 break;
2389 case sources.chutes:
2390 schedulers = ['N/A'];
2391 break;
2278 case sources.electronhub:2392 case sources.electronhub:
2279 schedulers = ['N/A'];2393 schedulers = ['N/A'];
2280 break;2394 break;
@@ -2293,6 +2407,12 @@ async function loadSchedulers() {
2293 case sources.google:2407 case sources.google:
2294 schedulers = ['N/A'];2408 schedulers = ['N/A'];
2295 break;2409 break;
2410 case sources.zai:
2411 schedulers = ['N/A'];
2412 break;
2413 case sources.openrouter:
2414 schedulers = ['N/A'];
2415 break;
2296 }2416 }
22972417
2298 for (const scheduler of schedulers) {2418 for (const scheduler of schedulers) {
@@ -2310,6 +2430,9 @@ async function loadSchedulers() {
2310}2430}
23112431
2312async function loadComfySchedulers() {2432async function loadComfySchedulers() {
2433 if (extension_settings.sd.comfy_type === comfyTypes.runpod_serverless) {
2434 return ['N/A'];
2435 }
2313 if (!extension_settings.sd.comfy_url) {2436 if (!extension_settings.sd.comfy_url) {
2314 return [];2437 return [];
2315 }2438 }
@@ -2375,6 +2498,9 @@ async function loadVaes() {
2375 case sources.huggingface:2498 case sources.huggingface:
2376 vaes = ['N/A'];2499 vaes = ['N/A'];
2377 break;2500 break;
2501 case sources.chutes:
2502 vaes = ['N/A'];
2503 break;
2378 case sources.electronhub:2504 case sources.electronhub:
2379 vaes = ['N/A'];2505 vaes = ['N/A'];
2380 break;2506 break;
@@ -2393,6 +2519,12 @@ async function loadVaes() {
2393 case sources.google:2519 case sources.google:
2394 vaes = ['N/A'];2520 vaes = ['N/A'];
2395 break;2521 break;
2522 case sources.zai:
2523 vaes = ['N/A'];
2524 break;
2525 case sources.openrouter:
2526 vaes = ['N/A'];
2527 break;
2396 }2528 }
23972529
2398 for (const vae of vaes) {2530 for (const vae of vaes) {
@@ -2434,6 +2566,9 @@ async function loadAutoVaes() {
2434}2566}
24352567
2436async function loadComfyVaes() {2568async function loadComfyVaes() {
2569 if (extension_settings.sd.comfy_type === comfyTypes.runpod_serverless) {
2570 return ['N/A'];
2571 }
2437 if (!extension_settings.sd.comfy_url) {2572 if (!extension_settings.sd.comfy_url) {
2438 return [];2573 return [];
2439 }2574 }
@@ -2456,10 +2591,6 @@ async function loadComfyVaes() {
2456}2591}
24572592
2458async function loadComfyWorkflows() {2593async function loadComfyWorkflows() {
2459 if (!extension_settings.sd.comfy_url) {
2460 return;
2461 }
2462
2463 try {2594 try {
2464 $('#sd_comfy_workflow').empty();2595 $('#sd_comfy_workflow').empty();
2465 const result = await fetch('/api/sd/comfy/workflows', {2596 const result = await fetch('/api/sd/comfy/workflows', {
@@ -2532,7 +2663,7 @@ function processReply(str) {
2532 str = str.normalize('NFD');2663 str = str.normalize('NFD');
25332664
2534 // Strip out non-alphanumeric characters barring model syntax exceptions2665 // Strip out non-alphanumeric characters barring model syntax exceptions
2535 str = str.replace(/[^a-zA-Z0-9.,:_(){}<>[\]\-'|#]+/g, ' ');2666 str = str.replace(/[^a-zA-Z0-9.,:_(){}<>[\]/\-'|#]+/g, ' ');
25362667
2537 str = str.replace(/\s+/g, ' '); // Collapse multiple whitespaces into one2668 str = str.replace(/\s+/g, ' '); // Collapse multiple whitespaces into one
2538 str = str.trim();2669 str = str.trim();
@@ -2865,7 +2996,7 @@ function getCharacterAvatarUrl() {
2865 if (context.groupId) {2996 if (context.groupId) {
2866 const groupMembers = context.groups.find(x => x.id === context.groupId)?.members;2997 const groupMembers = context.groups.find(x => x.id === context.groupId)?.members;
2867 const lastMessageAvatar = context.chat?.filter(x => !x.is_system && !x.is_user)?.slice(-1)[0]?.original_avatar;2998 const lastMessageAvatar = context.chat?.filter(x => !x.is_system && !x.is_user)?.slice(-1)[0]?.original_avatar;
2868 const randomMemberAvatar = Array.isArray(groupMembers) ? groupMembers[Math.floor(Math.random() * groupMembers.length)]?.avatar : null;2999 const randomMemberAvatar = Array.isArray(groupMembers) ? groupMembers[Math.floor(Math.random() * groupMembers.length)] : null;
2869 const avatarToUse = lastMessageAvatar || randomMemberAvatar;3000 const avatarToUse = lastMessageAvatar || randomMemberAvatar;
2870 return formatCharacterAvatar(avatarToUse);3001 return formatCharacterAvatar(avatarToUse);
2871 } else {3002 } else {
@@ -2953,7 +3084,16 @@ async function sendGenerationRequest(generationType, prompt, additionalNegativeP
2953 result = await generateAimlapiImage(prefixedPrompt, signal);3084 result = await generateAimlapiImage(prefixedPrompt, signal);
2954 break;3085 break;
2955 case sources.comfy:3086 case sources.comfy:
2956 result = await generateComfyImage(prefixedPrompt, negativePrompt, signal);3087 switch (extension_settings.sd.comfy_type) {
3088 case comfyTypes.runpod_serverless:
3089 result = await generateComfyRunPodImage(prefixedPrompt, negativePrompt, signal);
3090 break;
3091 case comfyTypes.standard:
3092 result = await generateComfyImage(prefixedPrompt, negativePrompt, signal);
3093 break;
3094 default:
3095 throw new Error('Unknown comfyUI server type.');
3096 }
2957 break;3097 break;
2958 case sources.togetherai:3098 case sources.togetherai:
2959 result = await generateTogetherAIImage(prefixedPrompt, negativePrompt, signal);3099 result = await generateTogetherAIImage(prefixedPrompt, negativePrompt, signal);
@@ -2967,6 +3107,9 @@ async function sendGenerationRequest(generationType, prompt, additionalNegativeP
2967 case sources.huggingface:3107 case sources.huggingface:
2968 result = await generateHuggingFaceImage(prefixedPrompt, signal);3108 result = await generateHuggingFaceImage(prefixedPrompt, signal);
2969 break;3109 break;
3110 case sources.chutes:
3111 result = await generateChutesImage(prefixedPrompt, negativePrompt, signal);
3112 break;
2970 case sources.electronhub:3113 case sources.electronhub:
2971 result = await generateElectronHubImage(prefixedPrompt, signal);3114 result = await generateElectronHubImage(prefixedPrompt, signal);
2972 break;3115 break;
@@ -2985,6 +3128,12 @@ async function sendGenerationRequest(generationType, prompt, additionalNegativeP
2985 case sources.google:3128 case sources.google:
2986 result = await generateGoogleImage(prefixedPrompt, negativePrompt, signal);3129 result = await generateGoogleImage(prefixedPrompt, negativePrompt, signal);
2987 break;3130 break;
3131 case sources.zai:
3132 result = await generateZaiImage(prefixedPrompt, signal);
3133 break;
3134 case sources.openrouter:
3135 result = await generateOpenRouterImage(prefixedPrompt, signal);
3136 break;
2988 }3137 }
29893138
2990 if (!result.data) {3139 if (!result.data) {
@@ -3546,9 +3695,9 @@ async function generateOpenAiImage(prompt, signal) {
3546 const dalle3PromptLimit = 4000;3695 const dalle3PromptLimit = 4000;
3547 const gptImgPromptLimit = 32000;3696 const gptImgPromptLimit = 32000;
35483697
3549 const isDalle2 = extension_settings.sd.model === 'dall-e-2';3698 const isDalle2 = /dall-e-2/.test(extension_settings.sd.model);
3550 const isDalle3 = extension_settings.sd.model === 'dall-e-3';3699 const isDalle3 = /dall-e-3/.test(extension_settings.sd.model);
3551 const isGptImg = extension_settings.sd.model === 'gpt-image-1';3700 const isGptImg = /gpt-image-1/.test(extension_settings.sd.model);
3552 const isSora2 = /sora-2/.test(extension_settings.sd.model);3701 const isSora2 = /sora-2/.test(extension_settings.sd.model);
35533702
3554 if (isDalle2 && prompt.length > dalle2PromptLimit) {3703 if (isDalle2 && prompt.length > dalle2PromptLimit) {
@@ -3679,25 +3828,17 @@ async function generateAimlapiImage(prompt, signal) {
3679}3828}
36803829
3681/**3830/**
3682 * Generates an image in ComfyUI using the provided prompt and configuration settings.3831 * Generates an image in local ComfyUI or serverless runpod using the provided prompt and configuration settings.
3683 *3832 *
3684 * @param {string} prompt - The main instruction used to guide the image generation.3833 * @param {string} prompt - The main instruction used to guide the image generation.
3685 * @param {string} negativePrompt - The instruction used to restrict the image generation.3834 * @param {string} negativePrompt - The instruction used to restrict the image generation.
3686 * @param {AbortSignal} signal - An AbortSignal object that can be used to cancel the request.3835 * @param {AbortSignal} signal - An AbortSignal object that can be used to cancel the request.
3836 * @param {string} basePath - ST server endpoint for the service. '/api/sd/comfy' for local, '/api/sd/comfyrunpod' for serverless.
3837 * @param {string[]} placeholders - Array of substitutions to apply to the workflow.
3838 * @param {string} url - The url of the service to call. Passed to ST server.
3687 * @returns {Promise<{format: string, data: string}>} - A promise that resolves when the image generation and processing are complete.3839 * @returns {Promise<{format: string, data: string}>} - A promise that resolves when the image generation and processing are complete.
3688 */3840 */
3689async function generateComfyImage(prompt, negativePrompt, signal) {3841async function generateComfyImageCommon(prompt, negativePrompt, signal, basePath, placeholders, url) {
3690 const placeholders = [
3691 'model',
3692 'vae',
3693 'sampler',
3694 'scheduler',
3695 'steps',
3696 'scale',
3697 'width',
3698 'height',
3699 ];
3700
3701 const workflowResponse = await fetch('/api/sd/comfy/workflow', {3842 const workflowResponse = await fetch('/api/sd/comfy/workflow', {
3702 method: 'POST',3843 method: 'POST',
3703 headers: getRequestHeaders(),3844 headers: getRequestHeaders(),
@@ -3752,12 +3893,12 @@ async function generateComfyImage(prompt, negativePrompt, signal) {
3752 console.log(`{3893 console.log(`{
3753 "prompt": ${workflow}3894 "prompt": ${workflow}
3754 }`);3895 }`);
3755 const promptResult = await fetch('/api/sd/comfy/generate', {3896 const promptResult = await fetch(`${basePath}/generate`, {
3756 method: 'POST',3897 method: 'POST',
3757 headers: getRequestHeaders(),3898 headers: getRequestHeaders(),
3758 signal: signal,3899 signal: signal,
3759 body: JSON.stringify({3900 body: JSON.stringify({
3760 url: extension_settings.sd.comfy_url,3901 url,
3761 prompt: `{3902 prompt: `{
3762 "prompt": ${workflow}3903 "prompt": ${workflow}
3763 }`,3904 }`,
@@ -3771,6 +3912,46 @@ async function generateComfyImage(prompt, negativePrompt, signal) {
3771 return { format, data };3912 return { format, data };
3772}3913}
37733914
3915/**
3916 * Generates an image in ComfyUI using the provided prompt and configuration settings.
3917 *
3918 * @param {string} prompt - The main instruction used to guide the image generation.
3919 * @param {string} negativePrompt - The instruction used to restrict the image generation.
3920 * @param {AbortSignal} signal - An AbortSignal object that can be used to cancel the request.
3921 * @returns {Promise<{format: string, data: string}>} - A promise that resolves when the image generation and processing are complete.
3922 */
3923async function generateComfyImage(prompt, negativePrompt, signal) {
3924 const placeholders = [
3925 'model',
3926 'vae',
3927 'sampler',
3928 'scheduler',
3929 'steps',
3930 'scale',
3931 'width',
3932 'height',
3933 ];
3934 return generateComfyImageCommon(prompt, negativePrompt, signal, '/api/sd/comfy', placeholders, extension_settings.sd.comfy_url);
3935}
3936
3937/**
3938 * Generates an image using ComfyUI through serverless runpod endpoint using the provided prompt and configuration settings.
3939 *
3940 * @param {string} prompt - The main instruction used to guide the image generation.
3941 * @param {string} negativePrompt - The instruction used to restrict the image generation.
3942 * @param {AbortSignal} signal - An AbortSignal object that can be used to cancel the request.
3943 * @returns {Promise<{format: string, data: string}>} - A promise that resolves when the image generation and processing are complete.
3944 */
3945async function generateComfyRunPodImage(prompt, negativePrompt, signal) {
3946 const placeholders = [
3947 'steps',
3948 'scale',
3949 'width',
3950 'height',
3951 ];
3952
3953 return generateComfyImageCommon(prompt, negativePrompt, signal, '/api/sd/comfyrunpod', placeholders, extension_settings.sd.comfy_runpod_url);
3954}
37743955
3775/**3956/**
3776 * Generates an image in Hugging Face Inference API using the provided prompt and configuration settings (model selected).3957 * Generates an image in Hugging Face Inference API using the provided prompt and configuration settings (model selected).
@@ -3799,6 +3980,38 @@ async function generateHuggingFaceImage(prompt, signal) {
3799}3980}
38003981
3801/**3982/**
3983 * Generates an image using the Chutes API.
3984 * @param {string} prompt - The main instruction used to guide the image generation.
3985 * @param {string} negativePrompt - The instruction used to restrict the image generation.
3986 * @param {AbortSignal} signal - An AbortSignal object that can be used to cancel the request.
3987 * @returns {Promise<{format: string, data: string}>} - A promise that resolves when the image generation and processing are complete.
3988 */
3989async function generateChutesImage(prompt, negativePrompt, signal) {
3990 const result = await fetch('/api/sd/chutes/generate', {
3991 method: 'POST',
3992 headers: getRequestHeaders(),
3993 signal: signal,
3994 body: JSON.stringify({
3995 model: extension_settings.sd.model,
3996 prompt: prompt,
3997 negative_prompt: negativePrompt,
3998 width: extension_settings.sd.width,
3999 height: extension_settings.sd.height,
4000 steps: extension_settings.sd.steps,
4001 guidance_scale: extension_settings.sd.scale,
4002 }),
4003 });
4004
4005 if (result.ok) {
4006 const data = await result.json();
4007 return { format: 'jpg', data: data.image };
4008 } else {
4009 const text = await result.text();
4010 throw new Error(text);
4011 }
4012}
4013
4014/**
3802 * Generates an image using the Electron Hub API.4015 * Generates an image using the Electron Hub API.
3803 * @param {string} prompt - The main instruction used to guide the image generation.4016 * @param {string} prompt - The main instruction used to guide the image generation.
3804 * @param {AbortSignal} signal - An AbortSignal object that can be used to cancel the request.4017 * @param {AbortSignal} signal - An AbortSignal object that can be used to cancel the request.
@@ -4022,6 +4235,74 @@ async function generateGoogleImage(prompt, negativePrompt, signal) {
4022 }4235 }
4023}4236}
40244237
4238/**
4239 * Generates an image using the Z.AI API.
4240 * @param {string} prompt The main instruction used to guide the image generation.
4241 * @param {AbortSignal} signal An AbortSignal object that can be used to cancel the request.
4242 * @returns {Promise<{format: string, data: string}>} A promise that resolves when the image generation and processing are complete.
4243 */
4244async function generateZaiImage(prompt, signal) {
4245 // Round width and height to nearest multiple of 16, and clamp to 512-2048 range
4246 let width = clamp(Math.round(extension_settings.sd.width / 16) * 16, 512, 2048);
4247 let height = clamp(Math.round(extension_settings.sd.height / 16) * 16, 512, 2048);
4248
4249 // Make sure the pixel count does not exceed 2^21px
4250 while ((width * height) > Math.pow(2, 21)) {
4251 if (width >= height) {
4252 width -= 16;
4253 } else {
4254 height -= 16;
4255 }
4256 }
4257
4258 const result = await fetch('/api/sd/zai/generate', {
4259 method: 'POST',
4260 headers: getRequestHeaders(),
4261 signal: signal,
4262 body: JSON.stringify({
4263 prompt: prompt,
4264 model: extension_settings.sd.model,
4265 quality: extension_settings.sd.openai_quality,
4266 size: `${width}x${height}`,
4267 }),
4268 });
4269
4270 if (result.ok) {
4271 const data = await result.json();
4272 return { format: data.format, data: data.image };
4273 }
4274
4275 const text = await result.text();
4276 throw new Error(text);
4277}
4278
4279/**
4280 * Generates an image using the OpenRouter API.
4281 * @param {string} prompt The main instruction used to guide the image generation.
4282 * @param {AbortSignal} signal An AbortSignal object that can be used to cancel the request.
4283 * @returns {Promise<{format: string, data: string}>}
4284 */
4285async function generateOpenRouterImage(prompt, signal) {
4286 const result = await fetch('/api/openrouter/image/generate', {
4287 method: 'POST',
4288 headers: getRequestHeaders(),
4289 signal: signal,
4290 body: JSON.stringify({
4291 model: extension_settings.sd.model,
4292 prompt: prompt,
4293 aspect_ratio: getClosestAspectRatio(extension_settings.sd.width, extension_settings.sd.height, 'stability'),
4294 }),
4295 });
4296
4297 if (result.ok) {
4298 const data = await result.json();
4299 return { format: 'jpg', data: data.image };
4300 }
4301
4302 const text = await result.text();
4303 throw new Error(text);
4304}
4305
4025async function onComfyOpenWorkflowEditorClick() {4306async function onComfyOpenWorkflowEditorClick() {
4026 let workflow = await (await fetch('/api/sd/comfy/workflow', {4307 let workflow = await (await fetch('/api/sd/comfy/workflow', {
4027 method: 'POST',4308 method: 'POST',
@@ -4207,6 +4488,7 @@ async function sendMessage(prompt, image, generationType, additionalNegativePref
4207 context.addOneMessage(message);4488 context.addOneMessage(message);
4208 await eventSource.emit(event_types.CHARACTER_MESSAGE_RENDERED, messageId, 'extension');4489 await eventSource.emit(event_types.CHARACTER_MESSAGE_RENDERED, messageId, 'extension');
4209 await context.saveChat();4490 await context.saveChat();
4491 setTimeout(() => context.scrollOnMediaLoad(), debounce_timeout.short);
4210}4492}
42114493
4212/**4494/**
@@ -4304,7 +4586,15 @@ function isValidState() {
4304 case sources.aimlapi:4586 case sources.aimlapi:
4305 return secret_state[SECRET_KEYS.AIMLAPI];4587 return secret_state[SECRET_KEYS.AIMLAPI];
4306 case sources.comfy:4588 case sources.comfy:
4307 return true;4589 switch (extension_settings.sd.comfy_type) {
4590 case comfyTypes.runpod_serverless:
4591 return !!extension_settings.sd.comfy_runpod_url &&
4592 secret_state[SECRET_KEYS.COMFY_RUNPOD];
4593 case comfyTypes.standard:
4594 return !!extension_settings.sd.comfy_url;
4595 default:
4596 return false;
4597 }
4308 case sources.togetherai:4598 case sources.togetherai:
4309 return secret_state[SECRET_KEYS.TOGETHERAI];4599 return secret_state[SECRET_KEYS.TOGETHERAI];
4310 case sources.pollinations:4600 case sources.pollinations:
@@ -4313,6 +4603,8 @@ function isValidState() {
4313 return secret_state[SECRET_KEYS.STABILITY];4603 return secret_state[SECRET_KEYS.STABILITY];
4314 case sources.huggingface:4604 case sources.huggingface:
4315 return secret_state[SECRET_KEYS.HUGGINGFACE];4605 return secret_state[SECRET_KEYS.HUGGINGFACE];
4606 case sources.chutes:
4607 return secret_state[SECRET_KEYS.CHUTES];
4316 case sources.electronhub:4608 case sources.electronhub:
4317 return secret_state[SECRET_KEYS.ELECTRONHUB];4609 return secret_state[SECRET_KEYS.ELECTRONHUB];
4318 case sources.nanogpt:4610 case sources.nanogpt:
@@ -4325,6 +4617,12 @@ function isValidState() {
4325 return secret_state[SECRET_KEYS.XAI];4617 return secret_state[SECRET_KEYS.XAI];
4326 case sources.google:4618 case sources.google:
4327 return secret_state[SECRET_KEYS.MAKERSUITE] || secret_state[SECRET_KEYS.VERTEXAI] || secret_state[SECRET_KEYS.VERTEXAI_SERVICE_ACCOUNT];4619 return secret_state[SECRET_KEYS.MAKERSUITE] || secret_state[SECRET_KEYS.VERTEXAI] || secret_state[SECRET_KEYS.VERTEXAI_SERVICE_ACCOUNT];
4620 case sources.zai:
4621 return secret_state[SECRET_KEYS.ZAI];
4622 case sources.openrouter:
4623 return secret_state[SECRET_KEYS.OPENROUTER];
4624 default:
4625 return false;
4328 }4626 }
4329}4627}
43304628
@@ -4378,6 +4676,10 @@ async function sdMessageButton($icon, { animate } = {}) {
4378 message.extra.media = [];4676 message.extra.media = [];
4379 }4677 }
43804678
4679 if (!message.extra.media.length && !message.extra.media_display) {
4680 message.extra.media_display = MEDIA_DISPLAY.GALLERY;
4681 }
4682
4381 /** @type {MediaAttachment} */4683 /** @type {MediaAttachment} */
4382 const selectedMedia = message.extra.media.length > 04684 const selectedMedia = message.extra.media.length > 0
4383 ? (message.extra.media[message.extra.media_index] ?? message.extra.media[message.extra.media.length - 1])4685 ? (message.extra.media[message.extra.media_index] ?? message.extra.media[message.extra.media.length - 1])
@@ -4506,6 +4808,10 @@ async function generateMediaSwipe(mediaAttachment, message, onStart, onComplete,
4506async function onImageSwiped({ message, element, direction }) {4808async function onImageSwiped({ message, element, direction }) {
4507 const { powerUserSettings, accountStorage } = getContext();4809 const { powerUserSettings, accountStorage } = getContext();
45084810
4811 if (!isValidState()) {
4812 return;
4813 }
4814
4509 if (!message || direction !== SWIPE_DIRECTION.RIGHT || powerUserSettings.image_overswipe !== IMAGE_OVERSWIPE.GENERATE) {4815 if (!message || direction !== SWIPE_DIRECTION.RIGHT || powerUserSettings.image_overswipe !== IMAGE_OVERSWIPE.GENERATE) {
4510 return;4816 return;
4511 }4817 }
@@ -4954,8 +5260,11 @@ jQuery(async () => {
4954 $('#sd_novel_decrisper').on('input', onNovelDecrisperInput);5260 $('#sd_novel_decrisper').on('input', onNovelDecrisperInput);
4955 $('#sd_novel_variety_boost').on('input', onNovelVarietyBoostInput);5261 $('#sd_novel_variety_boost').on('input', onNovelVarietyBoostInput);
4956 $('#sd_pollinations_enhance').on('input', onPollinationsEnhanceInput);5262 $('#sd_pollinations_enhance').on('input', onPollinationsEnhanceInput);
5263 $('#sd_comfy_type').on('change', onComfyTypeChange);
4957 $('#sd_comfy_validate').on('click', validateComfyUrl);5264 $('#sd_comfy_validate').on('click', validateComfyUrl);
5265 $('#sd_comfy_runpod_validate').on('click', validateComfyRunPodUrl);
4958 $('#sd_comfy_url').on('input', onComfyUrlInput);5266 $('#sd_comfy_url').on('input', onComfyUrlInput);
5267 $('#sd_comfy_runpod_url').on('input', onComfyRunPodUrlInput);
4959 $('#sd_comfy_workflow').on('change', onComfyWorkflowChange);5268 $('#sd_comfy_workflow').on('change', onComfyWorkflowChange);
4960 $('#sd_comfy_open_workflow_editor').on('click', onComfyOpenWorkflowEditorClick);5269 $('#sd_comfy_open_workflow_editor').on('click', onComfyOpenWorkflowEditorClick);
4961 $('#sd_comfy_new_workflow').on('click', onComfyNewWorkflowClick);5270 $('#sd_comfy_new_workflow').on('click', onComfyNewWorkflowClick);
@@ -5033,6 +5342,7 @@ jQuery(async () => {
5033 case SECRET_KEYS.FALAI:5342 case SECRET_KEYS.FALAI:
5034 case SECRET_KEYS.STABILITY:5343 case SECRET_KEYS.STABILITY:
5035 case SECRET_KEYS.AIMLAPI:5344 case SECRET_KEYS.AIMLAPI:
5345 case SECRET_KEYS.COMFY_RUNPOD:
5036 await loadSettingOptions();5346 await loadSettingOptions();
5037 break;5347 break;
5038 }5348 }
@@ -5064,6 +5374,25 @@ jQuery(async () => {
5064 return isNegative ? negativePrompt : characterPrompt;5374 return isNegative ? negativePrompt : characterPrompt;
5065 };5375 };
50665376
5067 MacrosParser.registerMacro('charPrefix', () => getMacroValue({ isNegative: false }), t`Character's positive positive Image Generation prompt prefix`);5377 if (power_user.experimental_macro_engine) {
5068 MacrosParser.registerMacro('charNegativePrefix', () => getMacroValue({ isNegative: true }), t`Character's negative Image Generation prompt prefix`);5378 macros.register('charPrefix', {
5379 category: MacroCategory.PROMPTS,
5380 description: t`Character's positive Image Generation prompt prefix`,
5381 handler: () => getMacroValue({ isNegative: false }),
5382 });
5383 macros.register('charNegativePrefix', {
5384 category: MacroCategory.PROMPTS,
5385 description: t`Character's negative Image Generation prompt prefix`,
5386 handler: () => getMacroValue({ isNegative: true }),
5387 });
5388 } else {
5389 MacrosParser.registerMacro('charPrefix',
5390 () => getMacroValue({ isNegative: false }),
5391 t`Character's positive Image Generation prompt prefix`,
5392 );
5393 MacrosParser.registerMacro('charNegativePrefix',
5394 () => getMacroValue({ isNegative: true }),
5395 t`Character's negative Image Generation prompt prefix`,
5396 );
5397 }
5069});5398});
public/scripts/extensions/stable-diffusion/settings.html+51 -12
@@ -39,6 +39,7 @@
39 <select id="sd_source">39 <select id="sd_source">
40 <option value="aimlapi">AI/ML API</option>40 <option value="aimlapi">AI/ML API</option>
41 <option value="bfl">BFL (Black Forest Labs)</option>41 <option value="bfl">BFL (Black Forest Labs)</option>
42 <option value="chutes">Chutes</option>
42 <option value="comfy">ComfyUI</option>43 <option value="comfy">ComfyUI</option>
43 <option value="drawthings">DrawThings HTTP API</option>44 <option value="drawthings">DrawThings HTTP API</option>
44 <option value="electronhub">Electron Hub</option>45 <option value="electronhub">Electron Hub</option>
@@ -49,6 +50,7 @@
49 <option value="nanogpt">NanoGPT</option>50 <option value="nanogpt">NanoGPT</option>
50 <option value="novel">NovelAI Diffusion</option>51 <option value="novel">NovelAI Diffusion</option>
51 <option value="openai">OpenAI</option>52 <option value="openai">OpenAI</option>
53 <option value="openrouter">OpenRouter</option>
52 <option value="pollinations">Pollinations</option>54 <option value="pollinations">Pollinations</option>
53 <option value="vlad">SD.Next (vladmandic)</option>55 <option value="vlad">SD.Next (vladmandic)</option>
54 <option value="stability">Stability AI</option>56 <option value="stability">Stability AI</option>
@@ -56,6 +58,7 @@
56 <option value="horde">Stable Horde</option>58 <option value="horde">Stable Horde</option>
57 <option value="togetherai">TogetherAI</option>59 <option value="togetherai">TogetherAI</option>
58 <option value="xai">xAI (Grok)</option>60 <option value="xai">xAI (Grok)</option>
61 <option value="zai">Z.AI (CogView)</option>
59 </select>62 </select>
60 <div data-sd-source="auto">63 <div data-sd-source="auto">
61 <label for="sd_auto_url">SD Web UI URL</label>64 <label for="sd_auto_url">SD Web UI URL</label>
@@ -94,6 +97,9 @@
94 <label for="sd_huggingface_model_id" data-i18n="Model ID">Model ID</label>97 <label for="sd_huggingface_model_id" data-i18n="Model ID">Model ID</label>
95 <input id="sd_huggingface_model_id" type="text" class="text_pole" data-i18n="[placeholder]e.g. black-forest-labs/FLUX.1-dev" placeholder="e.g. black-forest-labs/FLUX.1-dev" value="" />98 <input id="sd_huggingface_model_id" type="text" class="text_pole" data-i18n="[placeholder]e.g. black-forest-labs/FLUX.1-dev" placeholder="e.g. black-forest-labs/FLUX.1-dev" value="" />
96 </div>99 </div>
100 <div data-sd-source="chutes">
101 <i data-i18n="Hint: Save an API key in the Chutes (Chat Completion) API settings to use it here.">Hint: Save an API key in the Chutes (Chat Completion) API settings to use it here.</i>
102 </div>
97 <div data-sd-source="electronhub">103 <div data-sd-source="electronhub">
98 <i data-i18n="Hint: Save an API key in the Electron Hub (Chat Completion) API settings to use it here.">Hint: Save an API key in the Electron Hub (Chat Completion) API settings to use it here.</i>104 <i data-i18n="Hint: Save an API key in the Electron Hub (Chat Completion) API settings to use it here.">Hint: Save an API key in the Electron Hub (Chat Completion) API settings to use it here.</i>
99 <div class="flex-container" id="sd_electronhub_quality_row">105 <div class="flex-container" id="sd_electronhub_quality_row">
@@ -160,16 +166,19 @@
160 </div>166 </div>
161 </div>167 </div>
162 </div>168 </div>
163 <div data-sd-source="openai,aimlapi">169 <div data-sd-source="zai">
164 <div data-sd-model="dall-e-3" class="flex-container">170 <b>Will use Common API. Coding API is not supported!</b>
165 <div class="flex1">171 </div>
172 <div data-sd-source="openai,aimlapi,zai">
173 <div class="flex-container">
174 <div data-sd-model="dall-e-3" class="flex1">
166 <label for="sd_openai_style" data-i18n="Image Style">Image Style</label>175 <label for="sd_openai_style" data-i18n="Image Style">Image Style</label>
167 <select id="sd_openai_style">176 <select id="sd_openai_style">
168 <option value="vivid">Vivid</option>177 <option value="vivid">Vivid</option>
169 <option value="natural">Natural</option>178 <option value="natural">Natural</option>
170 </select>179 </select>
171 </div>180 </div>
172 <div class="flex1">181 <div data-sd-model="dall-e-3,cogview-4-250304" class="flex1">
173 <label for="sd_openai_quality" data-i18n="Image Quality">Image Quality</label>182 <label for="sd_openai_quality" data-i18n="Image Quality">Image Quality</label>
174 <select id="sd_openai_quality">183 <select id="sd_openai_quality">
175 <option value="standard" data-i18n="Standard">Standard</option>184 <option value="standard" data-i18n="Standard">Standard</option>
@@ -189,14 +198,44 @@
189 </div>198 </div>
190 </div>199 </div>
191 <div data-sd-source="comfy">200 <div data-sd-source="comfy">
192 <label for="sd_comfy_url">ComfyUI URL</label>201 <label for="sd_comfy_type">Server Type</label>
193 <div class="flex-container flexnowrap">202 <select id="sd_comfy_type">
194 <input id="sd_comfy_url" type="text" class="text_pole" data-i18n="[placeholder]sd_comfy_url" placeholder="Example: {{comfy_url}}" value="{{comfy_url}}" />203 <option value="standard">Standard Server</option>
195 <div id="sd_comfy_validate" class="menu_button menu_button_icon">204 <option value="runpod_serverless">RunPod Serverless Endpoint</option>
196 <i class="fa-solid fa-check"></i>205 </select>
197 <span data-i18n="Connect">206 <div data-sd-comfy-type="standard">
198 Connect207 <label for="sd_comfy_url">ComfyUI URL</label>
199 </span>208 <div class="flex-container flexnowrap">
209 <input id="sd_comfy_url" type="text" class="text_pole" data-i18n="[placeholder]sd_comfy_url" placeholder="Example: {{comfy_url}}" value="{{comfy_url}}" />
210 <div id="sd_comfy_validate" class="menu_button menu_button_icon">
211 <i class="fa-solid fa-check"></i>
212 <span data-i18n="Connect">
213 Connect
214 </span>
215 </div>
216 </div>
217 </div>
218 <div data-sd-comfy-type="runpod_serverless">
219 <label for="sd_comfy_runpod_url">ComfyUI RunPod URL</label>
220 <div class="flex-container flexnowrap">
221 <input id="sd_comfy_runpod_url" type="text" class="text_pole" data-i18n="[placeholder]sd_comfy_runpod_url" placeholder="eg: https://api.runpod.ai/v2/<your endpoint id>" value="{{comfy_runpod_url}}" />
222 <div id="sd_comfy_runpod_validate" class="menu_button menu_button_icon">
223 <i class="fa-solid fa-check"></i>
224 <span data-i18n="Connect">
225 Connect
226 </span>
227 </div>
228 </div>
229 <div class="flex-container flexnowrap alignItemsBaseline marginBot5">
230 <a href="https://console.runpod.io/user/settings" target="_blank" rel="noopener noreferrer">
231 <strong data-i18n="API Key">API Key</strong>
232 <i class="fa-solid fa-share-from-square"></i>
233 </a>
234 <span class="expander"></span>
235 <div id="sd_runpod_key" class="menu_button menu_button_icon manage-api-keys" data-key="api_key_comfy_runpod">
236 <i class="fa-fw fa-solid fa-key"></i>
237 <span data-i18n="Click to set">Click to set</span>
238 </div>
200 </div>239 </div>
201 </div>240 </div>
202 <p><i><b data-i18n="Important:">Important:</b></i><i data-i18n="The server must be accessible from the SillyTavern host machine."> The server must be accessible from the SillyTavern host machine.</i></p>241 <p><i><b data-i18n="Important:">Important:</b></i><i data-i18n="The server must be accessible from the SillyTavern host machine."> The server must be accessible from the SillyTavern host machine.</i></p>
public/scripts/extensions/translate/index.js+2 -2
@@ -210,7 +210,7 @@ async function translateIncomingMessage(messageId) {
210 return;210 return;
211 }211 }
212212
213 const textToTranslate = substituteParams(message.mes, context.name1, message.name);213 const textToTranslate = substituteParams(message.mes, { name2Override: message.name });
214 const translation = await translate(textToTranslate, extension_settings.translate.target_language);214 const translation = await translate(textToTranslate, extension_settings.translate.target_language);
215 message.extra.display_text = translation;215 message.extra.display_text = translation;
216216
@@ -238,7 +238,7 @@ async function translateIncomingMessageReasoning(messageId) {
238 return false;238 return false;
239 }239 }
240240
241 const textToTranslate = substituteParams(message.extra.reasoning, context.name1, message.name);241 const textToTranslate = substituteParams(message.extra.reasoning, { name2Override: message.name });
242 const translation = await translate(textToTranslate, extension_settings.translate.target_language);242 const translation = await translate(textToTranslate, extension_settings.translate.target_language);
243 message.extra.reasoning_display_text = translation;243 message.extra.reasoning_display_text = translation;
244244
public/scripts/extensions/tts/chutes.js+233 -0
@@ -0,0 +1,233 @@
1import { event_types, eventSource, getRequestHeaders } from '../../../script.js';
2import { SECRET_KEYS, secret_state } from '../../secrets.js';
3import { getPreviewString, saveTtsProviderSettings } from './index.js';
4
5export { ChutesTtsProvider };
6
7class ChutesTtsProvider {
8 settings;
9 voices = [];
10 models = [];
11 separator = ' . ';
12
13 defaultSettings = {
14 voiceMap: {},
15 model: 'kokoro',
16 speed: 1,
17 };
18
19 get settingsHtml() {
20 let html = `
21 <div class="flex-container alignItemsCenter">
22 <div class="flex1">Chutes TTS API</div>
23 <div id="chutes_tts_key" class="menu_button menu_button_icon manage-api-keys" data-key="api_key_chutes">
24 <i class="fa-solid fa-key"></i>
25 <span>API Key</span>
26 </div>
27 </div>
28 <div class="flex-container flexFlowColumn">
29 <div class="flex1">
30 <label for="chutes_tts_model">Model</label>
31 <select id="chutes_tts_model" class="text_pole"></select>
32 </div>
33 <div>
34 <label for="chutes_tts_speed">Speed <span id="chutes_tts_speed_output"></span></label>
35 <input type="range" id="chutes_tts_speed" value="1" min="0.25" max="3" step="0.05">
36 </div>
37 </div>`;
38 return html;
39 }
40
41 constructor() {
42 this.handler = async function (/** @type {string} */ key) {
43 if (key !== SECRET_KEYS.CHUTES) return;
44 $('#chutes_tts_key').toggleClass('success', !!secret_state[SECRET_KEYS.CHUTES]);
45 await this.onRefreshClick();
46 }.bind(this);
47 }
48
49 dispose() {
50 [event_types.SECRET_WRITTEN, event_types.SECRET_DELETED, event_types.SECRET_ROTATED].forEach(event => {
51 eventSource.removeListener(event, this.handler);
52 });
53 }
54
55 onSettingsChange() {
56 this.settings.model = $('#chutes_tts_model').val();
57 this.settings.speed = Number($('#chutes_tts_speed').val());
58 saveTtsProviderSettings();
59 }
60
61 async loadSettings(settings) {
62 if (Object.keys(settings).length === 0) {
63 Object.assign(settings, this.defaultSettings);
64 }
65
66 this.settings = settings;
67
68 if (!this.settings.voiceMap) {
69 this.settings.voiceMap = {};
70 }
71
72 // Update UI
73 $('#chutes_tts_model').val(this.settings.model);
74 $('#chutes_tts_speed').val(this.settings.speed);
75 $('#chutes_tts_speed_output').text(this.settings.speed);
76
77 $('#chutes_tts_key').toggleClass('success', !!secret_state[SECRET_KEYS.CHUTES]);
78 [event_types.SECRET_WRITTEN, event_types.SECRET_DELETED, event_types.SECRET_ROTATED].forEach(event => {
79 eventSource.on(event, this.handler);
80 });
81
82 await this.checkReady();
83
84 $('#chutes_tts_model').on('change', () => this.onSettingsChange());
85 $('#chutes_tts_speed').on('input', () => {
86 const value = $('#chutes_tts_speed').val();
87 $('#chutes_tts_speed_output').text(String(value));
88 this.onSettingsChange();
89 });
90 }
91
92 async checkReady() {
93 await this.updateModels();
94 if (this.models.length === 0) {
95 // No models available
96 }
97 await this.updateVoices();
98 }
99
100 async onRefreshClick() {
101 return await this.checkReady();
102 }
103
104 async updateModels() {
105 // For Chutes TTS, we always use the Kokoro model currently.
106 this.models = ['kokoro'];
107
108 $('#chutes_tts_model').empty();
109 $('#chutes_tts_model').append($('<option>').val('kokoro').text('Kokoro'));
110 $('#chutes_tts_model').val('kokoro');
111
112 this.settings.model = 'kokoro';
113 }
114
115 async updateVoices() {
116 // Kokoro voices list
117 const kokoroVoices = [
118 { id: 'af_alloy', name: 'Alloy (Female)', lang: 'en-US' },
119 { id: 'af_aoede', name: 'Aoede (Female)', lang: 'en-US' },
120 { id: 'af_bella', name: 'Bella (Female)', lang: 'en-US' },
121 { id: 'af_heart', name: 'Heart (Female) - Default', lang: 'en-US' },
122 { id: 'af_jessica', name: 'Jessica (Female)', lang: 'en-US' },
123 { id: 'af_kore', name: 'Kore (Female)', lang: 'en-US' },
124 { id: 'af_nicole', name: 'Nicole (Female)', lang: 'en-US' },
125 { id: 'af_nova', name: 'Nova (Female)', lang: 'en-US' },
126 { id: 'af_river', name: 'River (Female)', lang: 'en-US' },
127 { id: 'af_sarah', name: 'Sarah (Female)', lang: 'en-US' },
128 { id: 'af_sky', name: 'Sky (Female)', lang: 'en-US' },
129 { id: 'am_adam', name: 'Adam (Male)', lang: 'en-US' },
130 { id: 'am_echo', name: 'Echo (Male)', lang: 'en-US' },
131 { id: 'am_eric', name: 'Eric (Male)', lang: 'en-US' },
132 { id: 'am_fenrir', name: 'Fenrir (Male)', lang: 'en-US' },
133 { id: 'am_liam', name: 'Liam (Male)', lang: 'en-US' },
134 { id: 'am_michael', name: 'Michael (Male)', lang: 'en-US' },
135 { id: 'am_onyx', name: 'Onyx (Male)', lang: 'en-US' },
136 { id: 'am_puck', name: 'Puck (Male)', lang: 'en-US' },
137 { id: 'am_santa', name: 'Santa (Male)', lang: 'en-US' },
138 { id: 'bf_alice', name: 'Alice (British Female)', lang: 'en-GB' },
139 { id: 'bf_emma', name: 'Emma (British Female)', lang: 'en-GB' },
140 { id: 'bf_isabella', name: 'Isabella (British Female)', lang: 'en-GB' },
141 { id: 'bf_lily', name: 'Lily (British Female)', lang: 'en-GB' },
142 { id: 'bm_daniel', name: 'Daniel (British Male)', lang: 'en-GB' },
143 { id: 'bm_fable', name: 'Fable (British Male)', lang: 'en-GB' },
144 { id: 'bm_george', name: 'George (British Male)', lang: 'en-GB' },
145 { id: 'bm_lewis', name: 'Lewis (British Male)', lang: 'en-GB' },
146 { id: 'ef_dora', name: 'Dora (European Female)', lang: 'es-ES' },
147 { id: 'em_alex', name: 'Alex (European Male)', lang: 'es-ES' },
148 { id: 'em_santa', name: 'Santa (European Male)', lang: 'es-ES' },
149 { id: 'ff_siwis', name: 'Siwis (French Female)', lang: 'fr-FR' },
150 { id: 'hf_alpha', name: 'Alpha (Hindi Female)', lang: 'hi-IN' },
151 { id: 'hf_beta', name: 'Beta (Hindi Female)', lang: 'hi-IN' },
152 { id: 'hm_omega', name: 'Omega (Hindi Male)', lang: 'hi-IN' },
153 { id: 'hm_psi', name: 'Psi (Hindi Male)', lang: 'hi-IN' },
154 { id: 'if_sara', name: 'Sara (Italian Female)', lang: 'it-IT' },
155 { id: 'im_nicola', name: 'Nicola (Italian Male)', lang: 'it-IT' },
156 { id: 'jf_alpha', name: 'Alpha (Japanese Female)', lang: 'ja-JP' },
157 { id: 'jf_gongitsune', name: 'Gongitsune (Japanese Female)', lang: 'ja-JP' },
158 { id: 'jf_nezumi', name: 'Nezumi (Japanese Female)', lang: 'ja-JP' },
159 { id: 'jf_tebukuro', name: 'Tebukuro (Japanese Female)', lang: 'ja-JP' },
160 { id: 'jm_kumo', name: 'Kumo (Japanese Male)', lang: 'ja-JP' },
161 { id: 'pf_dora', name: 'Dora (Portuguese Female)', lang: 'pt-PT' },
162 { id: 'pm_alex', name: 'Alex (Portuguese Male)', lang: 'pt-PT' },
163 { id: 'pm_santa', name: 'Santa (Portuguese Male)', lang: 'pt-PT' },
164 { id: 'zf_xiaobei', name: 'Xiaobei (Chinese Female)', lang: 'zh-CN' },
165 { id: 'zf_xiaoni', name: 'Xiaoni (Chinese Female)', lang: 'zh-CN' },
166 { id: 'zf_xiaoxiao', name: 'Xiaoxiao (Chinese Female)', lang: 'zh-CN' },
167 { id: 'zf_xiaoyi', name: 'Xiaoyi (Chinese Female)', lang: 'zh-CN' },
168 { id: 'zm_yunjian', name: 'Yunjian (Chinese Male)', lang: 'zh-CN' },
169 { id: 'zm_yunxi', name: 'Yunxi (Chinese Male)', lang: 'zh-CN' },
170 { id: 'zm_yunxia', name: 'Yunxia (Chinese Male)', lang: 'zh-CN' },
171 { id: 'zm_yunyang', name: 'Yunyang (Chinese Male)', lang: 'zh-CN' },
172 ];
173
174 this.voices = kokoroVoices.map(v => ({
175 name: v.name,
176 voice_id: v.id,
177 lang: v.lang,
178 }));
179 }
180
181 async getVoice(voiceName) {
182 if (this.voices.length === 0) {
183 await this.updateVoices();
184 }
185 const voice = this.voices.find(v => v.name === voiceName || v.voice_id === voiceName);
186 return voice || this.voices.find(v => v.voice_id === 'af_heart');
187 }
188
189 async generateTts(text, voiceId) {
190 const response = await this.fetchTtsGeneration(text, voiceId);
191 return response;
192 }
193
194 async fetchTtsGeneration(text, voiceId) {
195 const apiKey = secret_state[SECRET_KEYS.CHUTES];
196
197 if (!apiKey) {
198 throw new Error('No Chutes API key found');
199 }
200
201 const response = await fetch('/api/openai/chutes/generate-voice', {
202 method: 'POST',
203 headers: getRequestHeaders(),
204 body: JSON.stringify({
205 input: text,
206 voice: voiceId || 'af_heart',
207 speed: this.settings.speed || 1,
208 }),
209 });
210
211 if (!response.ok) {
212 const error = await response.text();
213 throw new Error(`Chutes TTS failed: ${error}`);
214 }
215
216 return response;
217 }
218
219 async fetchTtsVoiceObjects() {
220 if (this.voices.length === 0) {
221 await this.updateVoices();
222 }
223
224 const voiceIds = this.voices
225 .map(voice => ({ name: voice.name, voice_id: voice.voice_id, preview_url: false }));
226 return voiceIds;
227 }
228
229 async previewTtsVoice(voiceId) {
230 const text = getPreviewString(voiceId);
231 await this.generateTts(text, voiceId);
232 }
233}
public/scripts/extensions/tts/edge.js+1 -1
@@ -259,7 +259,7 @@ class EdgeTtsProvider {
259 try {259 try {
260 const result = await fetch('/api/plugins/edge-tts/probe', {260 const result = await fetch('/api/plugins/edge-tts/probe', {
261 method: 'POST',261 method: 'POST',
262 headers: getRequestHeaders(),262 headers: getRequestHeaders({ omitContentType: true }),
263 });263 });
264 return result.ok;264 return result.ok;
265 } catch (e) {265 } catch (e) {
public/scripts/extensions/tts/electronhub.js+1 -1
@@ -187,7 +187,7 @@ class ElectronHubTtsProvider {
187 try {187 try {
188 const response = await fetch('/api/openai/electronhub/models', {188 const response = await fetch('/api/openai/electronhub/models', {
189 method: 'POST',189 method: 'POST',
190 headers: getRequestHeaders(),190 headers: getRequestHeaders({ omitContentType: true }),
191 });191 });
192 if (!response.ok) {192 if (!response.ok) {
193 throw new Error(`HTTP ${response.status}: ${await response.text()}`);193 throw new Error(`HTTP ${response.status}: ${await response.text()}`);
public/scripts/extensions/tts/elevenlabs.js+131 -106
@@ -1,23 +1,20 @@
1import { saveTtsProviderSettings } from './index.js';1import { saveTtsProviderSettings } from './index.js';
2import { event_types, eventSource, getRequestHeaders } from '/script.js';
3import { SECRET_KEYS, secret_state, writeSecret } from '/scripts/secrets.js';
4import { getBase64Async } from '/scripts/utils.js';
2export { ElevenLabsTtsProvider };5export { ElevenLabsTtsProvider };
36
4class ElevenLabsTtsProvider {7class ElevenLabsTtsProvider {
5 //########//
6 // Config //
7 //########//
8
9 settings;8 settings;
10 voices = [];9 voices = [];
11 separator = ' ... ... ... ';10 separator = ' ... ... ... ';
1211
13
14 defaultSettings = {12 defaultSettings = {
15 stability: 0.75,13 stability: 0.75,
16 similarity_boost: 0.75,14 similarity_boost: 0.75,
17 style_exaggeration: 0.00,15 style_exaggeration: 0.00,
18 speaker_boost: true,16 speaker_boost: true,
19 speed: 1.0,17 speed: 1.0,
20 apiKey: '',
21 model: 'eleven_turbo_v2_5',18 model: 'eleven_turbo_v2_5',
22 voiceMap: {},19 voiceMap: {},
23 };20 };
@@ -25,8 +22,15 @@ class ElevenLabsTtsProvider {
25 get settingsHtml() {22 get settingsHtml() {
26 let html = `23 let html = `
27 <div class="elevenlabs_tts_settings">24 <div class="elevenlabs_tts_settings">
28 <label for="elevenlabs_tts_api_key">API Key</label>25 <div class="flex-container alignItemsBaseline">
29 <input id="elevenlabs_tts_api_key" type="text" class="text_pole" placeholder="<API Key>"/>26 <h4 for="elevenlabs_tts_key" class="flex1 margin0">
27 <a href="https://elevenlabs.io/app/developers/api-keys" target="_blank">ElevenLabs TTS Key</a>
28 </h4>
29 <div id="elevenlabs_tts_key" class="menu_button menu_button_icon manage-api-keys" data-key="api_key_elevenlabs">
30 <i class="fa-solid fa-key"></i>
31 <span>Click to set</span>
32 </div>
33 </div>
30 <label for="elevenlabs_tts_model">Model</label>34 <label for="elevenlabs_tts_model">Model</label>
31 <select id="elevenlabs_tts_model" class="text_pole">35 <select id="elevenlabs_tts_model" class="text_pole">
32 <option value="eleven_v3">Eleven v3</option>36 <option value="eleven_v3">Eleven v3</option>
@@ -39,7 +43,6 @@ class ElevenLabsTtsProvider {
39 <option value="eleven_multilingual_v1">Multilingual v1 (Old)</option>43 <option value="eleven_multilingual_v1">Multilingual v1 (Old)</option>
40 <option value="eleven_turbo_v2">Turbo v2 (Old)</option>44 <option value="eleven_turbo_v2">Turbo v2 (Old)</option>
41 </select>45 </select>
42 <input id="eleven_labs_connect" class="menu_button" type="button" value="Connect" />
43 <label for="elevenlabs_tts_stability">Stability: <span id="elevenlabs_tts_stability_output"></span></label>46 <label for="elevenlabs_tts_stability">Stability: <span id="elevenlabs_tts_stability_output"></span></label>
44 <input id="elevenlabs_tts_stability" type="range" value="${this.defaultSettings.stability}" min="0" max="1" step="0.01" />47 <input id="elevenlabs_tts_stability" type="range" value="${this.defaultSettings.stability}" min="0" max="1" step="0.01" />
45 <label for="elevenlabs_tts_similarity_boost">Similarity Boost: <span id="elevenlabs_tts_similarity_boost_output"></span></label>48 <label for="elevenlabs_tts_similarity_boost">Similarity Boost: <span id="elevenlabs_tts_similarity_boost_output"></span></label>
@@ -72,6 +75,20 @@ class ElevenLabsTtsProvider {
72 return html;75 return html;
73 }76 }
7477
78 constructor() {
79 this.handler = async function (/** @type {string} */ key) {
80 if (key !== SECRET_KEYS.ELEVENLABS) return;
81 $('#elevenlabs_tts_key').toggleClass('success', !!secret_state[SECRET_KEYS.ELEVENLABS]);
82 await this.fetchTtsVoiceObjects();
83 }.bind(this);
84 }
85
86 dispose() {
87 [event_types.SECRET_WRITTEN, event_types.SECRET_DELETED, event_types.SECRET_ROTATED].forEach(event => {
88 eventSource.removeListener(event, this.handler);
89 });
90 }
91
75 shouldInvolveExtendedSettings() {92 shouldInvolveExtendedSettings() {
76 // Models that support extended settings (style_exaggeration, speaker_boost)93 // Models that support extended settings (style_exaggeration, speaker_boost)
77 const modelsWithExtendedSettings = [94 const modelsWithExtendedSettings = [
@@ -114,6 +131,18 @@ class ElevenLabsTtsProvider {
114 delete settings['multilingual'];131 delete settings['multilingual'];
115 }132 }
116133
134 if (Object.hasOwn(settings, 'apiKey')) {
135 if (settings.apiKey && !secret_state[SECRET_KEYS.ELEVENLABS]){
136 await writeSecret(SECRET_KEYS.ELEVENLABS, settings.apiKey);
137 }
138 delete settings['apiKey'];
139 }
140
141 $('#elevenlabs_tts_key').toggleClass('success', !!secret_state[SECRET_KEYS.ELEVENLABS]);
142 [event_types.SECRET_WRITTEN, event_types.SECRET_DELETED, event_types.SECRET_ROTATED].forEach(event => {
143 eventSource.on(event, this.handler);
144 });
145
117 for (const key in settings) {146 for (const key in settings) {
118 if (key in this.settings) {147 if (key in this.settings) {
119 this.settings[key] = settings[key];148 this.settings[key] = settings[key];
@@ -127,9 +156,7 @@ class ElevenLabsTtsProvider {
127 $('#elevenlabs_tts_style_exaggeration').val(this.settings.style_exaggeration);156 $('#elevenlabs_tts_style_exaggeration').val(this.settings.style_exaggeration);
128 $('#elevenlabs_tts_speaker_boost').prop('checked', this.settings.speaker_boost);157 $('#elevenlabs_tts_speaker_boost').prop('checked', this.settings.speaker_boost);
129 $('#elevenlabs_tts_speed').val(this.settings.speed);158 $('#elevenlabs_tts_speed').val(this.settings.speed);
130 $('#elevenlabs_tts_api_key').val(this.settings.apiKey);
131 $('#elevenlabs_tts_model').val(this.settings.model);159 $('#elevenlabs_tts_model').val(this.settings.model);
132 $('#eleven_labs_connect').on('click', () => { this.onConnectClick(); });
133 $('#elevenlabs_tts_similarity_boost').on('input', this.onSettingsChange.bind(this));160 $('#elevenlabs_tts_similarity_boost').on('input', this.onSettingsChange.bind(this));
134 $('#elevenlabs_tts_stability').on('input', this.onSettingsChange.bind(this));161 $('#elevenlabs_tts_stability').on('input', this.onSettingsChange.bind(this));
135 $('#elevenlabs_tts_style_exaggeration').on('input', this.onSettingsChange.bind(this));162 $('#elevenlabs_tts_style_exaggeration').on('input', this.onSettingsChange.bind(this));
@@ -157,23 +184,17 @@ class ElevenLabsTtsProvider {
157 }184 }
158185
159 async onRefreshClick() {186 async onRefreshClick() {
160 }187 await this.fetchTtsVoiceObjects();
161
162 async onConnectClick() {
163 // Update on Apply click
164 return await this.updateApiKey().catch((error) => {
165 toastr.error(`ElevenLabs: ${error}`);
166 });
167 }188 }
168189
169 setupVoiceCloningMenu() {190 setupVoiceCloningMenu() {
170 const audioFilesInput = document.getElementById('elevenlabs_tts_audio_files');191 const audioFilesInput = /** @type {HTMLInputElement} */ (document.getElementById('elevenlabs_tts_audio_files'));
171 const selectedFilesListElement = document.getElementById('elevenlabs_tts_selected_files_list');192 const selectedFilesListElement = document.getElementById('elevenlabs_tts_selected_files_list');
172 const cloneVoiceButton = document.getElementById('elevenlabs_tts_clone_voice_button');193 const cloneVoiceButton = document.getElementById('elevenlabs_tts_clone_voice_button');
173 const uploadAudioFileButton = document.getElementById('upload_audio_file');194 const uploadAudioFileButton = document.getElementById('upload_audio_file');
174 const voiceCloningNameInput = document.getElementById('elevenlabs_tts_voice_cloning_name');195 const voiceCloningNameInput = /** @type {HTMLInputElement} */ (document.getElementById('elevenlabs_tts_voice_cloning_name'));
175 const voiceCloningDescriptionInput = document.getElementById('elevenlabs_tts_voice_cloning_description');196 const voiceCloningDescriptionInput = /** @type {HTMLInputElement} */ (document.getElementById('elevenlabs_tts_voice_cloning_description'));
176 const voiceCloningLabelsInput = document.getElementById('elevenlabs_tts_voice_cloning_labels');197 const voiceCloningLabelsInput = /** @type {HTMLInputElement} */ (document.getElementById('elevenlabs_tts_voice_cloning_labels'));
177198
178 const updateCloneVoiceButtonVisibility = () => {199 const updateCloneVoiceButtonVisibility = () => {
179 cloneVoiceButton.style.display = audioFilesInput.files.length > 0 ? 'inline-block' : 'none';200 cloneVoiceButton.style.display = audioFilesInput.files.length > 0 ? 'inline-block' : 'none';
@@ -224,22 +245,11 @@ class ElevenLabsTtsProvider {
224 updateCloneVoiceButtonVisibility();245 updateCloneVoiceButtonVisibility();
225 }246 }
226247
227 async updateApiKey() {248 /**
228 // Using this call to validate API key249 * Get voice object by name
229 this.settings.apiKey = $('#elevenlabs_tts_api_key').val();250 * @param {string} voiceName Voice name to look up
230251 * @returns {Promise<Object>} Voice object
231 await this.fetchTtsVoiceObjects().catch(error => {252 */
232 throw 'TTS API key validation failed';
233 });
234 console.debug(`Saved new API_KEY: ${this.settings.apiKey}`);
235 $('#tts_status').text('');
236 this.onSettingsChange();
237 }
238
239 //#################//
240 // TTS Interfaces //
241 //#################//
242
243 async getVoice(voiceName) {253 async getVoice(voiceName) {
244 if (this.voices.length == 0) {254 if (this.voices.length == 0) {
245 this.voices = await this.fetchTtsVoiceObjects();255 this.voices = await this.fetchTtsVoiceObjects();
@@ -253,25 +263,30 @@ class ElevenLabsTtsProvider {
253 return match;263 return match;
254 }264 }
255265
256266 /**
267 * Generate TTS audio
268 * @param {string} text Text to synthesize
269 * @param {string} voiceId Voice ID to use for synthesis
270 * @returns {Promise<Response>} Response object containing audio data
271 */
257 async generateTts(text, voiceId) {272 async generateTts(text, voiceId) {
258 const historyId = await this.findTtsGenerationInHistory(text, voiceId);273 const historyId = await this.findTtsGenerationInHistory(text, voiceId);
259274
260 let response;
261 if (historyId) {275 if (historyId) {
262 console.debug(`Found existing TTS generation with id ${historyId}`);276 console.debug(`Found existing TTS generation with id ${historyId}`);
263 response = await this.fetchTtsFromHistory(historyId);277 return await this.fetchTtsFromHistory(historyId);
264 } else {278 } else {
265 console.debug('No existing TTS generation found, requesting new generation');279 console.debug('No existing TTS generation found, requesting new generation');
266 response = await this.fetchTtsGeneration(text, voiceId);280 return await this.fetchTtsGeneration(text, voiceId);
267 }281 }
268 return response;
269 }282 }
270283
271 //###################//284 /**
272 // Helper Functions //285 * Find existing TTS generation in history
273 //###################//286 * @param {string} message Message text used for TTS generation
274287 * @param {string} voiceId Voice ID used for TTS generation
288 * @returns {Promise<string>} History item ID if found, empty string otherwise
289 */
275 async findTtsGenerationInHistory(message, voiceId) {290 async findTtsGenerationInHistory(message, voiceId) {
276 const ttsHistory = await this.fetchTtsHistory();291 const ttsHistory = await this.fetchTtsHistory();
277 for (const history of ttsHistory) {292 for (const history of ttsHistory) {
@@ -285,40 +300,35 @@ class ElevenLabsTtsProvider {
285 return '';300 return '';
286 }301 }
287302
288
289 //###########//
290 // API CALLS //
291 //###########//
292 async fetchTtsVoiceObjects() {303 async fetchTtsVoiceObjects() {
293 const headers = {304 const response = await fetch('/api/speech/elevenlabs/voices', {
294 'xi-api-key': this.settings.apiKey,305 method: 'POST',
295 };306 headers: getRequestHeaders({ omitContentType: true }),
296 const response = await fetch('https://api.elevenlabs.io/v1/voices', {
297 headers: headers,
298 });307 });
299 if (!response.ok) {308 if (!response.ok) {
300 throw new Error(`HTTP ${response.status}: ${await response.text()}`);309 throw new Error(`HTTP ${response.status}. See server console for details.`);
301 }310 }
302 const responseJson = await response.json();311 const responseJson = await response.json();
303 return responseJson.voices;312 return responseJson.voices;
304 }313 }
305314
306 async fetchTtsVoiceSettings() {315 async fetchTtsVoiceSettings() {
307 const headers = {316 const response = await fetch('/api/speech/elevenlabs/voice-settings', {
308 'xi-api-key': this.settings.apiKey,317 method: 'POST',
309 };318 headers: getRequestHeaders({ omitContentType: true }),
310 const response = await fetch(319 });
311 'https://api.elevenlabs.io/v1/voices/settings/default',
312 {
313 headers: headers,
314 },
315 );
316 if (!response.ok) {320 if (!response.ok) {
317 throw new Error(`HTTP ${response.status}: ${await response.text()}`);321 throw new Error(`HTTP ${response.status}. See server console for details.`);
318 }322 }
319 return response.json();323 return response.json();
320 }324 }
321325
326 /**
327 * Fetch new TTS generation from ElevenLabs API
328 * @param {string} text Text to synthesize
329 * @param {string} voiceId Voice ID to use for synthesis
330 * @returns {Promise<Response>} Response object containing audio data
331 */
322 async fetchTtsGeneration(text, voiceId) {332 async fetchTtsGeneration(text, voiceId) {
323 let model = this.settings.model ?? 'eleven_monolingual_v1';333 let model = this.settings.model ?? 'eleven_monolingual_v1';
324 console.info(`Generating new TTS for voice_id ${voiceId}, model ${model}`);334 console.info(`Generating new TTS for voice_id ${voiceId}, model ${model}`);
@@ -335,75 +345,90 @@ class ElevenLabsTtsProvider {
335 request.voice_settings.style = Number(this.settings.style_exaggeration);345 request.voice_settings.style = Number(this.settings.style_exaggeration);
336 request.voice_settings.use_speaker_boost = Boolean(this.settings.speaker_boost);346 request.voice_settings.use_speaker_boost = Boolean(this.settings.speaker_boost);
337 }347 }
338 const response = await fetch(`https://api.elevenlabs.io/v1/text-to-speech/${voiceId}`, {348 const response = await fetch('/api/speech/elevenlabs/synthesize', {
339 method: 'POST',349 method: 'POST',
340 headers: {350 headers: getRequestHeaders(),
341 'xi-api-key': this.settings.apiKey,351 body: JSON.stringify({
342 'Content-Type': 'application/json',352 voiceId: voiceId,
343 },353 request: request,
344 body: JSON.stringify(request),354 }),
345 });355 });
346 if (!response.ok) {356 if (!response.ok) {
347 toastr.error(response.statusText, 'TTS Generation Failed');357 toastr.error(response.statusText, 'TTS Generation Failed');
348 throw new Error(`HTTP ${response.status}: ${await response.text()}`);358 throw new Error(`HTTP ${response.status}. See server console for details.`);
349 }359 }
350 return response;360 return response;
351 }361 }
352362
353 async fetchTtsFromHistory(history_item_id) {363 /**
354 console.info(`Fetched existing TTS with history_item_id ${history_item_id}`);364 * Fetch existing TTS audio from history
355 const response = await fetch(365 * @param {string} historyItemId History item ID to fetch audio for
356 `https://api.elevenlabs.io/v1/history/${history_item_id}/audio`,366 * @returns {Promise<Response>} Response object containing audio data
357 {367 */
358 headers: {368 async fetchTtsFromHistory(historyItemId) {
359 'xi-api-key': this.settings.apiKey,369 console.info(`Fetched existing TTS with history_item_id ${historyItemId}`);
360 },370 const response = await fetch('/api/speech/elevenlabs/history-audio', {
361 },371 method: 'POST',
362 );372 headers: getRequestHeaders(),
373 body: JSON.stringify({
374 historyItemId: historyItemId,
375 }),
376 });
363 if (!response.ok) {377 if (!response.ok) {
364 throw new Error(`HTTP ${response.status}: ${await response.text()}`);378 throw new Error(`HTTP ${response.status}. See server console for details.`);
365 }379 }
366 return response;380 return response;
367 }381 }
368382
383 /**
384 * Fetch TTS generation history
385 * @returns {Promise<Array>} Array of TTS history items
386 */
369 async fetchTtsHistory() {387 async fetchTtsHistory() {
370 const headers = {388 const response = await fetch('/api/speech/elevenlabs/history', {
371 'xi-api-key': this.settings.apiKey,389 method: 'POST',
372 };390 headers: getRequestHeaders({ omitContentType: true }),
373 const response = await fetch('https://api.elevenlabs.io/v1/history', {
374 headers: headers,
375 });391 });
376 if (!response.ok) {392 if (!response.ok) {
377 throw new Error(`HTTP ${response.status}: ${await response.text()}`);393 throw new Error(`HTTP ${response.status}. See server console for details.`);
378 }394 }
379 const responseJson = await response.json();395 const responseJson = await response.json();
380 return responseJson.history;396 return responseJson.history;
381 }397 }
382398
399 /**
400 * Add a new voice via ElevenLabs API
401 * @param {string} name Voice name
402 * @param {string} description Voice description
403 * @param {string} labels Voice labels
404 * @returns {Promise<Object>} Newly created voice object
405 */
383 async addVoice(name, description, labels) {406 async addVoice(name, description, labels) {
384 const selected_files = document.querySelectorAll('input[type="file"][name="audio_files"]');407 const audioFilesInput = /** @type {HTMLInputElement} */ (document.getElementById('elevenlabs_tts_audio_files'));
385 const formData = new FormData();408 if (!(audioFilesInput instanceof HTMLInputElement) || audioFilesInput.files.length === 0) {
409 throw new Error('No audio files selected for voice cloning.');
410 }
386411
387 formData.append('name', name);412 const data = {
388 formData.append('description', description);413 name: name,
389 formData.append('labels', labels);414 description: description,
415 labels: labels,
416 files: [],
417 };
390418
391 for (const file of selected_files) {419 for (const file of audioFilesInput.files) {
392 if (file.files.length > 0) {420 const base64Data = await getBase64Async(file);
393 formData.append('files', file.files[0]);421 data.files.push(base64Data);
394 }
395 }422 }
396423
397 const response = await fetch('https://api.elevenlabs.io/v1/voices/add', {424 const response = await fetch('/api/speech/elevenlabs/voices/add', {
398 method: 'POST',425 method: 'POST',
399 headers: {426 headers: getRequestHeaders(),
400 'xi-api-key': this.settings.apiKey,427 body: JSON.stringify(data),
401 },
402 body: formData,
403 });428 });
404429
405 if (!response.ok) {430 if (!response.ok) {
406 throw new Error(`HTTP ${response.status}: ${await response.text()}`);431 throw new Error(`HTTP ${response.status}. See server console for details.`);
407 }432 }
408433
409 return await response.json();434 return await response.json();
public/scripts/extensions/tts/index.js+3 -3
@@ -34,6 +34,7 @@ import { TtsWebuiProvider } from './tts-webui.js';
34import { PollinationsTtsProvider } from './pollinations.js';34import { PollinationsTtsProvider } from './pollinations.js';
35import { MiniMaxTtsProvider } from './minimax.js';35import { MiniMaxTtsProvider } from './minimax.js';
36import { ElectronHubTtsProvider } from './electronhub.js';36import { ElectronHubTtsProvider } from './electronhub.js';
37import { ChutesTtsProvider } from './chutes.js';
3738
38const UPDATE_INTERVAL = 1000;39const UPDATE_INTERVAL = 1000;
39const wrapper = new ModuleWorkerWrapper(moduleWorker);40const wrapper = new ModuleWorkerWrapper(moduleWorker);
@@ -120,6 +121,7 @@ const ttsProviders = {
120 AllTalk: AllTalkTtsProvider,121 AllTalk: AllTalkTtsProvider,
121 Azure: AzureTtsProvider,122 Azure: AzureTtsProvider,
122 Chatterbox: ChatterboxTtsProvider,123 Chatterbox: ChatterboxTtsProvider,
124 Chutes: ChutesTtsProvider,
123 Coqui: CoquiTtsProvider,125 Coqui: CoquiTtsProvider,
124 'CosyVoice (Unofficial)': CosyVoiceProvider,126 'CosyVoice (Unofficial)': CosyVoiceProvider,
125 Edge: EdgeTtsProvider,127 Edge: EdgeTtsProvider,
@@ -239,9 +241,7 @@ function isTtsProcessing() {
239241
240/**242/**
241 * Splits a message into lines and adds each non-empty line to the TTS job queue.243 * Splits a message into lines and adds each non-empty line to the TTS job queue.
242 * @param {Object} message - The message object to be processed.244 * @param {ChatMessage} message - The message object to be processed.
243 * @param {string} message.mes - The text of the message to be split into lines.
244 * @param {string} message.name - The name associated with the message.
245 * @returns {void}245 * @returns {void}
246 */246 */
247function processAndQueueTtsMessage(message) {247function processAndQueueTtsMessage(message) {
public/scripts/extensions/vectors/index.js+50 -2
@@ -69,6 +69,7 @@ const settings = {
69 vllm_model: '',69 vllm_model: '',
70 webllm_model: '',70 webllm_model: '',
71 google_model: 'text-embedding-005',71 google_model: 'text-embedding-005',
72 chutes_model: 'chutes-qwen-qwen3-embedding-8b',
72 summarize: false,73 summarize: false,
73 summarize_sent: false,74 summarize_sent: false,
74 summary_source: 'main',75 summary_source: 'main',
@@ -829,6 +830,9 @@ function getVectorsRequestBody(args = {}) {
829 body.vertexai_region = oai_settings.vertexai_region;830 body.vertexai_region = oai_settings.vertexai_region;
830 body.vertexai_express_project_id = oai_settings.vertexai_express_project_id;831 body.vertexai_express_project_id = oai_settings.vertexai_express_project_id;
831 break;832 break;
833 case 'chutes':
834 body.model = extension_settings.vectors.chutes_model;
835 break;
832 default:836 default:
833 break;837 break;
834 }838 }
@@ -913,6 +917,7 @@ async function insertVectorItems(collectionId, items) {
913function throwIfSourceInvalid() {917function throwIfSourceInvalid() {
914 if (settings.source === 'openai' && !secret_state[SECRET_KEYS.OPENAI] ||918 if (settings.source === 'openai' && !secret_state[SECRET_KEYS.OPENAI] ||
915 settings.source === 'electronhub' && !secret_state[SECRET_KEYS.ELECTRONHUB] ||919 settings.source === 'electronhub' && !secret_state[SECRET_KEYS.ELECTRONHUB] ||
920 settings.source === 'chutes' && !secret_state[SECRET_KEYS.CHUTES] ||
916 settings.source === 'openrouter' && !secret_state[SECRET_KEYS.OPENROUTER] ||921 settings.source === 'openrouter' && !secret_state[SECRET_KEYS.OPENROUTER] ||
917 settings.source === 'palm' && !secret_state[SECRET_KEYS.MAKERSUITE] ||922 settings.source === 'palm' && !secret_state[SECRET_KEYS.MAKERSUITE] ||
918 settings.source === 'vertexai' && !secret_state[SECRET_KEYS.VERTEXAI] && !secret_state[SECRET_KEYS.VERTEXAI_SERVICE_ACCOUNT] ||923 settings.source === 'vertexai' && !secret_state[SECRET_KEYS.VERTEXAI] && !secret_state[SECRET_KEYS.VERTEXAI_SERVICE_ACCOUNT] ||
@@ -1127,6 +1132,7 @@ function toggleSettings() {
1127 $('#together_vectorsModel').toggle(settings.source === 'togetherai');1132 $('#together_vectorsModel').toggle(settings.source === 'togetherai');
1128 $('#openai_vectorsModel').toggle(settings.source === 'openai');1133 $('#openai_vectorsModel').toggle(settings.source === 'openai');
1129 $('#electronhub_vectorsModel').toggle(settings.source === 'electronhub');1134 $('#electronhub_vectorsModel').toggle(settings.source === 'electronhub');
1135 $('#chutes_vectorsModel').toggle(settings.source === 'chutes');
1130 $('#openrouter_vectorsModel').toggle(settings.source === 'openrouter');1136 $('#openrouter_vectorsModel').toggle(settings.source === 'openrouter');
1131 $('#cohere_vectorsModel').toggle(settings.source === 'cohere');1137 $('#cohere_vectorsModel').toggle(settings.source === 'cohere');
1132 $('#ollama_vectorsModel').toggle(settings.source === 'ollama');1138 $('#ollama_vectorsModel').toggle(settings.source === 'ollama');
@@ -1147,14 +1153,51 @@ function toggleSettings() {
1147 case 'openrouter':1153 case 'openrouter':
1148 loadOpenRouterModels();1154 loadOpenRouterModels();
1149 break;1155 break;
1156 case 'chutes':
1157 loadChutesModels();
1158 break;
1159 }
1160}
1161
1162async function loadChutesModels() {
1163 try {
1164 const response = await fetch('/api/openai/chutes/models/embedding', {
1165 method: 'POST',
1166 headers: getRequestHeaders({ omitContentType: true }),
1167 });
1168 if (!response.ok) {
1169 throw new Error(`HTTP ${response.status}`);
1170 }
1171 /** @type {Array<any>} */
1172 const data = await response.json();
1173 const models = Array.isArray(data) ? data : [];
1174 populateChutesModelSelect(models);
1175 } catch (err) {
1176 console.warn('Chutes models fetch failed', err);
1177 populateChutesModelSelect([]);
1178 }
1179}
1180
1181function populateChutesModelSelect(models) {
1182 const select = $('#vectors_chutes_model');
1183 select.empty();
1184 for (const m of models) {
1185 const option = document.createElement('option');
1186 option.value = m.slug;
1187 option.text = m.name;
1188 select.append(option);
1189 }
1190 if (!settings.chutes_model && models.length) {
1191 settings.chutes_model = models[0].slug;
1150 }1192 }
1193 $('#vectors_chutes_model').val(settings.chutes_model);
1151}1194}
11521195
1153async function loadElectronHubModels() {1196async function loadElectronHubModels() {
1154 try {1197 try {
1155 const response = await fetch('/api/openai/electronhub/models', {1198 const response = await fetch('/api/openai/electronhub/models', {
1156 method: 'POST',1199 method: 'POST',
1157 headers: getRequestHeaders(),1200 headers: getRequestHeaders({ omitContentType: true }),
1158 });1201 });
1159 if (!response.ok) {1202 if (!response.ok) {
1160 throw new Error(`HTTP ${response.status}`);1203 throw new Error(`HTTP ${response.status}`);
@@ -1193,7 +1236,7 @@ async function loadOpenRouterModels() {
1193 try {1236 try {
1194 const response = await fetch('/api/openrouter/models/embedding', {1237 const response = await fetch('/api/openrouter/models/embedding', {
1195 method: 'POST',1238 method: 'POST',
1196 headers: getRequestHeaders(),1239 headers: getRequestHeaders({ omitContentType: true }),
1197 });1240 });
1198 if (!response.ok) {1241 if (!response.ok) {
1199 throw new Error(`HTTP ${response.status}`);1242 throw new Error(`HTTP ${response.status}`);
@@ -1630,6 +1673,11 @@ jQuery(async () => {
1630 Object.assign(extension_settings.vectors, settings);1673 Object.assign(extension_settings.vectors, settings);
1631 saveSettingsDebounced();1674 saveSettingsDebounced();
1632 });1675 });
1676 $('#vectors_chutes_model').val(settings.chutes_model).on('change', () => {
1677 settings.chutes_model = String($('#vectors_chutes_model').val());
1678 Object.assign(extension_settings.vectors, settings);
1679 saveSettingsDebounced();
1680 });
1633 $('#vectors_openrouter_model').val(settings.openrouter_model).on('change', () => {1681 $('#vectors_openrouter_model').val(settings.openrouter_model).on('change', () => {
1634 settings.openrouter_model = String($('#vectors_openrouter_model').val());1682 settings.openrouter_model = String($('#vectors_openrouter_model').val());
1635 Object.assign(extension_settings.vectors, settings);1683 Object.assign(extension_settings.vectors, settings);
public/scripts/extensions/vectors/settings.html+10 -0
@@ -10,6 +10,7 @@
10 Vectorization Source10 Vectorization Source
11 </label>11 </label>
12 <select id="vectors_source" class="text_pole">12 <select id="vectors_source" class="text_pole">
13 <option value="chutes">Chutes</option>
13 <option value="cohere">Cohere</option>14 <option value="cohere">Cohere</option>
14 <option value="electronhub">Electron Hub</option>15 <option value="electronhub">Electron Hub</option>
15 <option value="extras">Extras (deprecated)</option>16 <option value="extras">Extras (deprecated)</option>
@@ -28,6 +29,15 @@
28 <option value="webllm" data-i18n="WebLLM Extension">WebLLM Extension</option>29 <option value="webllm" data-i18n="WebLLM Extension">WebLLM Extension</option>
29 </select>30 </select>
30 </div>31 </div>
32 <div class="flex-container flexFlowColumn" id="chutes_vectorsModel">
33 <label for="vectors_chutes_model" data-i18n="Vectorization Model">
34 Vectorization Model
35 </label>
36 <select id="vectors_chutes_model" class="text_pole"></select>
37 <i data-i18n="Hint: Set your Chutes API key in API Connections.">
38 Hint: Set your Chutes API key in API Connections.
39 </i>
40 </div>
31 <div class="flex-container flexFlowColumn" id="electronhub_vectorsModel">41 <div class="flex-container flexFlowColumn" id="electronhub_vectorsModel">
32 <label for="vectors_electronhub_model" data-i18n="Vectorization Model">42 <label for="vectors_electronhub_model" data-i18n="Vectorization Model">
33 Vectorization Model43 Vectorization Model
public/scripts/group-chats.js+383 -145
@@ -17,6 +17,8 @@ import {
17 renderPaginationDropdown,17 renderPaginationDropdown,
18 paginationDropdownChangeHandler,18 paginationDropdownChangeHandler,
19 waitUntilCondition,19 waitUntilCondition,
20 uuidv4,
21 humanFileSize,
20} from './utils.js';22} from './utils.js';
21import { RA_CountCharTokens, humanizedDateTime, dragElement, favsToHotswap, getMessageTimeStamp } from './RossAscends-mods.js';23import { RA_CountCharTokens, humanizedDateTime, dragElement, favsToHotswap, getMessageTimeStamp } from './RossAscends-mods.js';
22import { power_user, loadMovingUIState, sortEntitiesList } from './power-user.js';24import { power_user, loadMovingUIState, sortEntitiesList } from './power-user.js';
@@ -37,8 +39,6 @@ import {
37 setCharacterName,39 setCharacterName,
38 setEditedMessageId,40 setEditedMessageId,
39 is_send_press,41 is_send_press,
40 name1,
41 name2,
42 resetChatState,42 resetChatState,
43 setSendButtonState,43 setSendButtonState,
44 getCharacters,44 getCharacters,
@@ -71,6 +71,7 @@ import {
71 isChatSaving,71 isChatSaving,
72 setExternalAbortController,72 setExternalAbortController,
73 baseChatReplace,73 baseChatReplace,
74 createLazyFields,
74 depth_prompt_depth_default,75 depth_prompt_depth_default,
75 loadItemizedPrompts,76 loadItemizedPrompts,
76 animation_duration,77 animation_duration,
@@ -109,7 +110,9 @@ export {
109let is_group_generating = false; // Group generation flag110let is_group_generating = false; // Group generation flag
110let is_group_automode_enabled = false;111let is_group_automode_enabled = false;
111let hideMutedSprites = false;112let hideMutedSprites = false;
113/** @type {Group[]} */
112let groups = [];114let groups = [];
115/** @type {string|null} */
113let selected_group = null;116let selected_group = null;
114let group_generation_id = null;117let group_generation_id = null;
115let fav_grp_checked = false;118let fav_grp_checked = false;
@@ -143,6 +146,11 @@ function setAutoModeWorker() {
143 autoModeWorker = setInterval(groupChatAutoModeWorker, autoModeDelay * 1000);146 autoModeWorker = setInterval(groupChatAutoModeWorker, autoModeDelay * 1000);
144}147}
145148
149/**
150 * Saves a group to the server.
151 * @param {Group} group Group object to save
152 * @param {boolean} reload Whether to reload characters after saving
153 */
146async function _save(group, reload = true) {154async function _save(group, reload = true) {
147 await fetch('/api/groups/edit', {155 await fetch('/api/groups/edit', {
148 method: 'POST',156 method: 'POST',
@@ -179,6 +187,11 @@ async function regenerateGroup() {
179 generateGroupWrapper(false, 'normal', { signal: abortController.signal });187 generateGroupWrapper(false, 'normal', { signal: abortController.signal });
180}188}
181189
190/**
191 * Loads group chat messages from the server.
192 * @param {string} chatId Chat ID
193 * @returns {Promise<ChatFile>} Array of chat messages
194 */
182async function loadGroupChat(chatId) {195async function loadGroupChat(chatId) {
183 const response = await fetch('/api/chats/group/get', {196 const response = await fetch('/api/chats/group/get', {
184 method: 'POST',197 method: 'POST',
@@ -188,12 +201,20 @@ async function loadGroupChat(chatId) {
188201
189 if (response.ok) {202 if (response.ok) {
190 const data = await response.json();203 const data = await response.json();
204 if (!Array.isArray(data)) {
205 return [];
206 }
191 return data;207 return data;
192 }208 }
193209
194 return [];210 return [];
195}211}
196212
213/**
214 * Validates a group by checking if all members exist and removing duplicates.
215 * @param {Group} group Group to validate
216 * @returns {Promise<void>}
217 */
197async function validateGroup(group) {218async function validateGroup(group) {
198 if (!group) return;219 if (!group) return;
199220
@@ -225,6 +246,12 @@ async function validateGroup(group) {
225 }246 }
226}247}
227248
249/**
250 * Loads the chat messages for a specific group.
251 * @param {string} groupId - The ID of the group to load chat messages for.
252 * @param {boolean} reload - Whether to reload the group chat after loading.
253 * @returns {Promise<void>} A promise that resolves when the chat messages have been loaded.
254 */
228export async function getGroupChat(groupId, reload = false) {255export async function getGroupChat(groupId, reload = false) {
229 const group = groups.find((x) => x.id === groupId);256 const group = groups.find((x) => x.id === groupId);
230 if (!group) {257 if (!group) {
@@ -238,9 +265,19 @@ export async function getGroupChat(groupId, reload = false) {
238265
239 const chat_id = group.chat_id;266 const chat_id = group.chat_id;
240 const data = await loadGroupChat(chat_id);267 const data = await loadGroupChat(chat_id);
241 const metadata = group.chat_metadata ?? {};268 const metadata = data?.[0]?.chat_metadata ?? {};
242 const freshChat = !metadata.tainted && (!Array.isArray(data) || !data.length);269 const freshChat = !metadata.tainted && (!Array.isArray(data) || !data.length);
243270
271 // Remove chat file header if present
272 if (Array.isArray(data) && data.length && Object.hasOwn(data[0], 'chat_metadata')) {
273 data.shift();
274 }
275
276 // Add integrity slug if missing
277 if (!metadata['integrity']) {
278 metadata['integrity'] = uuidv4();
279 }
280
244 await loadItemizedPrompts(getCurrentChatId());281 await loadItemizedPrompts(getCurrentChatId());
245282
246 if (group && Array.isArray(group.members) && freshChat) {283 if (group && Array.isArray(group.members) && freshChat) {
@@ -266,7 +303,6 @@ export async function getGroupChat(groupId, reload = false) {
266 }303 }
267 await saveGroupChat(groupId, false);304 await saveGroupChat(groupId, false);
268 } else if (Array.isArray(data) && data.length) {305 } else if (Array.isArray(data) && data.length) {
269 data[0].is_group = true;
270 chat.splice(0, chat.length, ...data);306 chat.splice(0, chat.length, ...data);
271 chat.forEach(ensureMessageMediaIsArray);307 chat.forEach(ensureMessageMediaIsArray);
272 chatElement.find('.mes').remove();308 chatElement.find('.mes').remove();
@@ -287,7 +323,7 @@ export async function getGroupChat(groupId, reload = false) {
287 * Retrieves the members of a group323 * Retrieves the members of a group
288 *324 *
289 * @param {string} [groupId=selected_group] - The ID of the group to retrieve members from. Defaults to the currently selected group.325 * @param {string} [groupId=selected_group] - The ID of the group to retrieve members from. Defaults to the currently selected group.
290 * @returns {import('../script.js').Character[]} An array of character objects representing the members of the group. If the group is not found, an empty array is returned.326 * @returns {Character[]} An array of character objects representing the members of the group. If the group is not found, an empty array is returned.
291 */327 */
292export function getGroupMembers(groupId = selected_group) {328export function getGroupMembers(groupId = selected_group) {
293 const group = groups.find((x) => x.id === groupId);329 const group = groups.find((x) => x.id === groupId);
@@ -421,7 +457,7 @@ export function getGroupDepthPrompts(groupId, characterId) {
421 continue;457 continue;
422 }458 }
423459
424 const depthPromptText = baseChatReplace(character.data?.extensions?.depth_prompt?.prompt?.trim(), name1, character.name) || '';460 const depthPromptText = baseChatReplace(character.data?.extensions?.depth_prompt?.prompt?.trim(), null, character.name) || '';
425 const depthPromptDepth = character.data?.extensions?.depth_prompt?.depth ?? depth_prompt_depth_default;461 const depthPromptDepth = character.data?.extensions?.depth_prompt?.depth ?? depth_prompt_depth_default;
426 const depthPromptRole = character.data?.extensions?.depth_prompt?.role ?? depth_prompt_role_default;462 const depthPromptRole = character.data?.extensions?.depth_prompt?.role ?? depth_prompt_role_default;
427463
@@ -440,29 +476,46 @@ export function getGroupDepthPrompts(groupId, characterId) {
440 * @returns {{description: string, personality: string, scenario: string, mesExamples: string}} Group character cards combined476 * @returns {{description: string, personality: string, scenario: string, mesExamples: string}} Group character cards combined
441 */477 */
442export function getGroupCharacterCards(groupId, characterId) {478export function getGroupCharacterCards(groupId, characterId) {
479 const lazy = getGroupCharacterCardsLazy(groupId, characterId);
480 if (!lazy) return null;
481
482 // Resolve all lazy fields into a plain object
483 return {
484 description: lazy.description,
485 personality: lazy.personality,
486 scenario: lazy.scenario,
487 mesExamples: lazy.mesExamples,
488 };
489}
490
491/**
492 * Returns group character cards with lazy evaluation.
493 * Each field is only processed when first accessed.
494 * @param {string} groupId Group ID
495 * @param {number} characterId Current Character ID
496 * @returns {{description: string, personality: string, scenario: string, mesExamples: string}} Group character cards with lazy getters
497 */
498export function getGroupCharacterCardsLazy(groupId, characterId) {
443 const group = groups.find(x => x.id === groupId);499 const group = groups.find(x => x.id === groupId);
444500
501 // If no group cards should be generated, return null so caller knows to fall back
445 if (!group || !group?.generation_mode || !Array.isArray(group.members) || !group.members.length) {502 if (!group || !group?.generation_mode || !Array.isArray(group.members) || !group.members.length) {
446 return null;503 return null;
447 }504 }
448505
449 /**506 /**
450 * Runs the macro engine on a text, with custom <FIELDNAME> replace507 * Runs baseChatReplace on a text, with custom <FIELDNAME> replace
451 * @param {string} value Value to replace508 * @param {string} value Value to replace
452 * @param {string} fieldName Name of the field509 * @param {string} fieldName Name of the field
453 * @param {string} characterName Name of the character510 * @param {string} characterName Name of the character
454 * @param {boolean} trim Whether to trim the value511 * @param {boolean} trim Whether to trim the value
455 * @returns {string} Replaced text512 * @returns {string} Replaced text
456 * */513 */
457 function customBaseChatReplace(value, fieldName, characterName, trim) {514 function customTransform(value, fieldName, characterName, trim) {
458 if (!value) {515 if (!value) return '';
459 return '';
460 }
461
462 // We should do the custom field name replacement first, and then run it through the normal macro engine with provided names
463 value = value.replace(/<FIELDNAME>/gi, fieldName);516 value = value.replace(/<FIELDNAME>/gi, fieldName);
464 value = trim ? value.trim() : value;517 value = trim ? value.trim() : value;
465 return baseChatReplace(value, name1, characterName);518 return baseChatReplace(value, null, characterName);
466 }519 }
467520
468 /**521 /**
@@ -472,62 +525,57 @@ export function getGroupCharacterCards(groupId, characterId) {
472 * @param {string} fieldName Name of the field525 * @param {string} fieldName Name of the field
473 * @param {function(string): string} [preprocess] Preprocess function526 * @param {function(string): string} [preprocess] Preprocess function
474 * @returns {string} Prepared text527 * @returns {string} Prepared text
475 * */528 */
476 function replaceAndPrepareForJoin(value, characterName, fieldName, preprocess = null) {529 function replaceAndPrepareForJoin(value, characterName, fieldName, preprocess = null) {
477 value = value.trim();530 value = value?.trim() ?? '';
478 if (!value) {531 if (!value) return '';
479 return '';
480 }
481
482 // Run preprocess function
483 if (typeof preprocess === 'function') {532 if (typeof preprocess === 'function') {
484 value = preprocess(value);533 value = preprocess(value);
485 }534 }
486535 const prefix = customTransform(group.generation_mode_join_prefix, fieldName, characterName, false);
487 // Prepare and replace prefixes536 const suffix = customTransform(group.generation_mode_join_suffix, fieldName, characterName, false);
488 const prefix = customBaseChatReplace(group.generation_mode_join_prefix, fieldName, characterName, false);537 value = customTransform(value, fieldName, characterName, true);
489 const suffix = customBaseChatReplace(group.generation_mode_join_suffix, fieldName, characterName, false);
490 // Also run the macro replacement on the actual content
491 value = customBaseChatReplace(value, fieldName, characterName, true);
492
493 return `${prefix}${value}${suffix}`;538 return `${prefix}${value}${suffix}`;
494 }539 }
495540
496 const scenarioOverride = String(chat_metadata['scenario'] || '');541 /**
497 const mesExamplesOverride = String(chat_metadata['mes_example'] || '');542 * Collects and joins field values from all group members
498543 * @param {string} fieldName Display name of the field
499 let descriptions = [];544 * @param {function(Character): string} getter Function to get field value from character
500 let personalities = [];545 * @param {function(string): string} [preprocess] Optional preprocess function
501 let scenarios = [];546 * @returns {string} Combined field values
502 let mesExamplesArray = [];547 */
503548 function collectField(fieldName, getter, preprocess = null) {
504 for (const member of group.members) {549 const values = [];
505 const index = characters.findIndex(x => x.avatar === member);550 for (const member of group.members) {
506 const character = characters[index];551 const index = characters.findIndex(x => x.avatar === member);
507552 const character = characters[index];
508 if (index === -1 || !character) {553 if (index === -1 || !character) continue;
509 console.debug(`Skipping missing member: ${member}`);554 if (group.disabled_members.includes(member) && characterId !== index && group.generation_mode !== group_generation_mode.APPEND_DISABLED) {
510 continue;555 continue;
511 }556 }
512557 values.push(replaceAndPrepareForJoin(getter(character), character.name, fieldName, preprocess));
513 if (group.disabled_members.includes(member) && characterId !== index && group.generation_mode !== group_generation_mode.APPEND_DISABLED) {
514 continue;
515 }558 }
516559 return values.filter(x => x.length).join('\n');
517 descriptions.push(replaceAndPrepareForJoin(character.description, character.name, 'Description'));
518 personalities.push(replaceAndPrepareForJoin(character.personality, character.name, 'Personality'));
519 scenarios.push(replaceAndPrepareForJoin(character.scenario, character.name, 'Scenario'));
520 mesExamplesArray.push(replaceAndPrepareForJoin(character.mes_example, character.name, 'Example Messages', (x) => !x.startsWith('<START>') ? `<START>\n${x}` : x));
521 }560 }
522561
523 const description = descriptions.filter(x => x.length).join('\n');562 const scenarioOverride = String(chat_metadata['scenario'] || '');
524 const personality = personalities.filter(x => x.length).join('\n');563 const mesExamplesOverride = String(chat_metadata['mes_example'] || '');
525 const scenario = baseChatReplace(scenarioOverride?.trim(), name1, name2) || scenarios.filter(x => x.length).join('\n');
526 const mesExamples = baseChatReplace(mesExamplesOverride?.trim(), name1, name2) || mesExamplesArray.filter(x => x.length).join('\n');
527564
528 return { description, personality, scenario, mesExamples };565 return createLazyFields({
566 description: () => collectField('Description', c => c.description),
567 personality: () => collectField('Personality', c => c.personality),
568 scenario: () => baseChatReplace(scenarioOverride?.trim()) || collectField('Scenario', c => c.scenario),
569 mesExamples: () => baseChatReplace(mesExamplesOverride?.trim()) ||
570 collectField('Example Messages', c => c.mes_example, x => !x.startsWith('<START>') ? `<START>\n${x}` : x),
571 });
529}572}
530573
574/**
575 * Gets the first message for a character.
576 * @param {Character} character Character object
577 * @returns {Promise<ChatMessage>} First message object
578 */
531async function getFirstCharacterMessage(character) {579async function getFirstCharacterMessage(character) {
532 let messageText = character.first_mes;580 let messageText = character.first_mes;
533581
@@ -552,7 +600,7 @@ async function getFirstCharacterMessage(character) {
552 mes['original_avatar'] = character.avatar;600 mes['original_avatar'] = character.avatar;
553 mes['extra'] = { 'gen_id': Date.now() * Math.random() * 1000000 };601 mes['extra'] = { 'gen_id': Date.now() * Math.random() * 1000000 };
554 mes['mes'] = messageText602 mes['mes'] = messageText
555 ? substituteParams(messageText.trim(), name1, character.name)603 ? substituteParams(messageText.trim(), { name2Override: character.name })
556 : '';604 : '';
557 mes['force_avatar'] =605 mes['force_avatar'] =
558 character.avatar != 'none'606 character.avatar != 'none'
@@ -566,20 +614,59 @@ function resetSelectedGroup() {
566 is_group_generating = false;614 is_group_generating = false;
567}615}
568616
569async function saveGroupChat(groupId, shouldSaveGroup) {617/**
618 * Saves a group chat to the server.
619 * @param {string} groupId Group ID
620 * @param {boolean} shouldSaveGroup Whether to save the group after saving the chat
621 * @param {boolean} force Force the saving on integrity error
622 * @returns {Promise<void>} A promise that resolves when the group chat has been saved.
623 */
624async function saveGroupChat(groupId, shouldSaveGroup, force = false) {
570 const group = groups.find(x => x.id == groupId);625 const group = groups.find(x => x.id == groupId);
571 const chat_id = group.chat_id;626 if (!group) {
627 console.warn('Group not found', groupId);
628 return;
629 }
630 const chatId = group.chat_id;
572 group['date_last_chat'] = Date.now();631 group['date_last_chat'] = Date.now();
632 /** @type {ChatHeader} */
633 const chatHeader = {
634 chat_metadata: { ...chat_metadata },
635 user_name: 'unused',
636 character_name: 'unused',
637 };
573 const response = await fetch('/api/chats/group/save', {638 const response = await fetch('/api/chats/group/save', {
574 method: 'POST',639 method: 'POST',
575 headers: getRequestHeaders(),640 headers: getRequestHeaders(),
576 body: JSON.stringify({ id: chat_id, chat: [...chat] }),641 body: JSON.stringify({ id: chatId, chat: [chatHeader, ...chat], force: force }),
577 });642 });
578643
579 if (!response.ok) {644 if (!response.ok) {
580 toastr.error(t`Check the server connection and reload the page to prevent data loss.`, t`Group Chat could not be saved`);645 const errorData = await response.json();
581 console.error('Group chat could not be saved', response);646 const isIntegrityError = errorData?.error === 'integrity' && !force;
582 return;647 if (!isIntegrityError) {
648 toastr.error(t`Check the server connection and reload the page to prevent data loss.`, t`Group Chat could not be saved`);
649 console.error('Group chat could not be saved', response);
650 return;
651 }
652
653 const popupResult = await Popup.show.input(
654 t`ERROR: Chat integrity check failed while saving the file.`,
655 t`<p>After you click OK, the page will be reloaded to prevent data corruption.</p>
656 <p>To confirm an overwrite (and potentially <b>LOSE YOUR DATA</b>), enter <code>OVERWRITE</code> (in all caps) in the box below before clicking OK.</p>`,
657 '',
658 { okButton: 'OK', cancelButton: false },
659 );
660
661 const forceSaveConfirmed = popupResult === 'OVERWRITE';
662
663 if (!forceSaveConfirmed) {
664 console.warn('Chat integrity check failed, and user did not confirm the overwrite. Reloading the page.');
665 window.location.reload();
666 return;
667 }
668
669 await saveGroupChat(groupId, shouldSaveGroup, true);
583 }670 }
584671
585 if (shouldSaveGroup) {672 if (shouldSaveGroup) {
@@ -587,6 +674,12 @@ async function saveGroupChat(groupId, shouldSaveGroup) {
587 }674 }
588}675}
589676
677/**
678 * Renames a group member across all groups and their chats.
679 * @param {string} oldAvatar Old avatar name
680 * @param {string} newAvatar New avatar name
681 * @param {string} newName New character name
682 */
590export async function renameGroupMember(oldAvatar, newAvatar, newName) {683export async function renameGroupMember(oldAvatar, newAvatar, newName) {
591 // Scan every group for our renamed character684 // Scan every group for our renamed character
592 for (const group of groups) {685 for (const group of groups) {
@@ -614,6 +707,11 @@ export async function renameGroupMember(oldAvatar, newAvatar, newName) {
614 if (Array.isArray(messages) && messages.length) {707 if (Array.isArray(messages) && messages.length) {
615 // Iterate over every chat message708 // Iterate over every chat message
616 for (const message of messages) {709 for (const message of messages) {
710 // Skip the chat header
711 if (Object.hasOwn(message, 'chat_metadata')) {
712 continue;
713 }
714
617 // Only look at character messages715 // Only look at character messages
618 if (message.is_user || message.is_system) {716 if (message.is_user || message.is_system) {
619 continue;717 continue;
@@ -630,6 +728,8 @@ export async function renameGroupMember(oldAvatar, newAvatar, newName) {
630 }728 }
631729
632 if (hadChanges) {730 if (hadChanges) {
731 await eventSource.emit(event_types.CHARACTER_RENAMED_IN_PAST_CHAT, messages, oldAvatar, newAvatar);
732
633 const saveChatResponse = await fetch('/api/chats/group/save', {733 const saveChatResponse = await fetch('/api/chats/group/save', {
634 method: 'POST',734 method: 'POST',
635 headers: getRequestHeaders(),735 headers: getRequestHeaders(),
@@ -652,15 +752,19 @@ export async function renameGroupMember(oldAvatar, newAvatar, newName) {
652 }752 }
653}753}
654754
755/**
756 * Fetches all groups from the server and processes them.
757 */
655async function getGroups() {758async function getGroups() {
656 const response = await fetch('/api/groups/all', {759 const response = await fetch('/api/groups/all', {
657 method: 'POST',760 method: 'POST',
658 headers: getRequestHeaders(),761 headers: getRequestHeaders({ omitContentType: true }),
659 });762 });
660763
661 if (response.ok) {764 if (response.ok) {
765 /** @type {Group[]} */
662 const data = await response.json();766 const data = await response.json();
663 groups = data.sort((a, b) => a.id - b.id);767 groups = data.slice();
664768
665 // Convert groups to new format769 // Convert groups to new format
666 for (const group of groups) {770 for (const group of groups) {
@@ -678,9 +782,6 @@ async function getGroups() {
678 .filter(x => x)782 .filter(x => x)
679 .filter(onlyUnique);783 .filter(onlyUnique);
680 }784 }
681 if (group.past_metadata == undefined) {
682 group.past_metadata = {};
683 }
684 if (typeof group.chat_id === 'number') {785 if (typeof group.chat_id === 'number') {
685 group.chat_id = String(group.chat_id);786 group.chat_id = String(group.chat_id);
686 }787 }
@@ -691,6 +792,11 @@ async function getGroups() {
691 }792 }
692}793}
693794
795/**
796 * Gets a group UI block for the list.
797 * @param {Group} group Group object
798 * @returns {JQuery<HTMLElement>} jQuery element representing the group block
799 */
694export function getGroupBlock(group) {800export function getGroupBlock(group) {
695 let count = 0;801 let count = 0;
696 let namesList = [];802 let namesList = [];
@@ -712,7 +818,7 @@ export function getGroupBlock(group) {
712 template.find('.ch_name').text(group.name).attr('title', `[Group] ${group.name}`);818 template.find('.ch_name').text(group.name).attr('title', `[Group] ${group.name}`);
713 template.find('.group_fav_icon').css('display', 'none');819 template.find('.group_fav_icon').css('display', 'none');
714 template.addClass(group.fav ? 'is_fav' : '');820 template.addClass(group.fav ? 'is_fav' : '');
715 template.find('.ch_fav').val(group.fav);821 template.find('.ch_fav').val(String(group.fav));
716 template.find('.group_select_counter').text(count + ' ' + (count != 1 ? t`characters` : t`character`));822 template.find('.group_select_counter').text(count + ' ' + (count != 1 ? t`characters` : t`character`));
717 template.find('.group_select_block_list').text(namesList.join(', '));823 template.find('.group_select_block_list').text(namesList.join(', '));
718824
@@ -728,6 +834,10 @@ export function getGroupBlock(group) {
728 return template;834 return template;
729}835}
730836
837/**
838 * Updates the avatar display for a given group.
839 * @param {Group} group Group object
840 */
731function updateGroupAvatar(group) {841function updateGroupAvatar(group) {
732 $('#group_avatar_preview').empty().append(getGroupAvatar(group));842 $('#group_avatar_preview').empty().append(getGroupAvatar(group));
733843
@@ -740,7 +850,11 @@ function updateGroupAvatar(group) {
740 favsToHotswap();850 favsToHotswap();
741}851}
742852
743// check if isDataURLor if it's a valid local file url853/**
854 * Checks if a URL is a valid image URL.
855 * @param {string} url URL to check
856 * @returns {boolean} True if valid, false otherwise
857 */
744function isValidImageUrl(url) {858function isValidImageUrl(url) {
745 // check if empty dict859 // check if empty dict
746 if (Object.keys(url).length === 0) {860 if (Object.keys(url).length === 0) {
@@ -749,6 +863,11 @@ function isValidImageUrl(url) {
749 return isDataURL(url) || (url && (url.startsWith('user') || url.startsWith('/user')));863 return isDataURL(url) || (url && (url.startsWith('user') || url.startsWith('/user')));
750}864}
751865
866/**
867 * Gets a group avatar element.
868 * @param {Group} group Group object
869 * @returns {JQuery<HTMLElement>} Group avatar element
870 */
752function getGroupAvatar(group) {871function getGroupAvatar(group) {
753 if (!group) {872 if (!group) {
754 return $(`<div class="avatar"><img src="${default_avatar}"></div>`);873 return $(`<div class="avatar"><img src="${default_avatar}"></div>`);
@@ -797,6 +916,11 @@ function getGroupAvatar(group) {
797 return groupAvatar;916 return groupAvatar;
798}917}
799918
919/**
920 * Gets chat IDs for a group.
921 * @param {string} groupId Group ID
922 * @returns {string[]} Array of chat IDs
923 */
800function getGroupChatNames(groupId) {924function getGroupChatNames(groupId) {
801 const group = groups.find(x => x.id === groupId);925 const group = groups.find(x => x.id === groupId);
802926
@@ -811,7 +935,14 @@ function getGroupChatNames(groupId) {
811 return names;935 return names;
812}936}
813937
814async function generateGroupWrapper(by_auto_mode, type = null, params = {}) {938/**
939 * Generates text for the group chat by queueing members according to the activation strategy.
940 * @param {boolean} byAutoMode If the generation was triggered by the auto mode.
941 * @param {string?} type Generation type
942 * @param {object} params Additional Generate parameters
943 * @returns {Promise<string|void>} Generated text or nothing if no generation occurred
944 */
945async function generateGroupWrapper(byAutoMode, type = null, params = {}) {
815 function throwIfAborted() {946 function throwIfAborted() {
816 if (params.signal instanceof AbortSignal && params.signal.aborted) {947 if (params.signal instanceof AbortSignal && params.signal.aborted) {
817 throw new Error('AbortSignal was fired. Group generation stopped');948 throw new Error('AbortSignal was fired. Group generation stopped');
@@ -830,7 +961,7 @@ async function generateGroupWrapper(by_auto_mode, type = null, params = {}) {
830961
831 // Auto-navigate back to group menu962 // Auto-navigate back to group menu
832 if (menu_type !== 'group_edit') {963 if (menu_type !== 'group_edit') {
833 select_group_chats(selected_group);964 select_group_chats(selected_group, false);
834 await delay(1);965 await delay(1);
835 }966 }
836967
@@ -859,7 +990,7 @@ async function generateGroupWrapper(by_auto_mode, type = null, params = {}) {
859 let activationText = '';990 let activationText = '';
860 let isUserInput = false;991 let isUserInput = false;
861992
862 if (userInput?.length && !by_auto_mode) {993 if (userInput?.length && !byAutoMode) {
863 isUserInput = true;994 isUserInput = true;
864 activationText = userInput;995 activationText = userInput;
865 } else {996 } else {
@@ -935,12 +1066,12 @@ async function generateGroupWrapper(by_auto_mode, type = null, params = {}) {
9351066
936 // Wait for generation to finish1067 // Wait for generation to finish
937 const generateType = ['swipe', 'impersonate', 'quiet', 'continue'].includes(type) ? type : 'normal';1068 const generateType = ['swipe', 'impersonate', 'quiet', 'continue'].includes(type) ? type : 'normal';
938 textResult = await Generate(generateType, { automatic_trigger: by_auto_mode, ...(params || {}) });1069 textResult = await Generate(generateType, { automatic_trigger: byAutoMode, ...(params || {}) });
939 let messageChunk = textResult?.messageChunk;1070 let messageChunk = textResult?.messageChunk;
9401071
941 if (messageChunk) {1072 if (messageChunk) {
942 while (shouldAutoContinue(messageChunk, type === 'impersonate')) {1073 while (shouldAutoContinue(messageChunk, type === 'impersonate')) {
943 textResult = await Generate('continue', { automatic_trigger: by_auto_mode, ...(params || {}) });1074 textResult = await Generate('continue', { automatic_trigger: byAutoMode, ...(params || {}) });
944 messageChunk = textResult?.messageChunk;1075 messageChunk = textResult?.messageChunk;
945 }1076 }
946 }1077 }
@@ -966,6 +1097,10 @@ async function generateGroupWrapper(by_auto_mode, type = null, params = {}) {
966 return Promise.resolve(textResult);1097 return Promise.resolve(textResult);
967}1098}
9681099
1100/**
1101 * Gets the generation ID of the last chat message.
1102 * @returns {number|null} Generation ID or null
1103 */
969function getLastMessageGenerationId() {1104function getLastMessageGenerationId() {
970 let generationId = null;1105 let generationId = null;
971 if (chat.length > 0) {1106 if (chat.length > 0) {
@@ -977,6 +1112,11 @@ function getLastMessageGenerationId() {
977 return generationId;1112 return generationId;
978}1113}
9791114
1115/**
1116 * Activate group chat members for 'impersonate' generation type.
1117 * @param {string[]} members Array of group member avatar ids
1118 * @returns {number[]} Array of character ids
1119 */
980function activateImpersonate(members) {1120function activateImpersonate(members) {
981 const randomIndex = Math.floor(Math.random() * members.length);1121 const randomIndex = Math.floor(Math.random() * members.length);
982 const activatedMembers = [members[randomIndex]];1122 const activatedMembers = [members[randomIndex]];
@@ -1039,6 +1179,11 @@ function activateSwipe(members, { allowSystem = false } = {}) {
1039 return memberIds;1179 return memberIds;
1040}1180}
10411181
1182/**
1183 * Activate group members for the list activation order.
1184 * @param {string[]} members Array of group member avatar ids
1185 * @returns {number[]} Array of character ids
1186 */
1042function activateListOrder(members) {1187function activateListOrder(members) {
1043 let activatedMembers = members.filter(onlyUnique);1188 let activatedMembers = members.filter(onlyUnique);
10441189
@@ -1092,6 +1237,15 @@ function activatePooledOrder(members, lastMessage, isUserInput) {
1092 return memberId !== -1 ? [memberId] : [];1237 return memberId !== -1 ? [memberId] : [];
1093}1238}
10941239
1240/**
1241 * Activate group members for the natural activation order.
1242 * @param {string[]} members Array of group member avatar ids
1243 * @param {string} input User input that triggered the generation
1244 * @param {ChatMessage} lastMessage Last message in the chat
1245 * @param {boolean} allowSelfResponses If the group allows self-responses
1246 * @param {boolean} isUserInput If the generation was triggered by user input
1247 * @returns {number[]} Array of character ids
1248 */
1095function activateNaturalOrder(members, input, lastMessage, allowSelfResponses, isUserInput) {1249function activateNaturalOrder(members, input, lastMessage, allowSelfResponses, isUserInput) {
1096 let activatedMembers = [];1250 let activatedMembers = [];
10971251
@@ -1168,6 +1322,11 @@ function activateNaturalOrder(members, input, lastMessage, allowSelfResponses, i
1168 return memberIds;1322 return memberIds;
1169}1323}
11701324
1325/**
1326 * Deletes a group from the server by ID.
1327 * @param {string} id Group ID to delete
1328 * @returns {Promise<void>} Promise that resolves when the group is deleted
1329 */
1171async function deleteGroup(id) {1330async function deleteGroup(id) {
1172 const group = groups.find((x) => x.id === id);1331 const group = groups.find((x) => x.id === id);
11731332
@@ -1197,6 +1356,13 @@ async function deleteGroup(id) {
1197 }1356 }
1198}1357}
11991358
1359/**
1360 * Edits a group by ID.
1361 * @param {string} id Group ID to edit
1362 * @param {boolean} immediately Whether to save immediately
1363 * @param {boolean} reload Whether to reload the groups after saving
1364 * @returns {Promise<void>} Promise that resolves when the group is edited
1365 */
1200export async function editGroup(id, immediately, reload = true) {1366export async function editGroup(id, immediately, reload = true) {
1201 let group = groups.find((x) => x.id === id);1367 let group = groups.find((x) => x.id === id);
12021368
@@ -1204,11 +1370,6 @@ export async function editGroup(id, immediately, reload = true) {
1204 return;1370 return;
1205 }1371 }
12061372
1207 if (id === selected_group) {
1208 // structuredClone may cause issues if metadata has non-cloneable references
1209 group['chat_metadata'] = JSON.parse(JSON.stringify(chat_metadata));
1210 }
1211
1212 if (immediately) {1373 if (immediately) {
1213 return await _save(group, reload);1374 return await _save(group, reload);
1214 }1375 }
@@ -1260,6 +1421,12 @@ async function groupChatAutoModeWorker() {
1260 await generateGroupWrapper(true, 'auto', { signal: groupAutoModeAbortController.signal });1421 await generateGroupWrapper(true, 'auto', { signal: groupAutoModeAbortController.signal });
1261}1422}
12621423
1424/**
1425 * Modifies a group member by adding or removing them.
1426 * @param {string} groupId Group ID
1427 * @param {JQuery<HTMLElement>} groupMember Group member element
1428 * @param {boolean} isDelete If true, removes the member; otherwise adds the member
1429 */
1263async function modifyGroupMember(groupId, groupMember, isDelete) {1430async function modifyGroupMember(groupId, groupMember, isDelete) {
1264 const id = groupMember.data('id');1431 const id = groupMember.data('id');
1265 const thisGroup = groups.find((x) => x.id == groupId);1432 const thisGroup = groups.find((x) => x.id == groupId);
@@ -1287,9 +1454,16 @@ async function modifyGroupMember(groupId, groupMember, isDelete) {
1287 $('#rm_group_submit').prop('disabled', !groupHasMembers);1454 $('#rm_group_submit').prop('disabled', !groupHasMembers);
1288}1455}
12891456
1290async function reorderGroupMember(chat_id, groupMember, direction) {1457/**
1458 * Reorders a group member up or down.
1459 * @param {string} groupId Group ID
1460 * @param {JQuery<HTMLElement>} groupMember Group member element
1461 * @param {string} direction Direction to move the member ('up' or 'down')
1462 * @returns {Promise<void>} Promise that resolves when the member has been reordered
1463 */
1464async function reorderGroupMember(groupId, groupMember, direction) {
1291 const id = groupMember.data('id');1465 const id = groupMember.data('id');
1292 const thisGroup = groups.find((x) => x.id == chat_id);1466 const thisGroup = groups.find((x) => x.id == groupId);
1293 const memberArray = thisGroup?.members ?? newGroupMembers;1467 const memberArray = thisGroup?.members ?? newGroupMembers;
12941468
1295 const indexOf = memberArray.indexOf(id);1469 const indexOf = memberArray.indexOf(id);
@@ -1312,7 +1486,7 @@ async function reorderGroupMember(chat_id, groupMember, direction) {
13121486
1313 // Existing groups need to modify members list1487 // Existing groups need to modify members list
1314 if (openGroupId) {1488 if (openGroupId) {
1315 await editGroup(chat_id, false, false);1489 await editGroup(groupId, false, false);
1316 updateGroupAvatar(thisGroup);1490 updateGroupAvatar(thisGroup);
1317 }1491 }
1318}1492}
@@ -1358,10 +1532,16 @@ async function onGroupNameInput() {
1358 let _thisGroup = groups.find((x) => x.id == openGroupId);1532 let _thisGroup = groups.find((x) => x.id == openGroupId);
1359 _thisGroup.name = $(this).val();1533 _thisGroup.name = $(this).val();
1360 $('#rm_button_selected_ch').children('h2').text(_thisGroup.name);1534 $('#rm_button_selected_ch').children('h2').text(_thisGroup.name);
1361 await editGroup(openGroupId);1535 await editGroup(openGroupId, false);
1362 }1536 }
1363}1537}
13641538
1539/**
1540 * Checks if a character with the given avatar ID is a member of the group.
1541 * @param {Group} group Group object
1542 * @param {string} avatarId Avatar ID to check
1543 * @returns {boolean} True if the avatar is a member of the group, false otherwise
1544 */
1365function isGroupMember(group, avatarId) {1545function isGroupMember(group, avatarId) {
1366 if (group && Array.isArray(group.members)) {1546 if (group && Array.isArray(group.members)) {
1367 return group.members.includes(avatarId);1547 return group.members.includes(avatarId);
@@ -1370,6 +1550,13 @@ function isGroupMember(group, avatarId) {
1370 }1550 }
1371}1551}
13721552
1553/**
1554 * Gets group characters based on filters.
1555 * @param {object} param
1556 * @param {boolean} [param.doFilter=false] Whether to apply filters
1557 * @param {boolean} [param.onlyMembers=false] Whether to include only group members
1558 * @returns {Array<{item: Character, id: number, type: string}>} Array of group character objects
1559 */
1373function getGroupCharacters({ doFilter = false, onlyMembers = false } = {}) {1560function getGroupCharacters({ doFilter = false, onlyMembers = false } = {}) {
1374 function sortMembersFn(a, b) {1561 function sortMembersFn(a, b) {
1375 const membersArray = thisGroup?.members ?? newGroupMembers;1562 const membersArray = thisGroup?.members ?? newGroupMembers;
@@ -1461,15 +1648,20 @@ function printGroupMembers() {
1461 });1648 });
1462}1649}
14631650
1651/**
1652 * Creates a jQuery element representing a group character block.
1653 * @param {Character} character Character object
1654 * @returns {JQuery<HTMLElement>} jQuery element representing the group character block
1655 */
1464function getGroupCharacterBlock(character) {1656function getGroupCharacterBlock(character) {
1465 const avatar = getThumbnailUrl('avatar', character.avatar);1657 const avatar = getThumbnailUrl('avatar', character.avatar);
1466 const template = $('#group_member_template .group_member').clone();1658 const template = $('#group_member_template .group_member').clone();
1467 const isFav = character.fav || character.fav == 'true';1659 const isFav = !!character.fav || character.fav == 'true';
1468 template.data('id', character.avatar);1660 template.data('id', character.avatar);
1469 template.find('.avatar img').attr({ 'src': avatar, 'title': character.avatar });1661 template.find('.avatar img').attr({ 'src': avatar, 'title': character.avatar });
1470 template.find('.ch_name').text(character.name);1662 template.find('.ch_name').text(character.name);
1471 template.attr('data-chid', characters.indexOf(character));1663 template.attr('data-chid', characters.indexOf(character));
1472 template.find('.ch_fav').val(isFav);1664 template.find('.ch_fav').val(String(isFav));
1473 template.toggleClass('is_fav', isFav);1665 template.toggleClass('is_fav', isFav);
14741666
1475 const auxFieldName = power_user.aux_field || 'character_version';1667 const auxFieldName = power_user.aux_field || 'character_version';
@@ -1503,6 +1695,11 @@ function getGroupCharacterBlock(character) {
1503 return template;1695 return template;
1504}1696}
15051697
1698/**
1699 * Checks if a group member is disabled.
1700 * @param {string} avatarId Avatar ID of the group member
1701 * @returns {boolean} True if the group member is disabled, false otherwise
1702 */
1506function isGroupMemberDisabled(avatarId) {1703function isGroupMemberDisabled(avatarId) {
1507 const thisGroup = openGroupId && groups.find((x) => x.id == openGroupId);1704 const thisGroup = openGroupId && groups.find((x) => x.id == openGroupId);
1508 return Boolean(thisGroup && thisGroup.disabled_members.includes(avatarId));1705 return Boolean(thisGroup && thisGroup.disabled_members.includes(avatarId));
@@ -1553,6 +1750,11 @@ async function onHideMutedSpritesClick(value) {
1553 }1750 }
1554}1751}
15551752
1753/**
1754 * Toggles the visibility of hidden controls based on the group's generation mode.
1755 * @param {Group} group Group object
1756 * @param {number|null} generationMode Generation mode, or null to use the group's current generation mode
1757 */
1556function toggleHiddenControls(group, generationMode = null) {1758function toggleHiddenControls(group, generationMode = null) {
1557 const isJoin = [group_generation_mode.APPEND, group_generation_mode.APPEND_DISABLED].includes(generationMode ?? group?.generation_mode);1759 const isJoin = [group_generation_mode.APPEND, group_generation_mode.APPEND_DISABLED].includes(generationMode ?? group?.generation_mode);
1558 $('#rm_group_generation_mode_join_prefix').parent().toggle(isJoin);1760 $('#rm_group_generation_mode_join_prefix').parent().toggle(isJoin);
@@ -1564,6 +1766,11 @@ function toggleHiddenControls(group, generationMode = null) {
1564 }1766 }
1565}1767}
15661768
1769/**
1770 * Opens a group creation/editing right menu.
1771 * @param {string|null} groupId ID of the group to select or null if creating a new group
1772 * @param {boolean} skipAnimation If true, skips the animation when selecting the group
1773 */
1567function select_group_chats(groupId, skipAnimation) {1774function select_group_chats(groupId, skipAnimation) {
1568 openGroupId = groupId;1775 openGroupId = groupId;
1569 newGroupMembers = [];1776 newGroupMembers = [];
@@ -1774,6 +1981,11 @@ function updateFavButtonState(state) {
1774 $('#group_favorite_button').toggleClass('fav_off', !fav_grp_checked);1981 $('#group_favorite_button').toggleClass('fav_off', !fav_grp_checked);
1775}1982}
17761983
1984/**
1985 * Opens a group chat by its ID and updates the UI accordingly.
1986 * @param {string} groupId ID of the group to open
1987 * @returns {Promise<boolean>} Whether the group was opened
1988 */
1777export async function openGroupById(groupId) {1989export async function openGroupById(groupId) {
1778 if (isChatSaving) {1990 if (isChatSaving) {
1779 toastr.info(t`Please wait until the chat is saved before switching characters.`, t`Your chat is still saving...`);1991 toastr.info(t`Please wait until the chat is saved before switching characters.`, t`Your chat is still saving...`);
@@ -1786,7 +1998,7 @@ export async function openGroupById(groupId) {
1786 }1998 }
17871999
1788 if (!is_send_press && !is_group_generating) {2000 if (!is_send_press && !is_group_generating) {
1789 select_group_chats(groupId);2001 select_group_chats(groupId, false);
17902002
1791 if (selected_group !== groupId) {2003 if (selected_group !== groupId) {
1792 groupChatQueueOrder = new Map();2004 groupChatQueueOrder = new Map();
@@ -1806,6 +2018,11 @@ export async function openGroupById(groupId) {
1806 return false;2018 return false;
1807}2019}
18082020
2021/**
2022 * Peeks the character definition from a group member element.
2023 * @param {JQuery<HTMLElement>} characterSelect Character select element
2024 * @returns {Promise<void>}
2025 */
1809async function openCharacterDefinition(characterSelect) {2026async function openCharacterDefinition(characterSelect) {
1810 if (is_group_generating) {2027 if (is_group_generating) {
1811 toastr.warning(t`Can't peek a character while group reply is being generated`);2028 toastr.warning(t`Can't peek a character while group reply is being generated`);
@@ -1834,7 +2051,7 @@ function filterGroupMembers() {
1834}2051}
18352052
1836async function createGroup() {2053async function createGroup() {
1837 let name = $('#rm_group_chat_name').val();2054 let name = $('#rm_group_chat_name').val().toString();
1838 let allowSelfResponses = !!$('#rm_group_allow_self_responses').prop('checked');2055 let allowSelfResponses = !!$('#rm_group_allow_self_responses').prop('checked');
1839 let activationStrategy = Number($('#rm_group_activation_strategy').find(':selected').val()) ?? group_activation_strategy.NATURAL;2056 let activationStrategy = Number($('#rm_group_activation_strategy').find(':selected').val()) ?? group_activation_strategy.NATURAL;
1840 let generationMode = Number($('#rm_group_generation_mode').find(':selected').val()) ?? group_generation_mode.SWAP;2057 let generationMode = Number($('#rm_group_generation_mode').find(':selected').val()) ?? group_generation_mode.SWAP;
@@ -1846,29 +2063,30 @@ async function createGroup() {
1846 name = t`Group: ${memberNames}`;2063 name = t`Group: ${memberNames}`;
1847 }2064 }
18482065
1849 const avatar_url = $('#group_avatar_preview img').attr('src');2066 const avatarUrl = $('#group_avatar_preview img').attr('src');
1850
1851 const chatName = humanizedDateTime();2067 const chatName = humanizedDateTime();
1852 const chats = [chatName];2068 const chats = [chatName];
18532069
2070 /** @type {Omit<Group, 'id'>} */
2071 const groupCreateModel = {
2072 name: name,
2073 members: members,
2074 avatar_url: isValidImageUrl(avatarUrl) ? avatarUrl : default_avatar,
2075 allow_self_responses: allowSelfResponses,
2076 hideMutedSprites: hideMutedSprites,
2077 activation_strategy: activationStrategy,
2078 generation_mode: generationMode,
2079 disabled_members: [],
2080 fav: fav_grp_checked,
2081 chat_id: chatName,
2082 chats: chats,
2083 auto_mode_delay: autoModeDelay,
2084 };
2085
1854 const createGroupResponse = await fetch('/api/groups/create', {2086 const createGroupResponse = await fetch('/api/groups/create', {
1855 method: 'POST',2087 method: 'POST',
1856 headers: getRequestHeaders(),2088 headers: getRequestHeaders(),
1857 body: JSON.stringify({2089 body: JSON.stringify(groupCreateModel),
1858 name: name,
1859 members: members,
1860 avatar_url: isValidImageUrl(avatar_url) ? avatar_url : default_avatar,
1861 allow_self_responses: allowSelfResponses,
1862 hideMutedSprites: hideMutedSprites,
1863 activation_strategy: activationStrategy,
1864 generation_mode: generationMode,
1865 disabled_members: [],
1866 chat_metadata: {},
1867 fav: fav_grp_checked,
1868 chat_id: chatName,
1869 chats: chats,
1870 auto_mode_delay: autoModeDelay,
1871 }),
1872 });2090 });
18732091
1874 if (createGroupResponse.ok) {2092 if (createGroupResponse.ok) {
@@ -1880,6 +2098,11 @@ async function createGroup() {
1880 }2098 }
1881}2099}
18822100
2101/**
2102 * Creates a new group chat within the specified group.
2103 * @param {string} groupId Group ID
2104 * @returns {Promise<void>} Promise that resolves when the new group chat is created
2105 */
1883export async function createNewGroupChat(groupId) {2106export async function createNewGroupChat(groupId) {
1884 const group = groups.find(x => x.id === groupId);2107 const group = groups.find(x => x.id === groupId);
18852108
@@ -1887,27 +2110,22 @@ export async function createNewGroupChat(groupId) {
1887 return;2110 return;
1888 }2111 }
18892112
1890 const oldChatName = group.chat_id;
1891 const newChatName = humanizedDateTime();
1892
1893 if (typeof group.past_metadata !== 'object') {
1894 group.past_metadata = {};
1895 }
1896
1897 await clearChat();2113 await clearChat();
1898 chat.length = 0;2114 chat.length = 0;
1899 if (oldChatName) {2115 const newChatName = humanizedDateTime();
1900 group.past_metadata[oldChatName] = Object.assign({}, chat_metadata);
1901 }
1902 group.chats.push(newChatName);2116 group.chats.push(newChatName);
1903 group.chat_id = newChatName;2117 group.chat_id = newChatName;
1904 group.chat_metadata = {};2118 updateChatMetadata({}, true);
1905 updateChatMetadata(group.chat_metadata, true);
19062119
1907 await editGroup(group.id, true, false);2120 await editGroup(group.id, true, false);
1908 await getGroupChat(group.id);2121 await getGroupChat(group.id);
1909}2122}
19102123
2124/**
2125 * Retrieves past chats for a specified group.
2126 * @param {string} groupId Group ID
2127 * @returns {Promise<Array>} Array of past chats
2128 */
1911export async function getGroupPastChats(groupId) {2129export async function getGroupPastChats(groupId) {
1912 const group = groups.find(x => x.id === groupId);2130 const group = groups.find(x => x.id === groupId);
19132131
@@ -1920,16 +2138,22 @@ export async function getGroupPastChats(groupId) {
1920 try {2138 try {
1921 for (const chatId of group.chats) {2139 for (const chatId of group.chats) {
1922 const messages = await loadGroupChat(chatId);2140 const messages = await loadGroupChat(chatId);
1923 let this_chat_file_size = (JSON.stringify(messages).length / 1024).toFixed(2) + 'kb';2141 if (!Array.isArray(messages)) {
1924 let chat_items = messages.length;2142 continue;
2143 }
2144 const fileSize = humanFileSize(JSON.stringify(messages).length);
2145 if (messages.length > 0 && Object.hasOwn(messages[0], 'chat_metadata')) {
2146 messages.shift();
2147 }
2148 const chatItems = messages.length;
1925 const lastMessage = messages.length ? messages[messages.length - 1].mes : '[The chat is empty]';2149 const lastMessage = messages.length ? messages[messages.length - 1].mes : '[The chat is empty]';
1926 const lastMessageDate = messages.length ? (messages[messages.length - 1].send_date || Date.now()) : Date.now();2150 const lastMessageDate = messages.length ? (messages[messages.length - 1].send_date || Date.now()) : Date.now();
1927 chats.push({2151 chats.push({
1928 'file_name': chatId,2152 'file_name': chatId,
1929 'mes': lastMessage,2153 'mes': lastMessage,
1930 'last_mes': lastMessageDate,2154 'last_mes': lastMessageDate,
1931 'file_size': this_chat_file_size,2155 'file_size': fileSize,
1932 'chat_items': chat_items,2156 'chat_items': chatItems,
1933 });2157 });
1934 }2158 }
1935 } catch (err) {2159 } catch (err) {
@@ -1938,6 +2162,12 @@ export async function getGroupPastChats(groupId) {
1938 return chats;2162 return chats;
1939}2163}
19402164
2165/**
2166 * Opens a specific group chat for the specified group by its ID.
2167 * @param {string} groupId Group ID
2168 * @param {string} chatId Chat ID
2169 * @returns {Promise<void>}
2170 */
1941export async function openGroupChat(groupId, chatId) {2171export async function openGroupChat(groupId, chatId) {
1942 await waitUntilCondition(() => !isChatSaving, debounce_timeout.extended, 10);2172 await waitUntilCondition(() => !isChatSaving, debounce_timeout.extended, 10);
1943 const group = groups.find(x => x.id === groupId);2173 const group = groups.find(x => x.id === groupId);
@@ -1948,17 +2178,21 @@ export async function openGroupChat(groupId, chatId) {
19482178
1949 await clearChat();2179 await clearChat();
1950 chat.length = 0;2180 chat.length = 0;
1951 const previousChat = group.chat_id;
1952 group.past_metadata[previousChat] = Object.assign({}, chat_metadata);
1953 group.chat_id = chatId;2181 group.chat_id = chatId;
1954 group.chat_metadata = group.past_metadata[chatId] || {};
1955 group['date_last_chat'] = Date.now();2182 group['date_last_chat'] = Date.now();
1956 updateChatMetadata(group.chat_metadata, true);2183 updateChatMetadata({}, true);
19572184
1958 await editGroup(groupId, true, false);2185 await editGroup(groupId, true, false);
1959 await getGroupChat(groupId);2186 await getGroupChat(groupId);
1960}2187}
19612188
2189/**
2190 * Renames a group chat within the specified group.
2191 * @param {string} groupId Group ID
2192 * @param {string} oldChatId Old chat ID
2193 * @param {string} newChatId New chat ID
2194 * @returns {Promise<void>} Promise that resolves when the group chat is renamed
2195 */
1962export async function renameGroupChat(groupId, oldChatId, newChatId) {2196export async function renameGroupChat(groupId, oldChatId, newChatId) {
1963 const group = groups.find(x => x.id === groupId);2197 const group = groups.find(x => x.id === groupId);
19642198
@@ -1972,8 +2206,6 @@ export async function renameGroupChat(groupId, oldChatId, newChatId) {
19722206
1973 group.chats.splice(group.chats.indexOf(oldChatId), 1);2207 group.chats.splice(group.chats.indexOf(oldChatId), 1);
1974 group.chats.push(newChatId);2208 group.chats.push(newChatId);
1975 group.past_metadata[newChatId] = (group.past_metadata[oldChatId] || {});
1976 delete group.past_metadata[oldChatId];
19772209
1978 await editGroup(groupId, true, true);2210 await editGroup(groupId, true, true);
1979}2211}
@@ -1990,12 +2222,7 @@ export async function deleteGroupChatByName(groupId, chatName) {
1990 return;2222 return;
1991 }2223 }
19922224
1993 if (typeof group.past_metadata !== 'object') {
1994 group.past_metadata = {};
1995 }
1996
1997 group.chats.splice(group.chats.indexOf(chatName), 1);2225 group.chats.splice(group.chats.indexOf(chatName), 1);
1998 delete group.past_metadata[chatName];
19992226
2000 const response = await fetch('/api/chats/group/delete', {2227 const response = await fetch('/api/chats/group/delete', {
2001 method: 'POST',2228 method: 'POST',
@@ -2011,12 +2238,8 @@ export async function deleteGroupChatByName(groupId, chatName) {
20112238
2012 // If the deleted chat was the current chat, switch to the last chat in the group2239 // If the deleted chat was the current chat, switch to the last chat in the group
2013 if (group.chat_id === chatName) {2240 if (group.chat_id === chatName) {
2014 group.chat_id = '';
2015 group.chat_metadata = {};
2016
2017 const newChatName = group.chats.length ? group.chats[group.chats.length - 1] : humanizedDateTime();2241 const newChatName = group.chats.length ? group.chats[group.chats.length - 1] : humanizedDateTime();
2018 group.chat_id = newChatName;2242 group.chat_id = newChatName;
2019 group.chat_metadata = group.past_metadata[newChatName] || {};
2020 }2243 }
20212244
2022 await editGroup(groupId, true, true);2245 await editGroup(groupId, true, true);
@@ -2038,12 +2261,10 @@ export async function deleteGroupChat(groupId, chatId, { jumpToNewChat = true }
2038 }2261 }
20392262
2040 group.chats.splice(group.chats.indexOf(chatId), 1);2263 group.chats.splice(group.chats.indexOf(chatId), 1);
2041 delete group.past_metadata[chatId];
20422264
2043 if (group.chat_id === chatId) {2265 if (group.chat_id === chatId) {
2044 group.chat_id = '';2266 group.chat_id = '';
2045 group.chat_metadata = {};2267 updateChatMetadata({}, true);
2046 updateChatMetadata(group.chat_metadata, true);
2047 }2268 }
20482269
2049 const response = await fetch('/api/chats/group/delete', {2270 const response = await fetch('/api/chats/group/delete', {
@@ -2093,6 +2314,8 @@ export async function importGroupChat(formData, { refresh = true } = {}) {
2093 await displayPastChats();2314 await displayPastChats();
2094 }2315 }
2095 }2316 }
2317
2318 return [data.res];
2096 }2319 }
20972320
2098 return data?.fileNames || [];2321 return data?.fileNames || [];
@@ -2101,6 +2324,14 @@ export async function importGroupChat(formData, { refresh = true } = {}) {
2101 return [];2324 return [];
2102}2325}
21032326
2327/**
2328 * Saves the current group chat as a bookmark chat.
2329 * @param {string} groupId Group ID
2330 * @param {string} name Name of the chat to save
2331 * @param {ChatMetadata?} metadata New metadata to save with the chat
2332 * @param {number|undefined} mesId Optional message ID to trim the chat up to
2333 * @returns {Promise<void>} Promise that resolves when the group chat is saved
2334 */
2104export async function saveGroupBookmarkChat(groupId, name, metadata, mesId) {2335export async function saveGroupBookmarkChat(groupId, name, metadata, mesId) {
2105 const group = groups.find(x => x.id === groupId);2336 const group = groups.find(x => x.id === groupId);
21062337
@@ -2108,11 +2339,18 @@ export async function saveGroupBookmarkChat(groupId, name, metadata, mesId) {
2108 return;2339 return;
2109 }2340 }
21102341
2111 group.past_metadata[name] = { ...chat_metadata, ...(metadata || {}) };
2112 group.chats.push(name);2342 group.chats.push(name);
21132343
2114 const trimmed_chat = (mesId !== undefined && mesId >= 0 && mesId < chat.length)2344 /** @type {ChatHeader} */
2115 ? chat.slice(0, parseInt(mesId) + 1)2345 const chatHeader = {
2346 chat_metadata: { ...chat_metadata, ...(metadata || {}) },
2347 user_name: 'unused',
2348 character_name: 'unused',
2349 };
2350
2351 /** @type {ChatMessage[]} */
2352 const trimmedChat = (mesId !== undefined && mesId >= 0 && mesId < chat.length)
2353 ? chat.slice(0, Number(mesId) + 1)
2116 : chat;2354 : chat;
21172355
2118 await editGroup(groupId, true, false);2356 await editGroup(groupId, true, false);
@@ -2120,7 +2358,7 @@ export async function saveGroupBookmarkChat(groupId, name, metadata, mesId) {
2120 const response = await fetch('/api/chats/group/save', {2358 const response = await fetch('/api/chats/group/save', {
2121 method: 'POST',2359 method: 'POST',
2122 headers: getRequestHeaders(),2360 headers: getRequestHeaders(),
2123 body: JSON.stringify({ id: name, chat: [...trimmed_chat] }),2361 body: JSON.stringify({ id: name, chat: [chatHeader, ...trimmedChat] }),
2124 });2362 });
21252363
2126 if (!response.ok) {2364 if (!response.ok) {
public/scripts/horde.js+2 -2
@@ -107,7 +107,7 @@ export async function checkHordeStatus() {
107 try {107 try {
108 const response = await fetch('/api/horde/status', {108 const response = await fetch('/api/horde/status', {
109 method: 'POST',109 method: 'POST',
110 headers: getRequestHeaders(),110 headers: getRequestHeaders({ omitContentType: true }),
111 });111 });
112112
113 if (!response.ok) {113 if (!response.ok) {
@@ -328,7 +328,7 @@ export function loadHordeSettings(settings) {
328async function showKudos() {328async function showKudos() {
329 const response = await fetch('/api/horde/user-info', {329 const response = await fetch('/api/horde/user-info', {
330 method: 'POST',330 method: 'POST',
331 headers: getRequestHeaders(),331 headers: getRequestHeaders({ omitContentType: true }),
332 });332 });
333333
334 if (!response.ok) {334 if (!response.ok) {
public/scripts/instruct-mode.js+7 -7
@@ -436,10 +436,10 @@ export function formatInstructModeChat(name, mes, isUser, isNarrator, forceAvata
436 let suffix = getSuffix() || '';436 let suffix = getSuffix() || '';
437437
438 if (instruct.macro) {438 if (instruct.macro) {
439 prefix = substituteParams(prefix, name1, name2);439 prefix = substituteParams(prefix, { name1Override: name1, name2Override: name2 });
440 prefix = prefix.replace(/{{name}}/gi, name || 'System');440 prefix = prefix.replace(/{{name}}/gi, name || 'System');
441441
442 suffix = substituteParams(suffix, name1, name2);442 suffix = substituteParams(suffix, { name1Override: name1, name2Override: name2 });
443 suffix = suffix.replace(/{{name}}/gi, name || 'System');443 suffix = suffix.replace(/{{name}}/gi, name || 'System');
444 }444 }
445445
@@ -524,10 +524,10 @@ export function formatInstructModeExamples(mesExamplesArray, name1, name2) {
524 let outputSuffix = power_user.instruct.output_suffix || '';524 let outputSuffix = power_user.instruct.output_suffix || '';
525525
526 if (power_user.instruct.macro) {526 if (power_user.instruct.macro) {
527 inputPrefix = substituteParams(inputPrefix, name1, name2);527 inputPrefix = substituteParams(inputPrefix, { name1Override: name1, name2Override: name2 });
528 outputPrefix = substituteParams(outputPrefix, name1, name2);528 outputPrefix = substituteParams(outputPrefix, { name1Override: name1, name2Override: name2 });
529 inputSuffix = substituteParams(inputSuffix, name1, name2);529 inputSuffix = substituteParams(inputSuffix, { name1Override: name1, name2Override: name2 });
530 outputSuffix = substituteParams(outputSuffix, name1, name2);530 outputSuffix = substituteParams(outputSuffix, { name1Override: name1, name2Override: name2 });
531531
532 inputPrefix = inputPrefix.replace(/{{name}}/gi, name1);532 inputPrefix = inputPrefix.replace(/{{name}}/gi, name1);
533 outputPrefix = outputPrefix.replace(/{{name}}/gi, name2);533 outputPrefix = outputPrefix.replace(/{{name}}/gi, name2);
@@ -631,7 +631,7 @@ export function formatInstructModePrompt(name, isImpersonate, promptBias, name1,
631 }631 }
632632
633 if (instruct.macro) {633 if (instruct.macro) {
634 sequence = substituteParams(sequence, name1, name2);634 sequence = substituteParams(sequence, { name1Override: name1, name2Override: name2 });
635 sequence = sequence.replace(/{{name}}/gi, name || 'System');635 sequence = sequence.replace(/{{name}}/gi, name || 'System');
636 }636 }
637637
public/scripts/keyboard.js+1 -0
@@ -19,6 +19,7 @@ const interactableSelectors = [
19 '.stscript_btn', // STscript buttons in the chat bar19 '.stscript_btn', // STscript buttons in the chat bar
20 '.select2_choice_clickable+span.select2-container .select2-selection__choice__display', // select2 control elements if they are meant to be clickable20 '.select2_choice_clickable+span.select2-container .select2-selection__choice__display', // select2 control elements if they are meant to be clickable
21 '.avatar_load_preview', // Char display avatar selection21 '.avatar_load_preview', // Char display avatar selection
22 '.bg_tabs_list .bg_tab_button', // Background tabs
22];23];
2324
24if (CSS.supports('selector(:has(*))')) {25if (CSS.supports('selector(:has(*))')) {
public/scripts/logprobs.js+1 -0
@@ -417,6 +417,7 @@ function createSwipe(messageId, prompt) {
417417
418 console.debug('cleanedPrompt: ', cleanedPrompt);418 console.debug('cleanedPrompt: ', cleanedPrompt);
419419
420 /** @type {SwipeInfo} */
420 const newSwipeInfo = {421 const newSwipeInfo = {
421 send_date: msg.send_date,422 send_date: msg.send_date,
422 gen_started: msg.gen_started,423 gen_started: msg.gen_started,
public/scripts/macros.js+83 -0
@@ -6,6 +6,8 @@ import { getInstructMacros } from './instruct-mode.js';
6import { getVariableMacros } from './variables.js';6import { getVariableMacros } from './variables.js';
7import { isMobile } from './RossAscends-mods.js';7import { isMobile } from './RossAscends-mods.js';
8import { inject_ids } from './constants.js';8import { inject_ids } from './constants.js';
9import { initRegisterMacros, macros as macroSystem } from './macros/macro-system.js';
10import { power_user } from './power-user.js';
911
10/**12/**
11 * @typedef Macro13 * @typedef Macro
@@ -33,6 +35,10 @@ Handlebars.registerHelper('helperMissing', function () {
33 * @property {string} description - Optional description of the macro35 * @property {string} description - Optional description of the macro
34 */36 */
3537
38/**
39 * @deprecated Use macros.registry.registerMacro (from scripts/macros/macro-system.js)
40 * or substituteParams({ dynamicMacros }) with the new macro engine.
41 */
36export class MacrosParser {42export class MacrosParser {
37 /**43 /**
38 * A map of registered macros.44 * A map of registered macros.
@@ -47,10 +53,97 @@ export class MacrosParser {
47 static #descriptions = new Map();53 static #descriptions = new Map();
4854
49 /**55 /**
56 * Logs a deprecation warning for MacrosParser APIs, pointing callers to
57 * the new macro engine registration surface.
58 *
59 * @param {string} method
60 * @param {string} replacement
61 * @returns {void}
62 */
63 static #logDeprecated(method, replacement) {
64 console.warn(`[DEPRECATED] MacrosParser.${method} is deprecated and will be removed in a future version. Use ${replacement} instead.`);
65 }
66
67 /**
68 * Bridges a legacy MacrosParser macro registration into the new macro
69 * engine when the experimental macro engine flag is enabled.
70 *
71 * This mirrors the simple "{{key}}" replacement behavior by registering
72 * a 0-arg macro in MacroRegistry that does not take arguments and returns
73 * the sanitized value from the legacy registry.
74 *
75 * @param {string} key
76 * @param {string|MacroFunction} value
77 * @param {string} description
78 * @returns {void}
79 */
80 static #registerMacroInNewEngine(key, value, description) {
81 if (!power_user.experimental_macro_engine) {
82 return;
83 }
84
85 // Like the old MacrosParser, we explicitly allow overriding macros, and only warn
86 if (macroSystem.registry.hasMacro(key)) {
87 console.warn(`Macro ${key} is already registered`);
88 }
89
90 const legacyValue = value;
91
92 macroSystem.registry.registerMacro(key, {
93 // Legacy MacrosParser macros never took arguments; keep the
94 // contract that only {{key}} without arguments is valid.
95 category: 'legacy',
96 description: typeof description === 'string' ? description : 'Automatically registered macro from MacrosParser',
97 handler: () => {
98 /** @type {string|MacroFunction|undefined} */
99 let stored = legacyValue;
100
101 if (typeof stored === 'function') {
102 try {
103 const nonce = uuidv4();
104 stored = stored(nonce);
105 } catch (e) {
106 console.warn(`Macro "${key}" function threw an error.`, e);
107 stored = '';
108 }
109 }
110
111 // Let the new macro engine's normalizeMacroResult handle type
112 // normalization for the returned value.
113 return stored;
114 },
115 });
116 }
117
118 /**
119 * Bridges a legacy MacrosParser macro unregistration into the new macro
120 * engine when the experimental macro engine flag is enabled.
121 *
122 * @param {string} key
123 * @returns {void}
124 */
125 static #unregisterMacroInNewEngine(key) {
126 if (!power_user.experimental_macro_engine) {
127 return;
128 }
129
130 macroSystem.registry.unregisterMacro(key);
131 }
132
public/scripts/macros/MacroBrowser.js+0 -0
public/scripts/macros/definitions/chat-macros.js+0 -0
public/scripts/macros/definitions/core-macros.js+0 -0
public/scripts/macros/definitions/env-macros.js+0 -0
public/scripts/macros/definitions/instruct-macros.js+0 -0
public/scripts/macros/definitions/state-macros.js+0 -0
public/scripts/macros/definitions/time-macros.js+0 -0
public/scripts/macros/definitions/variable-macros.js+0 -0
public/scripts/macros/engine/MacroCstWalker.js+0 -0
public/scripts/macros/engine/MacroDiagnostics.js+0 -0
public/scripts/macros/engine/MacroEngine.js+0 -0
public/scripts/macros/engine/MacroEnv.types.js+0 -0
public/scripts/macros/engine/MacroEnvBuilder.js+0 -0
public/scripts/macros/engine/MacroLexer.js+0 -0
public/scripts/macros/engine/MacroParser.js+0 -0
public/scripts/macros/engine/MacroRegistry.js+0 -0
public/scripts/macros/macro-system.js+0 -0
public/scripts/openai.js+0 -0
public/scripts/personas.js+0 -0
public/scripts/power-user.js+0 -0
public/scripts/preset-manager.js+0 -0
public/scripts/reasoning.js+0 -0
public/scripts/scrapers.js+0 -0
public/scripts/secrets.js+0 -0
public/scripts/slash-commands.js+0 -0
public/scripts/slash-commands/SlashCommandCommonEnumsProvider.js+0 -0
public/scripts/slash-commands/SlashCommandParser.js+0 -0
public/scripts/sse-stream.js+0 -0
public/scripts/st-context.js+0 -0
public/scripts/system-messages.js+0 -0
public/scripts/templates/macros.html+0 -0
public/scripts/textgen-models.js+0 -0
public/scripts/textgen-settings.js+0 -0
public/scripts/tokenizers.js+0 -0
public/scripts/tool-calling.js+0 -0
public/scripts/user.js+0 -0
public/scripts/utils.js+0 -0
public/scripts/variables.js+0 -0
public/scripts/welcome-screen.js+0 -0
public/scripts/world-info.js+0 -0
public/style.css+0 -0
src/byaf.js+0 -0
src/charx.js+0 -0
src/constants.js+0 -0
src/endpoints/backends/chat-completions.js+0 -0
src/endpoints/backends/text-completions.js+0 -0
src/endpoints/backups.js+0 -0
src/endpoints/characters.js+0 -0
src/endpoints/chats.js+0 -0
src/endpoints/data-maid.js+0 -0
src/endpoints/google.js+0 -0
src/endpoints/groups.js+0 -0
src/endpoints/images.js+0 -0
src/endpoints/openai.js+0 -0
src/endpoints/openrouter.js+0 -0
src/endpoints/search.js+0 -0
src/endpoints/secrets.js+0 -0
src/endpoints/speech.js+0 -0
src/endpoints/stable-diffusion.js+0 -0
src/endpoints/stats.js+0 -0
src/endpoints/translate.js+0 -0
src/endpoints/vectors.js+0 -0
src/endpoints/worldinfo.js+0 -0
src/prompt-converters.js+0 -0
src/server-main.js+0 -0
src/server-startup.js+0 -0
src/util.js+0 -0
src/vectors/openai-vectors.js+0 -0
start.sh+0 -0
tests/.eslintrc.cjs+0 -0
tests/frontend/MacroEngine.e2e.js+0 -0
tests/frontend/MacroEnvBuilder.e2e.js+0 -0
tests/frontend/MacroLexer.e2e.js+0 -0
tests/frontend/MacroParser.e2e.js+0 -0
tests/frontend/MacroRegistry.e2e.js+0 -0
tests/frontend/MacrosParser.e2e.js+0 -0
tests/frontend/frontent-test-utils.js+0 -0
tests/jest-e2e.config.json+0 -0
tests/jest.setup.js+0 -0
tests/jsconfig.json+0 -0
tests/mock-server.test.js+0 -0
tests/package-lock.json+0 -0
tests/package.json+0 -0
tests/playwright.config.js+0 -0
tests/sample.e2e.js+0 -0
tests/util.test.js+0 -0
tests/util/mock-server.js+0 -0
Diff truncated