Merge pull request #4925 from SillyTavern/staging Staging

1c0e7ea9556afcc3ae55c888287dff67a6aa42fc

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

Signed
167 files changed, +4754 -1220Showing whitespace changes
.dockerignore+4 -0
@@ -15,3 +15,7 @@ access.log
1515/public/scripts/extensions/third-party
1616/colab
1717.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.**
7979* Portions of CncAnon's TavernAITurbo mod used with permission
8080* Visual Novel Mode inspired by the work of PepperTaco (<https://github.com/peppertaco/Tavern/>)
8181* Noto Sans font by Google (OFL license)
82+* Lexer/Parser by Chevrotain (Apache-2.0 license) <https://github.com/chevrotain/chevrotain>
8283* Icon theme by Font Awesome <https://fontawesome.com> (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
8384* Default content by @OtisAlejandro (Seraphina character and lorebook) and @kallmeflocc (10K Discord Users Celebratory Background)
8485* 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:
287287gemini:
288288 # API endpoint version ("v1beta" or "v1alpha")
289289 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'
290300# -- SERVER PLUGIN CONFIGURATION --
291301enableServerPlugins: false
292302# Attempt to automatically update server plugins on startup
default/content/presets/instruct/Mistral V7-Tekken.json+0 -0
default/content/presets/openai/Default.json+4 -3
@@ -8,6 +8,8 @@
88 "openrouter_sort_models": "alphabetically",
99 "ai21_model": "jamba-large",
1010 "mistralai_model": "mistral-large-latest",
11+ "chutes_model": "deepseek-ai/DeepSeek-V3-0324",
12+ "chutes_sort_models": "alphabetically",
1113 "electronhub_model": "gpt-4o-mini",
1214 "electronhub_sort_models": "alphabetically",
1315 "electronhub_group_models": false,
@@ -28,7 +30,6 @@
2830 "repetition_penalty": 1,
2931 "openai_max_context": 4095,
3032 "openai_max_tokens": 300,
31- "wrap_in_quotes": false,
3233 "names_behavior": 0,
3334 "send_if_empty": "",
3435 "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 @@
231232 "show_external_models": false,
232233 "assistant_prefill": "",
233234 "assistant_impersonation": "",
234235 "claude_use_syspromptuse_sysprompt": false,
235236 "squash_system_messages": false,
236237 "image_inliningmedia_inlining": falsetrue,
237238 "bypass_status_check": false,
238239 "continue_prefill": false,
239240 "continue_postfix": " ",
default/content/settings.json+4 -2
@@ -191,6 +191,7 @@
191191 "custom_stopping_strings_macro": true,
192192 "fuzzy_search": true,
193193 "encode_tags": false,
194+ "experimental_macro_engine": false,
194195 "enableLabMode": false,
195196 "enableZenSliders": false,
196197 "ui_mode": 1,
@@ -463,7 +464,6 @@
463464 "stream_openai": true,
464465 "openai_max_context": 4095,
465466 "openai_max_tokens": 300,
466- "wrap_in_quotes": false,
467467 "prompts": [
468468 {
469469 "name": "Main Prompt",
@@ -632,6 +632,8 @@
632632 "show_external_models": false,
633633 "proxy_password": "",
634634 "assistant_prefill": "",
635635 "assistant_impersonation": "",
636+ "use_sysprompt": false,
637+ "squash_system_messages": false
636638 }
637639}
package-lock.json+103 -15
@@ -1,12 +1,12 @@
11{
22 "name": "sillytavern",
33 "version": "1.1415.0",
44 "lockfileVersion": 3,
55 "requires": true,
66 "packages": {
77 "": {
88 "name": "sillytavern",
99 "version": "1.1415.0",
1010 "hasInstallScript": true,
1111 "license": "AGPL-3.0",
1212 "dependencies": {
@@ -45,6 +45,7 @@
4545 "bowser": "^2.12.1",
4646 "bytes": "^3.1.2",
4747 "chalk": "^5.6.0",
48+ "chevrotain": "^11.0.3",
4849 "command-exists": "^1.2.9",
4950 "compression": "^1.8.1",
5051 "cookie-parser": "^1.4.6",
@@ -59,7 +60,6 @@
5960 "express": "^4.21.0",
6061 "form-data": "^4.0.4",
6162 "fuse.js": "^7.1.0",
62- "google-translate-api-browser": "^3.0.1",
6363 "google-translate-api-x": "^10.7.2",
6464 "handlebars": "^4.7.8",
6565 "helmet": "^8.1.0",
@@ -73,7 +73,7 @@
7373 "is-docker": "^3.0.0",
7474 "localforage": "^1.10.0",
7575 "lodash": "^4.17.21",
7676 "mime-types": "^3.0.12",
7777 "moment": "^2.30.1",
7878 "morphdom": "^2.7.7",
7979 "multer": "^2.0.2",
@@ -106,6 +106,7 @@
106106 "sillytavern": "src/server-global.js"
107107 },
108108 "devDependencies": {
109+ "@chevrotain/types": "^11.0.3",
109110 "@types/archiver": "^6.0.3",
110111 "@types/bytes": "^3.1.5",
111112 "@types/command-exists": "^1.2.3",
@@ -134,7 +135,8 @@
134135 "@types/yauzl": "^2.10.3",
135136 "eslint": "^8.57.1",
136137 "eslint-plugin-jest": "^27.9.0",
137138 "eslint-plugin-jsdoc": "^48.10.0",
139+ "eslint-plugin-playwright": "^2.3.0"
138140 },
139141 "engines": {
140142 "node": ">= 18"
@@ -171,6 +173,45 @@
171173 "integrity": "sha512-KlmTftToTtmb6aLVdne4NluS+POWputPF5J8v25UN/EQS+K9vahWEIe1NPRSFqBQclObkqHaj7JOnFrmnSm5MA==",
172174 "license": "Apache-2.0"
173175 },
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+ },
174215 "node_modules/@es-joy/jsdoccomment": {
175216 "version": "0.46.0",
176217 "resolved": "https://registry.npmjs.org/@es-joy/jsdoccomment/-/jsdoccomment-0.46.0.tgz",
@@ -3381,6 +3422,20 @@
33813422 "url": "https://github.com/sponsors/fb55"
33823423 }
33833424 },
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+ },
33843439 "node_modules/chrome-trace-event": {
33853440 "version": "1.0.4",
33863441 "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz",
@@ -4539,6 +4594,35 @@
45394594 "dev": true,
45404595 "license": "MIT"
45414596 },
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+ },
45424626 "node_modules/eslint-scope": {
45434627 "version": "7.2.2",
45444628 "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz",
@@ -5360,12 +5444,6 @@
53605444 "url": "https://github.com/sponsors/sindresorhus"
53615445 }
53625446 },
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- },
53695447 "node_modules/google-translate-api-x": {
53705448 "version": "10.7.2",
53715449 "resolved": "https://registry.npmjs.org/google-translate-api-x/-/google-translate-api-x-10.7.2.tgz",
@@ -6285,6 +6363,12 @@
62856363 "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
62866364 "license": "MIT"
62876365 },
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+ },
62886372 "node_modules/lodash.get": {
62896373 "version": "4.4.2",
62906374 "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz",
@@ -6418,15 +6502,19 @@
64186502 }
64196503 },
64206504 "node_modules/mime-types": {
64216505 "version": "3.0.12",
64226506 "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.12.tgz",
64236507 "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFDLbgzdk0h4juoQ9fCKXW4by0UJqj+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWAnOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==",
64246508 "license": "MIT",
64256509 "dependencies": {
64266510 "mime-db": "^1.54.0"
64276511 },
64286512 "engines": {
64296513 "node": ">= 0.618"
6514+ },
6515+ "funding": {
6516+ "type": "opencollective",
6517+ "url": "https://opencollective.com/express"
64306518 }
64316519 },
64326520 "node_modules/mime-types/node_modules/mime-db": {
package.json+6 -4
@@ -35,6 +35,7 @@
3535 "bowser": "^2.12.1",
3636 "bytes": "^3.1.2",
3737 "chalk": "^5.6.0",
38+ "chevrotain": "^11.0.3",
3839 "command-exists": "^1.2.9",
3940 "compression": "^1.8.1",
4041 "cookie-parser": "^1.4.6",
@@ -49,7 +50,6 @@
4950 "express": "^4.21.0",
5051 "form-data": "^4.0.4",
5152 "fuse.js": "^7.1.0",
52- "google-translate-api-browser": "^3.0.1",
5353 "google-translate-api-x": "^10.7.2",
5454 "handlebars": "^4.7.8",
5555 "helmet": "^8.1.0",
@@ -63,7 +63,7 @@
6363 "is-docker": "^3.0.0",
6464 "localforage": "^1.10.0",
6565 "lodash": "^4.17.21",
6666 "mime-types": "^3.0.12",
6767 "moment": "^2.30.1",
6868 "morphdom": "^2.7.7",
6969 "multer": "^2.0.2",
@@ -113,7 +113,7 @@
113113 "type": "git",
114114 "url": "https://github.com/SillyTavern/SillyTavern.git"
115115 },
116116 "version": "1.1415.0",
117117 "scripts": {
118118 "start": "node server.js",
119119 "debug": "node --inspect server.js",
@@ -137,6 +137,7 @@
137137 },
138138 "main": "server.js",
139139 "devDependencies": {
140+ "@chevrotain/types": "^11.0.3",
140141 "@types/archiver": "^6.0.3",
141142 "@types/bytes": "^3.1.5",
142143 "@types/command-exists": "^1.2.3",
@@ -164,7 +165,8 @@
164165 "@types/yargs": "^17.0.33",
165166 "@types/yauzl": "^2.10.3",
166167 "eslint": "^8.57.1",
168+ "eslint-plugin-jest": "^27.9.0",
167169 "eslint-plugin-jsdoc": "^48.10.0",
168170 "eslint-plugin-jestplaywright": "^272.93.0"
169171 }
170172}
public/css/animations.css+20 -0
@@ -132,3 +132,23 @@
132132 overflow-y: hidden;
133133 }
134134}
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 @@
6363 display: flex;
6464 gap: 5px;
6565 width: 100%;
66+ align-items: baseline;
6667}
6768
6869/* Control buttons in header */
@@ -83,14 +84,6 @@
8384 gap: 5px;
8485}
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-
9487#bg_menu_content,
9588#bg_custom_content {
9689 display: grid;
@@ -300,3 +293,80 @@
300293.bg_example:focus-within .BGSampleTitle {
301294 opacity: 1;
302295}
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 @@
3333 display: flex;
3434 flex-direction: column;
3535 gap: 0.5em;
36+ padding-right: var(--mes-right-spacing);
3637}
3738
3839.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 @@
9797 max-width: 100dvw;
9898 }
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+
100109 #extensions_settings,
101110 #extensions_settings2 {
102111 width: 100% !important;
public/css/rm-groups.css+8 -0
@@ -213,6 +213,14 @@
213213}
214214
215215.group_select.avatar {
216+ padding: 0;
217+}
218+
219+.group_select.missing-avatar.inline_avatar {
220+ justify-content: center;
221+}
222+
223+.group_select .avatar {
216224 flex: 0;
217225}
218226
public/css/select2-overrides.css+5 -0
@@ -16,6 +16,11 @@
1616 user-select: none;
1717}
1818
19+.select2-container .select2-selection.select2-selection--single.select2-selection--clearable .select2-selection__clear {
20+ top: 0;
21+ right: 25px;
22+}
23+
1924.select2-container .select2-selection .select2-selection__clear {
2025 color: var(--SmartThemeBodyColor);
2126 font-size: 20px;
public/css/toggle-dependent.css+7 -5
@@ -477,14 +477,11 @@ body.expandMessageActions .mes .mes_buttons .extraMesButtonsHint {
477477 display: none !important;
478478}
479479
480480#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 {
481482 display: none;
482483}
483484
484-#smooth_streaming:checked~#smooth_streaming_speed_control {
485- display: block;
486-}
487-
488485.mdhotkey_icon {
489486 opacity: 0.6;
490487}
@@ -532,6 +529,7 @@ label[for="bind_preset_to_connection"]:has(input:checked) {
532529}
533530
534531#openai_settings input[type="checkbox"]:not(:checked)~[data-cc-toggle],
532+#openai_settings input[type="checkbox"]:not(:checked)~*:has([data-cc-toggle]),
535533#openai_settings [data-cc-toggle="false"]>.icon-supported,
536534#openai_settings [data-cc-toggle="true"]>.icon-unsupported {
537535 display: none;
@@ -562,3 +560,7 @@ label[for="bind_preset_to_connection"]:has(input:checked) {
562560 filter: grayscale(0.5);
563561 cursor: not-allowed;
564562}
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';
66import { textgenerationwebui_settings } from './scripts/textgen-settings';
77import { FileAttachment } from './scripts/chats';
88import { ReasoningMessageExtra } from './scripts/reasoning';
9+import { IGNORE_SYMBOL, OVERSWIPE_BEHAVIOR } from './scripts/constants';
10+import { ToolInvocation } from './scripts/tool-calling';
11+import { getWorldInfoSettings } from './scripts/world-info';
912
1013declare global {
1114 // Custom types
@@ -14,7 +17,49 @@ declare global {
1417 type ReasoningSettings = typeof power_user.reasoning;
1518 type ChatCompletionSettings = typeof oai_settings;
1619 type TextCompletionSettings = typeof textgenerationwebui_settings;
20+ type WorldInfoSettings = ReturnType<typeof getWorldInfoSettings>;
1721 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
1964 interface ChatMessage {
2065 name?: string;
@@ -28,21 +73,35 @@ declare global {
2873 force_avatar?: string;
2974 original_avatar?: string;
3075 swipes?: string[];
3176 swipe_info?: Record<string, any>SwipeInfo[];
3277 swipe_id?: number;
33- extra?: ChatMessageExtra & Partial<ReasoningMessageExtra> & Record<string, any>;
78+ extra?: ChatMessageExtra;
3479 };
3580
3681 interface ChatMessageExtraSwipeInfo {
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;
3793 bias?: string;
3894 uses_system_ui?: boolean;
3995 memory?: string;
4096 display_text?: string;
4197 reasoning_display_text?: string;
4298 tool_invocations?: anyToolInvocation[];
4399 title?: string;
44100 isSmallSys?: boolean;
45101 token_count?: number;
102+ /** When false, the message cannot be swiped. */
103+ swipeable?: boolean;
104+ overswipe_behavior?: OVERSWIPE_BEHAVIOR;
46105 files?: FileAttachment[];
47106 inline_image?: boolean;
48107 media_display?: string;
@@ -62,6 +121,8 @@ declare global {
62121 generationType?: number;
63122 /** @deprecated Use `MediaAttachment.negative` instead */
64123 negative?: string;
124+ /** Will exclude this message from prompt processing */
125+ [IGNORE_SYMBOL]?: boolean;
65126 }
66127
67128 type MediaAttachment = MediaAttachmentProps & ImageGenerationAttachmentProps & ImageCaptionAttachmentProps;
@@ -93,6 +154,12 @@ declare global {
93154 paused: boolean;
94155 }
95156
157+ interface ChatCompletionMessage {
158+ name?: string;
159+ role: string;
160+ content: string;
161+ }
162+
96163 // Global namespace modules
97164 interface Window {
98165 ai: any;
@@ -165,4 +232,6 @@ declare global {
165232 rgba: string;
166233 }
167234 };
235+
236+ type SwipeEvent = JQuery.TriggeredEvent<any, any, HTMLElement, HTMLElement>;
168237}
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+219 -121
@@ -42,6 +42,7 @@
4242 <link rel="stylesheet" type="text/css" href="css/extensions-panel.css">
4343 <link rel="stylesheet" type="text/css" href="css/select2-overrides.css">
4444 <link rel="stylesheet" type="text/css" href="css/mobile-styles.css">
45+ <link rel="stylesheet" type="text/css" href="css/macros.css">
4546 <link rel="stylesheet" type="text/css" href="css/user.css">
4647 <link rel="icon" type="image/x-icon" href="favicon.ico">
4748 <!-- Scripts are loaded at the end of the body to improve page load speed -->
@@ -678,6 +679,9 @@
678679 <div data-source="electronhub">
679680 <span data-i18n="Max prompt cost:">Max prompt cost:</span> <span id="electronhub_max_prompt_cost" data-i18n="Unknown">Unknown</span>
680681 </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>
681685 <hr>
682686 <div class="range-block">
683687 <label for="stream_toggle" title="Enable OpenAI completion streaming" data-i18n="[title]Enable OpenAI completion streaming" class="checkbox_label widthFreeExpand">
@@ -693,7 +697,7 @@
693697 </span>
694698 </div>
695699 </div>
696700 <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">
697701 <div class="range-block-title" data-i18n="Temperature">
698702 Temperature
699703 </div>
@@ -706,7 +710,7 @@
706710 </div>
707711 </div>
708712 </div>
709713 <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">
710714 <div class="range-block-title" data-i18n="Frequency Penalty">
711715 Frequency Penalty
712716 </div>
@@ -719,7 +723,7 @@
719723 </div>
720724 </div>
721725 </div>
722726 <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">
723727 <div class="range-block-title" data-i18n="Presence Penalty">
724728 Presence Penalty
725729 </div>
@@ -732,7 +736,7 @@
732736 </div>
733737 </div>
734738 </div>
735739 <div class="range-block" data-source="claude,aimlapi,openrouter,makersuite,vertexai,cohere,perplexity,electronhub,chutes,nanogpt">
736740 <div class="range-block-title" data-i18n="Top K">
737741 Top K
738742 </div>
@@ -745,7 +749,7 @@
745749 </div>
746750 </div>
747751 </div>
748752 <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">
749753 <div class="range-block-title" data-i18n="Top P">
750754 Top P
751755 </div>
@@ -758,7 +762,7 @@
758762 </div>
759763 </div>
760764 </div>
761765 <div class="range-block" data-source="openrouter,nanogpt,chutes">
762766 <div class="range-block-title" data-i18n="Repetition Penalty">
763767 Repetition Penalty
764768 </div>
@@ -771,7 +775,7 @@
771775 </div>
772776 </div>
773777 </div>
774778 <div class="range-block" data-source="openrouter,nanogpt,chutes">
775779 <div class="range-block-title" data-i18n="Min P">
776780 Min P
777781 </div>
@@ -784,7 +788,7 @@
784788 </div>
785789 </div>
786790 </div>
787791 <div class="range-block" data-source="openrouter,nanogpt">
788792 <div class="range-block-title" data-i18n="Top A">
789793 Top A
790794 </div>
@@ -982,7 +986,7 @@
982986 </div>
983987 </div>
984988 </div>
985989 <div class="range-block" data-source="openai,openrouter,mistralai,custom,cohere,groq,electronhub,chutes,nanogpt,xai,pollinations,aimlapi,makersuite,vertexai,azure_openai">
986990 <div class="range-block-title justifyLeft" data-i18n="Seed">
987991 Seed
988992 </div>
@@ -1123,7 +1127,7 @@
11231127 <div id="grammar_block" class="wide100p">
11241128 <hr class="wide100p">
11251129 <h4 class="wide100p textAlignCenter"><span data-i18n="GBNF Grammar">GBNF Grammar</span>
11261130 <a href="https://github.com/ggerganovggml-org/llama.cpp/blob/master/grammars/README.md" target="_blank">
11271131 <small>
11281132 <div class="fa-solid fa-circle-question note-link-span"></div>
11291133 </small>
@@ -1246,7 +1250,7 @@
12461250 <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>
12471251 </small>
12481252 </div>
12491253 <div data-tg-type="mancer, vllm, aphrodite, tabby, infermaticai, llamacpp" data-tg-samplers="n" class="flex-container flexFlowColumn alignitemscenter flexBasis100p flexGrow flexShrink gap0">
12501254 <small data-i18n="Multiple swipes per generation">Multiple swipes per generation</small>
12511255 <input type="number" id="n_textgenerationwebui" class="text_pole textAlignCenter" min="1" value="1" step="1" />
12521256 </div>
@@ -1690,7 +1694,7 @@
16901694 <div class="flex-container justifyCenter alignitemscenter">
16911695 <span data-i18n="Grammar String">Grammar String</span>
16921696 <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>
16931697 <a href="https://github.com/ggerganovggml-org/llama.cpp/blob/master/grammars/README.md" target="_blank">
16941698 <div class="fa-solid fa-up-right-from-square note-link-span"></div>
16951699 </a>
16961700 </div>
@@ -1920,18 +1924,6 @@
19201924 </div>
19211925 </div>
19221926 <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">
19351927 <label for="continue_prefill" class="checkbox_label widthFreeExpand">
19361928 <input id="continue_prefill" type="checkbox" />
19371929 <span data-i18n="Continue prefill">Continue prefill</span>
@@ -1955,7 +1947,20 @@
19551947 </span>
19561948 </div>
19571949 </div>
19581950 <div class="range-block" data-source="makersuite,vertexai,aimlapi,openrouter,claude,xai,electronhubmakersuite,nanogptvertexai">
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">
19591964 <label for="openai_enable_web_search" class="checkbox_label flexWrap widthFreeExpand">
19601965 <input id="openai_enable_web_search" type="checkbox" />
19611966 <span data-i18n="Enable web search">Enable web search</span>
@@ -1969,7 +1974,7 @@
19691974 </b>
19701975 </div>
19711976 </div>
19721977 <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">
19731978 <label for="openai_function_calling" class="checkbox_label flexWrap widthFreeExpand">
19741979 <input id="openai_function_calling" type="checkbox" />
19751980 <span data-i18n="Enable function calling">Enable function calling</span>
@@ -1984,19 +1989,31 @@
19841989 <strong data-i18n="enable_functions_desc_4">Not supported when Prompt Post-Processing with "no tools" is used!</strong>
19851990 </div>
19861991 </div>
19871992 <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">
19881993 <label for="openai_image_inliningopenai_media_inlining" class="checkbox_label flexWrap widthFreeExpand">
19891994 <input id="openai_image_inliningopenai_media_inlining" type="checkbox" />
19901995 <span data-i18n="Send inline imagesmedia">Send inline imagesmedia</span>
19911996 <div idclass="openai_image_inlining_supported" data-ccflex-toggle="falsecontainer">
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">
19931998 <i class="icon-unsupportedsupported fa-solid fa-circle-exclamationimage" title="UnsupportedSupported by the current model" data-i18n="[title]UnsupportedSupported 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>
19942009 </div>
19952010 </label>
19962011 <div id="image_inlining_hint" class="flexBasis100p toggle-description justifyLeft">
19972012 <span data-i18n="image_inlining_hint_1">Sends imagesattached media in prompts if thesupported modelby supportsthe itmodel.</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>
19982015 </div>
19992016 <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">
20002017 <div class="flex-container oneline-dropdown">
20012018 <label for="openai_inline_image_quality" data-i18n="Inline Image Quality">
20022019 Inline Image Quality
@@ -2009,40 +2026,12 @@
20092026 </div>
20102027 </div>
20112028 </div>
20122029 <div id="request_images_block" class="range-block" data-source="makersuite,vertexai,openrouter">
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">
20412030 <label for="openai_request_images" class="checkbox_label widthFreeExpand">
20422031 <input id="openai_request_images" type="checkbox" />
20432032 <span>
20442033 <span data-i18n="Request inline images">Request inline images</span>
20452034 <i class="opacity50p fa-solid fa-circle-info" title="Gemini 2.05 Flash ExperimentalImage / Gemini 3 Pro Image"></i>
20462035 </span>
20472036 </label>
20482037 <div class="toggle-description justifyLeft marginBot5">
@@ -2053,22 +2042,39 @@
20532042 Incompatible with the following features: function calling, web search, system prompt.
20542043 </em>
20552044 </div>
2045+ <div id="request_images_settings" class="flex-container wide100p textAlignCenter">
2046+ <div class="flex1">
2047+ <label for="request_image_resolution">
2048+ <small data-i18n="Resolution">Resolution</small>
2049+ </label>
2050+ <select id="request_image_resolution" class="text_pole">
2051+ <option value="">Auto</option>
2052+ <option value="1K">1K</option>
2053+ <option value="2K">2K</option>
2054+ <option value="4K">4K</option>
2055+ </select>
2056+ </div>
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>
20562074 </div>
2057- <div class="range-block" data-source="makersuite,vertexai">
2058- <label for="use_makersuite_sysprompt" class="checkbox_label widthFreeExpand">
2059- <input id="use_makersuite_sysprompt" type="checkbox" />
2060- <span>
2061- <span data-i18n="Use system prompt">Use system prompt</span>
2062- <i class="opacity50p fa-solid fa-circle-info" title="Gemini 1.5+, LearnLM"></i>
2063- </span>
2064- </label>
2065- <div class="toggle-description justifyLeft marginBot5">
2066- <span data-i18n="Merges_all_system_messages_desc_1">
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.
2068- </span>
20692075 </div>
20702076 </div>
20712077 <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">
20722078 <label for="openai_show_thoughts" class="checkbox_label widthFreeExpand">
20732079 <input id="openai_show_thoughts" type="checkbox" />
20742080 <span data-i18n="Request model reasoning">Request model reasoning</span>
@@ -2082,7 +2088,7 @@
20822088 </strong>
20832089 </div>
20842090 </div>
20852091 <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">
20862092 <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.">
20872093 <label for="openai_reasoning_effort">
20882094 <span data-i18n="Reasoning Effort">Reasoning Effort</span>
@@ -2096,14 +2102,36 @@
20962102 <option data-i18n="openai_reasoning_effort_high" value="high">High</option>
20972103 <option data-i18n="openai_reasoning_effort_maximum" value="max">Maximum</option>
20982104 </select>
20992105 <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.">
21002106 OpenAI-style options: low, medium, high. Minimum and maximum are aliased to low and high. Auto does not send an effort level.
21012107 </div>
21022108 <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.">
21032109 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.
21042110 </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">
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.">
21062117 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.
21072135 </div>
21082136 </div>
21092137 </div>
@@ -2124,18 +2152,9 @@
21242152 <i class="fa-solid fa-circle-info"></i>
21252153 <span>Prefills won't work when function calling is enabled and any tools are registered.</span>
21262154 </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>
21362155 </div>
21372156 </div>
21382157 <div class="range-block m-t-1" data-source="openai,aimlapi,openrouter,custom,electronhub,azure_openai,chutes">
21392158 <div id="logit_bias_openai" class="range-block-title openai_restorable" data-i18n="Logit Bias">
21402159 Logit Bias
21412160 </div>
@@ -2642,8 +2661,8 @@
26422661 </div>
26432662 <div data-tg-type="llamacpp">
26442663 <div class="flex-container flexFlowColumn">
26452664 <a href="https://github.com/ggerganovggml-org/llama.cpp/blob/master/README.md#llama-server" target="_blank" data-i18n="ggerganovggml-org/llama.cpp">
26462665 ggerganovggml-org/llama.cpp (inference server)
26472666 </a>
26482667 </div>
26492668 <h4 data-i18n="API key (optional)">API key (optional)</h4>
@@ -2660,6 +2679,16 @@
26602679 <small data-i18n="Example: http://127.0.0.1:8080">Example: http://127.0.0.1:8080</small>
26612680 <input id="llamacpp_api_url_text" class="text_pole wide100p" value="" autocomplete="off" data-server-history="llamacpp">
26622681 </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>
26632692 </div>
26642693 <div data-tg-type="ollama">
26652694 <div class="flex-container flexFlowColumn">
@@ -2794,6 +2823,7 @@
27942823 <option value="ai21">AI21</option>
27952824 <option value="aimlapi">AI/ML API</option>
27962825 <option value="azure_openai">Azure OpenAI</option>
2826+ <option value="chutes">Chutes</option>
27972827 <option value="claude">Claude</option>
27982828 <option value="cohere">Cohere</option>
27992829 <!-- Temporarily disabled. -->
@@ -2929,6 +2959,11 @@
29292959 <div>
29302960 <h4 data-i18n="OpenAI Model">OpenAI Model</h4>
29312961 <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>
29322967 <optgroup label="GPT-5.1">
29332968 <option value="gpt-5.1">gpt-5.1</option>
29342969 <option value="gpt-5.1-2025-11-13">gpt-5.1-2025-11-13</option>
@@ -3033,6 +3068,8 @@
30333068 <h4 data-i18n="Claude Model">Claude Model</h4>
30343069 <select id="model_claude_select">
30353070 <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>
30363073 <option value="claude-sonnet-4-5">claude-sonnet-4-5</option>
30373074 <option value="claude-sonnet-4-5-20250929">claude-sonnet-4-5-20250929</option>
30383075 <option value="claude-haiku-4-5">claude-haiku-4-5</option>
@@ -3174,6 +3211,7 @@
31743211 <optgroup label="Gemini 3.0">
31753212 <option value="gemini-3-pro-preview">gemini-3-pro-preview</option>
31763213 <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>
31773215 </optgroup>
31783216 <optgroup label="Gemini 2.5">
31793217 <option value="gemini-2.5-pro">gemini-2.5-pro</option>
@@ -3346,6 +3384,7 @@
33463384 <optgroup label="Gemini 3.0">
33473385 <option value="gemini-3-pro-preview">gemini-3-pro-preview</option>
33483386 <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>
33493388 </optgroup>
33503389 <optgroup label="Gemini 2.5">
33513390 <option value="gemini-2.5-pro">gemini-2.5-pro</option>
@@ -3468,6 +3507,45 @@
34683507 </div>
34693508 </div>
34703509 </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>
34713549 <div id="nanogpt_form" data-source="nanogpt">
34723550 <h4 data-i18n="NanoGPT API Key">NanoGPT API Key</h4>
34733551 <div class="flex-container">
@@ -3733,7 +3811,11 @@
37333811 </select>
37343812 <h4 data-i18n="Z.AI Model">Z.AI Model</h4>
37353813 <select id="model_zai_select">
3814+ <option value="glm-4.7">glm-4.7</option>
37363815 <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>
37373819 <option value="glm-4.5v">glm-4.5v</option>
37383820 <option value="glm-4.5">glm-4.5</option>
37393821 <option value="glm-4.5-air">glm-4.5-air</option>
@@ -3741,6 +3823,7 @@
37413823 <option value="glm-4.5-airx">glm-4.5-airx</option>
37423824 <option value="glm-4.5-flash">glm-4.5-flash</option>
37433825 <option value="glm-4-32b-0414-128k">glm-4-32b-0414-128k</option>
3826+ <option value="autoglm-phone-multilingual">autoglm-phone-multilingual</option>
37443827 </select>
37453828 </div>
37463829 <div id="azure_openai_settings" data-source="azure_openai">
@@ -4697,7 +4780,7 @@
46974780 <div name="themeElements" class="flex-container flexFlowColumn flexNoGap">
46984781 <!-- <h4><span data-i18n="UI Colors">Theme Settings</span></h4> -->
46994782 <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.">
47014784 <span data-i18n="Avatar Style:">Avatars:</span>
47024785 <select id="avatar_style" class="widthNatural flex1 margin0 text_pole">
47034786 <option value="0" data-i18n="Circle">Circle</option>
@@ -4873,7 +4956,7 @@
48734956 <input id="mesIDDisplayEnabled" type="checkbox" />
48744957 <small data-i18n="Message IDs">Message IDs</small>
48754958 </label>
48764959 <label for="hideChatAvatarsEnabled" class="checkbox_label" title="Hide avatars, only in chat messages." data-i18n="[title]Hide avatars, only in chat messages.">
48774960 <input id="hideChatAvatarsEnabled" type="checkbox" />
48784961 <small data-i18n="Hide Chat Avatars">Hide Chat Avatars</small>
48794962 </label>
@@ -4975,14 +5058,21 @@
49755058 <small data-i18n="Clean-Up">Clean-Up</small>
49765059 </div>
49775060 </div>
49785061 <label id="smooth_streaming_control" class="checkbox_label flexWrap" for="smooth_streaming">
49795062 <input id="smooth_streaming" type="checkbox" />
49805063 <div class="flex-container alignItemsBaseline">
49815064 <small data-i18n="Smooth Streaming">
49825065 Smooth Streaming
49835066 </small>
49845067 </div>
4985- <div id="smooth_streaming_speed_control" class="flexBasis100p wide100p">
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">
49865076 <input type="range" id="smooth_streaming_speed" name="smooth_streaming_speed" min="0" max="100" step="10" value="50">
49875077 <div class="slider_hint">
49885078 <span data-i18n="Slow">Slow</span>
@@ -4990,7 +5080,6 @@
49905080 <span data-i18n="Fast">Fast</span>
49915081 </div>
49925082 </div>
4993- </label>
49945083 <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">
49955084 <input id="stream_fade_in" type="checkbox" />
49965085 <small data-i18n="Stream Fade-In">Stream Fade-In</small>
@@ -5191,6 +5280,11 @@
51915280 <input id="encode_tags" type="checkbox" />
51925281 <small data-i18n="Show tags in responses">Show &lt;tags&gt; in responses</small>
51935282 </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>
51945288 <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">
51955289 <input id="disable_group_trimming" type="checkbox" />
51965290 <small data-i18n="Relax message trim in Groups">Relax message trim in Groups</small>
@@ -5383,10 +5477,9 @@
53835477 <div id="Backgrounds" class="drawer-content closedDrawer bg-drawer-layout">
53845478 <div id="bg-header-fixed">
53855479 <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">
53875481 <ispan classdata-i18n="fa-solid fa-plusBackgrounds">Backgrounds</ispan>
5388- <span data-i18n="Add Background">Add Background</span>
5482+ </h3>
5389- </label>
53905483 <span class="expander"></span>
53915484 <select id="background_fitting" class="text_pole" data-i18n="[title]Background Fitting" title="Background Fitting">
53925485 <option value="classic" data-i18n="Classic">Classic</option>
@@ -5399,16 +5492,23 @@
53995492 <i class="fa-solid fa-wand-magic"></i>
54005493 <span data-i18n="Auto-select">Auto-select</span>
54015494 </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>
54025499 </div>
54035500 <div class="bg-header-row-2">
54045501 <input id="bg-filter" class="text_pole" type="search" data-i18n="[placeholder]Search..." placeholder="Search..." />
54055502 </div>
54065503 </div>
54075504 <div id="bgbg_tabs" class="heading-scrollablecontainer-contentwith-controls">
54085505 <divul class="heading-container-with-controlsbg_tabs_list">
5409- <h3 data-i18n="System Backgrounds" class="wide100p textAlignCenter heading-text">
5506+ <li class="bg_tab_button">
5410- System Backgrounds
5507+ <a href="#bg_global_tab" data-i18n="Global">Global</a>
54115508 </h3li>
5509+ <li class="bg_tab_button">
5510+ <a href="#bg_chat_tab" data-i18n="Chat">Chat</a>
5511+ </li>
54125512 <div class="heading-controls">
54135513 <button id="bg_thumb_zoom_out" class="menu_button menu_button_icon" title="Make thumbnails smaller" data-i18n="[title]Make thumbnails smaller">
54145514 <i class="fa-solid fa-minus"></i>
@@ -5417,20 +5517,19 @@
54175517 <i class="fa-solid fa-plus"></i>
54185518 </button>
54195519 </div>
5520+ </ul>
5521+ <div id="bg_global_tab" class="bg_tab_panel">
5522+ <div id="bg_menu_content" class="bg_list"></div>
54205523 </div>
54215524 <div id="bg_menu_contentbg_chat_tab" class="bg_listbg_tab_panel">
5422- </div>
5525+ <div id="bg_custom_content" class="bg_list"></div>
5423- <h3 data-i18n="Chat Backgrounds" class="wide100p textAlignCenter">
5424- Chat Backgrounds
5425- </h3>
54265526 <div id="bg_chat_hint" class="wide100p textAlignCenter">
54275527 <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>
54285528 </div>
5429- <div id="bg_custom_content" class="bg_list">
54305529 </div>
54315530 </div>
54325531 <form id="form_bg_upload" style="display: none;"hidden>
54335532 <input type="file" id="add_bg_button" name="avatar" accept="image/jpeg,image/png,image/gif,image/bmp,image/svg+xml*,video/*" multiple hidden>
54345533 </form>
54355534 <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">
54365535 <i class="fa-solid fa-chevron-up" aria-hidden="true"></i>
@@ -6379,7 +6478,7 @@
63796478 <div class="jg-menu">
63806479 <div data-action="lock" class="jg-button jg-lock fa-solid fa-lock fa-fw pointer" data-i18n="[title]Lock" title="Lock"></div>
63816480 <div data-action="unlock" class="jg-button jg-unlock fa-solid fa-lock-open fa-fw pointer" data-i18n="[title]Unlock" title="Unlock"></div>
63826481 <div data-action="copy" class="jg-button jg-copy fa-solid fa-file-arrow-up" data-i18n="[title]Copy to systemglobal backgrounds" title="Copy to systemglobal backgrounds"></div>
63836482 <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>
63846483 <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>
63856484 </div>
@@ -7047,7 +7146,7 @@
70477146 <div class="mes_timer"></div>
70487147 <div class="tokenCounterDisplay"></div>
70497148 </div>
70507149 <div class="swipe_left fa-solid fa-chevron-left" style="display: none;"></div>
70517150 <div class="mes_block">
70527151 <div class="ch_name flex-container justifySpaceBetween">
70537152 <div class="flex-container flex1 alignitemscenter">
@@ -7111,7 +7210,7 @@
71117210 <div class="mes_bias"></div>
71127211 </div>
71137212 <div class="flex-container swipeRightBlock flexFlowColumn flexNoGap">
71147213 <div class="swipe_right fa-solid fa-chevron-right" style="display: none;"></div>
71157214 <div class="swipes-counter"></div>
71167215 </div>
71177216 </div>
@@ -7210,9 +7309,8 @@
72107309 <div title="Move down" data-action="down" class="right_menu_button fa-solid fa-chevron-down" data-i18n="[title]Move down"></div>
72117310 </div>
72127311 <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>
72137312 <div title="Remove from group" data-action="remove" class="right_menu_button fa-solid fa-2xlxl 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>
72167314 </div>
72177315 </div>
72187316 </div>
public/jsconfig.json+7 -1
@@ -6,7 +6,13 @@
66 "moduleResolution": "node",
77 "allowUmdGlobalAccess": true,
88 "allowSyntheticDefaultImports": true,
99 "strictBindCallApply": true,
10+ "baseUrl": ".",
11+ "paths": {
12+ "/*": [
13+ "./*"
14+ ]
15+ }
1016 },
1117 "exclude": [
1218 "**/node_modules/**",
public/lib.js+3 -0
@@ -22,6 +22,7 @@ import morphdom from 'morphdom';
2222import { toggle as slideToggle } from 'slidetoggle';
2323import chalk from 'chalk';
2424import yaml from 'yaml';
25+import * as chevrotain from 'chevrotain';
2526
2627/**
2728 * Expose the libraries to the 'window' object.
@@ -100,6 +101,7 @@ export default {
100101 slideToggle,
101102 chalk,
102103 yaml,
104+ chevrotain,
103105};
104106
105107export {
@@ -124,4 +126,5 @@ export {
124126 slideToggle,
125127 chalk,
126128 yaml,
129+ chevrotain,
127130};
public/locales/ar-sa.json+6 -6
@@ -235,7 +235,7 @@
235235 "Combines consecutive system messages into one (excluding example dialogues). May improve coherence for some models.": "يجمع الرسائل المتتالية للنظام في رسالة واحدة (باستثناء الحوارات المثالية). قد يحسن التتابع لبعض النماذج.",
236236 "Enable function calling": "تمكين استدعاء الوظيفة",
237237 "Send inline images": "إرسال الصور المضمنة",
238238 "image_inlining_hint_1": "يرسل الصورملفات الوسائط في المطالبات إذا كان النموذج يدعمها .",
239239 "image_inlining_hint_2": "الإجراء على أي رسالة أو",
240240 "image_inlining_hint_3": "القائمة لإرفاق ملف صورة للدردشة.",
241241 "Inline Image Quality": "جودة الصورة المضمنة",
@@ -328,7 +328,7 @@
328328 "PygmalionAI/aphrodite-engine": "PygmalionAI/aphrodite-engine (وضع التغليف لواجهة برمجة التطبيقات OpenAI)",
329329 "Aphrodite API key": "مفتاح واجهة برمجة التطبيقات Aphrodite",
330330 "Aphrodite Model": "نموذج أفروديت",
331331 "ggerganovggml-org/llama.cpp": "ggerganovggml-org/llama.cpp (خادم إخراج)",
332332 "Example: http://127.0.0.1:8080": "مثال: http://127.0.0.1:8080",
333333 "Example: http://127.0.0.1:11434": "مثال: http://127.0.0.1:11434",
334334 "Ollama Model": "نموذج Ollama",
@@ -605,7 +605,7 @@
605605 "Model Icon": "أيقونة النموذج",
606606 "Show sequential message numbers in the chat log": "إظهار أرقام الرسائل التسلسلية في سجل الدردشة",
607607 "Message IDs": "معرفات الرسائل",
608608 "Hide avatars, only in chat messages.": "إخفاء الصور الرمزية في رسائل الدردشة.",
609609 "Hide Chat Avatars": "إخفاء الصور الرمزية للدردشة",
610610 "Show the number of tokens in each message in the chat log": "عرض عدد الرموز في كل رسالة في سجل الدردشة",
611611 "Show Message Token Count": "عرض عدد الرموز في الرسالة",
@@ -739,8 +739,8 @@
739739 "Filter": "تصفية",
740740 "Automatically select a background based on the chat context": "تحديد خلفية تلقائيًا استنادًا إلى سياق الدردشة",
741741 "Auto-select": "التحديد التلقائي",
742742 "System BackgroundsGlobal": "خلفيات النظامعالمي",
743743 "Chat Backgrounds": "خلفيات الدردشة",
744744 "bg_chat_hint_1": "خلفيات الدردشة التي تم إنشاؤها باستخدام",
745745 "bg_chat_hint_2": "سوف يظهر الامتداد هنا",
746746 "Extensions": "امتدادات",
@@ -913,7 +913,7 @@
913913 "Save": "حفظ",
914914 "Chat History": "تاريخ الدردشة",
915915 "Import Chat": "استيراد الدردشة",
916916 "Copy to systemglobal backgrounds": "نسخ إلى خلفياتالخلفيات النظامالعالمية",
917917 "Rename background": "إعادة تسمية الخلفية",
918918 "Lock": "قفل",
919919 "Unlock": "الغاء القفل",
public/locales/de-de.json+6 -6
@@ -235,7 +235,7 @@
235235 "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.",
236236 "Enable function calling": "Funktionsaufruf aktivieren",
237237 "Send inline images": "Inline-Bilder senden",
238238 "image_inlining_hint_1": "Sendet BilderMediendateien in Eingabeaufforderungen, wenn das Modell dies unterstützt.",
239239 "image_inlining_hint_2": "Aktion auf eine Nachricht oder die",
240240 "image_inlining_hint_3": "Menü, um eine Bilddatei an den Chat anzuhängen.",
241241 "Inline Image Quality": "Inline-Bildqualität",
@@ -328,7 +328,7 @@
328328 "PygmalionAI/aphrodite-engine": "PygmalionAI/aphrodite-engine (Wrappermodus für OpenAI API)",
329329 "Aphrodite API key": "Aphrodite API-Schlüssel",
330330 "Aphrodite Model": "Aphrodite-Modell",
331331 "ggerganovggml-org/llama.cpp": "ggerganovggml-org/llama.cpp (Output-Server)",
332332 "Example: http://127.0.0.1:8080": "Beispiel: http://127.0.0.1:8080",
333333 "Example: http://127.0.0.1:11434": "Beispiel: http://127.0.0.1:11434",
334334 "Ollama Model": "Ollama-Modell",
@@ -605,7 +605,7 @@
605605 "Model Icon": "Modell-Icon",
606606 "Show sequential message numbers in the chat log": "Zeige aufeinanderfolgende Nachrichtennummern im Chat-Log",
607607 "Message IDs": "Nachrichten-IDs",
608608 "Hide avatars, only in chat messages.": "Avatare in Chatnachrichten verbergen.",
609609 "Hide Chat Avatars": "Chat-Avatare ausblenden",
610610 "Show the number of tokens in each message in the chat log": "Zeige die Anzahl der Tokens in jeder Nachricht im Chat-Log",
611611 "Show Message Token Count": "Anzahl der Nachrichten-Token anzeigen",
@@ -739,8 +739,8 @@
739739 "Filter": "Filter",
740740 "Automatically select a background based on the chat context": "Automatisch einen Hintergrund basierend auf dem Chatkontext auswählen",
741741 "Auto-select": "Automatische Auswahl",
742742 "System BackgroundsGlobal": "SystemhintergründeGlobal",
743743 "Chat Backgrounds": "Chat-Hintergründe",
744744 "bg_chat_hint_1": "Chat-Hintergründe generiert mit dem",
745745 "bg_chat_hint_2": "Die Erweiterung wird hier angezeigt.",
746746 "Extensions": "Erweiterungen",
@@ -913,7 +913,7 @@
913913 "Save": "Speichern",
914914 "Chat History": "Chat-Verlauf",
915915 "Import Chat": "Chat importieren",
916916 "Copy to systemglobal backgrounds": "In Systemhintergründeglobale Hintergründe kopieren",
917917 "Rename background": "Hintergrund umbenennen",
918918 "Lock": "Sperren",
919919 "Unlock": "Freischalten",
public/locales/es-es.json+6 -6
@@ -235,7 +235,7 @@
235235 "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.",
236236 "Enable function calling": "Habilitar llamada a función",
237237 "Send inline images": "Enviar imágenes en línea",
238238 "image_inlining_hint_1": "Envía imágenesarchivos multimedia en mensajes si el modelo lo admite.",
239239 "image_inlining_hint_2": "acción sobre cualquier mensaje o el",
240240 "image_inlining_hint_3": "menú para adjuntar un archivo de imagen al chat.",
241241 "Inline Image Quality": "Calidad de imagen en línea",
@@ -328,7 +328,7 @@
328328 "PygmalionAI/aphrodite-engine": "PygmalionAI/aphrodite-engine (Modo envolvente para API de OpenAI)",
329329 "Aphrodite API key": "Clave de API de Aphrodite",
330330 "Aphrodite Model": "Modelo Afrodita",
331331 "ggerganovggml-org/llama.cpp": "ggerganovggml-org/llama.cpp (Servidor de salida)",
332332 "Example: http://127.0.0.1:8080": "Ejemplo: http://127.0.0.1:8080",
333333 "Example: http://127.0.0.1:11434": "Ejemplo: http://127.0.0.1:11434",
334334 "Ollama Model": "Modelo Ollama",
@@ -605,7 +605,7 @@
605605 "Model Icon": "Ícono del Modelo",
606606 "Show sequential message numbers in the chat log": "Mostrar números de mensaje secuenciales en el registro de chat",
607607 "Message IDs": "IDs de Mensaje",
608608 "Hide avatars, only in chat messages.": "Ocultar avatares en los mensajes de chat.",
609609 "Hide Chat Avatars": "Ocultar avatares de chat",
610610 "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",
611611 "Show Message Token Count": "Mostrar Conteo de Tokens en Mensaje",
@@ -739,8 +739,8 @@
739739 "Filter": "Filtro",
740740 "Automatically select a background based on the chat context": "Seleccionar automáticamente un fondo basado en el contexto del chat",
741741 "Auto-select": "Auto-seleccionar",
742742 "System BackgroundsGlobal": "Fondos del SistemaGlobal",
743743 "Chat Backgrounds": "Fondos de Chat",
744744 "bg_chat_hint_1": "Fondos de chat generados con el",
745745 "bg_chat_hint_2": "La extensión aparecerá aquí.",
746746 "Extensions": "Extensiones",
@@ -913,7 +913,7 @@
913913 "Save": "Guardar",
914914 "Chat History": "Historial de chat",
915915 "Import Chat": "Importar chat",
916916 "Copy to systemglobal backgrounds": "Copiar a fondos del sistemaglobales",
917917 "Rename background": "Cambiar nombre de fondo",
918918 "Lock": "Cerrar",
919919 "Unlock": "desbloquear",
public/locales/fr-fr.json+6 -6
@@ -227,7 +227,7 @@
227227 "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.",
228228 "Enable function calling": "Activer l'appel de fonction",
229229 "Send inline images": "Envoyer des images en ligne",
230230 "image_inlining_hint_1": "Envoie des imagesfichiers multimédias dans les prompts si le modèle le prend en charge.",
231231 "image_inlining_hint_2": "action sur n'importe quel message ou le",
232232 "image_inlining_hint_3": "menu pour joindre un fichier image au chat.",
233233 "Inline Image Quality": "Qualité d'image en ligne",
@@ -309,7 +309,7 @@
309309 "PygmalionAI/aphrodite-engine": "PygmalionAI/aphrodite-engine (mode wrapper pour l'API OpenAI)",
310310 "Aphrodite API key": "Clé API Aphrodite",
311311 "Aphrodite Model": "Modèle Aphrodite",
312312 "ggerganovggml-org/llama.cpp": "ggerganovggml-org/llama.cpp",
313313 "Example: http://127.0.0.1:8080": "Exemple : http://127.0.0.1:8080",
314314 "Example: http://127.0.0.1:11434": "Exemple : http://127.0.0.1:11434",
315315 "Ollama Model": "Modèle Ollama",
@@ -554,7 +554,7 @@
554554 "Model Icon": "Icône du modèle",
555555 "Show sequential message numbers in the chat log": "Afficher les numéros de message séquentiels dans le journal de discussion",
556556 "Message IDs": "Identifiants du message",
557557 "Hide avatars, only in chat messages.": "Masquez les avatars dans les messages de discussion.",
558558 "Hide Chat Avatars": "Masquer les avatars du chat",
559559 "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",
560560 "Show Message Token Count": "Afficher le nombre de tokens du message",
@@ -684,8 +684,8 @@
684684 "Filter": "Filtre",
685685 "Automatically select a background based on the chat context": "Sélectionner automatiquement un arrière-plan en fonction du contexte de la discussion",
686686 "Auto-select": "Sélection automatique",
687687 "System BackgroundsGlobal": "Arrière-plans du systèmeGlobal",
688688 "Chat Backgrounds": "Arrière-plans de la discussionDiscussion",
689689 "bg_chat_hint_1": "Les arrière-plans générés avec l'extension ",
690690 "bg_chat_hint_2": " apparaîtrons ici.",
691691 "Extensions": "Extensions",
@@ -856,7 +856,7 @@
856856 "Save": "Enregistrer",
857857 "Chat History": "Historique de la conversation",
858858 "Import Chat": "Importer une discussion",
859859 "Copy to systemglobal backgrounds": "Copier vers les arrière-plans du systèmeglobaux",
860860 "Rename background": "Renommer l'arrière-plan",
861861 "Lock": "Verrouiller",
862862 "Unlock": "Déverrouiller",
public/locales/is-is.json+6 -6
@@ -235,7 +235,7 @@
235235 "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.",
236236 "Enable function calling": "Virkja aðgerðarkall",
237237 "Send inline images": "Senda myndir í línu",
238238 "image_inlining_hint_1": "Sendir myndirmargmiðlunarskrár í skilaboðum ef líkanið styður það.",
239239 "image_inlining_hint_2": "aðgerð á hvaða skilaboðum sem er eða",
240240 "image_inlining_hint_3": "valmynd til að hengja myndskrá við spjallið.",
241241 "Inline Image Quality": "Innbyggð myndgæði",
@@ -326,7 +326,7 @@
326326 "PygmalionAI/aphrodite-engine": "PygmalionAI/aphrodite-engine (OpenAI forritunargrensl)",
327327 "Aphrodite API key": "Aphrodite API lykill",
328328 "Aphrodite Model": "Afródíta fyrirmynd",
329329 "ggerganovggml-org/llama.cpp": "ggerganovggml-org/llama.cpp (úttak þjónn)",
330330 "Example: http://127.0.0.1:8080": "Dæmi: http://127.0.0.1:8080",
331331 "Example: http://127.0.0.1:11434": "Dæmi: http://127.0.0.1:11434",
332332 "Ollama Model": "Ollama módel",
@@ -603,7 +603,7 @@
603603 "Model Icon": "Módel Tákn",
604604 "Show sequential message numbers in the chat log": "Sýna röðuð skilaboðanúmer í spjall skránni",
605605 "Message IDs": "Skilaboðaauðkenni",
606606 "Hide avatars, only in chat messages.": "Fela avatar í spjallskilaboðum.",
607607 "Hide Chat Avatars": "Fela spjallmyndir",
608608 "Show the number of tokens in each message in the chat log": "Sýna fjölda tákn í hverju skilaboði í spjall skránni",
609609 "Show Message Token Count": "Sýna fjölda tákn í skilaboðum",
@@ -737,8 +737,8 @@
737737 "Filter": "Sía",
738738 "Automatically select a background based on the chat context": "Velja sjálfkrafa bakgrunn út frá samhengi spjallsins",
739739 "Auto-select": "Sjálfval",
740740 "System BackgroundsGlobal": "Kerfis bakgrunnsmyndirAlmennt",
741741 "Chat Backgrounds": "Bakgrunnsmyndir spjallsSpjall",
742742 "bg_chat_hint_1": "Spjallbakgrunnur myndaður með",
743743 "bg_chat_hint_2": "viðbót mun birtast hér.",
744744 "Extensions": "Viðbætur",
@@ -911,7 +911,7 @@
911911 "Save": "Vista",
912912 "Chat History": "Spjall saga",
913913 "Import Chat": "Flytja inn spjall",
914914 "Copy to systemglobal backgrounds": "Afritaðu í kerfisbakgrunnalþjóðlegan bakgrunn",
915915 "Rename background": "Endurnefna bakgrunn",
916916 "Lock": "Læsa",
917917 "Unlock": "Opnaðu",
public/locales/it-it.json+6 -6
@@ -235,7 +235,7 @@
235235 "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.",
236236 "Enable function calling": "Abilita la chiamata alla funzione",
237237 "Send inline images": "Invia immagini inline",
238238 "image_inlining_hint_1": "Invia immaginifile multimediali nei prompt se il modello lo supporta.",
239239 "image_inlining_hint_2": "azione su qualsiasi messaggio o il",
240240 "image_inlining_hint_3": "menu per allegare un file immagine alla chat.",
241241 "Inline Image Quality": "Qualità dell'immagine in linea",
@@ -328,7 +328,7 @@
328328 "PygmalionAI/aphrodite-engine": "PygmalionAI/aphrodite-engine (Modalità wrapper per l'API OpenAI)",
329329 "Aphrodite API key": "Chiave API di Aphrodite",
330330 "Aphrodite Model": "Modello di Afrodite",
331331 "ggerganovggml-org/llama.cpp": "ggerganovggml-org/llama.cpp (Server di output)",
332332 "Example: http://127.0.0.1:8080": "Esempio: http://127.0.0.1:8080",
333333 "Example: http://127.0.0.1:11434": "Esempio: http://127.0.0.1:11434",
334334 "Ollama Model": "Modello Ollama",
@@ -605,7 +605,7 @@
605605 "Model Icon": "Icona del modello",
606606 "Show sequential message numbers in the chat log": "Mostra numeri di messaggi sequenziali nel registro della chat",
607607 "Message IDs": "ID Messaggio",
608608 "Hide avatars, only in chat messages.": "Nascondi gli avatar nei messaggi di chat.",
609609 "Hide Chat Avatars": "Nascondi avatar di chat",
610610 "Show the number of tokens in each message in the chat log": "Mostra il numero di token in ogni messaggio nel registro della chat",
611611 "Show Message Token Count": "Mostra Conteggio Token Messaggio",
@@ -739,8 +739,8 @@
739739 "Filter": "Filtro",
740740 "Automatically select a background based on the chat context": "Seleziona automaticamente uno sfondo in base al contesto della chat",
741741 "Auto-select": "Selezione automatica",
742742 "System BackgroundsGlobal": "Sfondi di sistemaGlobale",
743743 "Chat Backgrounds": "Sfondi Chat",
744744 "bg_chat_hint_1": "Sfondi chat generati con",
745745 "bg_chat_hint_2": "l'estensione apparirà qui.",
746746 "Extensions": "Estensioni",
@@ -913,7 +913,7 @@
913913 "Save": "Salva",
914914 "Chat History": "Cronologia Chat",
915915 "Import Chat": "Importa chat",
916916 "Copy to systemglobal backgrounds": "Copia negli sfondi di sistemaglobali",
917917 "Rename background": "Rinominare lo sfondo",
918918 "Lock": "Serratura",
919919 "Unlock": "Sbloccare",
public/locales/ja-jp.json+6 -6
@@ -235,7 +235,7 @@
235235 "Combines consecutive system messages into one (excluding example dialogues). May improve coherence for some models.": "連続するシステムメッセージを1つに結合します(例のダイアログを除く)。一部のモデルの一貫性を向上させる可能性があります。",
236236 "Enable function calling": "関数呼び出しを有効にする",
237237 "Send inline images": "インライン画像を送信",
238238 "image_inlining_hint_1": "モデルがサポートしている場合、プロンプトで画像を送信しますプロンプトでメディアファイルを送信します。",
239239 "image_inlining_hint_2": "メッセージに対するアクションまたは",
240240 "image_inlining_hint_3": "チャットに画像ファイルを添付するためのメニュー。",
241241 "Inline Image Quality": "インライン画像品質",
@@ -328,7 +328,7 @@
328328 "PygmalionAI/aphrodite-engine": "PygmalionAI/aphrodite-engine(OpenAI APIエンドポイントのパッケージングモード)",
329329 "Aphrodite API key": "アフロディーテAPIキー",
330330 "Aphrodite Model": "アフロディーテモデル",
331331 "ggerganovggml-org/llama.cpp": "ggerganovggml-org/llama.cpp(出力サーバー)",
332332 "Example: http://127.0.0.1:8080": "例: http://127.0.0.1:8080",
333333 "Example: http://127.0.0.1:11434": "例: http://127.0.0.1:11434",
334334 "Ollama Model": "Ollamaモデル",
@@ -605,7 +605,7 @@
605605 "Model Icon": "モデルアイコン",
606606 "Show sequential message numbers in the chat log": "チャットログに連続したメッセージ番号を表示する",
607607 "Message IDs": "メッセージID",
608608 "Hide avatars, only in chat messages.": "チャットメッセージ内のアバターを非表示にします。",
609609 "Hide Chat Avatars": "チャットアバターを非表示",
610610 "Show the number of tokens in each message in the chat log": "チャットログ内の各メッセージのトークン数を表示する",
611611 "Show Message Token Count": "メッセージトークンの数を表示",
@@ -739,8 +739,8 @@
739739 "Filter": "フィルター",
740740 "Automatically select a background based on the chat context": "チャットのコンテキストに基づいて背景を自動選択",
741741 "Auto-select": "自動選択",
742742 "System BackgroundsGlobal": "システムの背景グローバル",
743743 "Chat Backgrounds": "チャットの背景チャット",
744744 "bg_chat_hint_1": "",
745745 "bg_chat_hint_2": "拡張機能で生成したチャットの背景はここに表示されます。",
746746 "Extensions": "拡張機能",
@@ -913,7 +913,7 @@
913913 "Save": "保存",
914914 "Chat History": "チャット履歴",
915915 "Import Chat": "チャットをインポート",
916916 "Copy to systemglobal backgrounds": "システム背景にコピーグローバル背景にコピー",
917917 "Rename background": "背景の名前を変更",
918918 "Lock": "ロック",
919919 "Unlock": "ロック解除",
public/locales/ko-kr.json+6 -6
@@ -237,7 +237,7 @@
237237 "Combines consecutive system messages into one (excluding example dialogues). May improve coherence for some models.": "연속된 시스템 메시지를 하나로 결합합니다(예제 대화 제외). 일부 모델의 일관성을 향상시킬 수 있습니다.",
238238 "Enable function calling": "함수 호출 활성화",
239239 "Send inline images": "인라인 이미지 전송",
240240 "image_inlining_hint_1": "모델이 지원하는 경우 메시지로 이미지를미디어 파일을 보냅니다.",
241241 "Inline Image Quality": "인라인 이미지 품질",
242242 "openai_inline_image_quality_auto": "자동",
243243 "openai_inline_image_quality_low": "낮은",
@@ -328,7 +328,7 @@
328328 "PygmalionAI/aphrodite-engine": "PygmalionAI/aphrodite-engine (OpenAI API의 래퍼 모드)",
329329 "Aphrodite API key": "Aphrodite API 키",
330330 "Aphrodite Model": "Aphrodite 모델",
331331 "ggerganovggml-org/llama.cpp": "ggerganovggml-org/llama.cpp (출력 서버)",
332332 "Example: http://127.0.0.1:8080": "예: http://127.0.0.1:8080",
333333 "Example: http://127.0.0.1:11434": "예: http://127.0.0.1:11434",
334334 "Ollama Model": "Ollama 모델",
@@ -613,7 +613,7 @@
613613 "Model Icon": "모델 아이콘 표시",
614614 "Show sequential message numbers in the chat log": "채팅 로그에 순차적인 메시지 번호 표시",
615615 "Message IDs": "메시지 ID",
616616 "Hide avatars, only in chat messages.": "채팅 메시지에서 아바타 숨김.",
617617 "Hide Chat Avatars": "캐릭터 프로필 숨기기",
618618 "Show the number of tokens in each message in the chat log": "채팅 로그의 각 메시지에 토큰 수 표시",
619619 "Show Message Token Count": "메시지 토큰 개수 표시",
@@ -752,8 +752,8 @@
752752 "Filter": "필터",
753753 "Automatically select a background based on the chat context": "채팅 컨텍스트를 기반으로 자동으로 배경 선택",
754754 "Auto-select": "자동 선택",
755755 "System BackgroundsGlobal": "시스템 배경글로벌",
756756 "Chat Backgrounds": "채팅 배경",
757757 "bg_chat_hint_1": "다음으로 생성된 채팅 배경",
758758 "bg_chat_hint_2": "확장 프로그램이 여기에 표시됩니다.",
759759 "Extensions": "확장",
@@ -927,7 +927,7 @@
927927 "Save": "저장",
928928 "Chat History": "채팅 기록",
929929 "Import Chat": "채팅 가져오기",
930930 "Copy to systemglobal backgrounds": "시스템글로벌 배경에 복사",
931931 "Rename background": "배경 이름 바꾸기",
932932 "Lock": "잠금",
933933 "Unlock": "잠금 해제",
public/locales/nl-nl.json+6 -6
@@ -235,7 +235,7 @@
235235 "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.",
236236 "Enable function calling": "Schakel functieaanroepen in",
237237 "Send inline images": "Inline afbeeldingen verzenden",
238238 "image_inlining_hint_1": "Verzendt afbeeldingenmediabestanden in prompts als het model dit ondersteunt.",
239239 "Inline Image Quality": "Inline-beeldkwaliteit",
240240 "openai_inline_image_quality_auto": "Auto",
241241 "openai_inline_image_quality_low": "Laag",
@@ -324,7 +324,7 @@
324324 "PygmalionAI/aphrodite-engine": "PygmalionAI/aphrodite-engine (Wrappermodus voor OpenAI API)",
325325 "Aphrodite API key": "Aphrodite API-sleutel",
326326 "Aphrodite Model": "Aphrodite-model",
327327 "ggerganovggml-org/llama.cpp": "ggerganovggml-org/llama.cpp (Output-server)",
328328 "Example: http://127.0.0.1:8080": "Voorbeeld: http://127.0.0.1:8080",
329329 "Example: http://127.0.0.1:11434": "Voorbeeld: http://127.0.0.1:11434",
330330 "Ollama Model": "Ollama-model",
@@ -601,7 +601,7 @@
601601 "Model Icon": "Modelpictogram",
602602 "Show sequential message numbers in the chat log": "Toon opeenvolgende berichtnummers in het chatlogboek",
603603 "Message IDs": "Bericht-IDs",
604604 "Hide avatars, only in chat messages.": "Verberg avatars in chatberichten.",
605605 "Hide Chat Avatars": "Chatavatars verbergen",
606606 "Show the number of tokens in each message in the chat log": "Toon het aantal tokens in elk bericht in het chatlogboek",
607607 "Show Message Token Count": "Toon bericht token tellen",
@@ -735,8 +735,8 @@
735735 "Filter": "Filter",
736736 "Automatically select a background based on the chat context": "Automatisch een achtergrond selecteren op basis van de chatcontext",
737737 "Auto-select": "Automatisch selecteren",
738738 "System BackgroundsGlobal": "SysteemachtergrondenGlobaal",
739739 "Chat Backgrounds": "Achtergronden chatChat",
740740 "bg_chat_hint_1": "Chatachtergronden gegenereerd met de",
741741 "bg_chat_hint_2": "extensie zal hier verschijnen.",
742742 "Extensions": "Uitbreidingen",
@@ -909,7 +909,7 @@
909909 "Save": "Opslaan",
910910 "Chat History": "Chatgeschiedenis",
911911 "Import Chat": "Chat importeren",
912912 "Copy to systemglobal backgrounds": "Kopiëren naar systeemachtergrondenglobale achtergronden",
913913 "Rename background": "Achtergrond hernoemen",
914914 "Lock": "Slot",
915915 "Unlock": "Ontgrendelen",
public/locales/pt-pt.json+6 -6
@@ -235,7 +235,7 @@
235235 "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.",
236236 "Enable function calling": "Habilitar chamada de função",
237237 "Send inline images": "Enviar imagens inline",
238238 "image_inlining_hint_1": "Envia imagensficheiros multimédia em prompts se o modelo suportar.",
239239 "Inline Image Quality": "Qualidade de imagem embutida",
240240 "openai_inline_image_quality_auto": "Auto",
241241 "openai_inline_image_quality_low": "Baixo",
@@ -326,7 +326,7 @@
326326 "PygmalionAI/aphrodite-engine": "PygmalionAI/aphrodite-engine (Modo Wrapper para API OpenAI)",
327327 "Aphrodite API key": "Chave da API Aphrodite",
328328 "Aphrodite Model": "Modelo Afrodite",
329329 "ggerganovggml-org/llama.cpp": "ggerganovggml-org/llama.cpp (Servidor de Saída)",
330330 "Example: http://127.0.0.1:8080": "Exemplo: http://127.0.0.1:8080",
331331 "Example: http://127.0.0.1:11434": "Exemplo: http://127.0.0.1:11434",
332332 "Ollama Model": "Modelo Ollama",
@@ -603,7 +603,7 @@
603603 "Model Icon": "Ícone do Modelo",
604604 "Show sequential message numbers in the chat log": "Mostrar números sequenciais de mensagem no registro de chat",
605605 "Message IDs": "IDs de Mensagem",
606606 "Hide avatars, only in chat messages.": "Oculte avatares em mensagens de bate-papo.",
607607 "Hide Chat Avatars": "Ocultar avatares de bate-papo",
608608 "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",
609609 "Show Message Token Count": "Mostrar Contagem de Tokens da Mensagem",
@@ -737,8 +737,8 @@
737737 "Filter": "Filtro",
738738 "Automatically select a background based on the chat context": "Selecionar automaticamente um plano de fundo com base no contexto do chat",
739739 "Auto-select": "Seleção automática",
740740 "System BackgroundsGlobal": "Fundos do SistemaGlobal",
741741 "Chat Backgrounds": "Fundos de Chat",
742742 "bg_chat_hint_1": "Planos de fundo de bate-papo gerados com o",
743743 "bg_chat_hint_2": "a extensão aparecerá aqui.",
744744 "Extensions": "Extensões",
@@ -911,7 +911,7 @@
911911 "Save": "Salvar",
912912 "Chat History": "Histórico de Chat",
913913 "Import Chat": "Importar bate-papo",
914914 "Copy to systemglobal backgrounds": "Copiar para planos de fundo do sistemaglobais",
915915 "Rename background": "Renomear plano de fundo",
916916 "Lock": "Trancar",
917917 "Unlock": "Desbloquear",
public/locales/ru-ru.json+5 -7
@@ -108,7 +108,7 @@
108108 "Register": "Зарегистрироваться",
109109 "TogetherAI Model": "Модель TogetherAI",
110110 "Example: http://127.0.0.1:5001": "Пример: http://127.0.0.1:5001",
111111 "ggerganovggml-org/llama.cpp": "ggerganovggml-org/llama.cpp (сервер вывода)",
112112 "Example: http://127.0.0.1:8080": "Пример: http://127.0.0.1:8080",
113113 "Example: http://127.0.0.1:11434": "Пример: http://127.0.0.1:11434",
114114 "Ollama Model": "Модель Ollama",
@@ -244,7 +244,6 @@
244244 "Always include examples": "Всегда применять примеры",
245245 "Never include examples": "Никогда не применять примеры",
246246 "Forbid External Media": "Запрет внешних медиа",
247- "System Backgrounds": "Системные фоны",
248247 "Name": "Имя",
249248 "Auto-connect": "Подключаться автоматически",
250249 "First message": "Первое сообщение",
@@ -350,7 +349,7 @@
350349 "Minimum number of blacklisted words detected to trigger an auto-swipe": "Минимальное количество обнаруженных запрещённых слов, при котором срабатывает авто-свайп.",
351350 "User Message Blur Tint": "Ваши сообщения",
352351 "AI Message Blur Tint": "Сообщения ИИ",
353352 "Chat Backgrounds": "Фоны чатаЧат",
354353 "Chat Background": "Фон чата",
355354 "UI Background": "Фон UI",
356355 "Mad Lab Mode": "Режим безумца",
@@ -1020,7 +1019,7 @@
10201019 "The prompt to be sent.": "Текст промпта.",
10211020 "prompt_manager_forbid_overrides": "Запретить перезапись",
10221021 "This prompt cannot be overridden by character cards, even if overrides are preferred.": "Карточка персонажа не сможет перезаписать этот промпт, даже если настройки отдают приоритет именно ей.",
10231022 "image_inlining_hint_1": "Отправлять картинкимедиафайлы как часть промпта, если позволяет модель.",
10241023 "Contest Winners": "Победители конкурса",
10251024 "Rename Background": "Переименовать фон",
10261025 "Lock": "Закрепить",
@@ -1046,7 +1045,7 @@
10461045 "openai_inline_image_quality_high": "Высокое",
10471046 "Assistant Impersonation Prefill": "Префилл для ассистента при перевоплощении",
10481047 "Hide Chat Avatars": "Не показывать аватарки в чате",
10491048 "Hide avatars, only in chat messages.": "Скрыть аватарки сбоку от сообщений в чате",
10501049 "Flat": "Стандартный",
10511050 "Toggle character info panel": "Показать / скрыть инфо-панель",
10521051 "(For Chat Completion and Instruct Mode)": "(для Chat Completion и режима Instruct)",
@@ -1326,7 +1325,7 @@
13261325 "Auto Mode delay": "Auto Mode delay",
13271326 "Bulk_edit_characters": "Bulk edit characters\r\rClick to toggle characters\rShift + Click to select/deselect a range of characters\rRight-click for actions",
13281327 "Bulk select all characters": "Bulk select all characters",
13291328 "Copy to systemglobal backgrounds": "CopyСкопировать toв systemглобальные backgroundsфоны",
13301329 "Chat Scenario Override": "Chat Scenario Override",
13311330 "Chat Lorebook": "Chat Lorebook for",
13321331 "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 @@
24432442 "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 токена. При выборе значение Авто рассуждения не запрашиваются.",
24442443 "Use system prompt": "Включить системный промпт",
24452444 "Send inline videos": "Отправлять inline-видео",
2446- "video_inlining_hint_1": "Отправляет модели видео, если она поддерживает такую возможность.",
24472445 "video_inlining_hint_4": "Видео должно весить не более 20 Мб и длиться не дольше 1 минуты.",
24482446 "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 токенов). При выборе значения Авто модель определяет объём самостоятельно.",
24492447 "Google Vertex AI Configuration": "Настройки Google Vertex AI",
public/locales/th-th.json+6 -6
@@ -241,7 +241,7 @@
241241 "Allows the model to return its thinking process.": "อนุญาตให้โมเดลส่งคืนกระบวนการคิดของตัวเอง",
242242 "This setting affects visibility only.": "การตั้งค่านี้มีผลต่อการมองเห็นเท่านั้น",
243243 "Send inline images": "ส่งรูปภาพแบบ inline",
244244 "image_inlining_hint_1": "ส่งรูปภาพในพรอมตงไฟลสื่อในพรอมต์หากโมเดลรองรับ",
245245 "Inline Image Quality": "คุณภาพรูปภาพ Inline",
246246 "openai_inline_image_quality_auto": "อัตโนมัติ",
247247 "openai_inline_image_quality_low": "ต่ำ",
@@ -330,7 +330,7 @@
330330 "PygmalionAI/aphrodite-engine": "PygmalionAI/aphrodite-engine",
331331 "Aphrodite API key": "คีย์ API ของ Aphrodite",
332332 "Aphrodite Model": "โมเดลของ Aphrodite",
333333 "ggerganovggml-org/llama.cpp": "ggerganovggml-org/llama.cpp",
334334 "Example: http://127.0.0.1:8080": "ตัวอย่าง: http://127.0.0.1:8080",
335335 "Example: http://127.0.0.1:11434": "ตัวอย่าง: http://127.0.0.1:11434",
336336 "Ollama Model": "โมเดลของ Ollama",
@@ -611,7 +611,7 @@
611611 "Model Icon": "แสดงไอคอน Model AI ที่ตอบแชท",
612612 "Show sequential message numbers in the chat log": "แสดงหมายเลขลำดับข้อความในประวัติแชท",
613613 "Message IDs": "แสดงเลขจำนวนข้อความ",
614614 "Hide avatars, only in chat messages.": "ซ่อนภาพ avatar ในแชทข้อความ",
615615 "Hide Chat Avatars": "ซ่อนรูปโปรไฟล์แชท",
616616 "Show the number of tokens in each message in the chat log": "แสดงจำนวนโทเค็นในแต่ละข้อความในบันทึกแชท",
617617 "Show Message Token Count": "แสดงจำนวนโทเค็นที่ Ai ตอบแชท",
@@ -745,8 +745,8 @@
745745 "Filter": "ตัวกรอง",
746746 "Automatically select a background based on the chat context": "เลือกพื้นหลังโดยอัตโนมัติตามบริบทของแชท",
747747 "Auto-select": "เลือกอัตโนมัติ",
748748 "System BackgroundsGlobal": "พื้นหลังของระบบทั่วโลก",
749749 "Chat Backgrounds": "พื้นหลังแชทแชท",
750750 "bg_chat_hint_1": "พื้นหลังของแชทที่ถูกสร้างด้วย",
751751 "bg_chat_hint_2": "จะปรากฏที่นี่",
752752 "Extensions": "โปรแกรมส่วนขยาย",
@@ -921,7 +921,7 @@
921921 "Save": "บันทึก",
922922 "Chat History": "ประวัติการแชท",
923923 "Import Chat": "นำเข้าไฟล์แชท",
924924 "Copy to systemglobal backgrounds": "คัดลอกไปยังพื้นหลังนหลังทั่วโลก",
925925 "Rename background": "เปลี่ยนชื่อพื้นหลัง",
926926 "Lock": "ล็อค",
927927 "Unlock": "ปลดล็อค",
public/locales/uk-ua.json+6 -6
@@ -235,7 +235,7 @@
235235 "Combines consecutive system messages into one (excluding example dialogues). May improve coherence for some models.": "Об'єднує послідовні системні повідомлення в одне (крім прикладів діалогів). Може покращити співпрацю для деяких моделей.",
236236 "Enable function calling": "Увімкнути виклик функцій",
237237 "Send inline images": "Надсилати вбудовані зображення",
238238 "image_inlining_hint_1": "Надсилає зображеннямедіафайли у підказках, якщо модель це підтримує.",
239239 "Inline Image Quality": "Якість вбудованого зображення",
240240 "openai_inline_image_quality_auto": "Авто",
241241 "openai_inline_image_quality_low": "Низький",
@@ -326,7 +326,7 @@
326326 "PygmalionAI/aphrodite-engine": "PygmalionAI/aphrodite-engine (режим OpenAI API)",
327327 "Aphrodite API key": "Ключ API для Aphrodite",
328328 "Aphrodite Model": "Модель Афродіта",
329329 "ggerganovggml-org/llama.cpp": "ggerganovggml-org/llama.cpp (сервер виведення)",
330330 "Example: http://127.0.0.1:8080": "Приклад: http://127.0.0.1:8080",
331331 "Example: http://127.0.0.1:11434": "Приклад: http://127.0.0.1:11434",
332332 "Ollama Model": "Модель Ollama",
@@ -603,7 +603,7 @@
603603 "Model Icon": "Іконка моделі",
604604 "Show sequential message numbers in the chat log": "Показувати послідовні номери повідомлень у журналі чату",
605605 "Message IDs": "Ідентифікатори повідомлень",
606606 "Hide avatars, only in chat messages.": "Приховати аватари в повідомленнях чату.",
607607 "Hide Chat Avatars": "Приховати аватари чату",
608608 "Show the number of tokens in each message in the chat log": "Показувати кількість токенів у кожному повідомленні у журналі чату",
609609 "Show Message Token Count": "Кількість токенів у повідомленні",
@@ -737,8 +737,8 @@
737737 "Filter": "Фільтр",
738738 "Automatically select a background based on the chat context": "Автоматичний вибір фону на основі контексту чату",
739739 "Auto-select": "Автовибір",
740740 "System BackgroundsGlobal": "Системні фониГлобальні",
741741 "Chat Backgrounds": "Фони чатуЧат",
742742 "bg_chat_hint_1": "Фонове зображення чату, створене за допомогою",
743743 "bg_chat_hint_2": "тут з’явиться розширення.",
744744 "Extensions": "Розширення",
@@ -911,7 +911,7 @@
911911 "Save": "Зберегти",
912912 "Chat History": "Історія чату",
913913 "Import Chat": "Імпорт чату",
914914 "Copy to systemglobal backgrounds": "Скопіювати в системніглобальні фони",
915915 "Rename background": "Перейменувати фон",
916916 "Lock": "Замок",
917917 "Unlock": "Розблокувати",
public/locales/vi-vn.json+6 -6
@@ -235,7 +235,7 @@
235235 "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.",
236236 "Enable function calling": "Sử dụng tính năng gọi hàm (function calling)",
237237 "Send inline images": "Gửi hình ảnh nội bộ",
238238 "image_inlining_hint_1": "Gửi hìnhtệp ảnhphương tiện theo Prompt nếu kiểu máy hỗ trợ.",
239239 "Inline Image Quality": "Chất lượng hình ảnh nội tuyến",
240240 "openai_inline_image_quality_auto": "Tự động",
241241 "openai_inline_image_quality_low": "Thấp",
@@ -326,7 +326,7 @@
326326 "PygmalionAI/aphrodite-engine": "PygmalionAI/aphrodite-engine (Chế độ đóng gói cho Giao diện lập trình ứng dụng OpenAI)",
327327 "Aphrodite API key": "Key API Aphrodite",
328328 "Aphrodite Model": "Moddel cho Aphrodite",
329329 "ggerganovggml-org/llama.cpp": "ggerganovggml-org/llama.cpp",
330330 "Example: http://127.0.0.1:8080": "Ví dụ: http://127.0.0.1:8080",
331331 "Example: http://127.0.0.1:11434": "Ví dụ: http://127.0.0.1:11434",
332332 "Ollama Model": "Model Ollama",
@@ -603,7 +603,7 @@
603603 "Model Icon": "Biểu tượng Model",
604604 "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",
605605 "Message IDs": "ID Tin nhắn",
606606 "Hide avatars, only in chat messages.": "Ẩn hình đại diện trong tin nhắn trò chuyện.",
607607 "Hide Chat Avatars": "Ẩn hình đại diện trò chuyện",
608608 "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",
609609 "Show Message Token Count": "Hiển thị Số lượng Token trong Tin nhắn",
@@ -737,8 +737,8 @@
737737 "Filter": "Bộ lọc",
738738 "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",
739739 "Auto-select": "Tự động chọn",
740740 "System BackgroundsGlobal": "Nền HệToàn thốngcầu",
741741 "Chat Backgrounds": "Hình nềnTrò Chatchuyện",
742742 "bg_chat_hint_1": "Hình nền trò chuyện được tạo bằng",
743743 "bg_chat_hint_2": "tiện ích sẽ xuất hiện ở đây.",
744744 "Extensions": "Tiện ích",
@@ -911,7 +911,7 @@
911911 "Save": "Lưu",
912912 "Chat History": "Lịch sử Trò chuyện",
913913 "Import Chat": "Nhập trò chuyện",
914914 "Copy to systemglobal backgrounds": "Sao chép vào nền hệtoàn thốngcầu",
915915 "Rename background": "Đổi tên nền background",
916916 "Lock": "Khóa",
917917 "Unlock": "Mở khóa",
public/locales/zh-cn.json+5 -7
@@ -269,13 +269,12 @@
269269 "enable_functions_desc_3": "可以被各种扩展利用来提供附加功能。",
270270 "enable_functions_desc_4": "当提示词后处理没有选择工具时不支持。",
271271 "Send inline images": "发送图片",
272272 "image_inlining_hint_1": "如果模型支持,就可以在提示词中发送图片就可以在提示词中发送媒体文件。",
273273 "Inline Image Quality": "图片画质",
274274 "openai_inline_image_quality_auto": "自动",
275275 "openai_inline_image_quality_low": "低",
276276 "openai_inline_image_quality_high": "高",
277277 "Send inline videos": "发送视频",
278- "video_inlining_hint_1": "当模型支持时,将视频发送给模型。",
279278 "video_inlining_hint_4": "视频必须在 20MB 以下且时长不超过1分钟。",
280279 "Request inline images": "请求图片返回",
281280 "Allows the model to return image attachments.": "允许模型返回图片附件。",
@@ -397,7 +396,7 @@
397396 "PygmalionAI/aphrodite-engine": "PygmalionAI/aphrodite-engine(用于OpenAI API的包装器)",
398397 "Aphrodite API key": "Aphrodite API 密钥",
399398 "Aphrodite Model": "Aphrodite 模型",
400399 "ggerganovggml-org/llama.cpp": "ggerganovggml-org/llama.cpp",
401400 "Example: http://127.0.0.1:8080": "示例:http://127.0.0.1:8080",
402401 "Example: http://127.0.0.1:11434": "示例:http://127.0.0.1:11434",
403402 "Ollama Model": "Ollama 模型",
@@ -787,7 +786,7 @@
787786 "Model Icon": "模型图标",
788787 "Show sequential message numbers in the chat log": "在聊天记录中显示消息楼层",
789788 "Message IDs": "显示消息楼层",
790789 "Hide avatars, only in chat messages.": "在聊天记录中隐藏头像。",
791790 "Hide Chat Avatars": "隐藏头像",
792791 "Show the number of tokens in each message in the chat log": "在聊天记录中显示每条消息的词符数",
793792 "Show Message Token Count": "显示消息词符数",
@@ -962,8 +961,7 @@
962961 "Automatically select a background based on the chat context": "根据聊天上下文自动选择背景",
963962 "Auto-select": "自动选择",
964963 "Add Background": "添加背景",
965964 "System BackgroundsGlobal": "系统背景全局",
966- "Chat Backgrounds": "聊天背景",
967965 "bg_chat_hint_1": "使用生成的聊天背景",
968966 "bg_chat_hint_2": "扩展名将出现在这里。",
969967 "Extensions": "扩展",
@@ -1158,7 +1156,7 @@
11581156 "Save": "保存",
11591157 "Chat History": "聊天记录",
11601158 "Import Chat": "导入聊天",
11611159 "Copy to systemglobal backgrounds": "复制到系统背景复制到全局背景",
11621160 "Lock": "锁定",
11631161 "Unlock": "解锁",
11641162 "Rename Background": "重命名背景",
public/locales/zh-tw.json+5 -7
@@ -236,7 +236,7 @@
236236 "Combines consecutive system messages into one (excluding example dialogues). May improve coherence for some models.": "將連續的系統訊息合併為一個(不包括對話範例)。可能會提高某些模型的一致性。",
237237 "Enable function calling": "啟用函式呼叫",
238238 "Send inline images": "傳送內嵌圖片",
239239 "image_inlining_hint_1": "如果模型支援,則在提示詞中傳送圖片則在提示詞中傳送媒體檔案。",
240240 "Inline Image Quality": "內嵌圖片品質",
241241 "openai_inline_image_quality_auto": "自動",
242242 "openai_inline_image_quality_low": "低",
@@ -325,7 +325,7 @@
325325 "PygmalionAI/aphrodite-engine": "PygmalionAI/aphrodite 引擎",
326326 "Aphrodite API key": "Aphrodite API 金鑰",
327327 "Aphrodite Model": "Aphrodite 模型",
328328 "ggerganovggml-org/llama.cpp": "ggerganovggml-org/llama.cpp",
329329 "Example: http://127.0.0.1:8080": "範例:http://127.0.0.1:8080",
330330 "Example: http://127.0.0.1:11434": "範例:http://127.0.0.1:11434",
331331 "Ollama Model": "Ollama 模型",
@@ -602,7 +602,7 @@
602602 "Model Icon": "顯示模型圖示",
603603 "Show sequential message numbers in the chat log": "在聊天記錄中顯示連續的訊息編號。",
604604 "Message IDs": "顯示訊息 ID",
605605 "Hide avatars, only in chat messages.": "在聊天訊息中隱藏頭像",
606606 "Hide Chat Avatars": "隱藏聊天頭像",
607607 "Show the number of tokens in each message in the chat log": "在聊天記錄中顯示每條訊息中的符元數量",
608608 "Show Message Token Count": "顯示訊息符元數",
@@ -736,8 +736,7 @@
736736 "Filter": "篩選",
737737 "Automatically select a background based on the chat context": "根據聊天上下文自動選擇背景",
738738 "Auto-select": "自動選擇",
739739 "System BackgroundsGlobal": "系統背景圖片全域",
740- "Chat Backgrounds": "聊天背景圖片",
741740 "bg_chat_hint_1": "使用擴充功能",
742741 "bg_chat_hint_2": "所產生的背景圖片將在此顯示。",
743742 "Extensions": "擴充功能",
@@ -909,7 +908,7 @@
909908 "(Examples of chat dialog. Begin each example with START on a new line.)": "(聊天對話範例。每個範例以新的行並以「START」開始。)",
910909 "Chat History": "聊天記錄",
911910 "Import Chat": "匯入聊天",
912911 "Copy to systemglobal backgrounds": "複製到系統背景圖片複製到全局背景圖片",
913912 "Rename background": "重新命名背景圖片",
914913 "Lock": "上鎖",
915914 "Unlock": "解鎖",
@@ -2648,7 +2647,6 @@
26482647 "Min Keep": "最小保留",
26492648 "enable_functions_desc_4": "當使用「無工具」提示後處理時不支援!",
26502649 "Send inline videos": "傳送內嵌影片",
2651- "video_inlining_hint_1": "若模型支援,則在提示中傳送影片。",
26522650 "video_inlining_hint_4": "影片大小必須小於 20 MB 且長度低於 1 分鐘",
26532651 "This setting affects visibility only.": "此設定僅影響可見性。",
26542652 "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+851 -352
@@ -65,12 +65,11 @@ import {
6565 renameGroupMember,
6666 createNewGroupChat,
6767 getGroupAvatar,
68- editGroup,
6968 deleteGroupChat,
7069 renameGroupChat,
7170 importGroupChat,
7271 getGroupBlock,
7372 getGroupCharacterCardsgetGroupCharacterCardsLazy,
7473 getGroupDepthPrompts,
7574} from './scripts/group-chats.js';
7675
@@ -182,11 +181,13 @@ import {
182181 canUseNegativeLookbehind,
183182 trimSpaces,
184183 clamp,
184+ shakeElement,
185+ createTimeout,
185186} from './scripts/utils.js';
186187import { 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
188189import { cancelDebouncedMetadataSave, doDailyExtensionUpdatesCheck, extension_settings, initExtensions, loadExtensionSettings, runGenerationInterceptors } from './scripts/extensions.js';
189190import { COMMENT_NAME_DEFAULT, CONNECT_API_MAP, executeSlashCommandsOnChatInput, initDefaultSlashCommands, initSlashCommandAutoComplete, isExecutingCommandsFromChatInput, pauseScriptExecution, stopScriptExecution, UNIQUE_APIS } from './scripts/slash-commands.js';
190191import {
191192 tag_map,
192193 tags,
@@ -266,7 +267,7 @@ import { initServerHistory } from './scripts/server-history.js';
266267import { initSettingsSearch } from './scripts/setting-search.js';
267268import { initBulkEdit } from './scripts/bulk-edit.js';
268269import { getContext } from './scripts/st-context.js';
269270import { extractReasoningFromData, extractReasoningSignatureFromData, initReasoning, parseReasoningInSwipes, PromptReasoning, ReasoningHandler, removeReasoningFromString, updateReasoningUI } from './scripts/reasoning.js';
270271import { accountStorage } from './scripts/util/AccountStorage.js';
271272import { initWelcomeScreen, openPermanentAssistantChat, openPermanentAssistantCard, getPermanentAssistantAvatar } from './scripts/welcome-screen.js';
272273import { initDataMaid } from './scripts/data-maid.js';
@@ -278,6 +279,9 @@ import { applyStreamFadeIn } from './scripts/util/stream-fadein.js';
278279import { initDomHandlers } from './scripts/dom-handlers.js';
279280import { SimpleMutex } from './scripts/util/SimpleMutex.js';
280281import { AudioPlayer } from './scripts/audio-player.js';
282+import { MacroEnvBuilder } from './scripts/macros/engine/MacroEnvBuilder.js';
283+import { MacroEngine } from './scripts/macros/engine/MacroEngine.js';
284+import { addChatBackupsBrowser } from './scripts/chat-backups.js';
281285
282286// API OBJECT FOR EXTERNAL WIRING
283287globalThis.SillyTavern = {
@@ -373,18 +377,21 @@ export let name1 = default_user_name;
373377export let name2 = systemUserName;
374378/** @type {ChatMessage[]} */
375379export let chat = [];
376-export 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+ */
384+export let swipeState = SWIPE_STATE.NONE;
377385let chatSaveTimeout;
378386let importFlashTimeout;
379387export let isChatSaving = false;
380-let chat_create_date = '';
381388let firstRun = false;
382389let settingsReady = false;
383390let currentVersion = '0.0.0';
384391export let displayVersion = 'SillyTavern';
385392
386393let generation_started = new Date();
387394/** @type {import('./scripts/char-data.js').v1CharDataCharacter[]} */
388395export let characters = [];
389396/**
390397 * Stringified index of a currently chosen entity in the characters array.
@@ -411,6 +418,7 @@ export const chatElement = $('#chat');
411418
412419let dialogueResolve = null;
413420let dialogueCloseStop = false;
421+/** @type {ChatMetadata} */
414422export let chat_metadata = {};
415423/** @type {StreamingProcessor} */
416424export let streamingProcessor = null;
@@ -562,6 +570,7 @@ let chat_file_for_del = '';
562570export let online_status = 'no_connection';
563571
564572export let is_send_press = false; //Send generation
573+export const isGenerating = () => (is_send_press || is_group_generating);
565574
566575let this_del_mes = -1;
567576
@@ -575,7 +584,14 @@ export let settings;
575584export let amount_gen = 80; //default max length of AI generated responses
576585export let max_context = 2048;
577586
578-var swipes = true;
587+/** User preference for swipeable messages */
588+let swipes = true;
589+/** Forcefully hide swipes. */
590+export let swipesHidden = false;
591+/** @type {{ now: number, direction: string }} */
592+export let lastSwipeInfo = { now: performance.now(), direction: SWIPE_DIRECTION.RIGHT };
593+export let recentSwipes = 0;
594+
579595export let extension_prompts = {};
580596
581597export let main_api;// = "kobold";
@@ -629,7 +645,7 @@ export async function pingServer() {
629645 try {
630646 const result = await fetch('api/ping', {
631647 method: 'POST',
632648 headers: getRequestHeaders({ omitContentType: true }),
633649 });
634650
635651 if (!result.ok) {
@@ -693,6 +709,7 @@ async function firstLoadInit() {
693709 initBackgrounds();
694710 initAuthorsNote();
695711 await initPersonas();
712+ await initSlashCommandAutoComplete();
696713 initWorldInfo();
697714 initHorde();
698715 initRossMods();
@@ -730,7 +747,7 @@ function initStandaloneMode() {
730747 }
731748}
732749
733750export function cancelStatusCheck(reason = 'Manually cancelled status check') {
734751 abortStatusCheck?.abort(new AbortReason(reason));
735752 abortStatusCheck = new AbortController();
736753 setOnlineStatus('no_connection');
@@ -1029,9 +1046,6 @@ function verifyCharactersSearchSortRule() {
10291046 }
10301047}
10311048
1032-/** @typedef {object} Character - A character */
1033-/** @typedef {object} Group - A group */
1034-
10351049/**
10361050 * @typedef {object} Entity - Object representing a display entity
10371051 * @property {Character|Group|import('./scripts/tags.js').Tag|*} item - The item
@@ -1295,7 +1309,7 @@ export async function deleteCharacterChatByName(characterId, fileName) {
12951309 // Make sure all the data is loaded.
12961310 await unshallowCharacter(characterId);
12971311
12981312 /** @type {import('./scripts/char-data.js').v1CharDataCharacter} */
12991313 const character = characters[characterId];
13001314 if (!character) {
13011315 console.warn(`Character with ID ${characterId} not found.`);
@@ -1380,10 +1394,11 @@ export async function showMoreMessages(messagesToLoad = null) {
13801394
13811395 while (messageId > 0 && count > 0) {
13821396 let newMessageId = messageId - 1;
13831397 addOneMessage(chat[newMessageId], { insertBefore: messageId >= chat.length ? null : messageId, scroll: false, forceId: newMessageId, showSwipes: false });
13841398 count--;
13851399 messageId--;
13861400 }
1401+ refreshSwipeButtons();
13871402
13881403 if (messageId == 0) {
13891404 $('#show_more_messages').remove();
@@ -1414,13 +1429,13 @@ export async function printMessages() {
14141429
14151430 chatElement.find('.mes').removeClass('last_mes');
14161431 chatElement.find('.mes').last().addClass('last_mes');
14171432 refreshSwipeButtons(false, false);
14181433 applyStylePins();
14191434 scrollChatToBottom({ waitForFrame: true });
14201435 delay(debounce_timeout.short).then(() => scrollOnMediaLoad());
14211436}
14221437
14231438export function scrollOnMediaLoad() {
14241439 const started = Date.now();
14251440 const media = chatElement.find('.mes_block img, .mes_block video, .mes_block audio').toArray();
14261441 let mediaLoaded = 0;
@@ -1475,6 +1490,7 @@ export async function clearChat() {
14751490 if (is_delete_mode) {
14761491 $('#dialogue_del_mes_cancel').trigger('click');
14771492 }
1493+ //This will also remove non '.mes' elements, e.g. '<div id="show_more_messages">Show more messages</div>'.
14781494 chatElement.children().remove();
14791495 if ($('.zoomed_avatar[forChar]').length) {
14801496 console.debug('saw avatars to remove');
@@ -1579,9 +1595,17 @@ export async function reloadCurrentChat() {
15791595 * Send the message currently typed into the chat box.
15801596 */
15811597export 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.
15821604 if (is_send_press) return;
15831605 if (isExecutingCommandsFromChatInput) return;
15841606
1607+ hideSwipeButtons(); //Swipe buttons must be hidden now, otherwise concurrent generations are possible.
1608+
15851609 let generateType = 'normal';
15861610 // "Continue on send" is activated when the user hits "send" (or presses enter) on an empty chat box, and the last
15871611 // message was sent from a character (not the user or the system).
@@ -1601,7 +1625,9 @@ export async function sendTextareaMessage() {
16011625 await newAssistantChat({ temporary: false });
16021626 }
16031627
16041628 returnlet generation = await Generate(generateType);
1629+ showSwipeButtons();
1630+ return generation;
16051631}
16061632
16071633/**
@@ -2366,7 +2392,7 @@ export function addCopyToCodeBlocks(messageElement) {
23662392 * @param {boolean} [options.scroll=true] Whether to scroll to the new message
23672393 * @param {number} [options.insertBefore=null] Message ID to insert the new message before
23682394 * @param {number} [options.forceId=null] Force the message ID
23692395 * @param {boolean} [options.showSwipes=true] Whether to showrefresh the swipe buttons.
23702396 * @returns {void}
23712397 */
23722398export 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
24902516 });
24912517
24922518 if (type === 'swipe') {
2493- const messageId = forceId ?? chat.length - 1;
2519+ const swipeMessage = chatElement.find(`[mesid="${newMessageId}"]`);
2494- const swipeMessage = chatElement.find(`[mesid="${messageId}"]`);
24952520 swipeMessage.attr('swipeid', params.swipeId);
24962521 swipeMessage.find('.mes_text').html(messageText).attr('title', title);
24972522 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
25092534 swipeMessage.find('.tokenCounterDisplay').empty();
25102535 }
25112536 } 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);
25142538 appendMediaToMessage(mes, newMessage, scroll ? SCROLL_BEHAVIOR.ADJUST : SCROLL_BEHAVIOR.NONE);
2515- showSwipes && hideSwipeButtons();
25162539 }
25172540
25182541 addCopyToCodeBlocks(newMessage);
25192542
25202543 // Set the swipes counter for past messages, only visible if 'Show Swipes on All Message'all isnon-user enabledmessages.
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));
25252546 }
25262547
2527- if (showSwipes) {
2548+ //last_mes should always be updated.
2549+ chatElement.find('.mes').removeClass('last_mes');
25282550 chatElement.find('.mes').last().addClass('last_mes');
2529- chatElement.find('.mes').eq(-2).removeClass('last_mes');
2551+ if (showSwipes) {
25302552 refreshSwipeButtons();
25312553 }
25322554
25332555 // Don't scroll if not inserting last
25342556 if (!insertAfter && !insertBefore && scroll) {
25352557 scrollChatToBottom({ waitForFrame: true });
25362558 }
25372559
25382560 applyCharacterTagsToMessageDivs({ mesIds: newMessageId });
@@ -2642,14 +2664,13 @@ export function scrollChatToBottom({ waitForFrame } = {}) {
26422664}
26432665
26442666/**
2667+ * @deprecated Function is not needed anymore, as the new signature of substituteParams is more flexible.
2668+ *
26452669 * 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.
26492670 * @returns {string} The string with substituted parameters.
26502671 */
26512672export function substituteParamsExtended(content, additionalMacro = {}, postProcessFn = (x) => x) {
26522673 return substituteParams(content, undefined, undefined, undefined, undefined,{ true,dynamicMacros: additionalMacro, postProcessFn });
26532674}
26542675
26552676/**
@@ -2664,11 +2685,24 @@ export function substituteParamsExtended(content, additionalMacro = {}, postProc
26642685 * @param {(x: string) => string} [postProcessFn] - Post-processing function for each substituted macro.
26652686 * @returns {string} The string with substituted parameters.
26662687 */
26672688export function substituteParamssubstituteParamsLegacy(content, _name1, _name2, _original, _group, _replaceCharacterCard = true, additionalMacro = {}, postProcessFn = (x) => x) {
26682689 if (!content) {
26692690 return '';
26702691 }
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+
26722706 const environment = {};
26732707
26742708 if (typeof _original === 'string') {
@@ -2768,6 +2802,55 @@ export function substituteParams(content, _name1, _name2, _original, _group, _re
27682802 return evaluateMacros(content, environment, postProcessFn);
27692803}
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+ */
2823+export 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
27722855/**
27732856 * Gets stopping sequences for the prompt.
@@ -3076,10 +3159,19 @@ export async function getExtensionPrompt(position = extension_prompt_types.IN_PR
30763159 return values;
30773160}
30783161
3079-export 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+ */
3172+export function baseChatReplace(value, name1Override = null, name2Override = null) {
3173+ if (typeof value === 'string' && value.length > 0) {
3174+ value = substituteParams(value, { name1Override, name2Override, replaceCharacterCard: false });
30833175
30843176 if (power_user.collapse_newlines) {
30853177 value = collapseNewlines(value);
@@ -3091,11 +3183,7 @@ export function baseChatReplace(value, name1, name2) {
30913183}
30923184
30933185/**
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
30993187 * @property {string} system System prompt
31003188 * @property {string} mesExamples Message examples
31013189 * @property {string} description Description
@@ -3106,57 +3194,119 @@ export function baseChatReplace(value, name1, name2) {
31063194 * @property {string} version Character version
31073195 * @property {string} charDepthPrompt Character depth note
31083196 * @property {string} creatorNotes Character creator notes
3109- * @returns {CharacterCardFields} Character card fields
31103197 */
3111-export function getCharacterCardFields({ chid = null } = {}) {
3112- const currentChid = chid ?? this_chid;
31133198
3114- const result = {
3199+/**
3115- system: '',
3200+ * Helper to create an object with lazy, memoized getters from a map of field resolvers.
3116- mesExamples: '',
3201+ * @param {Record<string, () => string>} resolvers Map of field names to resolver functions
3117- description: '',
3202+ * @returns {CharacterCardFields} Object with lazy getters
3118- personality: '',
3203+ */
3119- persona: '',
3204+export function createLazyFields(resolvers) {
3120- scenario: '',
3205+ const result = /** @type {CharacterCardFields} */ ({});
3121- jailbreak: '',
3206+ for (const [key, resolver] of Object.entries(resolvers)) {
3122- version: '',
3207+ let cached;
3123- charDepthPrompt: '',
3208+ let resolved = false;
3124- creatorNotes: '',
3209+ Object.defineProperty(result, key, {
3125- };
3210+ get() {
3126- result.persona = baseChatReplace(power_user.persona_description?.trim(), name1, name2);
3211+ if (!resolved) {
3212+ cached = resolver();
3213+ resolved = true;
3214+ }
3215+ return cached;
3216+ },
3217+ enumerable: true,
3218+ configurable: true,
3219+ });
3220+ }
3221+ return result;
3222+}
31273223
3224+/**
3225+ * Returns the character card fields for the current character as lazy getters.
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+ */
3231+export function getCharacterCardFieldsLazy({ chid = undefined } = {}) {
3232+ const currentChid = chid ?? this_chid;
31283233 const character = characters[currentChid];
31293234
3130- if (!character) {
3235+ // For group chats, we need to check if group cards should be used
3131- return result;
3236+ const useGroupCards = selected_group && character;
3132- }
3237+ const groupCardsLazy = useGroupCards ? getGroupCharacterCardsLazy(selected_group, Number(currentChid)) : null;
31333238
3239+ /** @type {Record<string, () => string>} */
3240+ const resolvers = {
3241+ persona: () => baseChatReplace(power_user.persona_description?.trim()),
3242+ system: () => {
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 '';
31343274 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 '';
31353280 const exampleDialog = chat_metadata['mes_example'] || character.mes_example || '';
3136- const systemPrompt = chat_metadata['system_prompt'] || character.data?.system_prompt || '';
3281+ return baseChatReplace(exampleDialog.trim());
3137-
3282+ },
3138- result.description = baseChatReplace(character.description?.trim(), name1, name2);
3283+ };
3139- result.personality = baseChatReplace(character.personality?.trim(), name1, name2);
3140- result.scenario = baseChatReplace(scenarioText.trim(), name1, name2);
3141- result.mesExamples = baseChatReplace(exampleDialog.trim(), name1, name2);
3142- result.system = power_user.prefer_character_prompt ? baseChatReplace(systemPrompt.trim(), name1, name2) : '';
3143- result.jailbreak = power_user.prefer_character_jailbreak ? baseChatReplace(character.data?.post_history_instructions?.trim(), name1, name2) : '';
3144- result.version = character.data?.character_version ?? '';
3145- result.charDepthPrompt = baseChatReplace(character.data?.extensions?.depth_prompt?.prompt?.trim(), name1, name2);
3146- result.creatorNotes = baseChatReplace(character.data?.creator_notes?.trim(), name1, name2);
3147-
3148- if (selected_group) {
3149- const groupCards = getGroupCharacterCards(selected_group, Number(currentChid));
31503284
3151- if (groupCards) {
3285+ return createLazyFields(resolvers);
3152- result.description = groupCards.description;
3153- result.personality = groupCards.personality;
3154- result.scenario = groupCards.scenario;
3155- result.mesExamples = groupCards.mesExamples;
3156- }
31573286}
31583287
3159- return result;
3288+/**
3289+ * Returns the character card fields for the current character.
3290+ * @param {Object} [options={}]
3291+ * @param {number} [options.chid] Optional character index
3292+ * @returns {CharacterCardFields} Character card fields
3293+ */
3294+export function getCharacterCardFields({ chid = undefined } = {}) {
3295+ const lazy = getCharacterCardFieldsLazy({ chid });
3296+
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+ };
31603310}
31613311
31623312/**
@@ -3247,6 +3397,8 @@ class StreamingProcessor {
32473397 this.promptReasoning = promptReasoning;
32483398 /** @type {string[]} */
32493399 this.images = [];
3400+ /** @type {string?} */
3401+ this.reasoningSignature = null;
32503402 }
32513403
32523404 /**
@@ -3279,7 +3431,7 @@ class StreamingProcessor {
32793431 }
32803432
32813433 markUIGenStopped() {
32823434 activateSendButtonsunblockGeneration();
32833435 }
32843436
32853437 async onStartStreaming(text) {
@@ -3408,14 +3560,14 @@ class StreamingProcessor {
34083560 }
34093561
34103562 async onFinishStreaming(messageId, text) {
3411- this.markUIGenStopped();
34123563 await this.onProgressStreaming(messageId, text, true);
34133564 addCopyToCodeBlocks(const messageElement = chatElement.find(`.mes[mesid="${messageId}"]`));
3565+ const message = chat[messageId];
3566+ addCopyToCodeBlocks(messageElement);
34143567
34153568 await this.reasoningHandler.finish(messageId);
34163569
34173570 if (Array.isArray(this.swipes) && this.swipes.length > 0) {
3418- const message = chat[messageId];
34193571 const swipeInfoExtra = structuredClone(message.extra ?? {});
34203572 delete swipeInfoExtra.token_count;
34213573 delete swipeInfoExtra.reasoning;
@@ -3428,15 +3580,26 @@ class StreamingProcessor {
34283580 };
34293581 const swipeInfoArray = Array(this.swipes.length).fill().map(() => structuredClone(swipeInfo));
34303582 parseReasoningInSwipes(this.swipes, swipeInfoArray, message.extra?.reasoning_duration);
34313583 chat[messageId]message.swipes.push(...this.swipes);
34323584 chat[messageId]message.swipe_info.push(...swipeInfoArray);
34333585 }
34343586
3587+ syncMesToSwipe(messageId);
3588+ saveLogprobsForActiveMessage(this.messageLogprobs.filter(Boolean), this.continueMessage);
3589+
34353590 if (Array.isArray(this.images) && this.images.length > 0) {
34363591 await processImageAttachment(chat[messageId]message, { imageUrls: this.images });
34373592 appendMediaToMessage(chat[messageId]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;
34383599 }
34393600
3601+ this.markUIGenStopped();
3602+
34403603 if (this.type !== 'impersonate') {
34413604 await eventSource.emit(event_types.MESSAGE_RECEIVED, this.messageId, this.type);
34423605 await eventSource.emit(event_types.CHARACTER_MESSAGE_RENDERED, this.messageId, this.type);
@@ -3444,15 +3607,13 @@ class StreamingProcessor {
34443607 await eventSource.emit(event_types.IMPERSONATE_READY, text);
34453608 }
34463609
3447- syncMesToSwipe(messageId);
3610+ updateSwipeCounter(messageId, { message, messageElement });
3448- saveLogprobsForActiveMessage(this.messageLogprobs.filter(Boolean), this.continueMessage);
3449- await saveChatConditional();
3450- unblockGeneration();
34513611
34523612 const isAborted = this.abortController.signal.aborted;
34533613 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 });
34553615 }
3616+ await saveChatConditional();
34563617
34573618 playMessageSound();
34583619 }
@@ -3462,7 +3623,6 @@ class StreamingProcessor {
34623623 this.isStopped = true;
34633624
34643625 this.markUIGenStopped();
3465- unblockGeneration();
34663626
34673627 const noEmitTypes = ['swipe', 'impersonate', 'continue'];
34683628 if (!noEmitTypes.includes(this.type)) {
@@ -3532,6 +3692,7 @@ class StreamingProcessor {
35323692 // Get the updated reasoning string into the handler
35333693 this.reasoningHandler.updateReasoning(this.messageId, state?.reasoning);
35343694 this.images = state?.images ?? [];
3695+ this.reasoningSignature = state?.signature ?? null;
35353696 await eventSource.emit(event_types.STREAM_TOKEN_RECEIVED, text);
35363697 await sw.tick(async () => await this.onProgressStreaming(this.messageId, this.continueMessage + text));
35373698 }
@@ -4252,7 +4413,7 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
42524413 continue;
42534414 }
42544415
42554416 const formattedExample = baseChatReplace(exampleMessage, name1, name2);
42564417 const cleanedExample = parseMesExamples(formattedExample, isInstruct);
42574418
42584419 // Insert depending on before or after position
@@ -4296,9 +4457,9 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
42964457 if (main_api !== 'openai') {
42974458 if (power_user.sysprompt.enabled) {
42984459 system = power_user.prefer_character_prompt && system
42994460 ? substituteParams(system, name1,{ name2,original: (power_user.sysprompt.content ?? '') })
43004461 : baseChatReplace(power_user.sysprompt.content, name1, name2);
43014462 system = isInstruct ? substituteParams(system, name1,{ name2,original: power_user.sysprompt.content ?? '' }) : system;
43024463 } else {
43034464 // Nullify if it's not enabled
43044465 system = '';
@@ -4356,8 +4517,8 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
43564517
43574518 if (main_api !== 'openai' && power_user.sysprompt.enabled) {
43584519 jailbreak = power_user.prefer_character_jailbreak && jailbreak
43594520 ? substituteParams(jailbreak, name1,{ name2,original: (power_user.sysprompt.post_history ?? '') })
43604521 : baseChatReplace(power_user.sysprompt.post_history, name1, name2);
43614522
43624523 // Only inject the jb if there is one
43634524 if (jailbreak) {
@@ -5094,6 +5255,7 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
50945255 let title = extractTitleFromData(data);
50955256 let reasoning = extractReasoningFromData(data);
50965257 let imageUrls = extractImagesFromData(data);
5258+ const reasoningSignature = extractReasoningSignatureFromData(data);
50975259 kobold_horde_model = title;
50985260
50995261 const swipes = extractMultiSwipes(data, type);
@@ -5137,10 +5299,10 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
51375299 else {
51385300 // Without streaming we'll be having a full message on continuation. Treat it as a last chunk.
51395301 if (originalType !== 'continue') {
51405302 ({ type, getMessage } = await saveReply({ type, getMessage, title, swipes, reasoning, imageUrls, reasoningSignature }));
51415303 }
51425304 else {
51435305 ({ type, getMessage } = await saveReply({ type: 'appendFinal', getMessage, title, swipes, reasoning, imageUrls, reasoningSignature }));
51445306 }
51455307
51465308 // 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
51755337 const isAborted = abortController && abortController.signal.aborted;
51765338 if (!isAborted && power_user.auto_swipe && generatedTextFiltered(getMessage)) {
51775339 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+
51795342 }
51805343
51815344 console.debug('/api/chats/save called by /Generate');
@@ -5307,7 +5470,6 @@ function unblockGeneration(type) {
53075470
53085471 is_send_press = false;
53095472 activateSendButtons();
5310- showSwipeButtons();
53115473 setGenerationProgress(0);
53125474 flushEphemeralStoppingStrings();
53135475 flushWIInjections();
@@ -5851,7 +6013,7 @@ export function extractMessageFromData(data, activeApi = null) {
58516013 case 'koboldhorde':
58526014 return data.text;
58536015 case 'textgenerationwebui':
58546016 return data.choices?.[0]?.text ?? data.choices?.[0]?.message?.content ?? data.content ?? data.response ?? data[0]?.content ?? '';
58556017 case 'novel':
58566018 return data.output;
58576019 case 'openai':
@@ -5908,6 +6070,7 @@ export function extractJsonFromData(data, { mainApi = null, chatCompletionSource
59086070 case chat_completion_sources.COHERE:
59096071 case chat_completion_sources.XAI:
59106072 case chat_completion_sources.ELECTRONHUB:
6073+ case chat_completion_sources.CHUTES:
59116074 case chat_completion_sources.AZURE_OPENAI:
59126075 case chat_completion_sources.ZAI:
59136076 default:
@@ -5937,6 +6100,22 @@ function extractMultiSwipes(data, type) {
59376100 return swipes;
59386101 }
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+
59406119 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))) {
59416120 if (!Array.isArray(data.choices)) {
59426121 return swipes;
@@ -5950,18 +6129,18 @@ function extractMultiSwipes(data, type) {
59506129
59516130 for (let i = 1; i < data.choices.length; i++) {
59526131 const text = data?.choices[i]?.message?.content ?? data?.choices[i]?.text ?? '';
5953- const cleanedText = cleanUpMessage({
6132+ swipes.push(text);
6133+ }
6134+ }
6135+
6136+ const cleanedSwipes = swipes.map(text => cleanUpMessage({
59546137 getMessage: text,
59556138 isImpersonate: false,
59566139 isContinue: false,
59576140 displayIncompleteSentences: false,
59586141 }));
5959-
5960- swipes.push(cleanedText);
5961- }
5962- }
59636142
59646143 return swipescleanedSwipes;
59656144}
59666145
59676146/**
@@ -6172,16 +6351,17 @@ async function processImageAttachment(message, { imageUrls }) {
61726351 * @property {string[]} [swipes] Extra swipes
61736352 * @property {string} [reasoning] Message reasoning
61746353 * @property {string[]} [imageUrls] Links to images
6354+ * @property {string?} [reasoningSignature] Encrypted signature of the reasoning text
61756355 *
61766356 * @typedef {object} SaveReplyResult
61776357 * @property {string} type Type of generation
61786358 * @property {string} getMessage Generated message
61796359 */
61806360export async function saveReply({ type, getMessage, fromStreaming = false, title = '', swipes = [], reasoning = '', imageUrls = [], reasoningSignature = null }) {
61816361 // Backward compatibility
61826362 if (arguments.length > 1 && typeof arguments[0] !== 'object') {
61836363 console.trace('saveReply called with positional arguments. Please use an object instead.');
61846364 [type, getMessage, fromStreaming, title, swipes, reasoning, imageUrls, reasoningSignature] = arguments;
61856365 }
61866366
61876367 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
62176397 chat[chat.length - 1]['extra']['model'] = getGeneratingModel();
62186398 chat[chat.length - 1]['extra']['reasoning'] = reasoning;
62196399 chat[chat.length - 1]['extra']['reasoning_duration'] = null;
6400+ chat[chat.length - 1]['extra']['reasoning_signature'] = reasoningSignature;
62206401 await processImageAttachment(chat[chat.length - 1], { imageUrls });
62216402 if (power_user.message_token_count_enabled) {
62226403 const tokenCountText = (reasoning || '') + chat[chat.length - 1]['mes'];
@@ -6241,6 +6422,7 @@ export async function saveReply({ type, getMessage, fromStreaming = false, title
62416422 chat[chat.length - 1]['extra']['model'] = getGeneratingModel();
62426423 chat[chat.length - 1]['extra']['reasoning'] = reasoning;
62436424 chat[chat.length - 1]['extra']['reasoning_duration'] = null;
6425+ chat[chat.length - 1]['extra']['reasoning_signature'] = reasoningSignature;
62446426 await processImageAttachment(chat[chat.length - 1], { imageUrls });
62456427 if (power_user.message_token_count_enabled) {
62466428 const tokenCountText = (reasoning || '') + chat[chat.length - 1]['mes'];
@@ -6261,6 +6443,7 @@ export async function saveReply({ type, getMessage, fromStreaming = false, title
62616443 chat[chat.length - 1]['extra']['api'] = getGeneratingApi();
62626444 chat[chat.length - 1]['extra']['model'] = getGeneratingModel();
62636445 chat[chat.length - 1]['extra']['reasoning'] += reasoning;
6446+ chat[chat.length - 1]['extra']['reasoning_signature'] = reasoningSignature;
62646447 await processImageAttachment(chat[chat.length - 1], { imageUrls });
62656448 // We don't know if the reasoning duration extended, so we don't update it here on purpose.
62666449 if (power_user.message_token_count_enabled) {
@@ -6283,6 +6466,7 @@ export async function saveReply({ type, getMessage, fromStreaming = false, title
62836466 chat[chat.length - 1]['extra']['model'] = getGeneratingModel();
62846467 chat[chat.length - 1]['extra']['reasoning'] = reasoning;
62856468 chat[chat.length - 1]['extra']['reasoning_duration'] = null;
6469+ chat[chat.length - 1]['extra']['reasoning_signature'] = reasoningSignature;
62866470 if (power_user.trim_spaces) {
62876471 getMessage = getMessage.trim();
62886472 }
@@ -6362,6 +6546,63 @@ export async function saveReply({ type, getMessage, fromStreaming = false, title
63626546}
63636547
63646548/**
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+ */
6553+export 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+/**
63656606 * 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).
63666607 *
63676608 * If the swipe data is invalid in some way, this function will exit out without doing anything.
@@ -6515,7 +6756,7 @@ export function getGeneratingApi() {
65156756 }
65166757}
65176758
65186759export function getGeneratingModel(mes) {
65196760 let model = '';
65206761 switch (main_api) {
65216762 case 'kobold':
@@ -6543,6 +6784,7 @@ function getGeneratingModel(mes) {
65436784export function activateSendButtons() {
65446785 is_send_press = false;
65456786 hideStopButton();
6787+ showSwipeButtons();
65466788 delete document.body.dataset.generating;
65476789}
65486790
@@ -6551,6 +6793,7 @@ export function activateSendButtons() {
65516793 */
65526794export function deactivateSendButtons() {
65536795 showStopButton();
6796+ hideSwipeButtons();
65546797 document.body.dataset.generating = 'true';
65556798}
65566799
@@ -6731,7 +6974,7 @@ export async function renameCharacter(name = null, { silent = false, renameChats
67316974 }
67326975
67336976 // Also rename as a group member
67346977 await renameGroupMember(oldAvatar, newAvatar, newValue.toString());
67356978 const renamePastChatsConfirm = renameChats !== null
67366979 ? renameChats
67376980 : silent
@@ -6860,6 +7103,11 @@ export function saveChatDebounced() {
68607103 * @returns {Promise<void>}
68617104 */
68627105export 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+
68637111 if (arguments.length > 0 && typeof arguments[0] !== 'object') {
68647112 console.trace('saveChat called with positional arguments. Please use an object instead.');
68657113 [chatName, withMetadata, mesId, force] = arguments;
@@ -6879,26 +7127,17 @@ export async function saveChat({ chatName, withMetadata, mesId, force = false }
68797127 }
68807128
68817129 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
68897131 const trimmedChat = (mesId !== undefined && mesId >= 0 && mesId < chat.length)
68907132 ? chat.slice(0, Number(mesId) + 1)
68917133 : chat.slice();
68927134
6893- const chatToSave = [
7135+ /** @type {ChatHeader} */
6894- {
7136+ const chatHeader = {
6895- user_name: name1,
6896- character_name: name2,
6897- create_date: chat_create_date,
68987137 chat_metadata: metadata,
6899- },
7138+ user_name: 'unused',
6900- ...trimmedChat,
7139+ character_name: 'unused',
69017140 ]};
69027141
69037142 try {
69047143 const result = await fetch('/api/chats/save', {
@@ -6908,7 +7147,7 @@ export async function saveChat({ chatName, withMetadata, mesId, force = false }
69087147 body: JSON.stringify({
69097148 ch_name: characters[this_chid].name,
69107149 file_name: fileName,
69117150 chat: chatToSave[chatHeader, ...trimmedChat],
69127151 avatar_url: characters[this_chid].avatar,
69137152 force: force,
69147153 }),
@@ -7082,7 +7321,7 @@ export async function unshallowCharacter(characterId) {
70827321 return;
70837322 }
70847323
70857324 /** @type {import('./scripts/char-data.js').v1CharDataCharacter} */
70867325 const character = characters[characterId];
70877326 if (!character) {
70887327 console.debug('Character not found:', characterId);
@@ -7121,13 +7360,10 @@ export async function getChat() {
71217360 });
71227361 if (response[0] !== undefined) {
71237362 chat.splice(0, chat.length, ...response);
7124- chat_create_date = chat[0]['create_date'];
71257363 chat_metadata = chat[0]['chat_metadata'] ?? {};
71267364
71277365 chat.shift();
71287366 chat.forEach(ensureMessageMediaIsArray);
7129- } else {
7130- chat_create_date = humanizedDateTime();
71317367 }
71327368 if (!chat_metadata['integrity']) {
71337369 chat_metadata['integrity'] = uuidv4();
@@ -7221,8 +7457,8 @@ export async function openCharacterChat(file_name) {
72217457
72227458////////// OPTIMZED MAIN API CHANGE FUNCTION ////////////
72237459
72247460export function changeMainAPI(api = null) {
72257461 const selectedVal = api ?? $('#main_api').val();
72267462 //console.log(selectedVal);
72277463 const apiElements = {
72287464 'koboldhorde': {
@@ -7596,8 +7832,11 @@ function updateMessage(div) {
75967832 const mesElement = div.closest('.mes');
75977833 const mes = chat[mesElement.attr('mesid')];
75987834
7835+ // editing old messages
7836+ mes['extra'] ??= {};
7837+
75997838 let regexPlacement;
76007839 if (mes?.is_user) {
76017840 regexPlacement = regex_placement.USER_INPUT;
76027841 } else if (mes.extra?.type === 'narrator') {
76037842 regexPlacement = regex_placement.SLASH_COMMAND;
@@ -7627,15 +7866,11 @@ function updateMessage(div) {
76277866 }
76287867 mes['mes'] = text;
76297868 if (mes['swipe_id'] !== undefined) {
7869+ ensureSwipes(mes);
76307870 mes['swipes'][mes['swipe_id']] = text;
76317871 }
76327872
7633- // editing old messages
7873+ 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) {
76397874 mes.extra.bias = bias ?? null;
76407875 } else {
76417876 mes.extra.bias = null;
@@ -7706,8 +7941,7 @@ export async function messageEdit(editMessageId) {
77067941 this_edit_mes_id = editMessageId;
77077942 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
77127946 const chatScrollPosition = chatElement.scrollTop();
77137947 const messageBlock = messageElement.find('.mes_block');
@@ -7762,7 +7996,7 @@ async function messageEditCancel(messageId = this_edit_mes_id) {
77627996 if (this?.classList?.contains('mes_edit_cancel')) {
77637997 thisMesDiv = $(this).closest('.mes');
77647998 } else {
77657999 thisMesDiv = chatElement.children('.mes').filter(`[mesid="${messageId}"]`);
77668000 }
77678001
77688002 const thisMesBlock = thisMesDiv.find('.mes_block');
@@ -7843,11 +8077,17 @@ async function messageEditMove(sourceId, targetId) {
78438077 }
78448078
78458079 updateViewMessageIds();
8080+ refreshSwipeButtons();
78468081 await saveChatConditional();
78478082 return true;
78488083}
78498084
78508085async 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+
78518091 let { mesBlock, text, mes, bias } = updateMessage(div);
78528092 if (this_edit_mes_id == 0) {
78538093 text = substituteParams(text);
@@ -8016,7 +8256,7 @@ export async function displayPastChats(hightlightNames = []) {
80168256 });
80178257
80188258 // Define the search input listener
80198259 $('#select_chat_search').off('input').on('input', function () {
80208260 const searchQuery = $(this).val();
80218261 debouncedDisplay(searchQuery);
80228262 });
@@ -8026,6 +8266,8 @@ export async function displayPastChats(hightlightNames = []) {
80268266 const textSearchElement = $('#select_chat_search');
80278267 textSearchElement.trigger('click').trigger('focus').trigger('select');
80288268 }, 200);
8269+
8270+ addChatBackupsBrowser();
80298271}
80308272
80318273async function displayChats(searchQuery, currentChat, displayName, avatarImg, selected_group, highlightNames) {
@@ -8258,7 +8500,7 @@ export function select_selected_character(chid, { switchMenu = true } = {}) {
82588500 $('#talkativeness_slider').val(characters[chid].talkativeness || talkativeness_default);
82598501 $('#mes_example_textarea').val(characters[chid].mes_example);
82608502 $('#selected_chat_pole').val(characters[chid].chat);
82618503 $('#create_date_pole').val(timestampToMoment(characters[chid].create_date).toISOString());
82628504 $('#avatar_url_pole').val(characters[chid].avatar);
82638505 $('#chat_import_avatar_url').val(characters[chid].avatar);
82648506 $('#chat_import_character_name').val(characters[chid].name);
@@ -8572,99 +8814,191 @@ export function callPopup(text, type, inputValue = '', { okButton, rows, wide, w
85728814
85738815/**
85748816 * Update the swipe counter for mesId.
8817+ * By default, the swipe counter's opacity will appear greyed out. The opacity is changed with CSS.
85758818 * @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.
85768822 */
85778823export 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];
85798825 const currentMessagemessageElement ??= 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);
85828835}
85838836
85848837/**
85858838 * Swipe buttons areReturns oftentrue toggledif tomessages updateare theirgenerally positionswipeable.
8586- * This should be replaced with a more efficient function.
8839+ * @returns {boolean}
85878840 */
85888841export function refreshSwipeButtonsisSwipingAllowed() {
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+ );
85918852}
85928853
8593-export 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+ */
8862+export 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);
85988868 }
85998869
86008870 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.
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; }
86088889}
86098890
8610- // swipe_id should be set if alternate greetings are added
8891+/**
8611- if (chat.length == 1 && chat[0].swipe_id === undefined) {
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+ */
8900+export 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+ */
8927+export function refreshSwipeButtons(updateCounters = false, fade = true) {
8928+ //Never show swipe buttons on an empty chat.
8929+ if (chat?.length === 0) return false;
8930+
8931+ //If swipes are disabled or hidden, hide all swipe buttons.
8932+ if (!isSwipingAllowed()) {
8933+ $('body').addClass('hideAllSwipeButtons');
86128934 return;
8935+ //Don't hide all swipe buttons.
8936+ } else {
8937+ //CSS will hide all messages.
8938+ $('body').removeClass('hideAllSwipeButtons');
86138939 }
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 work
8943+ 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 happen
8954+ 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.
8958+ const isLastSwipe = (message?.swipes?.length ?? 1) - 1 <= (message?.swipe_id ?? 0);
8959+ const hasSwipes = (message?.swipes?.length > 1);
8960+ const overswipe = getOverswipeBehavior(messageId, message);
8961+
8962+ // Chevrons should always be shown on pristine greetings: https://github.com/SillyTavern/SillyTavern/pull/4712#issuecomment-3557893373
8963+ const pristineGreeting = overswipe == OVERSWIPE_BEHAVIOR.PRISTINE_GREETING;
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;
8969+
8970+ div.classList.toggle('last_swipe', isOverswipeable);
8971+
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');
86458980 }
8646- if ((chat[mesId].swipes.length - swipeId) === 1) {
8981+ });
8647- //chevron was moved out of hardcode in HTML to class toggle dependent on last_mes or not
8648- //necessary for 'swipe_right' div in past messages to have no chevron if 'show swipes for all messages' is turned on
8649- swipeRight.css('opacity', '0.7');
8650- swipeCounter.css('opacity', '0.7');
86518982}
8652-
8983+/**
8653- //allows for writing individual swipe counters for past messages
8984+ * This function is misleadingly named. It allows generation then refreshes the swipe buttons and counters.
8654- const lastSwipeCounter = $('.last_mes .swipes-counter');
8985+ */
8655- lastSwipeCounter.text(swipeCounterText).show();
8986+export function showSwipeButtons() {
8987+ swipesHidden = false;
8988+ refreshSwipeButtons();
86568989}
86578990
86588991/**
8992+ * This function is misleadingly named. It blocks generation then refreshes the swipe buttons and counters.
86598993 * @param {object} [options] Options
86608994 * @param {boolean} [options.hideCounters=false] Also hide the swipes counter.
86618995 */
86628996export function hideSwipeButtons({ hideCounters = false } = {}) {
86638997 isSwipingAllowedswipesHidden = falsetrue;
8664- chatElement.find('.swipe_right').hide();
8998+ refreshSwipeButtons();
8665- chatElement.find('.swipe_left').hide();
8999+
86669000 if (hideCounters === true) {
86679001 chatElement.find('.last_mes .swipes-counter').hideprop('hidden', true);
86689002 }
86699003}
86709004
@@ -8707,25 +9041,23 @@ export async function deleteSwipe(swipeId = null, messageId = chat.length - 1) {
87079041
87089042 // Select the next swipe, or the one before if it was the last one
87099043 const newSwipeId = Math.min(swipeId, message.swipes.length - 1);
8710- syncSwipeToMes(messageId, newSwipeId);
87119044
87129045 chat_metadata['tainted'] = true;
87139046
9047+ messageId = Number(messageId);
9048+ swipeId = Number(swipeId);
87149049 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
87169054 await saveChatConditional();
8717- await reloadCurrentChat();
87189055
87199056 return newSwipeId;
87209057}
87219058
87229059export 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- }
87299061}
87309062
87319063export async function saveChatConditional() {
@@ -8765,7 +9097,7 @@ export async function saveChatConditional() {
87659097 * @param {boolean} [options.refresh] Whether to refresh the group chat list after import
87669098 * @returns {Promise<string[]>} List of imported file names.
87679099 */
87689100export async function importCharacterChat(formData, { refresh = true } = {}) {
87699101 const fetchResult = await fetch('/api/chats/import', {
87709102 method: 'POST',
87719103 body: formData,
@@ -8809,7 +9141,7 @@ export function updateEditArrowClasses() {
88099141 return;
88109142 }
88119143
88129144 const message = chatElement.findchildren('.mes').filter(`.mes[mesid="${this_edit_mes_id}"]`);
88139145
88149146 const downButton = message.find('.mes_edit_down');
88159147 const upButton = message.find('.mes_edit_up');
@@ -9244,6 +9576,23 @@ export async function createOrEditCharacter(e) {
92449576}
92459577
92469578/**
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+ */
9583+export 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+/**
92479596 * Formats a counter for a swipe view.
92489597 * @param {number} current The current number of items.
92499598 * @param {number} total The total number of items.
@@ -9258,41 +9607,61 @@ function formatSwipeCounter(current, total) {
92589607
92599608/**
92609609 * Handles the swipe event.
92619610 * @param {JQuery.EventSwipeEvent} _eventevent Event.
92629611 * @param {'left'|'right'SWIPE_DIRECTION} direction The direction to swipe.
92639612 * @param {object} params Additional parameters.
92649613 * @param {stringimport('./scripts/constants.js').SWIPE_SOURCE} [params.source] The source of the swipe event. null, 'keyboard', 'auto_swipe', 'back' or 'delete'.
92659614 * @param {boolean} [params.repeated] Is the swipe event repeated.
92669615 * @param {objectChatMessage} [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.
92679619 */
92689620export async function swipe(_eventevent, direction, { source, repeated, message = chat[chat.length - 1], forceMesId, forceSwipeId, forceDuration } = {}) {
92699621 if (chat.length === 0) {
92709622 console.warn('Swipe was called on an empty chat.');
92719623 return;
92729624 }
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;
92829626 let messageIndex;
92839627
92849628 //Only set messageIndex if message exists because -1 is truthy.
92859629 if (message) {
92869630 messageIndex = chat.indexOf(message);
92879631 if (messageIndex === -1 && typeof (forceMesId) != 'number') {
92889632 console.error(`The message must exist in chat. ${message};`);
92899633 return;
92909634 }
92919635 }
92929636
92939637 const mesId = Number($(this)forceMesId ?? event?.currentTarget?.closest('.mes')?.attrgetAttribute('mesid') ?? messageIndex ?? chat.length - 1);
9638+
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;
92949663
92959664 const thisMesDiv = chatElement.children('.mes').filter(`.mes[mesid="${mesId}"]`);
92969665 const thisMesText = thisMesDiv.find('.mes_block .mes_text');
92979666 const thisMesDivHeight = thisMesDiv[0]?.scrollHeight;
92989667 const thisMesTextHeight = thisMesText[0]?.scrollHeight;
@@ -9301,43 +9670,108 @@ export async function swipe(_event, direction, { source, repeated, message = cha
93019670 return;
93029671 }
93039672 const originalSwipeId = Number(chat[mesId]?.['swipe_id'] ?? 0);
93049673 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) {
93119706 //Wait for the generation to end.
93129707 try {
9708+ //`mes_buttons` need to be hidden until the animation completes.
9709+ if (generation) {
9710+ document.body.dataset.swiping = 'true';
93139711 await generation;
93149712 }
9713+ }
93159714 catch (error) {
93169715 console.warn(`Swipe failed, Swiping back. ${error}`);
93179716 }
9318- //Allow for another swipe.
9319- showSwipeButtons();
93209717
93219718 //Clamp Id between swipes.
93229719 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-
93319721 await updateSwipeCounter(mesId);
93329722 //Fallback.
93339723 if (mesId != chat.length - 1) {
93349724 await updateSwipeCounter(chat.length - 1);
93359725 }
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();
93369770 }
93379771
93389772 async function standardSwipe(newSwipeId) {
93399773 //If swipe_id has changed, or the source is being deleted.
93409774 if (newSwipeId !== originalSwipeId || source == 'delete'SWIPE_SOURCE.DELETE || source == SWIPE_SOURCE.BACK) {
93419775 //Update the chat.
93429776 await loadFromSwipeId(mesId, newSwipeId);
93439777 //Transition to the new chat.
@@ -9347,6 +9781,27 @@ export async function swipe(_event, direction, { source, repeated, message = cha
93479781 }
93489782
93499783 /**
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+ /**
93509805 * Sets the message to the newSwipeId and loads it.
93519806 * @param {number} mesId
93529807 * @param {number} newSwipeId
@@ -9355,55 +9810,89 @@ export async function swipe(_event, direction, { source, repeated, message = cha
93559810 //Update the swipe_id.
93569811 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+
93659820 delete chat[mesId].extra.generationTypeswipe_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;
93699824 }
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
93869826 /**
93879827 * Animates a swipe for all messages >= mesId.
93889828 * @param {number} mesId
93899829 * @param {numberobject} xparams
93909830 * @param {numberstring} duration[params.xStart='opx']
9391- */
9831+ * @param {string} [params.xEnd='0px']
9392- async function animateSwipeTransition(mesId, x, duration) {
9832+ * @param {number} [params.duration=animation_duration]
9393- //Selects the swiped message.
9833+ * @param {string} [params.classes=''] Additional CSS classes to target during the swipe.
9394- const swipedMessagesDiv = chatElement.children().filter((index, div) => {
9834+ * @param {boolean} [params.freeze=true] When true, do not remove the class from the animation, leaving it stuck at xEnd.
9395- const $div = $(div);
9835+ * @returns {Promise<boolean|Function>} endSlide unfreezes the messages from xEnd.
9396- return mesId === Number($div.attr('mesid'));
9836+ */
9397- });
9837+ async function animateSwipeTransition(mesId, { xStart = '0px', xEnd = '0px', duration = animation_duration, classes = '', freeze = false } = {}) {
9398- const swipedElementsDiv = swipedMessagesDiv.children('.mes_block, .mesAvatarWrapper');
9838+ // If the animation_duration is zero, the 'animationend' promise will never resolve.
9839+ //Skip the animation if it's faster than 50ms.
9840+ if (duration <= 50) return;
9841+
9842+ //Select MAXIMUM_ANIMATED messages after mesId. Ideally, only visible messages would be animated.
9843+ const MAXIMUM_ANIMATED = 100;
9844+
9845+ const messages = chatElement.children('.mes');
9846+ const firstDisplayedMesId = Number(messages.first().attr('mesid'));
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);
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+ }
93999889
9400- //Swipe.
9890+ //If not frozen, end the slide now.
9401- await transitionPromise(swipedElementsDiv, {
9891+ return freeze ? endSlide : endSlide();
9402- x: x,
9892+ }
9403- duration: duration,
9893+ }
9404- easing: animation_easing,
9894+ console.warn(`No animatable messages were found after message #${mesId}.`);
9405- queue: false,
9895+ return false;
9406- });
94079896 }
94089897
94099898 function getMessageBottomHeight(thisMesDiv) {
@@ -9445,11 +9934,15 @@ export async function swipe(_event, direction, { source, repeated, message = cha
94459934 /**
94469935 * Anime a swipe, optionally running a generation.
94479936 * @param {boolean} run_generate
9937+ * @param {boolean} [skipSwipeOut=false]
94489938 */
94499939 async function animateSwipe(run_generate = false, skipSwipeOut = false) {
94509940
9941+ if (!skipSwipeOut) {
94519942 //Swipe out.
94529943 await animateSwipeTransition(mesId, { xEnd: `${swipeRange}px`, duration: swipeDuration });
9944+ }
9945+
94539946
94549947 if (run_generate) {
94559948 await updateSwipeCounter(mesId);
@@ -9465,7 +9958,8 @@ export async function swipe(_event, direction, { source, repeated, message = cha
94659958
94669959 //Only scroll when swiping the last message.
94679960 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
94709964 if (power_user.message_token_count_enabled) {
94719965 if (!chat[mesId].extra) {
@@ -9483,10 +9977,6 @@ export async function swipe(_event, direction, { source, repeated, message = cha
94839977 thisMesDiv.css('height', thisMesDivHeight);
94849978 expandNewMessage(thisMesDiv);
94859979
9486-
9487- //Jump to the opposite side.
9488- await animateSwipeTransition(mesId, -swipeRange, 0);
9489-
94909980 appendMediaToMessage(chat[mesId], thisMesDiv);
94919981
94929982 await eventSource.emit(event_types.MESSAGE_SWIPED, (mesId));
@@ -9494,12 +9984,10 @@ export async function swipe(_event, direction, { source, repeated, message = cha
94949984 if (run_generate && !is_send_press) {
94959985 is_send_press = true;
94969986 generation = Generate('swipe');
9497- } else if (Number(chat[mesId]['swipe_id']) !== chat[mesId]['swipes'].length) {
9498- saveChatDebounced();
94999987 }
95009988
95019989 //Swipe in from the opposite side.
9502- await animateSwipeTransition(mesId, 0, swipeDuration);
9990+ await animateSwipeTransition(mesId, { xStart: `${-swipeRange}px`, xEnd: `${0}px`, duration: swipeDuration });
95039991 }
95049992
95059993 if (mesId === Number(this_edit_mes_id)) {
@@ -9514,7 +10002,7 @@ export async function swipe(_event, direction, { source, repeated, message = cha
951410002 }
951510003
951610004 //If the swipe is not being deleted.
951710005 if (source != 'delete'SWIPE_SOURCE.DELETE && source != SWIPE_SOURCE.BACK) {
951810006
951910007 // Make sure ad-hoc changes to extras are saved before swiping away
952010008 syncMesToSwipe(mesId);
@@ -9533,19 +10021,19 @@ export async function swipe(_event, direction, { source, repeated, message = cha
953310021 }
953410022 // If the user is holding down the key and we're at the last or first swipe, don't do anything.
953510023 let isLastSwipe = (direction === SWIPE_DIRECTION.RIGHT) ? (chat[mesId].swipe_id === Math.max(0, chat[mesId]['swipes'].length - 1)) : chat[mesId].swipe_id === 0;
953610024 if (source === 'keyboard'SWIPE_SOURCE.KEYBOARD && repeated && isLastSwipe) {
953710025 await endSwipe();
953810026 return;
953910027 }
954010028 } else if (source == 'delete'SWIPE_SOURCE.DELETE || source == SWIPE_SOURCE.BACK) {
954110029 //If the swipe is being deleted or reverted.
954210030 await standardSwipe(newSwipeId);
954310031 return;
954410032 }
954510033
954610034 //If swiping left.
954710035 if (direction === SWIPE_DIRECTION.LEFT) {
9548- newSwipeId--;
10036+ if (forceSwipeId == null) newSwipeId--;
954910037 //Loop to last swipe if negative.
955010038 if (newSwipeId < 0) {
955110039 newSwipeId = Math.max(0, chat[mesId]['swipes'].length - 1);
@@ -9557,13 +10045,13 @@ export async function swipe(_event, direction, { source, repeated, message = cha
955710045 await endSwipe();
955810046 return;
955910047 }
956010048 await standardSwipe(newSwipeId);
956110049 return;
956210050 }
956310051 //If swiping right.
956410052 else if (direction === SWIPE_DIRECTION.RIGHT) {
956510053 // make new slot in array
9566- newSwipeId++;
10054+ if (forceSwipeId == null) newSwipeId++;
956710055
956810056 //Minimum of zero.
956910057 if (newSwipeId < 0) {
@@ -9573,35 +10061,37 @@ export async function swipe(_event, direction, { source, repeated, message = cha
957310061 return;
957410062 }
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.
957710065 if (newSwipeId >= chat[mesId]['swipes'].length && ((chat.length !== 1 || !isPristine))) {
957810066 newSwipeId = chat[mesId]['swipes'].length;
957910067
958010068 //Update the swipe_id.
958110069 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) {
958510075 //Cancel swipe.
958610076 chat[mesId]['swipe_id'] = originalSwipeId;
958710077 await endSwipe();
958810078 return;
958910079 } else {
9590- //Generate.
10080+ //Regenerate the message
9591- await loadFromSwipeId(mesId, newSwipeId);
10081+ else if (overswipe == OVERSWIPE_BEHAVIOR.REGENERATE) {
10082+ clearMessageData(chat[mesId]);
959210083 let run_generate = true;
10084+ //Generate.
959310085 await animateSwipe(run_generate);
959410086 await endSwipe();
959510087 return;
959610088 }
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) {
960110091 newSwipeId = 0;
960210092 }
960310093 }
960410094 await standardSwipe(newSwipeId);
960510095 return;
960610096 }
960710097}
@@ -9609,28 +10099,28 @@ export async function swipe(_event, direction, { source, repeated, message = cha
960910099/**
961010100 * @deprecated Use `swipe` instead.
961110101 * Handles the swipe to the left event.
961210102 * @param {JQuery.EventSwipeEvent} _event[event] Event.
961310103 * @param {object} params Additional parameters.
961410104 * @param {stringimport('./scripts/constants.js').SWIPE_SOURCE} [params.source] The source of the swipe event. null, 'keyboard', 'auto_swipe', 'back' or 'delete'.
961510105 * @param {boolean} [params.repeated] Is the swipe event repeated.
961610106 * @param {object} [params.message] The chat message to swipe.
961710107 */
961810108export async function swipe_left(_eventevent, { source, repeated, message } = {}) {
961910109 await swipe.call(this, _eventevent, SWIPE_DIRECTION.LEFT, { source: source, repeated: repeated, message: message });
962010110}
962110111
962210112/**
962310113 * @deprecated Use `swipe` instead.
962410114 * Handles the swipe to the right event.
962510115 * @param {JQuery.EventSwipeEvent} [_eventevent] Event.
962610116 * @param {object} params Additional parameters.
962710117 * @param {stringimport('./scripts/constants.js').SWIPE_SOURCE} [params.source] The source of the swipe event. null, 'keyboard', 'auto_swipe', 'back' or 'delete'.
962810118 * @param {boolean} [params.repeated] Is the swipe event repeated.
962910119 * @param {object} [params.message] The chat message to swipe.
963010120 */
963110121//MARK: swipe_right
963210122export async function swipe_right(_eventevent = null, { source, repeated, message } = {}) {
963310123 await swipe.call(this, _eventevent, SWIPE_DIRECTION.RIGHT, { source: source, repeated: repeated, message: message });
963410124}
963510125
963610126/**
@@ -10356,7 +10846,7 @@ jQuery(async function () {
1035610846 });
1035710847 $('#rm_button_selected_ch').on('click', function () {
1035810848 if (selected_group) {
1035910849 select_group_chats(selected_group, false);
1036010850 } else {
1036110851 selected_button = 'character_edit';
1036210852 select_selected_character(this_chid);
@@ -10800,10 +11290,12 @@ jQuery(async function () {
1080011290 }
1080111291
1080211292 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+ }
1080411298 if (is_send_press == false) {
10805- //hideSwipeButtons();
10806-
1080711299 if (selected_group) {
1080811300 regenerateGroup();
1080911301 }
@@ -10822,7 +11314,14 @@ jQuery(async function () {
1082211314 }
1082311315
1082411316 else if (id == 'option_continue') {
10825- if (this_edit_mes_id >= 0) return; // don't proceed if editing a message
11317+ 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
1082711326 if (is_send_press == false || fromSlashCommand) {
1082811327 is_send_press = true;
@@ -11289,7 +11788,7 @@ jQuery(async function () {
1128911788
1129011789 $('#rm_button_group_chats').on('click', function () {
1129111790 selected_button = 'group_chats';
1129211791 select_group_chats(null, false);
1129311792 });
1129411793
1129511794 $('#rm_button_back_from_group').on('click', function () {
public/scripts/PromptManager.js+4 -12
@@ -579,10 +579,6 @@ class PromptManager {
579579 const sourceName = this.promptSources[promptId];
580580 entrySource.textContent = sourceName;
581581 }
582-
583- if (!this.systemPrompts.includes(promptId)) {
584- injectionPositionField.removeAttribute('disabled');
585- }
586582 };
587583
588584 // Append prompt to selected character
@@ -1284,10 +1280,10 @@ class PromptManager {
12841280 const preparedPrompt = new Prompt(prompt);
12851281
12861282 if (typeof original === 'string') {
12871283 if (0 < groupMembers.length) preparedPrompt.content = substituteParams(prompt.content ?? '', null, null,{ original, groupOverride: groupMembers.join(', ') });
12881284 else preparedPrompt.content = substituteParams(prompt.content, null, null,{ original });
12891285 } else {
12901286 if (0 < groupMembers.length) preparedPrompt.content = substituteParams(prompt.content ?? '', null, null,{ null,groupOverride: groupMembers.join(', ') });
12911287 else preparedPrompt.content = substituteParams(prompt.content);
12921288 }
12931289
@@ -1399,10 +1395,6 @@ class PromptManager {
13991395 entrySource.textContent = sourceName;
14001396 }
14011397
1402- if (this.systemPrompts.includes(prompt.identifier)) {
1403- injectionPositionField.setAttribute('disabled', 'disabled');
1404- }
1405-
14061398 const resetPromptButton = document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_reset');
14071399 if (true === prompt.system_prompt) {
14081400 resetPromptButton.style.display = 'block';
@@ -1757,7 +1749,7 @@ class PromptManager {
17571749 ${isInjectionPrompt ? '<span class="fa-fw fa-solid fa-syringe" title="In-Chat Injection"></span>' : ''}
17581750 ${this.isPromptInspectionAllowed(prompt) ? `<a title="${encodedName}" class="prompt-manager-inspect-action">${encodedName}</a>` : `<span title="${encodedName}">${encodedName}</span>`}
17591751 ${roleIcon ? `<span data-role="${escapeHtml(prompt.role)}" class="fa-xs fa-solid ${roleIcon}" title="${roleTitle}"></span>` : ''}
17601752 ${isInjectionPrompt ? `<small class="prompt-manager-injection-depth">@ ${escapeHtml(prompt.injection_depth.toString())}</small>` : ''}
17611753 ${isOverriddenPrompt ? '<small class="fa-solid fa-address-card prompt-manager-overridden" title="Pulled from a character card"></small>' : ''}
17621754 </span>
17631755 <span>
public/scripts/RossAscends-mods.js+39 -42
@@ -37,7 +37,7 @@ import { debounce, getStringHash, isValidUrl } from './utils.js';
3737import { chat_completion_sources, oai_settings } from './openai.js';
3838import { getTokenCountAsync } from './tokenizers.js';
3939import { textgen_types, textgenerationwebui_settings as textgen_settings, getTextGenServer } from './textgen-settings.js';
4040import { debounce_timeout, SWIPE_SOURCE } from './constants.js';
4141
4242import { Popup } from './popup.js';
4343import { accountStorage } from './util/AccountStorage.js';
@@ -162,43 +162,37 @@ export function shouldSendOnEnter() {
162162 }
163163}
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`
169-export function humanizedDateTime() {
169+ */
170170 constexport nowfunction =humanizedDateTime(timestamp new= Date(Date.now()); {
171+ const date = new Date(timestamp);
171172 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(),
174180 };
175181 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');
177184 }
178185 return `${dt.year}-${dt.month}-${dt.day}@${dt.hour}h${dt.minute}m${dt.second}s${dt.millisecond}ms`;
179186}
180187
181-//this is a common format version to display a timestamp on each chat message
188+/**
182-//returns something like: June 19, 2023 2:20pm
189+ * Gets a timestamp for messages in ISO 8601 format.
183-export 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);
193+export function getMessageTimeStamp(timestamp = Date.now()) {
187194 const monthdate = months[d.getMonthnew Date(timestamp)];
188195 const day =return ddate.getDatetoISOString();
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;
202196}
203197
204198
@@ -403,6 +397,7 @@ function RA_autoconnect(PrevApi) {
403397 || (secret_state[SECRET_KEYS.COHERE] && oai_settings.chat_completion_source == chat_completion_sources.COHERE)
404398 || (secret_state[SECRET_KEYS.PERPLEXITY] && oai_settings.chat_completion_source == chat_completion_sources.PERPLEXITY)
405399 || (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)
406401 || (secret_state[SECRET_KEYS.SILICONFLOW] && oai_settings.chat_completion_source == chat_completion_sources.SILICONFLOW)
407402 || (secret_state[SECRET_KEYS.ELECTRONHUB] && oai_settings.chat_completion_source == chat_completion_sources.ELECTRONHUB)
408403 || (secret_state[SECRET_KEYS.NANOGPT] && oai_settings.chat_completion_source == chat_completion_sources.NANOGPT)
@@ -930,7 +925,7 @@ export function initRossMods() {
930925 var SwipeButR = $('.swipe_right:last');
931926 var SwipeTargetMesClassParent = $(e.target).closest('.last_mes');
932927 if (SwipeTargetMesClassParent !== null) {
933928 if (SwipeButR.cssis('display:visible') === 'flex') {
934929 SwipeButR.trigger('click');
935930 }
936931 }
@@ -954,7 +949,7 @@ export function initRossMods() {
954949 var SwipeButL = $('.swipe_left:last');
955950 var SwipeTargetMesClassParent = $(e.target).closest('.last_mes');
956951 if (SwipeTargetMesClassParent !== null) {
957952 if (SwipeButL.cssis('display:visible') === 'flex') {
958953 SwipeButL.trigger('click');
959954 }
960955 }
@@ -975,10 +970,10 @@ export function initRossMods() {
975970
976971 function isModifiedKeyboardEvent(event) {
977972 return (event instanceof KeyboardEvent &&
978973 (event.shiftKey ||
979974 event.ctrlKey ||
980975 event.altKey ||
981976 event.metaKey));
982977 }
983978
984979 $(document).on('keydown', async function (event) {
@@ -1112,29 +1107,31 @@ export function initRossMods() {
11121107
11131108 if (event.key == 'ArrowLeft') { //swipes left
11141109 if (
11151110 isSwipingAllowed() &&
11161111 !isNanogallery2LightboxActive() && // Check if lightbox is NOT active
11171112 $('#send_textarea').val() === '' &&
11181113 $('#character_popup').css('display') === 'none' &&
11191114 $('#shadow_select_chat_popup').css('display') === 'none' &&
11201115 !isInputElementInFocus() &&
11211116 !isModifiedKeyboardEvent(event) &&
1117+ !(document.activeElement instanceof HTMLVideoElement)
11221118 ) {
11231119 $('.swipe_left:last').trigger('click', { source: 'keyboard'SWIPE_SOURCE.KEYBOARD, repeated: event.repeat });
11241120 return;
11251121 }
11261122 }
11271123 if (event.key == 'ArrowRight') { //swipes right
11281124 if (
11291125 isSwipingAllowed() &&
11301126 !isNanogallery2LightboxActive() && // Check if lightbox is NOT active
11311127 $('#send_textarea').val() === '' &&
11321128 $('#character_popup').css('display') === 'none' &&
11331129 $('#shadow_select_chat_popup').css('display') === 'none' &&
11341130 !isInputElementInFocus() &&
11351131 !isModifiedKeyboardEvent(event) &&
1132+ !(document.activeElement instanceof HTMLVideoElement)
11361133 ) {
11371134 $('.swipe_right:last').trigger('click', { source: 'keyboard'SWIPE_SOURCE.KEYBOARD, repeated: event.repeat });
11381135 return;
11391136 }
11401137 }
public/scripts/a11y.js+14 -0
@@ -50,6 +50,14 @@ const toolbarSelectors = [
5050 '.jg-menu',
5151].join(', ');
5252
53+const tabListSelectors = [
54+ '#bg_tabs .bg_tabs_list',
55+].join(', ');
56+
57+const tabItemSelectors = [
58+ '#bg_tabs .bg_tabs_list .bg_tab_button',
59+].join(', ');
60+
5361/** @type {Record<string, (element: Element) => void>} */
5462const a11yRules = {
5563 [buttonSelectors]: (element) => {
@@ -64,6 +72,12 @@ const a11yRules = {
6472 [toolbarSelectors]: (element) => {
6573 element.setAttribute('role', 'toolbar');
6674 },
75+ [tabListSelectors]: (element) => {
76+ element.setAttribute('role', 'tablist');
77+ },
78+ [tabItemSelectors]: (element) => {
79+ element.setAttribute('role', 'tab');
80+ },
6781 '#toast-container .toast': (element) => {
6882 element.setAttribute('role', 'status');
6983 },
public/scripts/authors-note.js+21 -4
@@ -19,7 +19,7 @@ import { SlashCommand } from './slash-commands/SlashCommand.js';
1919import { ARGUMENT_TYPE, SlashCommandArgument } from './slash-commands/SlashCommandArgument.js';
2020export { MODULE_NAME as NOTE_MODULE_NAME };
2121import { t } from './i18n.js';
2222import { MacrosParsermacros, MacroCategory } from './macros/macro-system.js';
2323
2424const MODULE_NAME = '2_floating_prompt'; // <= Deliberate, for sorting lower than memory
2525
@@ -581,7 +581,24 @@ export function initAuthorsNote() {
581581 }));
582582 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`);
587585}
586+
587+function 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+
6+import { AutoCompleteOption } from './AutoCompleteOption.js';
7+import {
8+ formatMacroSignature,
9+ createSourceIndicator,
10+ createAliasIndicator,
11+ renderMacroDetails,
12+} from '../macros/MacroBrowser.js';
13+import { 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+
27+export 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+ */
218+export 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+149 -23
@@ -1,12 +1,15 @@
11import { Fuse, localforage } from '../lib.js';
22import { characters, chat_metadata, eventSource, event_types, generateQuietPrompt, getCurrentChatId, getRequestHeaders, getThumbnailUrl, saveMetadata, saveSettingsDebounced, this_chid } from '../script.js';
33import { openThirdPartyExtensionMenu, saveMetadataDebounced } from './extensions.js';
44import { SlashCommand } from './slash-commands/SlashCommand.js';
55import { SlashCommandParser } from './slash-commands/SlashCommandParser.js';
66import { createThumbnail, flashHighlight, getBase64Async, stringFormat, debounce, setupScrollToTop, saveBase64AsFile, getFileExtension } from './utils.js';
77import { debounce_timeout } from './constants.js';
88import { t } from './i18n.js';
99import { Popup } from './popup.js';
10+import { groups, selected_group } from './group-chats.js';
11+import { humanizedDateTime } from './RossAscends-mods.js';
12+import { deleteMediaFromServer } from './chats.js';
1013
1114const BG_METADATA_KEY = 'custom_background';
1215const LIST_METADATA_KEY = 'chat_backgrounds';
@@ -39,6 +42,26 @@ const THUMBNAIL_CONFIG = {
3942};
4043
4144/**
45+ * Background source types.
46+ * @readonly
47+ * @enum {number}
48+ */
49+const 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+ */
59+const BG_TABS = Object.freeze({
60+ [BG_SOURCES.GLOBAL]: 'bg_global_tab',
61+ [BG_SOURCES.CHAT]: 'bg_chat_tab',
62+});
63+
64+/**
4265 * Global IntersectionObserver instance for lazy loading backgrounds
4366 * @type {IntersectionObserver|null}
4467 */
@@ -187,6 +210,7 @@ function onLockBackgroundClick(event = null) {
187210
188211 // Update UI states to reflect the new lock.
189212 highlightLockedBackground();
213+ highlightSelectedBackground();
190214}
191215
192216/**
@@ -219,21 +243,27 @@ function removeBackgroundMetadata() {
219243 saveMetadataDebounced();
220244}
221245
222-function onSelectBackgroundClick() {
246+/**
247+ * Handles the click event for selecting a background.
248+ * @param {JQuery.Event} e Event
249+ */
250+function onSelectBackgroundClick(e) {
223251 const bgFile = $(this).attr('bgfile');
252+ const isCustom = $(this).attr('custom') === 'true';
224253 const backgroundCssUrl = getUrlParameter(this);
254+ const bypassGlobalLock = !isCustom && e.shiftKey;
225255
226256 if ((isChatBackgroundLocked() || isCustom) && !bypassGlobalLock) {
227257 // If a background is locked, update the locked background directly
228258 saveBackgroundMetadata(backgroundCssUrl);
229259 $('#bg1').css('background-image', backgroundCssUrl);
230- highlightLockedBackground();
231260 } else {
232261 // Otherwise, update the global background setting
233262 setBackground(bgFile, backgroundCssUrl);
234263 }
235264
236265 // Update UI highlights to reflect the changes.
266+ highlightLockedBackground();
237267 highlightSelectedBackground();
238268}
239269
@@ -375,20 +405,37 @@ async function onDeleteBackgroundClick(e) {
375405 const bgToDelete = $(this).closest('.bg_example');
376406 const url = bgToDelete.data('url');
377407 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+ });
379426 const bg = bgToDelete.attr('bgfile');
380427
381428 if (confirm) {
382429 // If it's not custom, it's a built-in background. Delete it from the server
383430 if (!isCustom) {
384431 await delBackground(bg);
385432 } else {
386433 const list = chat_metadata[LIST_METADATA_KEY] || [];
387434 const index = list.indexOf(bg);
388435 list.splice(index, 1);
389436 }
390437
391438 if (bg === background_settings.name || url === chat_metadata[BG_METADATA_KEY]) {
392439 const siblingSelector = '.bg_example';
393440 const nextBg = bgToDelete.next(siblingSelector);
394441 const prevBg = bgToDelete.prev(siblingSelector);
@@ -409,13 +456,18 @@ async function onDeleteBackgroundClick(e) {
409456
410457 if (url === chat_metadata[BG_METADATA_KEY]) {
411458 removeBackgroundMetadata();
412- highlightLockedBackground();
413459 }
414460
415461 if (isCustom) {
462+ if (deleteFromServer) {
463+ await deleteMediaFromServer(bg);
464+ }
416465 renderChatBackgrounds();
417466 saveMetadataDebouncedawait saveMetadata();
418467 }
468+
469+ highlightLockedBackground();
470+ highlightSelectedBackground();
419471 }
420472}
421473
@@ -607,25 +659,43 @@ async function delBackground(bg) {
607659 }
608660}
609661
610-async 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+ */
667+async function onBackgroundUploadSelected(e) {
668+ const input = e.currentTarget;
612669
613670 if (!(forminput instanceof HTMLFormElementHTMLInputElement)) {
614671 console.error('form_bg_uploadInvalid isinput notelement afor formbackground upload');
615672 return;
616673 }
617674
618- const formData = new FormData(form);
675+ for (const file of input.files) {
619-
676+ if (file.size === 0) {
620- const file = formData.get('avatar');
677+ continue;
621- if (!(file instanceof File) || file.size === 0) {
622- form.reset();
623- return;
624678 }
625679
680+ const formData = new FormData();
681+ formData.append('avatar', file);
682+
626683 await convertFileIfVideo(formData);
684+ switch (getActiveBackgroundTab()) {
685+ case BG_SOURCES.GLOBAL:
627686 await uploadBackground(formData);
628- form.reset();
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+ }
695+ }
696+
697+ // Allow re-uploading the same file again by clearing the input value
698+ input.value = '';
629699}
630700
631701/**
@@ -699,6 +769,50 @@ async function uploadBackground(formData) {
699769}
700770
701771/**
772+ * Upload a chat background using a FormData object.
773+ * @param {FormData} formData FormData containing the background file
774+ * @returns {Promise<void>}
775+ */
776+async 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+/**
702816 * @param {string} bg
703817 */
704818function highlightNewBackground(bg) {
@@ -746,6 +860,14 @@ function onBackgroundFilterInput() {
746860
747861const debouncedOnBackgroundFilterInput = debounce(onBackgroundFilterInput, debounce_timeout.standard);
748862
863+/**
864+ * Gets the active background tab source.
865+ * @returns {BG_SOURCES} Active background tab source
866+ */
867+export function getActiveBackgroundTab() {
868+ return $('#bg_tabs').tabs('option', 'active');
869+}
870+
749871export function initBackgrounds() {
750872 eventSource.on(event_types.CHAT_CHANGED, onChatChanged);
751873 eventSource.on(event_types.FORCE_SET_BACKGROUND, forceSetBackground);
@@ -797,7 +919,7 @@ export function initBackgrounds() {
797919 applyThumbnailColumns(background_settings.thumbnailColumns + 1);
798920 });
799921 $('#auto_background').on('click', autoBackgroundCommand);
800922 $('#add_bg_button').on('change', (e) => onBackgroundUploadSelected(e.originalEvent));
801923 $('#bg-filter').on('input', () => debouncedOnBackgroundFilterInput());
802924 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
803925 name: 'lockbg',
@@ -839,9 +961,13 @@ export function initBackgrounds() {
839961 await onChatChanged();
840962 });
841963
964+ Object.values(BG_TABS).forEach(tabId => {
842965 setupScrollToTop({
843- scrollContainerId: 'bg-scrollable-content',
966+ scrollContainerId: tabId,
844967 buttonId: 'bg-scroll-top',
845968 drawerId: 'Backgrounds',
846969 });
970+ });
971+
972+ $('#bg_tabs').tabs();
847973}
public/scripts/bookmarks.js+25 -20
@@ -11,6 +11,7 @@ import {
1111 chat,
1212 saveChatConditional,
1313 saveItemizedPrompts,
14+ setActiveGroup,
1415} from '../script.js';
1516import { humanizedDateTime } from './RossAscends-mods.js';
1617import {
@@ -298,28 +299,35 @@ export async function convertSoloToGroupChat() {
298299 const chats = [chatName];
299300 const members = [character.avatar];
300301 const favChecked = character.fav || character.fav == 'true';
301302 /** @type {anyChatMetadata} */
302303 const metadata = Object.assign({}, chat_metadata);
303304 delete metadata.main_chat;
304-
305+ /** @type {ChatHeader} */
305- const createGroupResponse = await fetch('/api/groups/create', {
306+ const chatHeader = {
306307 methodchat_metadata: 'POST'metadata,
307308 headersuser_name: getRequestHeaders()'unused',
308- body: JSON.stringify({
309+ character_name: 'unused',
310+ };
311+ /** @type {Omit<Group, 'id'>} */
312+ const groupCreateModel = {
309313 name: name,
310314 members: members,
311315 avatar_url: avatar,
312316 allow_self_responses: false,
313317 activation_strategy: group_activation_strategy.NATURAL,
314318 disabled_members: [],
315- chat_metadata: metadata,
316319 fav: favChecked,
317320 chat_id: chatName,
318321 chats: chats,
319322 hideMutedSprites: false,
320323 generation_mode: group_generation_mode.SWAP,
321324 auto_mode_delay: DEFAULT_AUTO_MODE_DELAY,
322- }),
325+ };
326+
327+ const createGroupResponse = await fetch('/api/groups/create', {
328+ method: 'POST',
329+ headers: getRequestHeaders(),
330+ body: JSON.stringify(groupCreateModel),
323331 });
324332
325333 if (!createGroupResponse.ok) {
@@ -327,6 +335,7 @@ export async function convertSoloToGroupChat() {
327335 return;
328336 }
329337
338+ /** @type {Group} */
330339 const group = await createGroupResponse.json();
331340
332341 // Convert tags list and assign to group
@@ -336,39 +345,34 @@ export async function convertSoloToGroupChat() {
336345 await getCharacters();
337346
338347 // Convert chat to group format
339348 const groupChat = [...chat].slicemap(m => structuredClone(m));
340349 const genIdFirst = Date.now();
341350
342351 for (let index = 0; index < groupChat.length; index++) {
343352 const message = groupChat[index];
344353
345- // Save group-chat marker
346- if (index == 0) {
347- // @ts-ignore
348- message.is_group = true;
349- }
350-
351354 // Skip messages we don't care about
352355 if (message.is_user || message.is_system || message.extra?.type === system_message_types.NARRATOR || message.force_avatar !== undefined) {
353356 continue;
354357 }
355358
359+ if (!message.extra || typeof message.extra !== 'object') {
360+ message.extra = {};
361+ }
362+
356363 // Set force fields for solo character
357364 message.name = character.name;
358365 message.original_avatar = character.avatar;
359366 message.force_avatar = getThumbnailUrl('avatar', character.avatar);
360-
361367 // 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- }
365369 }
366370
367371 // Save group chat
368372 const createChatResponse = await fetch('/api/chats/group/save', {
369373 method: 'POST',
370374 headers: getRequestHeaders(),
371375 body: JSON.stringify({ id: chatName, chat: [chatHeader, ...groupChat] }),
372376 });
373377
374378 if (!createChatResponse.ok) {
@@ -378,6 +382,7 @@ export async function convertSoloToGroupChat() {
378382 }
379383
380384 // Click on the freshly selected group to open it
385+ setActiveGroup(group.id);
381386 await openGroupById(group.id);
382387
383388 toastr.success(t`The chat has been successfully converted!`);
public/scripts/chat-backups.js+335 -0
@@ -0,0 +1,335 @@
1+import { t } from './i18n.js';
2+import { callGenericPopup, Popup, POPUP_TYPE } from './popup.js';
3+import { getFileExtension, sortMoments, timestampToMoment } from './utils.js';
4+import { displayPastChats, getRequestHeaders, importCharacterChat } from '/script.js';
5+import { importGroupChat } from './group-chats.js';
6+
7+class 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+
324+const backupsBrowser = new BackupsBrowser();
325+
326+export 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 {
1010 event_types,
1111 getCurrentChatId,
1212 getRequestHeaders,
13- name1,
1413 name2,
1514 reloadCurrentChat,
1615 saveSettingsDebounced,
@@ -349,8 +348,7 @@ async function validateFile(file) {
349348export function hasPendingFileAttachment() {
350349 const fileInput = document.getElementById('file_form_input');
351350 if (!(fileInput instanceof HTMLInputElement)) return false;
352351 const file =return fileInput.files[.length > 0];
353- return !!file;
354352}
355353
356354/**
@@ -2140,13 +2138,15 @@ export function initChatUtilities() {
21402138 await viewMessageFile(messageId, fileIndex);
21412139 });
21422140
21432141 $(document).on('click', '.assistant_note_export', async function (_e) {
2144- const chatToSave = [
2142+ /** @type {ChatHeader} */
2145- {
2143+ const chatHeader = {
2146- user_name: name1,
2147- character_name: name2,
21482144 chat_metadata: chat_metadata,
2149- },
2145+ user_name: 'unused',
2146+ character_name: 'unused',
2147+ };
2148+ const chatToSave = [
2149+ chatHeader,
21502150 ...chat.filter(x => x?.extra?.type !== system_message_types.ASSISTANT_NOTE),
21512151 ];
21522152
@@ -2182,9 +2182,31 @@ export function initChatUtilities() {
21822182 fileInput.click();
21832183 });
21842184
2185+ const fileInput = document.getElementById('file_form_input');
2186+
21852187 // Do not change. #attachFile is added by extension.
21862188 $(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');
21882210 });
21892211
21902212 // Do not change. #manageAttachments is added by extension.
@@ -2347,11 +2369,6 @@ export function initChatUtilities() {
23472369 await onImageSwiped(messageId, messageBlock, SWIPE_DIRECTION.RIGHT);
23482370 });
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- });
23552372 $('#file_form').on('reset', function () {
23562373 $('#file_form').addClass('displayNone');
23572374 });
@@ -2377,17 +2394,16 @@ export function initChatUtilities() {
23772394 * @returns {Promise<void>}
23782395 */
23792396 async function handleFileAttach(files) {
2380- const fileInput = document.getElementById('file_form_input');
23812397 if (!(fileInput instanceof HTMLInputElement)) return;
23822398
23832399 // Workaround for Firefox: Use a DataTransfer object to indirectly set fileInput.files
23842400 const dataTransfer = new DataTransfer();
23852401 for (let i = 0;const ifile <of filesfileInput.length; i++files) {
23862402 dataTransfer.items.add(files[i]file);
23872403 }
23882404
23892405 // Preserve existing non-duplicate files in the input
23902406 for (const file of fileInput.files) {
23912407 if (!Array.from(dataTransfer.files).some(f => isSameFile(f, file))) {
23922408 dataTransfer.items.add(file);
23932409 }
public/scripts/constants.js+56 -5
@@ -1,5 +1,6 @@
11/**
22 * Common debounce timeout values to use with `debounce` calls.
3+ * @readonly
34 * @enum {number}
45 */
56export const debounce_timeout = {
@@ -68,8 +69,8 @@ export const COMETAPI_IGNORE_PATTERNS = [
6869];
6970
7071/**
71- * @enum {string}
7272 * @readonly
73+ * @enum {string}
7374 */
7475export const MEDIA_SOURCE = {
7576 API: 'api',
@@ -79,8 +80,8 @@ export const MEDIA_SOURCE = {
7980};
8081
8182/**
82- * @enum {string}
8383 * @readonly
84+ * @enum {string}
8485 */
8586export const MEDIA_DISPLAY = {
8687 LIST: 'list',
@@ -88,8 +89,8 @@ export const MEDIA_DISPLAY = {
8889};
8990
9091/**
91- * @enum {string}
9292 * @readonly
93+ * @enum {string}
9394 */
9495export const IMAGE_OVERSWIPE = {
9596 GENERATE: 'generate',
@@ -118,9 +119,20 @@ export const MEDIA_TYPE = {
118119};
119120
120121/**
122+ * Bitwise flag-style media request types.
123+ * @readonly
124+ * @enum {number}
125+ */
126+export const MEDIA_REQUEST_TYPE = {
127+ IMAGE: 0b001,
128+ VIDEO: 0b010,
129+ AUDIO: 0b100,
130+};
131+
132+/**
121133 * Scroll behavior options when appending media to messages.
122- * @enum {string}
123134 * @readonly
135+ * @enum {string}
124136 */
125137export const SCROLL_BEHAVIOR = {
126138 NONE: 'none',
@@ -129,9 +141,48 @@ export const SCROLL_BEHAVIOR = {
129141};
130142
131143/**
132- * @type {{readonly LEFT: 'left', readonly RIGHT: 'right'}}
144+ * @readonly
145+ * @enum {string}
146+ */
147+export 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}
133163 */
134164export const SWIPE_DIRECTION = {
135165 LEFT: 'left',
136166 RIGHT: 'right',
137167};
168+
169+/**
170+ * @readonly
171+ * @enum {string}
172+ */
173+export 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+ */
184+export const SWIPE_STATE = {
185+ NONE: 'none',
186+ SWIPING: 'swiping',
187+ EDITING: 'editing',
188+};
public/scripts/custom-request.js+140 -124
@@ -1,9 +1,9 @@
11import { getPresetManager } from './preset-manager.js';
22import { extractJsonFromData, extractMessageFromData, getGenerateUrl, getRequestHeaders, name1, name2 } from '../script.js';
33import { getTextGenServer, createTextGenGenerationData, setting_names, textgenerationwebui_settings } from './textgen-settings.js';
44import { extractReasoningFromData } from './reasoning.js';
55import { formatInstructModeChat, formatInstructModePrompt, getInstructStoppingSequences, names_behavior_types } from './instruct-mode.js';
66import { getStreamingReply, tryParseStreamingError, createGenerationParameters, settingsToUpdate, oai_settings } from './openai.js';
77import EventSourceStream from './sse-stream.js';
88
99// #region Type Definitions
@@ -34,6 +34,7 @@ import EventSourceStream from './sse-stream.js';
3434
3535/**
3636 * @typedef {Object} ChatCompletionMessage
37+ * @property {string} [name] - The name of the message author (optional)
3738 * @property {string} role - The role of the message author (e.g., "user", "assistant", "system")
3839 * @property {string} content - The content of the message
3940 */
@@ -125,7 +126,7 @@ export class TextCompletionService {
125126
126127 const json = await response.json();
127128 if (!response.ok || json.error) {
128- throw json;
129+ throw new Error(String(json.error?.message || 'Response not OK'));
129130 }
130131
131132 if (!extractData) {
@@ -188,57 +189,27 @@ export class TextCompletionService {
188189 }
189190
190191 /**
191- * Process and send a text completion request with optional preset & instruct
192+ * Return a formatted prompt string given an array of messages, a chosen instruct preset, and instruct settings.
192193 * @param {Record<string, any> & TextCompletionRequestBase & {prompt: (ChatCompletionMessage & {ignoreInstruct?: boolean})[] |string}} customprompt An array of messages
193- * @param {Object} options - Configuration options
194+ * @param {InstructSettings|string} instructPreset Either the name of an instruct preset or the instruct preset object itself.
194- * @param {string?} [options.presetName] - Name of the preset to use for generation settings
195+ * @param {Partial<InstructSettings>} instructSettings Optional instruct settings
195- * @param {string?} [options.instructName] - Name of instruct preset for message formatting
196- * @param {Partial<InstructSettings>?} [options.instructSettings] - Override instruct settings
197- * @param {boolean} extractData - Whether to extract structured data from response
198- * @param {AbortSignal?} [signal]
199- * @returns {Promise<ExtractedData | (() => AsyncGenerator<StreamResponse>)>} If not streaming, returns extracted data; if streaming, returns a function that creates an AsyncGenerator
200- * @throws {Error}
201196 */
202- static async processRequest(
197+ static constructPrompt(prompt, instructPreset, instructSettings) {
203- custom,
198+ // InstructPreset may either be a name or itself a preset
204- options = {},
199+ if (typeof instructPreset === 'string') {
205- extractData = true,
200+ const instructPresetManager = getPresetManager('instruct');
206- signal = null,
201+ instructPreset = instructPresetManager?.getCompletionPresetByName(instructPreset);
207- ) {
208- const { presetName, instructName } = options;
209- let requestData = { ...custom };
210- const prompt = custom.prompt;
211-
212- // Apply generation preset if specified
213- if (presetName) {
214- const presetManager = getPresetManager(this.TYPE);
215- if (presetManager) {
216- const preset = presetManager.getCompletionPresetByName(presetName);
217- if (preset) {
218- // Convert preset to payload and merge with custom parameters
219- const presetPayload = this.presetToGeneratePayload(preset, {});
220- requestData = { ...presetPayload, ...requestData };
221- } else {
222- console.warn(`Preset "${presetName}" not found, continuing with default settings`);
223- }
224- } else {
225- console.warn('Preset manager not found, continuing with default settings');
226- }
227202 }
228203
229-
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) {
237204 // Clone the preset to avoid modifying the original
238205 instructPreset = structuredClone(instructPreset);
239- instructPreset.names_behavior = names_behavior_types.NONE;
206+ if (instructSettings) { // apply any additional settings
240- if (options.instructSettings) {
207+ Object.assign(instructPreset, instructSettings);
241- Object.assign(instructPreset, options.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;
242213 }
243214
244215 // Format messages using instruct formatting
@@ -254,52 +225,76 @@ export class TextCompletionService {
254225 // 2. If prefill is active, format all messages except the last one
255226 if (!isLastMessage || !prefillActive) {
256227 messageContent = formatInstructModeChat(
257- message.role,
228+ message.name ?? message.role,
258229 message.content,
259230 message.role === 'user',
260231 message.role === 'system',
261232 undefined,
262- undefined,
233+ name1, // for macros
263- undefined,
234+ name2, // for macros
264235 undefined,
265236 instructPreset,
266237 );
267238 }
268239
269240 // Add prompt formatting for the last message.
241+ // e.g. "<|im_start|>assistant"
270242 if (isLastMessage) {
271- if (!prefillActive) { // e.g. "<|im_start|>user:"
243+ let last_line = formatInstructModePrompt(
272- messageContent += formatInstructModePrompt(
244+ 'assistant', // for sequences using {{name}}
273- undefined,
245+ false, // not an impersonation
274- false,
246+ prefillActive ? message.content : undefined, // if using prefill, last message is the prefill
275- undefined,
247+ name1, // for macros
276- undefined,
248+ name2, // for macros
277- undefined,
249+ true, // quiet
278- false,
279250 false,
280251 instructPreset,
281252 );
282- } else { // e.g. "<|im_start|>assistant: Hello, my name is"
253+
283- const overriddenInstructPreset = structuredClone(instructPreset);
254+ if (prefillActive) { // content is the prefilled message
284- overriddenInstructPreset.output_suffix = '';
255+ if (last_line.endsWith('\n') && !message.content.endsWith('\n')) {
285- overriddenInstructPreset.wrap = false;
256+ last_line = last_line.slice(0, -1); // remove newline after prefill if it's not in the prefill itself
286- messageContent = formatInstructModeChat(
257+ }
287- message.role,
258+ messageContent = last_line;
288- message.content,
259+ } else { // append last line to content (e.g. "<|im_start|>assistant:")
289- false, // since it is assistant
260+ messageContent += last_line;
290- false,
291- undefined,
292- undefined,
293- undefined,
294- undefined,
295- overriddenInstructPreset,
296- );
297261 }
298262 }
299263 }
300264 formattedMessages.push(messageContent);
301265 }
302266 requestData.prompt = return formattedMessages.join('');
267+ }
268+
269+
270+ /**
271+ * Process and send a text completion request with optional preset & instruct
272+ * @param {TextCompletionPayload} requestData
273+ * @param {Object} options - Configuration options
274+ * @param {string?} [options.presetName] - Name of the preset to use for generation settings
275+ * @param {string?} [options.instructName] - Name of instruct preset for message formatting
276+ * @param {Partial<InstructSettings>?} [options.instructSettings] - Override instruct settings
277+ * @param {boolean} extractData - Whether to extract structured data from response
278+ * @param {AbortSignal?} [signal]
279+ * @returns {Promise<ExtractedData | (() => AsyncGenerator<StreamResponse>)>} If not streaming, returns extracted data; if streaming, returns a function that creates an AsyncGenerator
280+ * @throws {Error}
281+ */
282+ static async processRequest(requestData, options = {}, extractData = true, signal = null) {
283+ const { presetName, instructName } = options;
284+
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);
303298 const stoppingStrings = getInstructStoppingSequences({ customInstruct: instructPreset, useStopStrings: false });
304299 requestData.stop = stoppingStrings;
305300 requestData.stopping_strings = stoppingStrings;
@@ -307,18 +302,33 @@ export class TextCompletionService {
307302 console.warn(`Instruct preset "${instructName}" not found, using basic formatting`);
308303 requestData.prompt = prompt.map(x => x.content).join('\n\n');
309304 }
305+ } else {
306+ requestData.prompt = prompt.map(x => x.content).join('\n\n');
307+ }
310308 } else if (typeof prompt === 'string') {
311309 requestData.prompt = prompt;
310+ }
311+
312+ // Apply generation preset if specified
313+ if (presetName) {
314+ const presetManager = getPresetManager(this.TYPE);
315+ if (presetManager) {
316+ const preset = presetManager.getCompletionPresetByName(presetName);
317+ if (preset) {
318+ // Convert preset to payload and merge with custom data
319+ requestData = this.presetToGeneratePayload(preset, {}, requestData);
312320 } else {
313- requestData.prompt = prompt.map(x => x.content).join('\n\n');
321+ console.warn(`Preset "${presetName}" not found, continuing with default settings`);
322+ }
323+ } else {
324+ console.warn('Preset manager not found, continuing with default settings');
325+ }
314326 }
315327
316- // @ts-ignore
328+ const response = await this.sendRequest(requestData, extractData, signal);
317- const data = this.createRequestData(requestData);
318329
319- const response = await this.sendRequest(data, extractData, signal);
320330 // Remove stopping strings from the end
321331 if (!datarequestData.stream && extractData) {
322332 /** @type {ExtractedData} */
323333 // @ts-ignore
324334 const extractedData = response;
@@ -377,31 +387,30 @@ export class TextCompletionService {
377387 * Converts a preset to a valid text completion payload.
378388 * Only supports temperature.
379389 * @param {Object} preset - The preset configuration
380390 * @param {Object} customPresetoverridePreset - Additional parameters to override preset values
391+ * @param {Object} overridePayload - Additional parameters to override payload values
381392 * @returns {Object} - Formatted payload for text completion API
382393 */
383394 static presetToGeneratePayload(preset, customPresetoverridePreset = {}, overridePayload = {}) {
384395 if (!preset || typeof preset !== 'object') {
385396 throw new Error('Invalid preset: must be an object');
386397 }
387398
388399 // Mergeapply preset with custom parametersoverrides
389400 const settingspreset = { ...preset, ...customPresetoverridePreset };
390-
391- // Initialize base payload with common parameters
392- let payload = {
393- 'temperature': settings.temp >= 0 ? Number(settings.temp) : undefined,
394- 'min_p': settings.min_p >= 0 ? Number(settings.min_p) : undefined,
395- };
396401
397- // Remove undefined values to avoid API errors
402+ // Only take fields from the preset specified in setting_names to use as TextCompletionSettings
398- Object.keys(payload).forEach(key => {
403+ const settings = structuredClone(textgenerationwebui_settings);
399- if (payload[key] === undefined) {
404+ for (const [key, value] of Object.entries(preset)) {
400- delete payload[key];
405+ if (!setting_names.includes(key)) continue;
406+ settings[key] = value;
401407 }
402- });
403408
404- return payload;
409+ // convert to a generation payload
410+ const payload = createTextGenGenerationData(settings, overridePayload.model, overridePayload.prompt, preset.genamt);
411+
412+ // apply overrides
413+ return this.createRequestData({ ...payload, ...overridePayload });
405414 }
406415}
407416
@@ -427,8 +436,7 @@ export class ChatCompletionService {
427436 reverse_proxy,
428437 proxy_password,
429438 custom_prompt_post_processing,
430439 use_makersuite_syspromptuse_sysprompt: true,
431- claude_use_sysprompt: true,
432440 ...props,
433441 };
434442
@@ -462,7 +470,7 @@ export class ChatCompletionService {
462470 if (!data.stream) {
463471 const json = await response.json();
464472 if (!response.ok || json.error) {
465- throw json;
473+ throw new Error(String(json.error?.message || 'Response not OK'));
466474 }
467475
468476 if (!extractData) {
@@ -524,7 +532,7 @@ export class ChatCompletionService {
524532
525533 /**
526534 * Process and send a chat completion request with optional preset
527535 * @param {ChatCompletionPayload} customrequestData - payload data, overriding preset if given
528536 * @param {Object} options - Configuration options
529537 * @param {string?} [options.presetName] - Name of the preset to use for generation settings
530538 * @param {boolean} [extractData=true] - Whether to extract structured data from response
@@ -532,9 +540,9 @@ export class ChatCompletionService {
532540 * @returns {Promise<ExtractedData | (() => AsyncGenerator<StreamResponse>)>} If not streaming, returns extracted data; if streaming, returns a function that creates an AsyncGenerator
533541 * @throws {Error}
534542 */
535543 static async processRequest(customrequestData, options, extractData = true, signal = null) {
536544 const { presetName } = options;
537545 let requestData = { ..this.custom }createRequestData(requestData);
538546
539547 // Apply generation preset if specified
540548 if (presetName) {
@@ -543,8 +551,7 @@ export class ChatCompletionService {
543551 const preset = presetManager.getCompletionPresetByName(presetName);
544552 if (preset) {
545553 // Convert preset to payload and merge with custom parameters
546554 const presetPayloadrequestData = await this.presetToGeneratePayload(preset, {}, requestData);
547- requestData = { ...presetPayload, ...requestData };
548555 } else {
549556 console.warn(`Preset "${presetName}" not found, continuing with default settings`);
550557 }
@@ -553,38 +560,47 @@ export class ChatCompletionService {
553560 }
554561 }
555562
556563 const datareturn =await this.createRequestDatasendRequest(requestData, extractData, signal);
557-
558- return await this.sendRequest(data, extractData, signal);
559564 }
560565
561566 /**
562567 * Converts a preset to a valid chat completion payload
563568 * Only supports temperature.
564569 * @param {Object} preset - The preset configuration
565570 * @param {Object} customParamsoverridePreset - Additional parameters to override preset values
566571 * @returnsparam {Object} overridePayload - FormattedAdditional payloadparameters forto chatoverride completionpayload APIvalues
572+ * @returns {Promise<any>} - Formatted payload for chat completion API
567573 */
568574 static async presetToGeneratePayload(preset, customParamsoverridePreset = {}, overridePayload = {}) {
569575 if (!preset || typeof preset !== 'object') {
570576 throw new Error('Invalid preset: must be an object');
571577 }
572578
573579 // Mergeapply preset with custom parametersoverrides
574580 const settingspreset = { ...preset, ...customParamsoverridePreset };
575581
576582 // InitializeFix baseany payloadfields withbefore commonconverting parametersto 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
581585 // RemoveConvert undefinedfrom valuespreset to avoid API errorsChatCompletionSettings
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)) {
584588 delete const payloadsettingToUpdate = settingsToUpdate[key];
589+ if (!settingToUpdate) continue;
590+ settings[settingToUpdate[1]] = value;
585591 }
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];
586597 });
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 });
589605 }
590606}
public/scripts/data-maid.js+1 -1
@@ -68,7 +68,7 @@ class DataMaidDialog {
6868 async getReport() {
6969 const response = await fetch('/api/data-maid/report', {
7070 method: 'POST',
7171 headers: getRequestHeaders({ omitContentType: true }),
7272 });
7373
7474 if (!response.ok) {
public/scripts/events.js+1 -0
@@ -91,6 +91,7 @@ export const event_types = {
9191 PRESET_RENAMED_BEFORE: 'preset_renamed_before',
9292 MAIN_API_CHANGED: 'main_api_changed',
9393 WORLDINFO_ENTRIES_LOADED: 'worldinfo_entries_loaded',
94+ WORLDINFO_SCAN_DONE: 'worldinfo_scan_done',
9495 MEDIA_ATTACHMENT_DELETED: 'media_attachment_deleted',
9596};
9697
public/scripts/extensions/assets/index.js+3 -3
@@ -139,8 +139,8 @@ async function downloadAssetsList(url) {
139139 assetTypeMenu.append(await renderExtensionTemplateAsync('assets', 'installation'));
140140 }
141141
142142 for (const iasset inof availableAssets[assetType].sort((a, b) => a?.name && b?.name && a['name'].localeCompare(b['name']))) {
143143 const asseti = availableAssets[assetType][i].indexOf(asset);
144144 const elemId = `assets_install_${assetType}_${i}`;
145145 let element = $('<div />', { id: elemId, class: 'asset-download-button right_menu_button' });
146146 const label = $('<i class="fa-fw fa-solid fa-download fa-lg"></i>');
@@ -425,7 +425,7 @@ async function updateCurrentAssets() {
425425 try {
426426 const result = await fetch('/api/assets/get', {
427427 method: 'POST',
428428 headers: getRequestHeaders({ omitContentType: true }),
429429 });
430430 currentAssets = result.ok ? (await result.json()) : {};
431431 }
public/scripts/extensions/attachments/index.js+1 -1
@@ -218,7 +218,7 @@ function cleanUpAttachments() {
218218
219219/**
220220 * Clean up character attachments when a character is deleted.
221221 * @param {{character: import('../../char-data.js').v1CharDataCharacter}} data Event data
222222 */
223223function cleanUpCharacterAttachments(data) {
224224 const avatar = data?.character?.avatar;
public/scripts/extensions/caption/index.js+7 -4
@@ -10,7 +10,7 @@ import { SlashCommand } from '../../slash-commands/SlashCommand.js';
1010import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from '../../slash-commands/SlashCommandArgument.js';
1111import { commonEnumProviders } from '../../slash-commands/SlashCommandCommonEnumsProvider.js';
1212import { callGenericPopup, Popup, POPUP_TYPE } from '../../popup.js';
1313import { debounce_timeout, MEDIA_DISPLAY, MEDIA_SOURCE, MEDIA_TYPE, SCROLL_BEHAVIOR } from '../../constants.js';
1414export { MODULE_NAME };
1515
1616const MODULE_NAME = 'caption';
@@ -108,7 +108,7 @@ async function wrapCaptionTemplate(caption) {
108108 'Review and edit the generated caption:',
109109 'Press "Cancel" to abort the caption sending.',
110110 messageText,
111111 { rows: 58, okButton: 'Send' });
112112
113113 if (!messageText) {
114114 throw new Error('User aborted the caption sending.');
@@ -211,6 +211,7 @@ async function sendCaptionedMessage(caption, image, mimeType) {
211211 context.addOneMessage(message);
212212 await eventSource.emit(event_types.USER_MESSAGE_RENDERED, messageId);
213213 await context.saveChat();
214+ setTimeout(() => context.scrollOnMediaLoad(), debounce_timeout.short);
214215}
215216
216217/**
@@ -454,7 +455,7 @@ function isVideoCaptioningAvailable() {
454455 return false;
455456 }
456457
457458 return ['google', 'vertexai', 'zai'].includes(extension_settings.caption.multimodal_api);
458459}
459460
460461jQuery(async function () {
@@ -503,6 +504,7 @@ jQuery(async function () {
503504 'aimlapi': SECRET_KEYS.AIMLAPI,
504505 'moonshot': SECRET_KEYS.MOONSHOT,
505506 'nanogpt': SECRET_KEYS.NANOGPT,
507+ 'chutes': SECRET_KEYS.CHUTES,
506508 'electronhub': SECRET_KEYS.ELECTRONHUB,
507509 'zai': SECRET_KEYS.ZAI,
508510 };
@@ -592,7 +594,7 @@ jQuery(async function () {
592594 const options = Array.from(dropdown.options);
593595 const response = await fetch(url, {
594596 method: 'POST',
595597 headers: getRequestHeaders({ omitContentType: true }),
596598 });
597599 if (!response.ok) {
598600 return;
@@ -616,6 +618,7 @@ jQuery(async function () {
616618 await processEndpoint('aimlapi', '/api/backends/chat-completions/multimodal-models/aimlapi');
617619 await processEndpoint('pollinations', '/api/backends/chat-completions/multimodal-models/pollinations');
618620 await processEndpoint('nanogpt', '/api/backends/chat-completions/multimodal-models/nanogpt');
621+ await processEndpoint('chutes', '/api/backends/chat-completions/multimodal-models/chutes');
619622 await processEndpoint('electronhub', '/api/backends/chat-completions/multimodal-models/electronhub');
620623 await processEndpoint('mistral', '/api/backends/chat-completions/multimodal-models/mistral');
621624 await processEndpoint('xai', '/api/backends/chat-completions/multimodal-models/xai');
public/scripts/extensions/caption/settings.html+12 -3
@@ -18,6 +18,7 @@
1818 <label for="caption_multimodal_api" data-i18n="API">API</label>
1919 <select id="caption_multimodal_api" class="flex1 text_pole">
2020 <option value="aimlapi">AI/ML API</option>
21+ <option value="chutes">Chutes</option>
2122 <option value="anthropic">Claude</option>
2223 <option value="cohere">Cohere</option>
2324 <option value="custom" data-i18n="Custom (OpenAI-compatible)">Custom (OpenAI-compatible)</option>
@@ -55,6 +56,9 @@
5556 <option data-type="moonshot" value="moonshot-v1-8k-vision-preview">moonshot-v1-8k-vision-preview</option>
5657 <option data-type="moonshot" value="moonshot-v1-32k-vision-preview">moonshot-v1-32k-vision-preview</option>
5758 <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>
5862 <option data-type="openai" value="gpt-5.1">gpt-5.1</option>
5963 <option data-type="openai" value="gpt-5.1-2025-11-13">gpt-5.1-2025-11-13</option>
6064 <option data-type="openai" value="gpt-5.1-chat-latest">gpt-5.1-chat-latest</option>
@@ -85,6 +89,8 @@
8589 <option data-type="openai" value="o4-mini-2025-04-16">o4-mini-2025-04-16</option>
8690 <option data-type="openai" value="gpt-4.5-preview">gpt-4.5-preview</option>
8791 <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>
8894 <option data-type="anthropic" value="claude-sonnet-4-5">claude-sonnet-4-5</option>
8995 <option data-type="anthropic" value="claude-sonnet-4-5-20250929">claude-sonnet-4-5-20250929</option>
9096 <option data-type="anthropic" value="claude-haiku-4-5">claude-haiku-4-5</option>
@@ -106,6 +112,7 @@
106112 <option data-type="anthropic" value="claude-3-haiku-20240307">claude-3-haiku-20240307</option>
107113 <option data-type="google" value="gemini-3-pro-preview">gemini-3-pro-preview</option>
108114 <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>
109116 <option data-type="google" value="gemini-2.5-pro">gemini-2.5-pro</option>
110117 <option data-type="google" value="gemini-2.5-pro-preview-06-05">gemini-2.5-pro-preview-06-05</option>
111118 <option data-type="google" value="gemini-2.5-pro-preview-05-06">gemini-2.5-pro-preview-05-06</option>
@@ -135,6 +142,7 @@
135142 <option data-type="google" value="gemini-robotics-er-1.5-preview">gemini-robotics-er-1.5-preview</option>
136143 <option data-type="vertexai" value="gemini-3-pro-preview">gemini-3-pro-preview</option>
137144 <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>
138146 <option data-type="vertexai" value="gemini-2.5-pro">gemini-2.5-pro</option>
139147 <option data-type="vertexai" value="gemini-2.5-flash">gemini-2.5-flash</option>
140148 <option data-type="vertexai" value="gemini-2.5-flash-lite">gemini-2.5-flash-lite</option>
@@ -159,7 +167,11 @@
159167 <option data-type="ollama" value="mistral-small3.2">mistral-small3.2</option>
160168 <option data-type="ollama" value="llama3.2-vision">llama3.2-vision</option>
161169 <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>
162173 <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>
163175 <option data-type="llamacpp" value="llamacpp_current" data-i18n="currently_loaded">[Currently loaded]</option>
164176 <option data-type="ooba" value="ooba_current" data-i18n="currently_loaded">[Currently loaded]</option>
165177 <option data-type="koboldcpp" value="koboldcpp_current" data-i18n="currently_loaded">[Currently loaded]</option>
@@ -167,9 +179,6 @@
167179 <option data-type="custom" value="custom_current" data-i18n="currently_selected">[Currently selected]</option>
168180 </select>
169181 </div>
170- <div data-type="zai">
171- <b>Will use Common API. Coding API is not supported!</b>
172- </div>
173182 <div data-type="ollama">
174183 <div>
175184 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 =
667667 }
668668}
669669
670-async 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+ */
677+async function setSpriteFolderCommand({ name }, folder) {
671678 if (!folder) {
672679 console.log('Clearing sprite set');
673680 folder = '';
@@ -675,8 +682,12 @@ async function setSpriteFolderCommand(_, folder) {
675682
676683 if (folder.startsWith('/') || folder.startsWith('\\')) {
677684 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+ }
678689 folder = folder.slice(1);
679690 folder = `${name || currentLastMessage.name}/${folder}`;
680691 }
681692
682693 $('#expression_override').val(folder.trim());
@@ -1414,7 +1425,7 @@ export async function getExpressionsList({ filterAvailable = false } = {}) {
14141425 if (extension_settings.expressions.api == EXPRESSION_API.local) {
14151426 const apiResult = await fetch('/api/extra/classify/labels', {
14161427 method: 'POST',
14171428 headers: getRequestHeaders({ omitContentType: true }),
14181429 });
14191430
14201431 if (apiResult.ok) {
@@ -2322,6 +2333,16 @@ function migrateSettings() {
23222333 name: 'expression-folder-override',
23232334 aliases: ['spriteoverride', 'costume'],
23242335 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+ ],
23252346 unnamedArgumentList: [
23262347 new SlashCommandArgument(
23272348 'optional folder', [ARGUMENT_TYPE.STRING], false,
public/scripts/extensions/gallery/index.js+42 -14
@@ -8,7 +8,7 @@ import {
88 animation_easing,
99} from '../../../script.js';
1010import { groups, selected_group } from '../../group-chats.js';
1111import { loadFileToDocument, delay, getBase64Async, getSanitizedFilename, saveBase64AsFile, getFileExtension, getVideoThumbnail, clamp } from '../../utils.js';
1212import { loadMovingUIState } from '../../power-user.js';
1313import { dragElement } from '../../RossAscends-mods.js';
1414import { SlashCommandParser } from '../../slash-commands/SlashCommandParser.js';
@@ -19,17 +19,14 @@ import { commonEnumProviders } from '../../slash-commands/SlashCommandCommonEnum
1919import { t, translate } from '../../i18n.js';
2020import { Popup } from '../../popup.js';
2121import { deleteMediaFromServer } from '../../chats.js';
22+import { MEDIA_REQUEST_TYPE, VIDEO_EXTENSIONS } from '../../constants.js';
2223
24+const isVideo = (/** @type {string} */ url) => VIDEO_EXTENSIONS.some(ext => new RegExp(`.${ext}$`, 'i').test(url));
2325const extensionName = 'gallery';
2426const extensionFolderPath = `scripts/extensions/${extensionName}/`;
2527let firstTime = true;
2628let deleteModeActive = false;
2729
28-// Exposed defaults for future tweaking
29-let thumbnailHeight = 150;
30-let paginationVisiblePages = 10;
31-let paginationMaxLinesPerPage = 2;
32-let galleryMaxRows = 3;
3330
3431// Remove all draggables associated with the gallery
3532$('#movingDivs').on('click', '.dragClose', function () {
@@ -98,7 +95,7 @@ function initSettings() {
9895
9996/**
10097 * Retrieves the gallery folder for a given character.
10198 * @param {import('../../char-data.js').v1CharDataCharacter} char Character data
10299 * @returns {string} The gallery folder for the character
103100 */
104101function getGalleryFolder(char) {
@@ -122,17 +119,34 @@ async function getGalleryItems(url) {
122119 folder: url,
123120 sortField: sortObj.field,
124121 sortOrder: sortObj.order,
122+ type: MEDIA_REQUEST_TYPE.IMAGE | MEDIA_REQUEST_TYPE.VIDEO,
125123 }),
126124 });
127125
128126 url = await getSanitizedFilename(url);
129127
130128 const data = await response.json();
131- const items = data.map((file) => ({
129+ const items = [];
130+
131+ for (const file of data) {
132+ const item = {
132133 src: `user/images/${url}/${file}`,
133134 srct: `user/images/${url}/${file}`,
134135 title: '', // Optional title for each item
135136 }));
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
137151 return items;
138152}
@@ -145,7 +159,7 @@ async function getGalleryFolders() {
145159 try {
146160 const response = await fetch('/api/images/folders', {
147161 method: 'POST',
148162 headers: getRequestHeaders({ omitContentType: true }),
149163 });
150164
151165 if (!response.ok) {
@@ -198,6 +212,12 @@ function getSortOrder() {
198212 * @returns {Promise<void>} - Promise representing the completion of the gallery initialization.
199213 */
200214async 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+
201221 const nonce = `nonce-${Math.random().toString(36).substring(2, 15)}`;
202222 const gallery = $('#dragGallery');
203223 gallery.addClass(nonce);
@@ -210,6 +230,7 @@ async function initGallery(items, url) {
210230 galleryMaxRows: galleryMaxRows,
211231 galleryPaginationTopButtons: false,
212232 galleryNavigationOverlayButtons: true,
233+ galleryPaginationMode: 'rectangles',
213234 galleryTheme: {
214235 navigationBar: { background: 'none', borderTop: '', borderBottom: '', borderRight: '', borderLeft: '' },
215236 navigationBreadcrumb: { background: '#111', color: '#fff', colorHover: '#ccc', borderRadius: '4px' },
@@ -399,7 +420,7 @@ async function makeMovable(url) {
399420 // Create a hidden file input
400421 const fileInput = document.createElement('input');
401422 fileInput.type = 'file';
402423 fileInput.accept = 'image/*,video/*';
403424 fileInput.multiple = true;
404425 fileInput.style.display = 'none';
405426
@@ -617,12 +638,19 @@ function makeDragImg(id, url) {
617638 const newElement = document.importNode(template.content, true);
618639
619640 // Step 2: Append the given image
620641 const imgElemmediaElement = document.createElementisVideo('img'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+
622650 let uniqueId = `draggable_${id}`;
623651 const draggableElem = /** @type {HTMLElement} */ (newElement.querySelector('.draggable'));
624652 if (draggableElem) {
625653 draggableElem.appendChild(imgElemmediaElement);
626654
627655 // Find a unique id for the draggable element
628656
public/scripts/extensions/gallery/style.css+4 -0
@@ -49,3 +49,7 @@
4949 opacity: 1;
5050 filter: unset;
5151}
52+
53+.galleryImageDraggable video {
54+ width: 100%;
55+}
public/scripts/extensions/memory/index.js+15 -3
@@ -19,17 +19,18 @@ import {
1919 animation_easing,
2020} from '../../../script.js';
2121import { is_group_generating, selected_group } from '../../group-chats.js';
2222import { loadMovingUIState, power_user } from '../../power-user.js';
2323import { dragElement } from '../../RossAscends-mods.js';
2424import { getTextTokens, getTokenCountAsync, tokenizers } from '../../tokenizers.js';
2525import { debounce_timeout } from '../../constants.js';
2626import { SlashCommandParser } from '../../slash-commands/SlashCommandParser.js';
2727import { SlashCommand } from '../../slash-commands/SlashCommand.js';
2828import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from '../../slash-commands/SlashCommandArgument.js';
2929import { MacrosParsermacros, MacroCategory } from '../../macros/macro-system.js';
3030import { countWebLlmTokens, generateWebLlmChatPrompt, getWebLlmContextSize, isWebLlmSupported } from '../shared.js';
3131import { commonEnumProviders } from '../../slash-commands/SlashCommandCommonEnumsProvider.js';
3232import { removeReasoningFromString } from '../../reasoning.js';
33+import { MacrosParser } from '/scripts/macros.js';
3334export { MODULE_NAME };
3435
3536const MODULE_NAME = '1_memory';
@@ -1094,5 +1095,16 @@ jQuery(async function () {
10941095 returns: ARGUMENT_TYPE.STRING,
10951096 }));
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+ }
10981110});
public/scripts/extensions/quick-reply/index.js+1 -1
@@ -152,7 +152,7 @@ const handleCharChange = () => {
152152 lastCharId = this_chid;
153153
154154 // If no character is loaded, there's nothing more to do.
155155 /** @type {import('../../char-data.js').v1CharDataCharacter} */
156156 const character = characters[this_chid];
157157 if (!character || selected_group) {
158158 return;
public/scripts/extensions/regex/engine.js+63 -8
@@ -5,8 +5,8 @@ import { regexFromString } from '../../utils.js';
55import { lodash } from '../../../lib.js';
66
77/**
8- * @enum {number} Regex scripts types
98 * @readonly
9+ * @enum {number} Regex scripts types
1010 */
1111export const SCRIPT_TYPES = {
1212 // ORDER MATTERS: defines the regex script priority
@@ -35,6 +35,61 @@ export const SCRIPT_TYPE_UNKNOWN = -1;
3535const DEFAULT_GET_REGEX_SCRIPTS_OPTIONS = Object.freeze({ allowedOnly: false });
3636
3737/**
38+ * Manages the compiled regex cache with LRU eviction.
39+ */
40+export 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+/**
3893 * Retrieves the list of regex scripts by combining the scripts from the extension settings and the character data
3994 *
4095 * @param {GetRegexScriptsOptions} options Options for retrieving the regex scripts
@@ -105,7 +160,7 @@ export async function saveScriptsByType(scripts, scriptType) {
105160
106161/**
107162 * Check if character's regexes are allowed to be used; if character is undefined, returns false
108163 * @param {import('../../char-data.js').v1CharDataCharacter|undefined} character
109164 * @returns {boolean}
110165 */
111166export function isScopedScriptsAllowed(character) {
@@ -114,7 +169,7 @@ export function isScopedScriptsAllowed(character) {
114169
115170/**
116171 * Allow character's regexes to be used; if character is undefined, do nothing
117172 * @param {import('../../char-data.js').v1CharDataCharacter|undefined} character
118173 * @returns {void}
119174 */
120175export function allowScopedScripts(character) {
@@ -133,7 +188,7 @@ export function allowScopedScripts(character) {
133188
134189/**
135190 * Disallow character's regexes to be used; if character is undefined, do nothing
136191 * @param {import('../../char-data.js').v1CharDataCharacter|undefined} character
137192 * @returns {void}
138193 */
139194export function disallowScopedScripts(character) {
@@ -220,8 +275,8 @@ export function getCurrentPresetName() {
220275}
221276
222277/**
223- * @enum {number} Where the regex script should be applied
224278 * @readonly
279+ * @enum {number} Where the regex script should be applied
225280 */
226281export const regex_placement = {
227282 /**
@@ -237,8 +292,8 @@ export const regex_placement = {
237292};
238293
239294/**
240- * @enum {number} How to substitute parameters in the find regex
241295 * @readonly
296+ * @enum {number} How to substitute parameters in the find regex
242297 */
243298export const substitute_find_regex = {
244299 NONE: 0,
@@ -353,7 +408,7 @@ export function runRegexScript(regexScript, rawString, { characterOverride } = {
353408 }
354409 };
355410 const regexString = getRegexString();
356411 const findRegex = regexFromStringRegexProvider.instance.get(regexString);
357412
358413 // The user skill issued. Return with nothing.
359414 if (!findRegex) {
@@ -402,7 +457,7 @@ export function runRegexScript(regexScript, rawString, { characterOverride } = {
402457function filterString(rawString, trimStrings, { characterOverride } = {}) {
403458 let finalString = rawString;
404459 trimStrings.forEach((trimString) => {
405460 const subTrimString = substituteParams(trimString, undefined,{ name2Override: characterOverride });
406461 finalString = finalString.replaceAll(subTrimString, '');
407462 });
408463
public/scripts/extensions/regex/index.js+3 -1
@@ -8,7 +8,7 @@ import { commonEnumProviders, enumIcons } from '../../slash-commands/SlashComman
88import { SlashCommandEnumValue, enumTypes } from '../../slash-commands/SlashCommandEnumValue.js';
99import { SlashCommandParser } from '../../slash-commands/SlashCommandParser.js';
1010import { download, equalsIgnoreCaseAndAccents, escapeHtml, getFileText, getSortableDelay, isFalseBoolean, isTrueBoolean, regexFromString, setInfoBlock, uuidv4 } from '../../utils.js';
1111import { 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';
1212import { t } from '../../i18n.js';
1313import { accountStorage } from '../../util/AccountStorage.js';
1414import { getPresetManager } from '../../preset-manager.js';
@@ -1628,6 +1628,8 @@ async function checkCharEmbeddedRegexScripts() {
16281628 }
16291629 }
16301630
1631+ // Clear cache and reload scripts
1632+ RegexProvider.instance.clear();
16311633 await loadRegexScripts();
16321634}
16331635
public/scripts/extensions/shared.js+56 -2
@@ -1,7 +1,7 @@
11import { CONNECT_API_MAP, getRequestHeaders } from '../../script.js';
22import { extension_settings, openThirdPartyExtensionMenu } from '../extensions.js';
33import { t } from '../i18n.js';
44import { oai_settings, proxies, ZAI_ENDPOINT } from '../openai.js';
55import { SECRET_KEYS, secret_state } from '../secrets.js';
66import { textgen_types, textgenerationwebui_settings } from '../textgen-settings.js';
77import { getTokenCountAsync } from '../tokenizers.js';
@@ -115,6 +115,10 @@ export async function getMultimodalCaption(base64Img, prompt) {
115115 requestBody.custom_exclude_body = oai_settings.custom_exclude_body;
116116 }
117117
118+ if (extension_settings.caption.multimodal_api === 'zai') {
119+ requestBody.zai_endpoint = oai_settings.zai_endpoint || ZAI_ENDPOINT.COMMON;
120+ }
121+
118122 function getEndpointUrl() {
119123 switch (extension_settings.caption.multimodal_api) {
120124 case 'google':
@@ -257,6 +261,10 @@ function throwIfInvalidModel(useReverseProxy) {
257261 throw new Error('Electron Hub API key is not set.');
258262 }
259263
264+ if (multimodalApi === 'chutes' && !secret_state[SECRET_KEYS.CHUTES]) {
265+ throw new Error('Chutes API key is not set.');
266+ }
267+
260268 if (multimodalApi === 'zai' && !secret_state[SECRET_KEYS.ZAI]) {
261269 throw new Error('Z.AI API key is not set.');
262270 }
@@ -393,7 +401,7 @@ export class ConnectionManagerRequestService {
393401 throw new Error('Connection Manager is not available');
394402 }
395403
396- const profile = context.extensionSettings.connectionManager.profiles.find((p) => p.id === profileId);
404+ const profile = this.getProfile(profileId);
397405 const selectedApiMap = this.validateProfile(profile);
398406
399407 try {
@@ -413,6 +421,8 @@ export class ConnectionManagerRequestService {
413421 model: profile.model,
414422 chat_completion_source: selectedApiMap.source,
415423 custom_url: profile['api-url'],
424+ vertexai_region: profile['api-url'],
425+ zai_endpoint: profile['api-url'],
416426 reverse_proxy: proxyPreset?.url,
417427 proxy_password: proxyPreset?.password,
418428 custom_prompt_post_processing: profile['prompt-post-processing'],
@@ -450,6 +460,38 @@ export class ConnectionManagerRequestService {
450460 }
451461
452462 /**
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+ /**
453495 * Respects allowed types.
454496 * @returns {import('./connection-manager/index.js').ConnectionProfile[]}
455497 */
@@ -464,6 +506,18 @@ export class ConnectionManagerRequestService {
464506 }
465507
466508 /**
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+ /**
467521 * @param {import('./connection-manager/index.js').ConnectionProfile?} [profile]
468522 * @returns {boolean}
469523 */
public/scripts/extensions/stable-diffusion/index.js+383 -54
@@ -57,9 +57,11 @@ import { SlashCommandEnumValue } from '../../slash-commands/SlashCommandEnumValu
5757import { callGenericPopup, Popup, POPUP_TYPE } from '../../popup.js';
5858import { commonEnumProviders } from '../../slash-commands/SlashCommandCommonEnumsProvider.js';
5959import { ToolManager } from '../../tool-calling.js';
6060import { MacrosParsermacros, MacroCategory } from '../../macros/macro-system.js';
6161import { t, translate } from '../../i18n.js';
6262import { oai_settings } from '../../openai.js';
63+import { power_user } from '/scripts/power-user.js';
64+import { MacrosParser } from '/scripts/macros.js';
6365
6466export { MODULE_NAME };
6567
@@ -81,12 +83,19 @@ const sources = {
8183 pollinations: 'pollinations',
8284 stability: 'stability',
8385 huggingface: 'huggingface',
86+ chutes: 'chutes',
8487 electronhub: 'electronhub',
8588 nanogpt: 'nanogpt',
8689 bfl: 'bfl',
8790 falai: 'falai',
8891 xai: 'xai',
8992 google: 'google',
93+ zai: 'zai',
94+ openrouter: 'openrouter',
95+};
96+const comfyTypes = {
97+ standard: 'standard',
98+ runpod_serverless: 'runpod_serverless',
9099};
91100
92101const initiators = {
@@ -317,9 +326,13 @@ const defaultSettings = {
317326 styles: defaultStyles,
318327
319328 // ComyUI settings
329+ comfy_type: 'standard',
330+
320331 comfy_url: 'http://127.0.0.1:8188',
321332 comfy_workflow: 'Default_Comfy_Workflow.json',
322333
334+ comfy_runpod_url: '',
335+
323336 // Pollinations settings
324337 pollinations_enhance: false,
325338
@@ -432,6 +445,10 @@ function toggleSourceControls() {
432445 const source = $(this).data('sd-source').split(',');
433446 $(this).toggle(source.includes(extension_settings.sd.source));
434447 });
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+ });
435452}
436453
437454async function loadSettings() {
@@ -512,8 +529,10 @@ async function loadSettings() {
512529 $('#sd_openai_style').val(extension_settings.sd.openai_style);
513530 $('#sd_openai_quality').val(extension_settings.sd.openai_quality);
514531 $('#sd_openai_duration').val(extension_settings.sd.openai_duration);
532+ $('#sd_comfy_type').val(extension_settings.sd.comfy_type);
515533 $('#sd_comfy_url').val(extension_settings.sd.comfy_url);
516534 $('#sd_comfy_prompt').val(extension_settings.sd.comfy_prompt);
535+ $('#sd_comfy_runpod_url').val(extension_settings.sd.comfy_runpod_url);
517536 $('#sd_snap').prop('checked', extension_settings.sd.snap);
518537 $('#sd_clip_skip').val(extension_settings.sd.clip_skip);
519538 $('#sd_clip_skip_value').val(extension_settings.sd.clip_skip);
@@ -734,7 +753,7 @@ async function onSaveStyleClick() {
734753async function refinePrompt(prompt, isNegative) {
735754 if (extension_settings.sd.refine_mode) {
736755 const text = isNegative ? '<h3>Review and edit the <i>negative</i> prompt:</h3>' : '<h3>Review and edit the prompt:</h3>';
737756 const refinedPrompt = await callGenericPopup(text + 'Press "Cancel" to abort the image generation.', POPUP_TYPE.INPUT, prompt.trim(), { rows: 58, okButton: 'Continue' });
738757
739758 if (refinedPrompt) {
740759 return String(refinedPrompt);
@@ -1011,6 +1030,11 @@ async function onSourceChange() {
10111030 await loadSettingOptions();
10121031}
10131032
1033+async function onComfyTypeChange() {
1034+ extension_settings.sd.comfy_type = $('#sd_comfy_type').find(':selected').val();
1035+ await onSourceChange();
1036+}
1037+
10141038function onFunctionToolInput() {
10151039 extension_settings.sd.function_tool = !!$(this).prop('checked');
10161040 saveSettingsDebounced();
@@ -1161,11 +1185,15 @@ function onHrSecondPassStepsInput() {
11611185}
11621186
11631187function onComfyUrlInput() {
1164- // Remove trailing slashes
11651188 extension_settings.sd.comfy_url = String($('#sd_comfy_url').val());
11661189 saveSettingsDebounced();
11671190}
11681191
1192+function onComfyRunPodUrlInput() {
1193+ extension_settings.sd.comfy_runpod_url = String($('#sd_comfy_runpod_url').val());
1194+ saveSettingsDebounced();
1195+}
1196+
11691197function onHFModelInput() {
11701198 extension_settings.sd.huggingface_model_id = $('#sd_huggingface_model_id').val();
11711199 saveSettingsDebounced();
@@ -1291,6 +1319,30 @@ async function validateComfyUrl() {
12911319 }
12921320}
12931321
1322+async 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+
12941346async function onModelChange() {
12951347 const selectedModel = $('#sd_model').find(':selected');
12961348 extension_settings.sd.model = selectedModel.val();
@@ -1304,24 +1356,13 @@ async function onModelChange() {
13041356
13051357 switchModelSpecificControls(extension_settings.sd.model);
13061358
13071359 const cloudSourcesupdateRemoteModelSources = [
13081360 sources.hordeauto,
13091361 sources.novelvlad,
13101362 sources.openaiextras,
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,
13221363 ];
13231364
13241365 if (cloudSources!updateRemoteModelSources.includes(extension_settings.sd.source)) {
13251366 return;
13261367 }
13271368
@@ -1531,6 +1572,9 @@ async function loadSamplers() {
15311572 case sources.huggingface:
15321573 samplers = ['N/A'];
15331574 break;
1575+ case sources.chutes:
1576+ samplers = ['N/A'];
1577+ break;
15341578 case sources.electronhub:
15351579 samplers = ['N/A'];
15361580 break;
@@ -1540,12 +1584,21 @@ async function loadSamplers() {
15401584 case sources.bfl:
15411585 samplers = ['N/A'];
15421586 break;
1587+ case sources.falai:
1588+ samplers = ['N/A'];
1589+ break;
15431590 case sources.xai:
15441591 samplers = ['N/A'];
15451592 break;
15461593 case sources.google:
15471594 samplers = ['N/A'];
15481595 break;
1596+ case sources.zai:
1597+ samplers = ['N/A'];
1598+ break;
1599+ case sources.openrouter:
1600+ samplers = ['N/A'];
1601+ break;
15491602 }
15501603
15511604 for (const sampler of samplers) {
@@ -1565,7 +1618,7 @@ async function loadSamplers() {
15651618async function loadHordeSamplers() {
15661619 const result = await fetch('/api/horde/sd-samplers', {
15671620 method: 'POST',
15681621 headers: getRequestHeaders({ omitContentType: true }),
15691622 });
15701623
15711624 if (result.ok) {
@@ -1665,6 +1718,9 @@ async function loadNovelSamplers() {
16651718}
16661719
16671720async function loadComfySamplers() {
1721+ if (extension_settings.sd.comfy_type === comfyTypes.runpod_serverless) {
1722+ return ['N/A'];
1723+ }
16681724 if (!extension_settings.sd.comfy_url) {
16691725 return [];
16701726 }
@@ -1730,6 +1786,9 @@ async function loadModels() {
17301786 case sources.huggingface:
17311787 models = [{ value: '', text: t`<Enter Model ID above>` }];
17321788 break;
1789+ case sources.chutes:
1790+ models = await loadChutesModels();
1791+ break;
17331792 case sources.electronhub:
17341793 models = await loadElectronHubModels();
17351794 break;
@@ -1748,6 +1807,12 @@ async function loadModels() {
17481807 case sources.google:
17491808 models = await loadGoogleModels();
17501809 break;
1810+ case sources.zai:
1811+ models = await loadZaiModels();
1812+ break;
1813+ case sources.openrouter:
1814+ models = await loadOpenRouterModels();
1815+ break;
17511816 }
17521817
17531818 if (extension_settings.sd.source === sources.electronhub) {
@@ -1858,7 +1923,7 @@ async function loadFalaiModels() {
18581923
18591924 const result = await fetch('/api/sd/falai/models', {
18601925 method: 'POST',
18611926 headers: getRequestHeaders({ omitContentType: true }),
18621927 });
18631928
18641929 if (result.ok) {
@@ -1877,7 +1942,7 @@ async function loadXAIModels() {
18771942async function loadPollinationsModels() {
18781943 const result = await fetch('/api/sd/pollinations/models', {
18791944 method: 'POST',
18801945 headers: getRequestHeaders({ omitContentType: true }),
18811946 });
18821947
18831948 if (result.ok) {
@@ -1895,7 +1960,7 @@ async function loadTogetherAIModels() {
18951960
18961961 const result = await fetch('/api/sd/together/models', {
18971962 method: 'POST',
18981963 headers: getRequestHeaders({ omitContentType: true }),
18991964 });
19001965
19011966 if (result.ok) {
@@ -1905,6 +1970,27 @@ async function loadTogetherAIModels() {
19051970 return [];
19061971}
19071972
1973+async 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+
19081994async function loadElectronHubModels() {
19091995 if (!secret_state[SECRET_KEYS.ELECTRONHUB]) {
19101996 console.debug('Electron Hub API key is not set.');
@@ -1913,7 +1999,7 @@ async function loadElectronHubModels() {
19131999
19142000 const result = await fetch('/api/sd/electronhub/models', {
19152001 method: 'POST',
19162002 headers: getRequestHeaders({ omitContentType: true }),
19172003 });
19182004
19192005 function getModelName(model) {
@@ -1946,7 +2032,7 @@ async function loadNanoGPTModels() {
19462032
19472033 const result = await fetch('/api/sd/nanogpt/models', {
19482034 method: 'POST',
19492035 headers: getRequestHeaders({ omitContentType: true }),
19502036 });
19512037
19522038 if (result.ok) {
@@ -1959,7 +2045,7 @@ async function loadNanoGPTModels() {
19592045async function loadHordeModels() {
19602046 const result = await fetch('/api/horde/sd-models', {
19612047 method: 'POST',
19622048 headers: getRequestHeaders({ omitContentType: true }),
19632049 });
19642050
19652051
@@ -2080,6 +2166,8 @@ async function loadDrawthingsModels() {
20802166
20812167async function loadOpenAiModels() {
20822168 return [
2169+ { value: 'gpt-image-1.5', text: 'gpt-image-1.5' },
2170+ { value: 'gpt-image-1-mini', text: 'gpt-image-1-mini' },
20832171 { value: 'gpt-image-1', text: 'gpt-image-1' },
20842172 { value: 'dall-e-3', text: 'dall-e-3' },
20852173 { value: 'dall-e-2', text: 'dall-e-2' },
@@ -2093,7 +2181,7 @@ async function loadAimlapiModels() {
20932181
20942182 const result = await fetch('/api/sd/aimlapi/models', {
20952183 method: 'POST',
20962184 headers: getRequestHeaders({ omitContentType: true }),
20972185 });
20982186
20992187 if (!result.ok) {
@@ -2205,11 +2293,34 @@ async function loadGoogleModels() {
22052293 ].map(name => ({ value: name, text: name }));
22062294}
22072295
2296+async function loadZaiModels() {
2297+ return ['cogview-4-250304'].map(name => ({ value: name, text: name }));
2298+}
2299+
2300+async 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+
22082313function loadNovelSchedulers() {
22092314 return ['karras', 'native', 'exponential', 'polyexponential'];
22102315}
22112316
22122317async 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+ }
22132324 if (!extension_settings.sd.comfy_url) {
22142325 return [];
22152326 }
@@ -2275,6 +2386,9 @@ async function loadSchedulers() {
22752386 case sources.huggingface:
22762387 schedulers = ['N/A'];
22772388 break;
2389+ case sources.chutes:
2390+ schedulers = ['N/A'];
2391+ break;
22782392 case sources.electronhub:
22792393 schedulers = ['N/A'];
22802394 break;
@@ -2293,6 +2407,12 @@ async function loadSchedulers() {
22932407 case sources.google:
22942408 schedulers = ['N/A'];
22952409 break;
2410+ case sources.zai:
2411+ schedulers = ['N/A'];
2412+ break;
2413+ case sources.openrouter:
2414+ schedulers = ['N/A'];
2415+ break;
22962416 }
22972417
22982418 for (const scheduler of schedulers) {
@@ -2310,6 +2430,9 @@ async function loadSchedulers() {
23102430}
23112431
23122432async function loadComfySchedulers() {
2433+ if (extension_settings.sd.comfy_type === comfyTypes.runpod_serverless) {
2434+ return ['N/A'];
2435+ }
23132436 if (!extension_settings.sd.comfy_url) {
23142437 return [];
23152438 }
@@ -2375,6 +2498,9 @@ async function loadVaes() {
23752498 case sources.huggingface:
23762499 vaes = ['N/A'];
23772500 break;
2501+ case sources.chutes:
2502+ vaes = ['N/A'];
2503+ break;
23782504 case sources.electronhub:
23792505 vaes = ['N/A'];
23802506 break;
@@ -2393,6 +2519,12 @@ async function loadVaes() {
23932519 case sources.google:
23942520 vaes = ['N/A'];
23952521 break;
2522+ case sources.zai:
2523+ vaes = ['N/A'];
2524+ break;
2525+ case sources.openrouter:
2526+ vaes = ['N/A'];
2527+ break;
23962528 }
23972529
23982530 for (const vae of vaes) {
@@ -2434,6 +2566,9 @@ async function loadAutoVaes() {
24342566}
24352567
24362568async function loadComfyVaes() {
2569+ if (extension_settings.sd.comfy_type === comfyTypes.runpod_serverless) {
2570+ return ['N/A'];
2571+ }
24372572 if (!extension_settings.sd.comfy_url) {
24382573 return [];
24392574 }
@@ -2456,10 +2591,6 @@ async function loadComfyVaes() {
24562591}
24572592
24582593async function loadComfyWorkflows() {
2459- if (!extension_settings.sd.comfy_url) {
2460- return;
2461- }
2462-
24632594 try {
24642595 $('#sd_comfy_workflow').empty();
24652596 const result = await fetch('/api/sd/comfy/workflows', {
@@ -2532,7 +2663,7 @@ function processReply(str) {
25322663 str = str.normalize('NFD');
25332664
25342665 // Strip out non-alphanumeric characters barring model syntax exceptions
25352666 str = str.replace(/[^a-zA-Z0-9.,:_(){}<>[\]/\-'|#]+/g, ' ');
25362667
25372668 str = str.replace(/\s+/g, ' '); // Collapse multiple whitespaces into one
25382669 str = str.trim();
@@ -2865,7 +2996,7 @@ function getCharacterAvatarUrl() {
28652996 if (context.groupId) {
28662997 const groupMembers = context.groups.find(x => x.id === context.groupId)?.members;
28672998 const lastMessageAvatar = context.chat?.filter(x => !x.is_system && !x.is_user)?.slice(-1)[0]?.original_avatar;
28682999 const randomMemberAvatar = Array.isArray(groupMembers) ? groupMembers[Math.floor(Math.random() * groupMembers.length)]?.avatar : null;
28693000 const avatarToUse = lastMessageAvatar || randomMemberAvatar;
28703001 return formatCharacterAvatar(avatarToUse);
28713002 } else {
@@ -2953,8 +3084,17 @@ async function sendGenerationRequest(generationType, prompt, additionalNegativeP
29533084 result = await generateAimlapiImage(prefixedPrompt, signal);
29543085 break;
29553086 case sources.comfy:
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:
29563092 result = await generateComfyImage(prefixedPrompt, negativePrompt, signal);
29573093 break;
3094+ default:
3095+ throw new Error('Unknown comfyUI server type.');
3096+ }
3097+ break;
29583098 case sources.togetherai:
29593099 result = await generateTogetherAIImage(prefixedPrompt, negativePrompt, signal);
29603100 break;
@@ -2967,6 +3107,9 @@ async function sendGenerationRequest(generationType, prompt, additionalNegativeP
29673107 case sources.huggingface:
29683108 result = await generateHuggingFaceImage(prefixedPrompt, signal);
29693109 break;
3110+ case sources.chutes:
3111+ result = await generateChutesImage(prefixedPrompt, negativePrompt, signal);
3112+ break;
29703113 case sources.electronhub:
29713114 result = await generateElectronHubImage(prefixedPrompt, signal);
29723115 break;
@@ -2985,6 +3128,12 @@ async function sendGenerationRequest(generationType, prompt, additionalNegativeP
29853128 case sources.google:
29863129 result = await generateGoogleImage(prefixedPrompt, negativePrompt, signal);
29873130 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;
29883137 }
29893138
29903139 if (!result.data) {
@@ -3546,9 +3695,9 @@ async function generateOpenAiImage(prompt, signal) {
35463695 const dalle3PromptLimit = 4000;
35473696 const gptImgPromptLimit = 32000;
35483697
35493698 const isDalle2 = extension_settings.sd.model === '/dall-e-2'/.test(extension_settings.sd.model);
35503699 const isDalle3 = extension_settings.sd.model === '/dall-e-3'/.test(extension_settings.sd.model);
35513700 const isGptImg = extension_settings.sd.model === '/gpt-image-1'/.test(extension_settings.sd.model);
35523701 const isSora2 = /sora-2/.test(extension_settings.sd.model);
35533702
35543703 if (isDalle2 && prompt.length > dalle2PromptLimit) {
@@ -3679,25 +3828,17 @@ async function generateAimlapiImage(prompt, signal) {
36793828}
36803829
36813830/**
36823831 * Generates an image in local ComfyUI or serverless runpod using the provided prompt and configuration settings.
36833832 *
36843833 * @param {string} prompt - The main instruction used to guide the image generation.
36853834 * @param {string} negativePrompt - The instruction used to restrict the image generation.
36863835 * @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.
36873839 * @returns {Promise<{format: string, data: string}>} - A promise that resolves when the image generation and processing are complete.
36883840 */
36893841async function generateComfyImagegenerateComfyImageCommon(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-
37013842 const workflowResponse = await fetch('/api/sd/comfy/workflow', {
37023843 method: 'POST',
37033844 headers: getRequestHeaders(),
@@ -3752,12 +3893,12 @@ async function generateComfyImage(prompt, negativePrompt, signal) {
37523893 console.log(`{
37533894 "prompt": ${workflow}
37543895 }`);
37553896 const promptResult = await fetch('/api/sd/comfy`${basePath}/generate'`, {
37563897 method: 'POST',
37573898 headers: getRequestHeaders(),
37583899 signal: signal,
37593900 body: JSON.stringify({
3760- url: extension_settings.sd.comfy_url,
3901+ url,
37613902 prompt: `{
37623903 "prompt": ${workflow}
37633904 }`,
@@ -3771,6 +3912,46 @@ async function generateComfyImage(prompt, negativePrompt, signal) {
37713912 return { format, data };
37723913}
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+ */
3923+async 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+ */
3945+async 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
37753956/**
37763957 * 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) {
37993980}
38003981
38013982/**
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+ */
3989+async 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+/**
38024015 * Generates an image using the Electron Hub API.
38034016 * @param {string} prompt - The main instruction used to guide the image generation.
38044017 * @param {AbortSignal} signal - An AbortSignal object that can be used to cancel the request.
@@ -4022,6 +4235,74 @@ async function generateGoogleImage(prompt, negativePrompt, signal) {
40224235 }
40234236}
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+ */
4244+async 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+ */
4285+async 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+
40254306async function onComfyOpenWorkflowEditorClick() {
40264307 let workflow = await (await fetch('/api/sd/comfy/workflow', {
40274308 method: 'POST',
@@ -4207,6 +4488,7 @@ async function sendMessage(prompt, image, generationType, additionalNegativePref
42074488 context.addOneMessage(message);
42084489 await eventSource.emit(event_types.CHARACTER_MESSAGE_RENDERED, messageId, 'extension');
42094490 await context.saveChat();
4491+ setTimeout(() => context.scrollOnMediaLoad(), debounce_timeout.short);
42104492}
42114493
42124494/**
@@ -4304,7 +4586,15 @@ function isValidState() {
43044586 case sources.aimlapi:
43054587 return secret_state[SECRET_KEYS.AIMLAPI];
43064588 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+ }
43084598 case sources.togetherai:
43094599 return secret_state[SECRET_KEYS.TOGETHERAI];
43104600 case sources.pollinations:
@@ -4313,6 +4603,8 @@ function isValidState() {
43134603 return secret_state[SECRET_KEYS.STABILITY];
43144604 case sources.huggingface:
43154605 return secret_state[SECRET_KEYS.HUGGINGFACE];
4606+ case sources.chutes:
4607+ return secret_state[SECRET_KEYS.CHUTES];
43164608 case sources.electronhub:
43174609 return secret_state[SECRET_KEYS.ELECTRONHUB];
43184610 case sources.nanogpt:
@@ -4325,6 +4617,12 @@ function isValidState() {
43254617 return secret_state[SECRET_KEYS.XAI];
43264618 case sources.google:
43274619 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;
43284626 }
43294627}
43304628
@@ -4378,6 +4676,10 @@ async function sdMessageButton($icon, { animate } = {}) {
43784676 message.extra.media = [];
43794677 }
43804678
4679+ if (!message.extra.media.length && !message.extra.media_display) {
4680+ message.extra.media_display = MEDIA_DISPLAY.GALLERY;
4681+ }
4682+
43814683 /** @type {MediaAttachment} */
43824684 const selectedMedia = message.extra.media.length > 0
43834685 ? (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,
45064808async function onImageSwiped({ message, element, direction }) {
45074809 const { powerUserSettings, accountStorage } = getContext();
45084810
4811+ if (!isValidState()) {
4812+ return;
4813+ }
4814+
45094815 if (!message || direction !== SWIPE_DIRECTION.RIGHT || powerUserSettings.image_overswipe !== IMAGE_OVERSWIPE.GENERATE) {
45104816 return;
45114817 }
@@ -4954,8 +5260,11 @@ jQuery(async () => {
49545260 $('#sd_novel_decrisper').on('input', onNovelDecrisperInput);
49555261 $('#sd_novel_variety_boost').on('input', onNovelVarietyBoostInput);
49565262 $('#sd_pollinations_enhance').on('input', onPollinationsEnhanceInput);
5263+ $('#sd_comfy_type').on('change', onComfyTypeChange);
49575264 $('#sd_comfy_validate').on('click', validateComfyUrl);
5265+ $('#sd_comfy_runpod_validate').on('click', validateComfyRunPodUrl);
49585266 $('#sd_comfy_url').on('input', onComfyUrlInput);
5267+ $('#sd_comfy_runpod_url').on('input', onComfyRunPodUrlInput);
49595268 $('#sd_comfy_workflow').on('change', onComfyWorkflowChange);
49605269 $('#sd_comfy_open_workflow_editor').on('click', onComfyOpenWorkflowEditorClick);
49615270 $('#sd_comfy_new_workflow').on('click', onComfyNewWorkflowClick);
@@ -5033,6 +5342,7 @@ jQuery(async () => {
50335342 case SECRET_KEYS.FALAI:
50345343 case SECRET_KEYS.STABILITY:
50355344 case SECRET_KEYS.AIMLAPI:
5345+ case SECRET_KEYS.COMFY_RUNPOD:
50365346 await loadSettingOptions();
50375347 break;
50385348 }
@@ -5064,6 +5374,25 @@ jQuery(async () => {
50645374 return isNegative ? negativePrompt : characterPrompt;
50655375 };
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+ }
50695398});
public/scripts/extensions/stable-diffusion/settings.html+43 -4
@@ -39,6 +39,7 @@
3939 <select id="sd_source">
4040 <option value="aimlapi">AI/ML API</option>
4141 <option value="bfl">BFL (Black Forest Labs)</option>
42+ <option value="chutes">Chutes</option>
4243 <option value="comfy">ComfyUI</option>
4344 <option value="drawthings">DrawThings HTTP API</option>
4445 <option value="electronhub">Electron Hub</option>
@@ -49,6 +50,7 @@
4950 <option value="nanogpt">NanoGPT</option>
5051 <option value="novel">NovelAI Diffusion</option>
5152 <option value="openai">OpenAI</option>
53+ <option value="openrouter">OpenRouter</option>
5254 <option value="pollinations">Pollinations</option>
5355 <option value="vlad">SD.Next (vladmandic)</option>
5456 <option value="stability">Stability AI</option>
@@ -56,6 +58,7 @@
5658 <option value="horde">Stable Horde</option>
5759 <option value="togetherai">TogetherAI</option>
5860 <option value="xai">xAI (Grok)</option>
61+ <option value="zai">Z.AI (CogView)</option>
5962 </select>
6063 <div data-sd-source="auto">
6164 <label for="sd_auto_url">SD Web UI URL</label>
@@ -94,6 +97,9 @@
9497 <label for="sd_huggingface_model_id" data-i18n="Model ID">Model ID</label>
9598 <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="" />
9699 </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>
97103 <div data-sd-source="electronhub">
98104 <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>
99105 <div class="flex-container" id="sd_electronhub_quality_row">
@@ -160,16 +166,19 @@
160166 </div>
161167 </div>
162168 </div>
163169 <div data-sd-source="openai,aimlapizai">
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">
166175 <label for="sd_openai_style" data-i18n="Image Style">Image Style</label>
167176 <select id="sd_openai_style">
168177 <option value="vivid">Vivid</option>
169178 <option value="natural">Natural</option>
170179 </select>
171180 </div>
172181 <div data-sd-model="dall-e-3,cogview-4-250304" class="flex1">
173182 <label for="sd_openai_quality" data-i18n="Image Quality">Image Quality</label>
174183 <select id="sd_openai_quality">
175184 <option value="standard" data-i18n="Standard">Standard</option>
@@ -189,6 +198,12 @@
189198 </div>
190199 </div>
191200 <div data-sd-source="comfy">
201+ <label for="sd_comfy_type">Server Type</label>
202+ <select id="sd_comfy_type">
203+ <option value="standard">Standard Server</option>
204+ <option value="runpod_serverless">RunPod Serverless Endpoint</option>
205+ </select>
206+ <div data-sd-comfy-type="standard">
192207 <label for="sd_comfy_url">ComfyUI URL</label>
193208 <div class="flex-container flexnowrap">
194209 <input id="sd_comfy_url" type="text" class="text_pole" data-i18n="[placeholder]sd_comfy_url" placeholder="Example: {{comfy_url}}" value="{{comfy_url}}" />
@@ -199,6 +214,30 @@
199214 </span>
200215 </div>
201216 </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>
239+ </div>
240+ </div>
202241 <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>
203242 <label for="sd_comfy_workflow">ComfyUI Workflow</label>
204243 <div class="flex-container flexnowrap">
public/scripts/extensions/translate/index.js+2 -2
@@ -210,7 +210,7 @@ async function translateIncomingMessage(messageId) {
210210 return;
211211 }
212212
213213 const textToTranslate = substituteParams(message.mes, context.name1,{ name2Override: message.name });
214214 const translation = await translate(textToTranslate, extension_settings.translate.target_language);
215215 message.extra.display_text = translation;
216216
@@ -238,7 +238,7 @@ async function translateIncomingMessageReasoning(messageId) {
238238 return false;
239239 }
240240
241241 const textToTranslate = substituteParams(message.extra.reasoning, context.name1,{ name2Override: message.name });
242242 const translation = await translate(textToTranslate, extension_settings.translate.target_language);
243243 message.extra.reasoning_display_text = translation;
244244
public/scripts/extensions/tts/chutes.js+233 -0
@@ -0,0 +1,233 @@
1+import { event_types, eventSource, getRequestHeaders } from '../../../script.js';
2+import { SECRET_KEYS, secret_state } from '../../secrets.js';
3+import { getPreviewString, saveTtsProviderSettings } from './index.js';
4+
5+export { ChutesTtsProvider };
6+
7+class 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 {
259259 try {
260260 const result = await fetch('/api/plugins/edge-tts/probe', {
261261 method: 'POST',
262262 headers: getRequestHeaders({ omitContentType: true }),
263263 });
264264 return result.ok;
265265 } catch (e) {
public/scripts/extensions/tts/electronhub.js+1 -1
@@ -187,7 +187,7 @@ class ElectronHubTtsProvider {
187187 try {
188188 const response = await fetch('/api/openai/electronhub/models', {
189189 method: 'POST',
190190 headers: getRequestHeaders({ omitContentType: true }),
191191 });
192192 if (!response.ok) {
193193 throw new Error(`HTTP ${response.status}: ${await response.text()}`);
public/scripts/extensions/tts/elevenlabs.js+131 -106
@@ -1,23 +1,20 @@
11import { saveTtsProviderSettings } from './index.js';
2+import { event_types, eventSource, getRequestHeaders } from '/script.js';
3+import { SECRET_KEYS, secret_state, writeSecret } from '/scripts/secrets.js';
4+import { getBase64Async } from '/scripts/utils.js';
25export { ElevenLabsTtsProvider };
36
47class ElevenLabsTtsProvider {
5- //########//
6- // Config //
7- //########//
8-
98 settings;
109 voices = [];
1110 separator = ' ... ... ... ';
1211
13-
1412 defaultSettings = {
1513 stability: 0.75,
1614 similarity_boost: 0.75,
1715 style_exaggeration: 0.00,
1816 speaker_boost: true,
1917 speed: 1.0,
20- apiKey: '',
2118 model: 'eleven_turbo_v2_5',
2219 voiceMap: {},
2320 };
@@ -25,8 +22,15 @@ class ElevenLabsTtsProvider {
2522 get settingsHtml() {
2623 let html = `
2724 <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>
3034 <label for="elevenlabs_tts_model">Model</label>
3135 <select id="elevenlabs_tts_model" class="text_pole">
3236 <option value="eleven_v3">Eleven v3</option>
@@ -39,7 +43,6 @@ class ElevenLabsTtsProvider {
3943 <option value="eleven_multilingual_v1">Multilingual v1 (Old)</option>
4044 <option value="eleven_turbo_v2">Turbo v2 (Old)</option>
4145 </select>
42- <input id="eleven_labs_connect" class="menu_button" type="button" value="Connect" />
4346 <label for="elevenlabs_tts_stability">Stability: <span id="elevenlabs_tts_stability_output"></span></label>
4447 <input id="elevenlabs_tts_stability" type="range" value="${this.defaultSettings.stability}" min="0" max="1" step="0.01" />
4548 <label for="elevenlabs_tts_similarity_boost">Similarity Boost: <span id="elevenlabs_tts_similarity_boost_output"></span></label>
@@ -72,6 +75,20 @@ class ElevenLabsTtsProvider {
7275 return html;
7376 }
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+
7592 shouldInvolveExtendedSettings() {
7693 // Models that support extended settings (style_exaggeration, speaker_boost)
7794 const modelsWithExtendedSettings = [
@@ -114,6 +131,18 @@ class ElevenLabsTtsProvider {
114131 delete settings['multilingual'];
115132 }
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+
117146 for (const key in settings) {
118147 if (key in this.settings) {
119148 this.settings[key] = settings[key];
@@ -127,9 +156,7 @@ class ElevenLabsTtsProvider {
127156 $('#elevenlabs_tts_style_exaggeration').val(this.settings.style_exaggeration);
128157 $('#elevenlabs_tts_speaker_boost').prop('checked', this.settings.speaker_boost);
129158 $('#elevenlabs_tts_speed').val(this.settings.speed);
130- $('#elevenlabs_tts_api_key').val(this.settings.apiKey);
131159 $('#elevenlabs_tts_model').val(this.settings.model);
132- $('#eleven_labs_connect').on('click', () => { this.onConnectClick(); });
133160 $('#elevenlabs_tts_similarity_boost').on('input', this.onSettingsChange.bind(this));
134161 $('#elevenlabs_tts_stability').on('input', this.onSettingsChange.bind(this));
135162 $('#elevenlabs_tts_style_exaggeration').on('input', this.onSettingsChange.bind(this));
@@ -157,23 +184,17 @@ class ElevenLabsTtsProvider {
157184 }
158185
159186 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- });
167188 }
168189
169190 setupVoiceCloningMenu() {
170191 const audioFilesInput = /** @type {HTMLInputElement} */ (document.getElementById('elevenlabs_tts_audio_files'));
171192 const selectedFilesListElement = document.getElementById('elevenlabs_tts_selected_files_list');
172193 const cloneVoiceButton = document.getElementById('elevenlabs_tts_clone_voice_button');
173194 const uploadAudioFileButton = document.getElementById('upload_audio_file');
174195 const voiceCloningNameInput = /** @type {HTMLInputElement} */ (document.getElementById('elevenlabs_tts_voice_cloning_name'));
175196 const voiceCloningDescriptionInput = /** @type {HTMLInputElement} */ (document.getElementById('elevenlabs_tts_voice_cloning_description'));
176197 const voiceCloningLabelsInput = /** @type {HTMLInputElement} */ (document.getElementById('elevenlabs_tts_voice_cloning_labels'));
177198
178199 const updateCloneVoiceButtonVisibility = () => {
179200 cloneVoiceButton.style.display = audioFilesInput.files.length > 0 ? 'inline-block' : 'none';
@@ -224,22 +245,11 @@ class ElevenLabsTtsProvider {
224245 updateCloneVoiceButtonVisibility();
225246 }
226247
227- async updateApiKey() {
248+ /**
228- // Using this call to validate API key
249+ * Get voice object by name
229- this.settings.apiKey = $('#elevenlabs_tts_api_key').val();
250+ * @param {string} voiceName Voice name to look up
230-
251+ * @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-
243253 async getVoice(voiceName) {
244254 if (this.voices.length == 0) {
245255 this.voices = await this.fetchTtsVoiceObjects();
@@ -253,25 +263,30 @@ class ElevenLabsTtsProvider {
253263 return match;
254264 }
255265
256-
266+ /**
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+ */
257272 async generateTts(text, voiceId) {
258273 const historyId = await this.findTtsGenerationInHistory(text, voiceId);
259274
260- let response;
261275 if (historyId) {
262276 console.debug(`Found existing TTS generation with id ${historyId}`);
263277 response =return await this.fetchTtsFromHistory(historyId);
264278 } else {
265279 console.debug('No existing TTS generation found, requesting new generation');
266280 response =return await this.fetchTtsGeneration(text, voiceId);
267281 }
268- return response;
269282 }
270283
271- //###################//
284+ /**
272- // Helper Functions //
285+ * Find existing TTS generation in history
273- //###################//
286+ * @param {string} message Message text used for TTS generation
274-
287+ * @param {string} voiceId Voice ID used for TTS generation
288+ * @returns {Promise<string>} History item ID if found, empty string otherwise
289+ */
275290 async findTtsGenerationInHistory(message, voiceId) {
276291 const ttsHistory = await this.fetchTtsHistory();
277292 for (const history of ttsHistory) {
@@ -285,40 +300,35 @@ class ElevenLabsTtsProvider {
285300 return '';
286301 }
287302
288-
289- //###########//
290- // API CALLS //
291- //###########//
292303 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,
298307 });
299308 if (!response.ok) {
300309 throw new Error(`HTTP ${response.status}:. ${awaitSee responseserver console for details.text()}`);
301310 }
302311 const responseJson = await response.json();
303312 return responseJson.voices;
304313 }
305314
306315 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- );
316320 if (!response.ok) {
317321 throw new Error(`HTTP ${response.status}:. ${awaitSee responseserver console for details.text()}`);
318322 }
319323 return response.json();
320324 }
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+ */
322332 async fetchTtsGeneration(text, voiceId) {
323333 let model = this.settings.model ?? 'eleven_monolingual_v1';
324334 console.info(`Generating new TTS for voice_id ${voiceId}, model ${model}`);
@@ -335,75 +345,90 @@ class ElevenLabsTtsProvider {
335345 request.voice_settings.style = Number(this.settings.style_exaggeration);
336346 request.voice_settings.use_speaker_boost = Boolean(this.settings.speaker_boost);
337347 }
338348 const response = await fetch(`https:/'/api.elevenlabs.io/v1/text-to-speech/${voiceId}`elevenlabs/synthesize', {
339349 method: 'POST',
340350 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+ }),
345355 });
346356 if (!response.ok) {
347357 toastr.error(response.statusText, 'TTS Generation Failed');
348358 throw new Error(`HTTP ${response.status}:. ${awaitSee responseserver console for details.text()}`);
349359 }
350360 return response;
351361 }
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+ });
363377 if (!response.ok) {
364378 throw new Error(`HTTP ${response.status}:. ${awaitSee responseserver console for details.text()}`);
365379 }
366380 return response;
367381 }
368382
383+ /**
384+ * Fetch TTS generation history
385+ * @returns {Promise<Array>} Array of TTS history items
386+ */
369387 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,
375391 });
376392 if (!response.ok) {
377393 throw new Error(`HTTP ${response.status}:. ${awaitSee responseserver console for details.text()}`);
378394 }
379395 const responseJson = await response.json();
380396 return responseJson.history;
381397 }
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+ */
383406 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
391419 for (const file of selected_filesaudioFilesInput.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- }
395422 }
396423
397424 const response = await fetch('https://api.elevenlabs.io/v1speech/elevenlabs/voices/add', {
398425 method: 'POST',
399426 headers: {getRequestHeaders(),
400- 'xi-api-key': this.settings.apiKey,
427+ body: JSON.stringify(data),
401- },
402- body: formData,
403428 });
404429
405430 if (!response.ok) {
406431 throw new Error(`HTTP ${response.status}:. ${awaitSee responseserver console for details.text()}`);
407432 }
408433
409434 return await response.json();
public/scripts/extensions/tts/index.js+3 -3
@@ -34,6 +34,7 @@ import { TtsWebuiProvider } from './tts-webui.js';
3434import { PollinationsTtsProvider } from './pollinations.js';
3535import { MiniMaxTtsProvider } from './minimax.js';
3636import { ElectronHubTtsProvider } from './electronhub.js';
37+import { ChutesTtsProvider } from './chutes.js';
3738
3839const UPDATE_INTERVAL = 1000;
3940const wrapper = new ModuleWorkerWrapper(moduleWorker);
@@ -120,6 +121,7 @@ const ttsProviders = {
120121 AllTalk: AllTalkTtsProvider,
121122 Azure: AzureTtsProvider,
122123 Chatterbox: ChatterboxTtsProvider,
124+ Chutes: ChutesTtsProvider,
123125 Coqui: CoquiTtsProvider,
124126 'CosyVoice (Unofficial)': CosyVoiceProvider,
125127 Edge: EdgeTtsProvider,
@@ -239,9 +241,7 @@ function isTtsProcessing() {
239241
240242/**
241243 * Splits a message into lines and adds each non-empty line to the TTS job queue.
242244 * @param {ObjectChatMessage} 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.
245245 * @returns {void}
246246 */
247247function processAndQueueTtsMessage(message) {
public/scripts/extensions/vectors/index.js+50 -2
@@ -69,6 +69,7 @@ const settings = {
6969 vllm_model: '',
7070 webllm_model: '',
7171 google_model: 'text-embedding-005',
72+ chutes_model: 'chutes-qwen-qwen3-embedding-8b',
7273 summarize: false,
7374 summarize_sent: false,
7475 summary_source: 'main',
@@ -829,6 +830,9 @@ function getVectorsRequestBody(args = {}) {
829830 body.vertexai_region = oai_settings.vertexai_region;
830831 body.vertexai_express_project_id = oai_settings.vertexai_express_project_id;
831832 break;
833+ case 'chutes':
834+ body.model = extension_settings.vectors.chutes_model;
835+ break;
832836 default:
833837 break;
834838 }
@@ -913,6 +917,7 @@ async function insertVectorItems(collectionId, items) {
913917function throwIfSourceInvalid() {
914918 if (settings.source === 'openai' && !secret_state[SECRET_KEYS.OPENAI] ||
915919 settings.source === 'electronhub' && !secret_state[SECRET_KEYS.ELECTRONHUB] ||
920+ settings.source === 'chutes' && !secret_state[SECRET_KEYS.CHUTES] ||
916921 settings.source === 'openrouter' && !secret_state[SECRET_KEYS.OPENROUTER] ||
917922 settings.source === 'palm' && !secret_state[SECRET_KEYS.MAKERSUITE] ||
918923 settings.source === 'vertexai' && !secret_state[SECRET_KEYS.VERTEXAI] && !secret_state[SECRET_KEYS.VERTEXAI_SERVICE_ACCOUNT] ||
@@ -1127,6 +1132,7 @@ function toggleSettings() {
11271132 $('#together_vectorsModel').toggle(settings.source === 'togetherai');
11281133 $('#openai_vectorsModel').toggle(settings.source === 'openai');
11291134 $('#electronhub_vectorsModel').toggle(settings.source === 'electronhub');
1135+ $('#chutes_vectorsModel').toggle(settings.source === 'chutes');
11301136 $('#openrouter_vectorsModel').toggle(settings.source === 'openrouter');
11311137 $('#cohere_vectorsModel').toggle(settings.source === 'cohere');
11321138 $('#ollama_vectorsModel').toggle(settings.source === 'ollama');
@@ -1147,14 +1153,51 @@ function toggleSettings() {
11471153 case 'openrouter':
11481154 loadOpenRouterModels();
11491155 break;
1156+ case 'chutes':
1157+ loadChutesModels();
1158+ break;
1159+ }
1160+}
1161+
1162+async 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+
1181+function 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;
11501192 }
1193+ $('#vectors_chutes_model').val(settings.chutes_model);
11511194}
11521195
11531196async function loadElectronHubModels() {
11541197 try {
11551198 const response = await fetch('/api/openai/electronhub/models', {
11561199 method: 'POST',
11571200 headers: getRequestHeaders({ omitContentType: true }),
11581201 });
11591202 if (!response.ok) {
11601203 throw new Error(`HTTP ${response.status}`);
@@ -1193,7 +1236,7 @@ async function loadOpenRouterModels() {
11931236 try {
11941237 const response = await fetch('/api/openrouter/models/embedding', {
11951238 method: 'POST',
11961239 headers: getRequestHeaders({ omitContentType: true }),
11971240 });
11981241 if (!response.ok) {
11991242 throw new Error(`HTTP ${response.status}`);
@@ -1630,6 +1673,11 @@ jQuery(async () => {
16301673 Object.assign(extension_settings.vectors, settings);
16311674 saveSettingsDebounced();
16321675 });
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+ });
16331681 $('#vectors_openrouter_model').val(settings.openrouter_model).on('change', () => {
16341682 settings.openrouter_model = String($('#vectors_openrouter_model').val());
16351683 Object.assign(extension_settings.vectors, settings);
public/scripts/extensions/vectors/settings.html+10 -0
@@ -10,6 +10,7 @@
1010 Vectorization Source
1111 </label>
1212 <select id="vectors_source" class="text_pole">
13+ <option value="chutes">Chutes</option>
1314 <option value="cohere">Cohere</option>
1415 <option value="electronhub">Electron Hub</option>
1516 <option value="extras">Extras (deprecated)</option>
@@ -28,6 +29,15 @@
2829 <option value="webllm" data-i18n="WebLLM Extension">WebLLM Extension</option>
2930 </select>
3031 </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>
3141 <div class="flex-container flexFlowColumn" id="electronhub_vectorsModel">
3242 <label for="vectors_electronhub_model" data-i18n="Vectorization Model">
3343 Vectorization Model
public/scripts/group-chats.js+367 -129
@@ -17,6 +17,8 @@ import {
1717 renderPaginationDropdown,
1818 paginationDropdownChangeHandler,
1919 waitUntilCondition,
20+ uuidv4,
21+ humanFileSize,
2022} from './utils.js';
2123import { RA_CountCharTokens, humanizedDateTime, dragElement, favsToHotswap, getMessageTimeStamp } from './RossAscends-mods.js';
2224import { power_user, loadMovingUIState, sortEntitiesList } from './power-user.js';
@@ -37,8 +39,6 @@ import {
3739 setCharacterName,
3840 setEditedMessageId,
3941 is_send_press,
40- name1,
41- name2,
4242 resetChatState,
4343 setSendButtonState,
4444 getCharacters,
@@ -71,6 +71,7 @@ import {
7171 isChatSaving,
7272 setExternalAbortController,
7373 baseChatReplace,
74+ createLazyFields,
7475 depth_prompt_depth_default,
7576 loadItemizedPrompts,
7677 animation_duration,
@@ -109,7 +110,9 @@ export {
109110let is_group_generating = false; // Group generation flag
110111let is_group_automode_enabled = false;
111112let hideMutedSprites = false;
113+/** @type {Group[]} */
112114let groups = [];
115+/** @type {string|null} */
113116let selected_group = null;
114117let group_generation_id = null;
115118let fav_grp_checked = false;
@@ -143,6 +146,11 @@ function setAutoModeWorker() {
143146 autoModeWorker = setInterval(groupChatAutoModeWorker, autoModeDelay * 1000);
144147}
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+ */
146154async function _save(group, reload = true) {
147155 await fetch('/api/groups/edit', {
148156 method: 'POST',
@@ -179,6 +187,11 @@ async function regenerateGroup() {
179187 generateGroupWrapper(false, 'normal', { signal: abortController.signal });
180188}
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+ */
182195async function loadGroupChat(chatId) {
183196 const response = await fetch('/api/chats/group/get', {
184197 method: 'POST',
@@ -188,12 +201,20 @@ async function loadGroupChat(chatId) {
188201
189202 if (response.ok) {
190203 const data = await response.json();
204+ if (!Array.isArray(data)) {
205+ return [];
206+ }
191207 return data;
192208 }
193209
194210 return [];
195211}
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+ */
197218async function validateGroup(group) {
198219 if (!group) return;
199220
@@ -225,6 +246,12 @@ async function validateGroup(group) {
225246 }
226247}
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+ */
228255export async function getGroupChat(groupId, reload = false) {
229256 const group = groups.find((x) => x.id === groupId);
230257 if (!group) {
@@ -238,9 +265,19 @@ export async function getGroupChat(groupId, reload = false) {
238265
239266 const chat_id = group.chat_id;
240267 const data = await loadGroupChat(chat_id);
241268 const metadata = groupdata?.[0]?.chat_metadata ?? {};
242269 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+
244281 await loadItemizedPrompts(getCurrentChatId());
245282
246283 if (group && Array.isArray(group.members) && freshChat) {
@@ -266,7 +303,6 @@ export async function getGroupChat(groupId, reload = false) {
266303 }
267304 await saveGroupChat(groupId, false);
268305 } else if (Array.isArray(data) && data.length) {
269- data[0].is_group = true;
270306 chat.splice(0, chat.length, ...data);
271307 chat.forEach(ensureMessageMediaIsArray);
272308 chatElement.find('.mes').remove();
@@ -287,7 +323,7 @@ export async function getGroupChat(groupId, reload = false) {
287323 * Retrieves the members of a group
288324 *
289325 * @param {string} [groupId=selected_group] - The ID of the group to retrieve members from. Defaults to the currently selected group.
290326 * @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.
291327 */
292328export function getGroupMembers(groupId = selected_group) {
293329 const group = groups.find((x) => x.id === groupId);
@@ -421,7 +457,7 @@ export function getGroupDepthPrompts(groupId, characterId) {
421457 continue;
422458 }
423459
424460 const depthPromptText = baseChatReplace(character.data?.extensions?.depth_prompt?.prompt?.trim(), name1null, character.name) || '';
425461 const depthPromptDepth = character.data?.extensions?.depth_prompt?.depth ?? depth_prompt_depth_default;
426462 const depthPromptRole = character.data?.extensions?.depth_prompt?.role ?? depth_prompt_role_default;
427463
@@ -440,29 +476,46 @@ export function getGroupDepthPrompts(groupId, characterId) {
440476 * @returns {{description: string, personality: string, scenario: string, mesExamples: string}} Group character cards combined
441477 */
442478export 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+ */
498+export function getGroupCharacterCardsLazy(groupId, characterId) {
443499 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
445502 if (!group || !group?.generation_mode || !Array.isArray(group.members) || !group.members.length) {
446503 return null;
447504 }
448505
449506 /**
450507 * Runs the macro enginebaseChatReplace on a text, with custom <FIELDNAME> replace
451508 * @param {string} value Value to replace
452509 * @param {string} fieldName Name of the field
453510 * @param {string} characterName Name of the character
454511 * @param {boolean} trim Whether to trim the value
455512 * @returns {string} Replaced text
456513 * */
457514 function customBaseChatReplacecustomTransform(value, fieldName, characterName, trim) {
458515 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
463516 value = value.replace(/<FIELDNAME>/gi, fieldName);
464517 value = trim ? value.trim() : value;
465518 return baseChatReplace(value, name1null, characterName);
466519 }
467520
468521 /**
@@ -472,62 +525,57 @@ export function getGroupCharacterCards(groupId, characterId) {
472525 * @param {string} fieldName Name of the field
473526 * @param {function(string): string} [preprocess] Preprocess function
474527 * @returns {string} Prepared text
475528 * */
476529 function replaceAndPrepareForJoin(value, characterName, fieldName, preprocess = null) {
477530 value = value?.trim() ?? '';
478531 if (!value) {return '';
479- return '';
480- }
481-
482- // Run preprocess function
483532 if (typeof preprocess === 'function') {
484533 value = preprocess(value);
485534 }
486-
535+ const prefix = customTransform(group.generation_mode_join_prefix, fieldName, characterName, false);
487- // Prepare and replace prefixes
536+ const suffix = customTransform(group.generation_mode_join_suffix, fieldName, characterName, false);
488537 const prefixvalue = customBaseChatReplacecustomTransform(group.generation_mode_join_prefixvalue, fieldName, characterName, falsetrue);
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-
493538 return `${prefix}${value}${suffix}`;
494539 }
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
498-
543+ * @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+ */
503-
548+ function collectField(fieldName, getter, preprocess = null) {
549+ const values = [];
504550 for (const member of group.members) {
505551 const index = characters.findIndex(x => x.avatar === member);
506552 const character = characters[index];
507-
553+ if (index === -1 || !character) continue;
508- if (index === -1 || !character) {
509- console.debug(`Skipping missing member: ${member}`);
510- continue;
511- }
512-
513554 if (group.disabled_members.includes(member) && characterId !== index && group.generation_mode !== group_generation_mode.APPEND_DISABLED) {
514555 continue;
515556 }
516-
557+ values.push(replaceAndPrepareForJoin(getter(character), character.name, fieldName, preprocess));
517- descriptions.push(replaceAndPrepareForJoin(character.description, character.name, 'Description'));
558+ }
518- personalities.push(replaceAndPrepareForJoin(character.personality, character.name, 'Personality'));
559+ return values.filter(x => x.length).join('\n');
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));
521560 }
522561
523562 const descriptionscenarioOverride = descriptions.filterString(xchat_metadata['scenario'] =>|| x.length).join('\n');
524563 const personalitymesExamplesOverride = personalities.filterString(xchat_metadata['mes_example'] =>|| x.length).join('\n');
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+ });
529572}
530573
574+/**
575+ * Gets the first message for a character.
576+ * @param {Character} character Character object
577+ * @returns {Promise<ChatMessage>} First message object
578+ */
531579async function getFirstCharacterMessage(character) {
532580 let messageText = character.first_mes;
533581
@@ -552,7 +600,7 @@ async function getFirstCharacterMessage(character) {
552600 mes['original_avatar'] = character.avatar;
553601 mes['extra'] = { 'gen_id': Date.now() * Math.random() * 1000000 };
554602 mes['mes'] = messageText
555603 ? substituteParams(messageText.trim(), name1,{ name2Override: character.name })
556604 : '';
557605 mes['force_avatar'] =
558606 character.avatar != 'none'
@@ -566,27 +614,72 @@ function resetSelectedGroup() {
566614 is_group_generating = false;
567615}
568616
569-async 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+ */
624+async function saveGroupChat(groupId, shouldSaveGroup, force = false) {
570625 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;
572631 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+ };
573638 const response = await fetch('/api/chats/group/save', {
574639 method: 'POST',
575640 headers: getRequestHeaders(),
576641 body: JSON.stringify({ id: chat_idchatId, chat: [chatHeader, ...chat], force: force }),
577642 });
578643
579644 if (!response.ok) {
645+ const errorData = await response.json();
646+ const isIntegrityError = errorData?.error === 'integrity' && !force;
647+ if (!isIntegrityError) {
580648 toastr.error(t`Check the server connection and reload the page to prevent data loss.`, t`Group Chat could not be saved`);
581649 console.error('Group chat could not be saved', response);
582650 return;
583651 }
584652
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);
670+ }
671+
585672 if (shouldSaveGroup) {
586673 await editGroup(groupId, false, false);
587674 }
588675}
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+ */
590683export async function renameGroupMember(oldAvatar, newAvatar, newName) {
591684 // Scan every group for our renamed character
592685 for (const group of groups) {
@@ -614,6 +707,11 @@ export async function renameGroupMember(oldAvatar, newAvatar, newName) {
614707 if (Array.isArray(messages) && messages.length) {
615708 // Iterate over every chat message
616709 for (const message of messages) {
710+ // Skip the chat header
711+ if (Object.hasOwn(message, 'chat_metadata')) {
712+ continue;
713+ }
714+
617715 // Only look at character messages
618716 if (message.is_user || message.is_system) {
619717 continue;
@@ -630,6 +728,8 @@ export async function renameGroupMember(oldAvatar, newAvatar, newName) {
630728 }
631729
632730 if (hadChanges) {
731+ await eventSource.emit(event_types.CHARACTER_RENAMED_IN_PAST_CHAT, messages, oldAvatar, newAvatar);
732+
633733 const saveChatResponse = await fetch('/api/chats/group/save', {
634734 method: 'POST',
635735 headers: getRequestHeaders(),
@@ -652,15 +752,19 @@ export async function renameGroupMember(oldAvatar, newAvatar, newName) {
652752 }
653753}
654754
755+/**
756+ * Fetches all groups from the server and processes them.
757+ */
655758async function getGroups() {
656759 const response = await fetch('/api/groups/all', {
657760 method: 'POST',
658761 headers: getRequestHeaders({ omitContentType: true }),
659762 });
660763
661764 if (response.ok) {
765+ /** @type {Group[]} */
662766 const data = await response.json();
663- groups = data.sort((a, b) => a.id - b.id);
767+ groups = data.slice();
664768
665769 // Convert groups to new format
666770 for (const group of groups) {
@@ -678,9 +782,6 @@ async function getGroups() {
678782 .filter(x => x)
679783 .filter(onlyUnique);
680784 }
681- if (group.past_metadata == undefined) {
682- group.past_metadata = {};
683- }
684785 if (typeof group.chat_id === 'number') {
685786 group.chat_id = String(group.chat_id);
686787 }
@@ -691,6 +792,11 @@ async function getGroups() {
691792 }
692793}
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+ */
694800export function getGroupBlock(group) {
695801 let count = 0;
696802 let namesList = [];
@@ -712,7 +818,7 @@ export function getGroupBlock(group) {
712818 template.find('.ch_name').text(group.name).attr('title', `[Group] ${group.name}`);
713819 template.find('.group_fav_icon').css('display', 'none');
714820 template.addClass(group.fav ? 'is_fav' : '');
715821 template.find('.ch_fav').val(String(group.fav));
716822 template.find('.group_select_counter').text(count + ' ' + (count != 1 ? t`characters` : t`character`));
717823 template.find('.group_select_block_list').text(namesList.join(', '));
718824
@@ -728,6 +834,10 @@ export function getGroupBlock(group) {
728834 return template;
729835}
730836
837+/**
838+ * Updates the avatar display for a given group.
839+ * @param {Group} group Group object
840+ */
731841function updateGroupAvatar(group) {
732842 $('#group_avatar_preview').empty().append(getGroupAvatar(group));
733843
@@ -740,7 +850,11 @@ function updateGroupAvatar(group) {
740850 favsToHotswap();
741851}
742852
743-// check if isDataURLor if it's a valid local file url
853+/**
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+ */
744858function isValidImageUrl(url) {
745859 // check if empty dict
746860 if (Object.keys(url).length === 0) {
@@ -749,6 +863,11 @@ function isValidImageUrl(url) {
749863 return isDataURL(url) || (url && (url.startsWith('user') || url.startsWith('/user')));
750864}
751865
866+/**
867+ * Gets a group avatar element.
868+ * @param {Group} group Group object
869+ * @returns {JQuery<HTMLElement>} Group avatar element
870+ */
752871function getGroupAvatar(group) {
753872 if (!group) {
754873 return $(`<div class="avatar"><img src="${default_avatar}"></div>`);
@@ -797,6 +916,11 @@ function getGroupAvatar(group) {
797916 return groupAvatar;
798917}
799918
919+/**
920+ * Gets chat IDs for a group.
921+ * @param {string} groupId Group ID
922+ * @returns {string[]} Array of chat IDs
923+ */
800924function getGroupChatNames(groupId) {
801925 const group = groups.find(x => x.id === groupId);
802926
@@ -811,7 +935,14 @@ function getGroupChatNames(groupId) {
811935 return names;
812936}
813937
814-async 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+ */
945+async function generateGroupWrapper(byAutoMode, type = null, params = {}) {
815946 function throwIfAborted() {
816947 if (params.signal instanceof AbortSignal && params.signal.aborted) {
817948 throw new Error('AbortSignal was fired. Group generation stopped');
@@ -830,7 +961,7 @@ async function generateGroupWrapper(by_auto_mode, type = null, params = {}) {
830961
831962 // Auto-navigate back to group menu
832963 if (menu_type !== 'group_edit') {
833964 select_group_chats(selected_group, false);
834965 await delay(1);
835966 }
836967
@@ -859,7 +990,7 @@ async function generateGroupWrapper(by_auto_mode, type = null, params = {}) {
859990 let activationText = '';
860991 let isUserInput = false;
861992
862993 if (userInput?.length && !by_auto_modebyAutoMode) {
863994 isUserInput = true;
864995 activationText = userInput;
865996 } else {
@@ -935,12 +1066,12 @@ async function generateGroupWrapper(by_auto_mode, type = null, params = {}) {
9351066
9361067 // Wait for generation to finish
9371068 const generateType = ['swipe', 'impersonate', 'quiet', 'continue'].includes(type) ? type : 'normal';
9381069 textResult = await Generate(generateType, { automatic_trigger: by_auto_modebyAutoMode, ...(params || {}) });
9391070 let messageChunk = textResult?.messageChunk;
9401071
9411072 if (messageChunk) {
9421073 while (shouldAutoContinue(messageChunk, type === 'impersonate')) {
9431074 textResult = await Generate('continue', { automatic_trigger: by_auto_modebyAutoMode, ...(params || {}) });
9441075 messageChunk = textResult?.messageChunk;
9451076 }
9461077 }
@@ -966,6 +1097,10 @@ async function generateGroupWrapper(by_auto_mode, type = null, params = {}) {
9661097 return Promise.resolve(textResult);
9671098}
9681099
1100+/**
1101+ * Gets the generation ID of the last chat message.
1102+ * @returns {number|null} Generation ID or null
1103+ */
9691104function getLastMessageGenerationId() {
9701105 let generationId = null;
9711106 if (chat.length > 0) {
@@ -977,6 +1112,11 @@ function getLastMessageGenerationId() {
9771112 return generationId;
9781113}
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+ */
9801120function activateImpersonate(members) {
9811121 const randomIndex = Math.floor(Math.random() * members.length);
9821122 const activatedMembers = [members[randomIndex]];
@@ -1039,6 +1179,11 @@ function activateSwipe(members, { allowSystem = false } = {}) {
10391179 return memberIds;
10401180}
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+ */
10421187function activateListOrder(members) {
10431188 let activatedMembers = members.filter(onlyUnique);
10441189
@@ -1092,6 +1237,15 @@ function activatePooledOrder(members, lastMessage, isUserInput) {
10921237 return memberId !== -1 ? [memberId] : [];
10931238}
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+ */
10951249function activateNaturalOrder(members, input, lastMessage, allowSelfResponses, isUserInput) {
10961250 let activatedMembers = [];
10971251
@@ -1168,6 +1322,11 @@ function activateNaturalOrder(members, input, lastMessage, allowSelfResponses, i
11681322 return memberIds;
11691323}
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+ */
11711330async function deleteGroup(id) {
11721331 const group = groups.find((x) => x.id === id);
11731332
@@ -1197,6 +1356,13 @@ async function deleteGroup(id) {
11971356 }
11981357}
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+ */
12001366export async function editGroup(id, immediately, reload = true) {
12011367 let group = groups.find((x) => x.id === id);
12021368
@@ -1204,11 +1370,6 @@ export async function editGroup(id, immediately, reload = true) {
12041370 return;
12051371 }
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-
12121373 if (immediately) {
12131374 return await _save(group, reload);
12141375 }
@@ -1260,6 +1421,12 @@ async function groupChatAutoModeWorker() {
12601421 await generateGroupWrapper(true, 'auto', { signal: groupAutoModeAbortController.signal });
12611422}
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+ */
12631430async function modifyGroupMember(groupId, groupMember, isDelete) {
12641431 const id = groupMember.data('id');
12651432 const thisGroup = groups.find((x) => x.id == groupId);
@@ -1287,9 +1454,16 @@ async function modifyGroupMember(groupId, groupMember, isDelete) {
12871454 $('#rm_group_submit').prop('disabled', !groupHasMembers);
12881455}
12891456
1290-async 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+ */
1464+async function reorderGroupMember(groupId, groupMember, direction) {
12911465 const id = groupMember.data('id');
12921466 const thisGroup = groups.find((x) => x.id == chat_idgroupId);
12931467 const memberArray = thisGroup?.members ?? newGroupMembers;
12941468
12951469 const indexOf = memberArray.indexOf(id);
@@ -1312,7 +1486,7 @@ async function reorderGroupMember(chat_id, groupMember, direction) {
13121486
13131487 // Existing groups need to modify members list
13141488 if (openGroupId) {
13151489 await editGroup(chat_idgroupId, false, false);
13161490 updateGroupAvatar(thisGroup);
13171491 }
13181492}
@@ -1358,10 +1532,16 @@ async function onGroupNameInput() {
13581532 let _thisGroup = groups.find((x) => x.id == openGroupId);
13591533 _thisGroup.name = $(this).val();
13601534 $('#rm_button_selected_ch').children('h2').text(_thisGroup.name);
13611535 await editGroup(openGroupId, false);
13621536 }
13631537}
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+ */
13651545function isGroupMember(group, avatarId) {
13661546 if (group && Array.isArray(group.members)) {
13671547 return group.members.includes(avatarId);
@@ -1370,6 +1550,13 @@ function isGroupMember(group, avatarId) {
13701550 }
13711551}
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+ */
13731560function getGroupCharacters({ doFilter = false, onlyMembers = false } = {}) {
13741561 function sortMembersFn(a, b) {
13751562 const membersArray = thisGroup?.members ?? newGroupMembers;
@@ -1461,15 +1648,20 @@ function printGroupMembers() {
14611648 });
14621649}
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+ */
14641656function getGroupCharacterBlock(character) {
14651657 const avatar = getThumbnailUrl('avatar', character.avatar);
14661658 const template = $('#group_member_template .group_member').clone();
14671659 const isFav = !!character.fav || character.fav == 'true';
14681660 template.data('id', character.avatar);
14691661 template.find('.avatar img').attr({ 'src': avatar, 'title': character.avatar });
14701662 template.find('.ch_name').text(character.name);
14711663 template.attr('data-chid', characters.indexOf(character));
14721664 template.find('.ch_fav').val(String(isFav));
14731665 template.toggleClass('is_fav', isFav);
14741666
14751667 const auxFieldName = power_user.aux_field || 'character_version';
@@ -1503,6 +1695,11 @@ function getGroupCharacterBlock(character) {
15031695 return template;
15041696}
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+ */
15061703function isGroupMemberDisabled(avatarId) {
15071704 const thisGroup = openGroupId && groups.find((x) => x.id == openGroupId);
15081705 return Boolean(thisGroup && thisGroup.disabled_members.includes(avatarId));
@@ -1553,6 +1750,11 @@ async function onHideMutedSpritesClick(value) {
15531750 }
15541751}
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+ */
15561758function toggleHiddenControls(group, generationMode = null) {
15571759 const isJoin = [group_generation_mode.APPEND, group_generation_mode.APPEND_DISABLED].includes(generationMode ?? group?.generation_mode);
15581760 $('#rm_group_generation_mode_join_prefix').parent().toggle(isJoin);
@@ -1564,6 +1766,11 @@ function toggleHiddenControls(group, generationMode = null) {
15641766 }
15651767}
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+ */
15671774function select_group_chats(groupId, skipAnimation) {
15681775 openGroupId = groupId;
15691776 newGroupMembers = [];
@@ -1774,6 +1981,11 @@ function updateFavButtonState(state) {
17741981 $('#group_favorite_button').toggleClass('fav_off', !fav_grp_checked);
17751982}
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+ */
17771989export async function openGroupById(groupId) {
17781990 if (isChatSaving) {
17791991 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) {
17861998 }
17871999
17882000 if (!is_send_press && !is_group_generating) {
17892001 select_group_chats(groupId, false);
17902002
17912003 if (selected_group !== groupId) {
17922004 groupChatQueueOrder = new Map();
@@ -1806,6 +2018,11 @@ export async function openGroupById(groupId) {
18062018 return false;
18072019}
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+ */
18092026async function openCharacterDefinition(characterSelect) {
18102027 if (is_group_generating) {
18112028 toastr.warning(t`Can't peek a character while group reply is being generated`);
@@ -1834,7 +2051,7 @@ function filterGroupMembers() {
18342051}
18352052
18362053async function createGroup() {
18372054 let name = $('#rm_group_chat_name').val().toString();
18382055 let allowSelfResponses = !!$('#rm_group_allow_self_responses').prop('checked');
18392056 let activationStrategy = Number($('#rm_group_activation_strategy').find(':selected').val()) ?? group_activation_strategy.NATURAL;
18402057 let generationMode = Number($('#rm_group_generation_mode').find(':selected').val()) ?? group_generation_mode.SWAP;
@@ -1846,29 +2063,30 @@ async function createGroup() {
18462063 name = t`Group: ${memberNames}`;
18472064 }
18482065
18492066 const avatar_urlavatarUrl = $('#group_avatar_preview img').attr('src');
1850-
18512067 const chatName = humanizedDateTime();
18522068 const chats = [chatName];
18532069
1854- const createGroupResponse = await fetch('/api/groups/create', {
2070+ /** @type {Omit<Group, 'id'>} */
1855- method: 'POST',
2071+ const groupCreateModel = {
1856- headers: getRequestHeaders(),
1857- body: JSON.stringify({
18582072 name: name,
18592073 members: members,
18602074 avatar_url: isValidImageUrl(avatar_urlavatarUrl) ? avatar_urlavatarUrl : default_avatar,
18612075 allow_self_responses: allowSelfResponses,
18622076 hideMutedSprites: hideMutedSprites,
18632077 activation_strategy: activationStrategy,
18642078 generation_mode: generationMode,
18652079 disabled_members: [],
1866- chat_metadata: {},
18672080 fav: fav_grp_checked,
18682081 chat_id: chatName,
18692082 chats: chats,
18702083 auto_mode_delay: autoModeDelay,
1871- }),
2084+ };
2085+
2086+ const createGroupResponse = await fetch('/api/groups/create', {
2087+ method: 'POST',
2088+ headers: getRequestHeaders(),
2089+ body: JSON.stringify(groupCreateModel),
18722090 });
18732091
18742092 if (createGroupResponse.ok) {
@@ -1880,6 +2098,11 @@ async function createGroup() {
18802098 }
18812099}
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+ */
18832106export async function createNewGroupChat(groupId) {
18842107 const group = groups.find(x => x.id === groupId);
18852108
@@ -1887,27 +2110,22 @@ export async function createNewGroupChat(groupId) {
18872110 return;
18882111 }
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-
18972113 await clearChat();
18982114 chat.length = 0;
1899- if (oldChatName) {
2115+ const newChatName = humanizedDateTime();
1900- group.past_metadata[oldChatName] = Object.assign({}, chat_metadata);
1901- }
19022116 group.chats.push(newChatName);
19032117 group.chat_id = newChatName;
1904- group.chat_metadata = {};
2118+ updateChatMetadata({}, true);
1905- updateChatMetadata(group.chat_metadata, true);
19062119
19072120 await editGroup(group.id, true, false);
19082121 await getGroupChat(group.id);
19092122}
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+ */
19112129export async function getGroupPastChats(groupId) {
19122130 const group = groups.find(x => x.id === groupId);
19132131
@@ -1920,16 +2138,22 @@ export async function getGroupPastChats(groupId) {
19202138 try {
19212139 for (const chatId of group.chats) {
19222140 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;
19252149 const lastMessage = messages.length ? messages[messages.length - 1].mes : '[The chat is empty]';
19262150 const lastMessageDate = messages.length ? (messages[messages.length - 1].send_date || Date.now()) : Date.now();
19272151 chats.push({
19282152 'file_name': chatId,
19292153 'mes': lastMessage,
19302154 'last_mes': lastMessageDate,
19312155 'file_size': this_chat_file_sizefileSize,
19322156 'chat_items': chat_itemschatItems,
19332157 });
19342158 }
19352159 } catch (err) {
@@ -1938,6 +2162,12 @@ export async function getGroupPastChats(groupId) {
19382162 return chats;
19392163}
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+ */
19412171export async function openGroupChat(groupId, chatId) {
19422172 await waitUntilCondition(() => !isChatSaving, debounce_timeout.extended, 10);
19432173 const group = groups.find(x => x.id === groupId);
@@ -1948,17 +2178,21 @@ export async function openGroupChat(groupId, chatId) {
19482178
19492179 await clearChat();
19502180 chat.length = 0;
1951- const previousChat = group.chat_id;
1952- group.past_metadata[previousChat] = Object.assign({}, chat_metadata);
19532181 group.chat_id = chatId;
1954- group.chat_metadata = group.past_metadata[chatId] || {};
19552182 group['date_last_chat'] = Date.now();
19562183 updateChatMetadata(group.chat_metadata{}, true);
19572184
19582185 await editGroup(groupId, true, false);
19592186 await getGroupChat(groupId);
19602187}
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+ */
19622196export async function renameGroupChat(groupId, oldChatId, newChatId) {
19632197 const group = groups.find(x => x.id === groupId);
19642198
@@ -1972,8 +2206,6 @@ export async function renameGroupChat(groupId, oldChatId, newChatId) {
19722206
19732207 group.chats.splice(group.chats.indexOf(oldChatId), 1);
19742208 group.chats.push(newChatId);
1975- group.past_metadata[newChatId] = (group.past_metadata[oldChatId] || {});
1976- delete group.past_metadata[oldChatId];
19772209
19782210 await editGroup(groupId, true, true);
19792211}
@@ -1990,12 +2222,7 @@ export async function deleteGroupChatByName(groupId, chatName) {
19902222 return;
19912223 }
19922224
1993- if (typeof group.past_metadata !== 'object') {
1994- group.past_metadata = {};
1995- }
1996-
19972225 group.chats.splice(group.chats.indexOf(chatName), 1);
1998- delete group.past_metadata[chatName];
19992226
20002227 const response = await fetch('/api/chats/group/delete', {
20012228 method: 'POST',
@@ -2011,12 +2238,8 @@ export async function deleteGroupChatByName(groupId, chatName) {
20112238
20122239 // If the deleted chat was the current chat, switch to the last chat in the group
20132240 if (group.chat_id === chatName) {
2014- group.chat_id = '';
2015- group.chat_metadata = {};
2016-
20172241 const newChatName = group.chats.length ? group.chats[group.chats.length - 1] : humanizedDateTime();
20182242 group.chat_id = newChatName;
2019- group.chat_metadata = group.past_metadata[newChatName] || {};
20202243 }
20212244
20222245 await editGroup(groupId, true, true);
@@ -2038,12 +2261,10 @@ export async function deleteGroupChat(groupId, chatId, { jumpToNewChat = true }
20382261 }
20392262
20402263 group.chats.splice(group.chats.indexOf(chatId), 1);
2041- delete group.past_metadata[chatId];
20422264
20432265 if (group.chat_id === chatId) {
20442266 group.chat_id = '';
2045- group.chat_metadata = {};
2267+ updateChatMetadata({}, true);
2046- updateChatMetadata(group.chat_metadata, true);
20472268 }
20482269
20492270 const response = await fetch('/api/chats/group/delete', {
@@ -2093,6 +2314,8 @@ export async function importGroupChat(formData, { refresh = true } = {}) {
20932314 await displayPastChats();
20942315 }
20952316 }
2317+
2318+ return [data.res];
20962319 }
20972320
20982321 return data?.fileNames || [];
@@ -2101,6 +2324,14 @@ export async function importGroupChat(formData, { refresh = true } = {}) {
21012324 return [];
21022325}
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+ */
21042335export async function saveGroupBookmarkChat(groupId, name, metadata, mesId) {
21052336 const group = groups.find(x => x.id === groupId);
21062337
@@ -2108,11 +2339,18 @@ export async function saveGroupBookmarkChat(groupId, name, metadata, mesId) {
21082339 return;
21092340 }
21102341
2111- group.past_metadata[name] = { ...chat_metadata, ...(metadata || {}) };
21122342 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)
21162354 : chat;
21172355
21182356 await editGroup(groupId, true, false);
@@ -2120,7 +2358,7 @@ export async function saveGroupBookmarkChat(groupId, name, metadata, mesId) {
21202358 const response = await fetch('/api/chats/group/save', {
21212359 method: 'POST',
21222360 headers: getRequestHeaders(),
21232361 body: JSON.stringify({ id: name, chat: [chatHeader, ...trimmed_chattrimmedChat] }),
21242362 });
21252363
21262364 if (!response.ok) {
public/scripts/horde.js+2 -2
@@ -107,7 +107,7 @@ export async function checkHordeStatus() {
107107 try {
108108 const response = await fetch('/api/horde/status', {
109109 method: 'POST',
110110 headers: getRequestHeaders({ omitContentType: true }),
111111 });
112112
113113 if (!response.ok) {
@@ -328,7 +328,7 @@ export function loadHordeSettings(settings) {
328328async function showKudos() {
329329 const response = await fetch('/api/horde/user-info', {
330330 method: 'POST',
331331 headers: getRequestHeaders({ omitContentType: true }),
332332 });
333333
334334 if (!response.ok) {
public/scripts/instruct-mode.js+7 -7
@@ -436,10 +436,10 @@ export function formatInstructModeChat(name, mes, isUser, isNarrator, forceAvata
436436 let suffix = getSuffix() || '';
437437
438438 if (instruct.macro) {
439439 prefix = substituteParams(prefix, { name1Override: name1, name2Override: name2 });
440440 prefix = prefix.replace(/{{name}}/gi, name || 'System');
441441
442442 suffix = substituteParams(suffix, { name1Override: name1, name2Override: name2 });
443443 suffix = suffix.replace(/{{name}}/gi, name || 'System');
444444 }
445445
@@ -524,10 +524,10 @@ export function formatInstructModeExamples(mesExamplesArray, name1, name2) {
524524 let outputSuffix = power_user.instruct.output_suffix || '';
525525
526526 if (power_user.instruct.macro) {
527527 inputPrefix = substituteParams(inputPrefix, { name1Override: name1, name2Override: name2 });
528528 outputPrefix = substituteParams(outputPrefix, { name1Override: name1, name2Override: name2 });
529529 inputSuffix = substituteParams(inputSuffix, { name1Override: name1, name2Override: name2 });
530530 outputSuffix = substituteParams(outputSuffix, { name1Override: name1, name2Override: name2 });
531531
532532 inputPrefix = inputPrefix.replace(/{{name}}/gi, name1);
533533 outputPrefix = outputPrefix.replace(/{{name}}/gi, name2);
@@ -631,7 +631,7 @@ export function formatInstructModePrompt(name, isImpersonate, promptBias, name1,
631631 }
632632
633633 if (instruct.macro) {
634634 sequence = substituteParams(sequence, { name1Override: name1, name2Override: name2 });
635635 sequence = sequence.replace(/{{name}}/gi, name || 'System');
636636 }
637637
public/scripts/keyboard.js+1 -0
@@ -19,6 +19,7 @@ const interactableSelectors = [
1919 '.stscript_btn', // STscript buttons in the chat bar
2020 '.select2_choice_clickable+span.select2-container .select2-selection__choice__display', // select2 control elements if they are meant to be clickable
2121 '.avatar_load_preview', // Char display avatar selection
22+ '.bg_tabs_list .bg_tab_button', // Background tabs
2223];
2324
2425if (CSS.supports('selector(:has(*))')) {
public/scripts/logprobs.js+1 -0
@@ -417,6 +417,7 @@ function createSwipe(messageId, prompt) {
417417
418418 console.debug('cleanedPrompt: ', cleanedPrompt);
419419
420+ /** @type {SwipeInfo} */
420421 const newSwipeInfo = {
421422 send_date: msg.send_date,
422423 gen_started: msg.gen_started,
public/scripts/macros.js+125 -2
@@ -6,6 +6,8 @@ import { getInstructMacros } from './instruct-mode.js';
66import { getVariableMacros } from './variables.js';
77import { isMobile } from './RossAscends-mods.js';
88import { inject_ids } from './constants.js';
9+import { initRegisterMacros, macros as macroSystem } from './macros/macro-system.js';
10+import { power_user } from './power-user.js';
911
1012/**
1113 * @typedef Macro
@@ -33,6 +35,10 @@ Handlebars.registerHelper('helperMissing', function () {
3335 * @property {string} description - Optional description of the macro
3436 */
3537
38+/**
39+ * @deprecated Use macros.registry.registerMacro (from scripts/macros/macro-system.js)
40+ * or substituteParams({ dynamicMacros }) with the new macro engine.
41+ */
3642export class MacrosParser {
3743 /**
3844 * A map of registered macros.
@@ -47,10 +53,97 @@ export class MacrosParser {
4753 static #descriptions = new Map();
4854
4955 /**
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+
133+ /**
50134 * Returns an iterator over all registered macros.
51135 * @returns {IterableIterator<CustomMacro>}
52136 */
53137 static [Symbol.iterator] = function* () {
138+ // When experimental macro engine is active, yield from the new registry
139+ if (power_user.experimental_macro_engine) {
140+ // Exclude hidden aliases for consistency with autocomplete behavior
141+ for (const def of macroSystem.registry.getAllMacros({ excludeHiddenAliases: true })) {
142+ yield { key: def.name, description: def.description || '' };
143+ }
144+ return;
145+ }
146+
54147 for (const macro of MacrosParser.#macros.keys()) {
55148 yield { key: macro, description: MacrosParser.#descriptions.get(macro) };
56149 }
@@ -62,6 +155,7 @@ export class MacrosParser {
62155 * @returns {string|MacroFunction|undefined} The macro value
63156 */
64157 static get(key) {
158+ MacrosParser.#logDeprecated('get', 'macros.registry.getMacro (from scripts/macros/macro-system.js)');
65159 return MacrosParser.#macros.get(key);
66160 }
67161
@@ -71,6 +165,11 @@ export class MacrosParser {
71165 * @returns {boolean} True if the macro is registered, false otherwise
72166 */
73167 static has(key) {
168+ MacrosParser.#logDeprecated('has', 'macros.registry.hasMacro (from scripts/macros/macro-system.js)');
169+ if (power_user.experimental_macro_engine) {
170+ return macroSystem.registry.hasMacro(key);
171+ }
172+
74173 return MacrosParser.#macros.has(key);
75174 }
76175
@@ -81,6 +180,7 @@ export class MacrosParser {
81180 * @param {string} [description] Optional description of the macro
82181 */
83182 static registerMacro(key, value, description = '') {
183+ MacrosParser.#logDeprecated('registerMacro', 'macros.registry.registerMacro (from scripts/macros/macro-system.js) or substituteParams({ dynamicMacros })');
84184 if (typeof key !== 'string') {
85185 throw new Error('Macro key must be a string');
86186 }
@@ -101,6 +201,11 @@ export class MacrosParser {
101201 value = this.sanitizeMacroValue(value);
102202 }
103203
204+ MacrosParser.#registerMacroInNewEngine(key, value, description);
205+ if (power_user.experimental_macro_engine) {
206+ return;
207+ }
208+
104209 if (this.#macros.has(key)) {
105210 console.warn(`Macro ${key} is already registered`);
106211 }
@@ -118,6 +223,7 @@ export class MacrosParser {
118223 * @param {string} key Macro name (key)
119224 */
120225 static unregisterMacro(key) {
226+ MacrosParser.#logDeprecated('unregisterMacro', 'macros.registry.unregisterMacro (from scripts/macros/macro-system.js)');
121227 if (typeof key !== 'string') {
122228 throw new Error('Macro key must be a string');
123229 }
@@ -129,6 +235,11 @@ export class MacrosParser {
129235 throw new Error('Macro key must not be empty or whitespace only');
130236 }
131237
238+ if (power_user.experimental_macro_engine) {
239+ MacrosParser.#unregisterMacroInNewEngine(key);
240+ return;
241+ }
242+
132243 const deleted = this.#macros.delete(key);
133244
134245 if (!deleted) {
@@ -597,10 +708,15 @@ export function evaluateMacros(content, env, postProcessFn) {
597708}
598709
599710export function initMacros() {
711+ // Only manually register those is new macro engine is not on. In the new one, they are already registered automatically
712+ if (!power_user.experimental_macro_engine) {
600713 function initLastGenerationType() {
601714 let lastGenerationType = '';
602715
603716 MacrosParser.registerMacro('lastGenerationType', () => lastGenerationType);
717+ () => lastGenerationType,
718+ 'Returns the type of the last generation (e.g., "normal", "swipe", "continue", "impersonate", "quiet").',
719+ );
604720
605721 eventSource.on(event_types.GENERATION_STARTED, (type, _params, isDryRun) => {
606722 if (isDryRun) return;
@@ -612,6 +728,13 @@ export function initMacros() {
612728 });
613729 }
614730
615- MacrosParser.registerMacro('isMobile', () => String(isMobile()));
731+ MacrosParser.registerMacro('isMobile',
732+ () => String(isMobile()),
733+ 'Returns "true" if the user is on a mobile device, "false" otherwise.',
734+ );
616735 initLastGenerationType();
617736 }
737+
738+ // TODO: Needs to be moved once old macros are deprecated and removed
739+ initRegisterMacros();
740+}
public/scripts/macros/MacroBrowser.js+4 -0
@@ -0,0 +1,680 @@
1+/**
2+ * MacroBrowser - Dynamic documentation browser for macros.
3+ * Similar to SlashCommandBrowser but for the macro system.
4+ */
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