Merge pull request #4925 from SillyTavern/staging Staging
Signed| @@ -15,3 +15,7 @@ access.log | ||
| 15 | 15 | /public/scripts/extensions/third-party |
| 16 | 16 | /colab |
| 17 | 17 | .gemini |
| 18 | +/docker/config | |
| 19 | +/docker/extensions | |
| 20 | +/docker/data | |
| 21 | +/docker/plugins | |
| @@ -79,6 +79,7 @@ GNU Affero General Public License for more details.** | ||
| 79 | 79 | * Portions of CncAnon's TavernAITurbo mod used with permission |
| 80 | 80 | * Visual Novel Mode inspired by the work of PepperTaco (<https://github.com/peppertaco/Tavern/>) |
| 81 | 81 | * Noto Sans font by Google (OFL license) |
| 82 | +* Lexer/Parser by Chevrotain (Apache-2.0 license) <https://github.com/chevrotain/chevrotain> | |
| 82 | 83 | * Icon theme by Font Awesome <https://fontawesome.com> (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) |
| 83 | 84 | * Default content by @OtisAlejandro (Seraphina character and lorebook) and @kallmeflocc (10K Discord Users Celebratory Background) |
| 84 | 85 | * Docker guide by [@mrguymiah](https://github.com/mrguymiah) and [@Bronya-Rand](https://github.com/Bronya-Rand) |
| @@ -287,6 +287,16 @@ claude: | ||
| 287 | 287 | gemini: |
| 288 | 288 | # API endpoint version ("v1beta" or "v1alpha") |
| 289 | 289 | apiVersion: 'v1beta' |
| 290 | + # Enables caching of the system prompt (if supported). Only for OpenRouter. | |
| 291 | + # -- IMPORTANT! -- | |
| 292 | + # Use only when the prompt before the chat history is static and doesn't change between requests | |
| 293 | + # (e.g {{random}} macro or lorebooks not as in-chat injections). | |
| 294 | + # Otherwise, you'll just waste money on cache misses. | |
| 295 | + enableSystemPromptCache: false | |
| 296 | + # https://ai.google.dev/gemini-api/docs/imagen#imagen-configuration | |
| 297 | + image: | |
| 298 | + # Leave empty to use the API-default value. | |
| 299 | + personGeneration: 'allow_adult' | |
| 290 | 300 | # -- SERVER PLUGIN CONFIGURATION -- |
| 291 | 301 | enableServerPlugins: false |
| 292 | 302 | # Attempt to automatically update server plugins on startup |
| @@ -8,6 +8,8 @@ | ||
| 8 | 8 | "openrouter_sort_models": "alphabetically", |
| 9 | 9 | "ai21_model": "jamba-large", |
| 10 | 10 | "mistralai_model": "mistral-large-latest", |
| 11 | + "chutes_model": "deepseek-ai/DeepSeek-V3-0324", | |
| 12 | + "chutes_sort_models": "alphabetically", | |
| 11 | 13 | "electronhub_model": "gpt-4o-mini", |
| 12 | 14 | "electronhub_sort_models": "alphabetically", |
| 13 | 15 | "electronhub_group_models": false, |
| @@ -28,7 +30,6 @@ | ||
| 28 | 30 | "repetition_penalty": 1, |
| 29 | 31 | "openai_max_context": 4095, |
| 30 | 32 | "openai_max_tokens": 300, |
| 31 | - "wrap_in_quotes": false, | |
| 32 | 33 | "names_behavior": 0, |
| 33 | 34 | "send_if_empty": "", |
| 34 | 35 | "impersonation_prompt": "[Write your next reply from the point of view of {{user}}, using the chat history so far as a guideline for the writing style of {{user}}. Don't write as {{char}} or system. Don't describe actions of {{char}}.]", |
| @@ -231,9 +232,9 @@ | ||
| 231 | 232 | "show_external_models": false, |
| 232 | 233 | "assistant_prefill": "", |
| 233 | 234 | "assistant_impersonation": "", |
| 234 | 235 | "claude_use_syspromptuse_sysprompt": false, |
| 235 | 236 | "squash_system_messages": false, |
| 236 | 237 | "image_inliningmedia_inlining": falsetrue, |
| 237 | 238 | "bypass_status_check": false, |
| 238 | 239 | "continue_prefill": false, |
| 239 | 240 | "continue_postfix": " ", |
| @@ -191,6 +191,7 @@ | ||
| 191 | 191 | "custom_stopping_strings_macro": true, |
| 192 | 192 | "fuzzy_search": true, |
| 193 | 193 | "encode_tags": false, |
| 194 | + "experimental_macro_engine": false, | |
| 194 | 195 | "enableLabMode": false, |
| 195 | 196 | "enableZenSliders": false, |
| 196 | 197 | "ui_mode": 1, |
| @@ -463,7 +464,6 @@ | ||
| 463 | 464 | "stream_openai": true, |
| 464 | 465 | "openai_max_context": 4095, |
| 465 | 466 | "openai_max_tokens": 300, |
| 466 | - "wrap_in_quotes": false, | |
| 467 | 467 | "prompts": [ |
| 468 | 468 | { |
| 469 | 469 | "name": "Main Prompt", |
| @@ -632,6 +632,8 @@ | ||
| 632 | 632 | "show_external_models": false, |
| 633 | 633 | "proxy_password": "", |
| 634 | 634 | "assistant_prefill": "", |
| 635 | 635 | "assistant_impersonation": "", |
| 636 | + "use_sysprompt": false, | |
| 637 | + "squash_system_messages": false | |
| 636 | 638 | } |
| 637 | 639 | } |
| @@ -1,12 +1,12 @@ | ||
| 1 | 1 | { |
| 2 | 2 | "name": "sillytavern", |
| 3 | 3 | "version": "1.1415.0", |
| 4 | 4 | "lockfileVersion": 3, |
| 5 | 5 | "requires": true, |
| 6 | 6 | "packages": { |
| 7 | 7 | "": { |
| 8 | 8 | "name": "sillytavern", |
| 9 | 9 | "version": "1.1415.0", |
| 10 | 10 | "hasInstallScript": true, |
| 11 | 11 | "license": "AGPL-3.0", |
| 12 | 12 | "dependencies": { |
| @@ -45,6 +45,7 @@ | ||
| 45 | 45 | "bowser": "^2.12.1", |
| 46 | 46 | "bytes": "^3.1.2", |
| 47 | 47 | "chalk": "^5.6.0", |
| 48 | + "chevrotain": "^11.0.3", | |
| 48 | 49 | "command-exists": "^1.2.9", |
| 49 | 50 | "compression": "^1.8.1", |
| 50 | 51 | "cookie-parser": "^1.4.6", |
| @@ -59,7 +60,6 @@ | ||
| 59 | 60 | "express": "^4.21.0", |
| 60 | 61 | "form-data": "^4.0.4", |
| 61 | 62 | "fuse.js": "^7.1.0", |
| 62 | - "google-translate-api-browser": "^3.0.1", | |
| 63 | 63 | "google-translate-api-x": "^10.7.2", |
| 64 | 64 | "handlebars": "^4.7.8", |
| 65 | 65 | "helmet": "^8.1.0", |
| @@ -73,7 +73,7 @@ | ||
| 73 | 73 | "is-docker": "^3.0.0", |
| 74 | 74 | "localforage": "^1.10.0", |
| 75 | 75 | "lodash": "^4.17.21", |
| 76 | 76 | "mime-types": "^3.0.12", |
| 77 | 77 | "moment": "^2.30.1", |
| 78 | 78 | "morphdom": "^2.7.7", |
| 79 | 79 | "multer": "^2.0.2", |
| @@ -106,6 +106,7 @@ | ||
| 106 | 106 | "sillytavern": "src/server-global.js" |
| 107 | 107 | }, |
| 108 | 108 | "devDependencies": { |
| 109 | + "@chevrotain/types": "^11.0.3", | |
| 109 | 110 | "@types/archiver": "^6.0.3", |
| 110 | 111 | "@types/bytes": "^3.1.5", |
| 111 | 112 | "@types/command-exists": "^1.2.3", |
| @@ -134,7 +135,8 @@ | ||
| 134 | 135 | "@types/yauzl": "^2.10.3", |
| 135 | 136 | "eslint": "^8.57.1", |
| 136 | 137 | "eslint-plugin-jest": "^27.9.0", |
| 137 | 138 | "eslint-plugin-jsdoc": "^48.10.0", |
| 139 | + "eslint-plugin-playwright": "^2.3.0" | |
| 138 | 140 | }, |
| 139 | 141 | "engines": { |
| 140 | 142 | "node": ">= 18" |
| @@ -171,6 +173,45 @@ | ||
| 171 | 173 | "integrity": "sha512-KlmTftToTtmb6aLVdne4NluS+POWputPF5J8v25UN/EQS+K9vahWEIe1NPRSFqBQclObkqHaj7JOnFrmnSm5MA==", |
| 172 | 174 | "license": "Apache-2.0" |
| 173 | 175 | }, |
| 176 | + "node_modules/@chevrotain/cst-dts-gen": { | |
| 177 | + "version": "11.0.3", | |
| 178 | + "resolved": "https://registry.npmjs.org/@chevrotain/cst-dts-gen/-/cst-dts-gen-11.0.3.tgz", | |
| 179 | + "integrity": "sha512-BvIKpRLeS/8UbfxXxgC33xOumsacaeCKAjAeLyOn7Pcp95HiRbrpl14S+9vaZLolnbssPIUuiUd8IvgkRyt6NQ==", | |
| 180 | + "license": "Apache-2.0", | |
| 181 | + "dependencies": { | |
| 182 | + "@chevrotain/gast": "11.0.3", | |
| 183 | + "@chevrotain/types": "11.0.3", | |
| 184 | + "lodash-es": "4.17.21" | |
| 185 | + } | |
| 186 | + }, | |
| 187 | + "node_modules/@chevrotain/gast": { | |
| 188 | + "version": "11.0.3", | |
| 189 | + "resolved": "https://registry.npmjs.org/@chevrotain/gast/-/gast-11.0.3.tgz", | |
| 190 | + "integrity": "sha512-+qNfcoNk70PyS/uxmj3li5NiECO+2YKZZQMbmjTqRI3Qchu8Hig/Q9vgkHpI3alNjr7M+a2St5pw5w5F6NL5/Q==", | |
| 191 | + "license": "Apache-2.0", | |
| 192 | + "dependencies": { | |
| 193 | + "@chevrotain/types": "11.0.3", | |
| 194 | + "lodash-es": "4.17.21" | |
| 195 | + } | |
| 196 | + }, | |
| 197 | + "node_modules/@chevrotain/regexp-to-ast": { | |
| 198 | + "version": "11.0.3", | |
| 199 | + "resolved": "https://registry.npmjs.org/@chevrotain/regexp-to-ast/-/regexp-to-ast-11.0.3.tgz", | |
| 200 | + "integrity": "sha512-1fMHaBZxLFvWI067AVbGJav1eRY7N8DDvYCTwGBiE/ytKBgP8azTdgyrKyWZ9Mfh09eHWb5PgTSO8wi7U824RA==", | |
| 201 | + "license": "Apache-2.0" | |
| 202 | + }, | |
| 203 | + "node_modules/@chevrotain/types": { | |
| 204 | + "version": "11.0.3", | |
| 205 | + "resolved": "https://registry.npmjs.org/@chevrotain/types/-/types-11.0.3.tgz", | |
| 206 | + "integrity": "sha512-gsiM3G8b58kZC2HaWR50gu6Y1440cHiJ+i3JUvcp/35JchYejb2+5MVeJK0iKThYpAa/P2PYFV4hoi44HD+aHQ==", | |
| 207 | + "license": "Apache-2.0" | |
| 208 | + }, | |
| 209 | + "node_modules/@chevrotain/utils": { | |
| 210 | + "version": "11.0.3", | |
| 211 | + "resolved": "https://registry.npmjs.org/@chevrotain/utils/-/utils-11.0.3.tgz", | |
| 212 | + "integrity": "sha512-YslZMgtJUyuMbZ+aKvfF3x1f5liK4mWNxghFRv7jqRR9C3R3fAOGTTKvxXDa2Y1s9zSbcpuO0cAxDYsc9SrXoQ==", | |
| 213 | + "license": "Apache-2.0" | |
| 214 | + }, | |
| 174 | 215 | "node_modules/@es-joy/jsdoccomment": { |
| 175 | 216 | "version": "0.46.0", |
| 176 | 217 | "resolved": "https://registry.npmjs.org/@es-joy/jsdoccomment/-/jsdoccomment-0.46.0.tgz", |
| @@ -3381,6 +3422,20 @@ | ||
| 3381 | 3422 | "url": "https://github.com/sponsors/fb55" |
| 3382 | 3423 | } |
| 3383 | 3424 | }, |
| 3425 | + "node_modules/chevrotain": { | |
| 3426 | + "version": "11.0.3", | |
| 3427 | + "resolved": "https://registry.npmjs.org/chevrotain/-/chevrotain-11.0.3.tgz", | |
| 3428 | + "integrity": "sha512-ci2iJH6LeIkvP9eJW6gpueU8cnZhv85ELY8w8WiFtNjMHA5ad6pQLaJo9mEly/9qUyCpvqX8/POVUTf18/HFdw==", | |
| 3429 | + "license": "Apache-2.0", | |
| 3430 | + "dependencies": { | |
| 3431 | + "@chevrotain/cst-dts-gen": "11.0.3", | |
| 3432 | + "@chevrotain/gast": "11.0.3", | |
| 3433 | + "@chevrotain/regexp-to-ast": "11.0.3", | |
| 3434 | + "@chevrotain/types": "11.0.3", | |
| 3435 | + "@chevrotain/utils": "11.0.3", | |
| 3436 | + "lodash-es": "4.17.21" | |
| 3437 | + } | |
| 3438 | + }, | |
| 3384 | 3439 | "node_modules/chrome-trace-event": { |
| 3385 | 3440 | "version": "1.0.4", |
| 3386 | 3441 | "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", |
| @@ -4539,6 +4594,35 @@ | ||
| 4539 | 4594 | "dev": true, |
| 4540 | 4595 | "license": "MIT" |
| 4541 | 4596 | }, |
| 4597 | + "node_modules/eslint-plugin-playwright": { | |
| 4598 | + "version": "2.3.0", | |
| 4599 | + "resolved": "https://registry.npmjs.org/eslint-plugin-playwright/-/eslint-plugin-playwright-2.3.0.tgz", | |
| 4600 | + "integrity": "sha512-7UeUuIb5SZrNkrUGb2F+iwHM97kn33/huajcVtAaQFCSMUYGNFvjzRPil5C0OIppslPfuOV68M/zsisXx+/ZvQ==", | |
| 4601 | + "dev": true, | |
| 4602 | + "license": "MIT", | |
| 4603 | + "dependencies": { | |
| 4604 | + "globals": "^16.4.0" | |
| 4605 | + }, | |
| 4606 | + "engines": { | |
| 4607 | + "node": ">=16.9.0" | |
| 4608 | + }, | |
| 4609 | + "peerDependencies": { | |
| 4610 | + "eslint": ">=8.40.0" | |
| 4611 | + } | |
| 4612 | + }, | |
| 4613 | + "node_modules/eslint-plugin-playwright/node_modules/globals": { | |
| 4614 | + "version": "16.5.0", | |
| 4615 | + "resolved": "https://registry.npmjs.org/globals/-/globals-16.5.0.tgz", | |
| 4616 | + "integrity": "sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==", | |
| 4617 | + "dev": true, | |
| 4618 | + "license": "MIT", | |
| 4619 | + "engines": { | |
| 4620 | + "node": ">=18" | |
| 4621 | + }, | |
| 4622 | + "funding": { | |
| 4623 | + "url": "https://github.com/sponsors/sindresorhus" | |
| 4624 | + } | |
| 4625 | + }, | |
| 4542 | 4626 | "node_modules/eslint-scope": { |
| 4543 | 4627 | "version": "7.2.2", |
| 4544 | 4628 | "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", |
| @@ -5360,12 +5444,6 @@ | ||
| 5360 | 5444 | "url": "https://github.com/sponsors/sindresorhus" |
| 5361 | 5445 | } |
| 5362 | 5446 | }, |
| 5363 | - "node_modules/google-translate-api-browser": { | |
| 5364 | - "version": "3.0.1", | |
| 5365 | - "resolved": "https://registry.npmjs.org/google-translate-api-browser/-/google-translate-api-browser-3.0.1.tgz", | |
| 5366 | - "integrity": "sha512-KTLodkyGBWMK9IW6QIeJ2zCuju4Z0CLpbkADKo+yLhbSTD4l+CXXpQ/xaynGVAzeBezzJG6qn8MLeqOq3SmW0A==", | |
| 5367 | - "license": "MIT" | |
| 5368 | - }, | |
| 5369 | 5447 | "node_modules/google-translate-api-x": { |
| 5370 | 5448 | "version": "10.7.2", |
| 5371 | 5449 | "resolved": "https://registry.npmjs.org/google-translate-api-x/-/google-translate-api-x-10.7.2.tgz", |
| @@ -6285,6 +6363,12 @@ | ||
| 6285 | 6363 | "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", |
| 6286 | 6364 | "license": "MIT" |
| 6287 | 6365 | }, |
| 6366 | + "node_modules/lodash-es": { | |
| 6367 | + "version": "4.17.21", | |
| 6368 | + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz", | |
| 6369 | + "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==", | |
| 6370 | + "license": "MIT" | |
| 6371 | + }, | |
| 6288 | 6372 | "node_modules/lodash.get": { |
| 6289 | 6373 | "version": "4.4.2", |
| 6290 | 6374 | "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", |
| @@ -6418,15 +6502,19 @@ | ||
| 6418 | 6502 | } |
| 6419 | 6503 | }, |
| 6420 | 6504 | "node_modules/mime-types": { |
| 6421 | 6505 | "version": "3.0.12", |
| 6422 | 6506 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.12.tgz", |
| 6423 | 6507 | "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFDLbgzdk0h4juoQ9fCKXW4by0UJqj+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWAnOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", |
| 6424 | 6508 | "license": "MIT", |
| 6425 | 6509 | "dependencies": { |
| 6426 | 6510 | "mime-db": "^1.54.0" |
| 6427 | 6511 | }, |
| 6428 | 6512 | "engines": { |
| 6429 | 6513 | "node": ">= 0.618" |
| 6514 | + }, | |
| 6515 | + "funding": { | |
| 6516 | + "type": "opencollective", | |
| 6517 | + "url": "https://opencollective.com/express" | |
| 6430 | 6518 | } |
| 6431 | 6519 | }, |
| 6432 | 6520 | "node_modules/mime-types/node_modules/mime-db": { |
| @@ -35,6 +35,7 @@ | ||
| 35 | 35 | "bowser": "^2.12.1", |
| 36 | 36 | "bytes": "^3.1.2", |
| 37 | 37 | "chalk": "^5.6.0", |
| 38 | + "chevrotain": "^11.0.3", | |
| 38 | 39 | "command-exists": "^1.2.9", |
| 39 | 40 | "compression": "^1.8.1", |
| 40 | 41 | "cookie-parser": "^1.4.6", |
| @@ -49,7 +50,6 @@ | ||
| 49 | 50 | "express": "^4.21.0", |
| 50 | 51 | "form-data": "^4.0.4", |
| 51 | 52 | "fuse.js": "^7.1.0", |
| 52 | - "google-translate-api-browser": "^3.0.1", | |
| 53 | 53 | "google-translate-api-x": "^10.7.2", |
| 54 | 54 | "handlebars": "^4.7.8", |
| 55 | 55 | "helmet": "^8.1.0", |
| @@ -63,7 +63,7 @@ | ||
| 63 | 63 | "is-docker": "^3.0.0", |
| 64 | 64 | "localforage": "^1.10.0", |
| 65 | 65 | "lodash": "^4.17.21", |
| 66 | 66 | "mime-types": "^3.0.12", |
| 67 | 67 | "moment": "^2.30.1", |
| 68 | 68 | "morphdom": "^2.7.7", |
| 69 | 69 | "multer": "^2.0.2", |
| @@ -113,7 +113,7 @@ | ||
| 113 | 113 | "type": "git", |
| 114 | 114 | "url": "https://github.com/SillyTavern/SillyTavern.git" |
| 115 | 115 | }, |
| 116 | 116 | "version": "1.1415.0", |
| 117 | 117 | "scripts": { |
| 118 | 118 | "start": "node server.js", |
| 119 | 119 | "debug": "node --inspect server.js", |
| @@ -137,6 +137,7 @@ | ||
| 137 | 137 | }, |
| 138 | 138 | "main": "server.js", |
| 139 | 139 | "devDependencies": { |
| 140 | + "@chevrotain/types": "^11.0.3", | |
| 140 | 141 | "@types/archiver": "^6.0.3", |
| 141 | 142 | "@types/bytes": "^3.1.5", |
| 142 | 143 | "@types/command-exists": "^1.2.3", |
| @@ -164,7 +165,8 @@ | ||
| 164 | 165 | "@types/yargs": "^17.0.33", |
| 165 | 166 | "@types/yauzl": "^2.10.3", |
| 166 | 167 | "eslint": "^8.57.1", |
| 168 | + "eslint-plugin-jest": "^27.9.0", | |
| 167 | 169 | "eslint-plugin-jsdoc": "^48.10.0", |
| 168 | 170 | "eslint-plugin-jestplaywright": "^272.93.0" |
| 169 | 171 | } |
| 170 | 172 | } |
| @@ -132,3 +132,23 @@ | ||
| 132 | 132 | overflow-y: hidden; |
| 133 | 133 | } |
| 134 | 134 | } |
| 135 | + | |
| 136 | +@keyframes slide { | |
| 137 | + 0% { | |
| 138 | + transform: translateX(var(--slide-mes-x-start, 0px)); | |
| 139 | + } | |
| 140 | + | |
| 141 | + 100% { | |
| 142 | + transform: translateX(var(--slide-mes-x-end, 0px)); | |
| 143 | + } | |
| 144 | +} | |
| 145 | + | |
| 146 | +@-webkit-keyframes slide { | |
| 147 | + 0% { | |
| 148 | + transform: translateX(var(--slide-mes-x-start, 0px)); | |
| 149 | + } | |
| 150 | + | |
| 151 | + 100% { | |
| 152 | + transform: translateX(var(--slide-mes-x-end, 0px)); | |
| 153 | + } | |
| 154 | +} | |
| @@ -63,6 +63,7 @@ | ||
| 63 | 63 | display: flex; |
| 64 | 64 | gap: 5px; |
| 65 | 65 | width: 100%; |
| 66 | + align-items: baseline; | |
| 66 | 67 | } |
| 67 | 68 | |
| 68 | 69 | /* Control buttons in header */ |
| @@ -83,14 +84,6 @@ | ||
| 83 | 84 | gap: 5px; |
| 84 | 85 | } |
| 85 | 86 | |
| 86 | -#bg-scrollable-content { | |
| 87 | - flex-grow: 1; | |
| 88 | - overflow-y: auto; | |
| 89 | - overflow-x: hidden; | |
| 90 | - padding: 0 5px 15px; | |
| 91 | - position: relative; | |
| 92 | -} | |
| 93 | - | |
| 94 | 87 | #bg_menu_content, |
| 95 | 88 | #bg_custom_content { |
| 96 | 89 | display: grid; |
| @@ -300,3 +293,80 @@ | ||
| 300 | 293 | .bg_example:focus-within .BGSampleTitle { |
| 301 | 294 | opacity: 1; |
| 302 | 295 | } |
| 296 | + | |
| 297 | +#bg_tabs { | |
| 298 | + display: flex; | |
| 299 | + flex-direction: column; | |
| 300 | + flex-grow: 1; | |
| 301 | + height: 100%; | |
| 302 | + overflow-y: hidden; | |
| 303 | + gap: 5px; | |
| 304 | +} | |
| 305 | + | |
| 306 | +#bg_tabs .bg_tabs_list { | |
| 307 | + display: flex; | |
| 308 | + flex-direction: row; | |
| 309 | + gap: 5px; | |
| 310 | + background: none; | |
| 311 | + border: none; | |
| 312 | + margin: 5px 0; | |
| 313 | + padding: 0; | |
| 314 | + padding-right: 4em; | |
| 315 | + position: relative; | |
| 316 | +} | |
| 317 | + | |
| 318 | +#bg_tabs .bg_tabs_list .bg_tab_button { | |
| 319 | + display: flex; | |
| 320 | + align-items: center; | |
| 321 | + justify-content: center; | |
| 322 | + text-align: center; | |
| 323 | + min-width: 8em; | |
| 324 | + list-style: none; | |
| 325 | + color: var(--SmartThemeBodyColor); | |
| 326 | + background-color: var(--SmartThemeBlurTintColor); | |
| 327 | + border: 1px solid var(--SmartThemeBorderColor); | |
| 328 | + border-radius: 5px; | |
| 329 | + padding: 0; | |
| 330 | + cursor: pointer; | |
| 331 | + font-weight: 600; | |
| 332 | + margin: 0 !important; | |
| 333 | + filter: none; | |
| 334 | + opacity: 0.8; | |
| 335 | + transition: opacity var(--animation-duration-2x); | |
| 336 | +} | |
| 337 | + | |
| 338 | +#bg_tabs .bg_tabs_list .bg_tab_button:hover { | |
| 339 | + opacity: 1; | |
| 340 | +} | |
| 341 | + | |
| 342 | +#bg_tabs .bg_tabs_list .bg_tab_button.ui-tabs-active { | |
| 343 | + background-color: color-mix(in srgb, var(--SmartThemeQuoteColor) 33%, var(--SmartThemeBlurTintColor) 66%); | |
| 344 | +} | |
| 345 | + | |
| 346 | +#bg_tabs.ui-widget-content { | |
| 347 | + border: none !important; | |
| 348 | +} | |
| 349 | + | |
| 350 | +#bg_tabs .bg_tabs_list::before, | |
| 351 | +#bg_tabs .bg_tabs_list::after { | |
| 352 | + display: none; | |
| 353 | +} | |
| 354 | + | |
| 355 | +#bg_tabs .bg_tabs_list .bg_tab_button a { | |
| 356 | + width: 100%; | |
| 357 | + height: 100%; | |
| 358 | + padding: 3px 5px; | |
| 359 | + cursor: pointer; | |
| 360 | + color: var(--SmartThemeBodyColor); | |
| 361 | +} | |
| 362 | + | |
| 363 | +#bg_tabs .bg_tab_panel { | |
| 364 | + flex-grow: 1; | |
| 365 | + height: 100%; | |
| 366 | + overflow-y: auto; | |
| 367 | + padding: 10px; | |
| 368 | +} | |
| 369 | + | |
| 370 | +#bg_custom_content:not(:empty)~#bg_chat_hint { | |
| 371 | + display: none; | |
| 372 | +} | |
| @@ -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 | +} | |
| @@ -33,6 +33,7 @@ | ||
| 33 | 33 | display: flex; |
| 34 | 34 | flex-direction: column; |
| 35 | 35 | gap: 0.5em; |
| 36 | + padding-right: var(--mes-right-spacing); | |
| 36 | 37 | } |
| 37 | 38 | |
| 38 | 39 | .mes_file_container .right_menu_button { |
| @@ -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 | +} | |
| @@ -97,6 +97,15 @@ | ||
| 97 | 97 | max-width: 100dvw; |
| 98 | 98 | } |
| 99 | 99 | |
| 100 | + #bg_tabs .bg_tabs_list .bg_tab_button { | |
| 101 | + min-width: 6em; | |
| 102 | + } | |
| 103 | + | |
| 104 | + #add_background_button_top>span, | |
| 105 | + #auto_background>span { | |
| 106 | + display: none; | |
| 107 | + } | |
| 108 | + | |
| 100 | 109 | #extensions_settings, |
| 101 | 110 | #extensions_settings2 { |
| 102 | 111 | width: 100% !important; |
| @@ -213,6 +213,14 @@ | ||
| 213 | 213 | } |
| 214 | 214 | |
| 215 | 215 | .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 { | |
| 216 | 224 | flex: 0; |
| 217 | 225 | } |
| 218 | 226 | |
| @@ -16,6 +16,11 @@ | ||
| 16 | 16 | user-select: none; |
| 17 | 17 | } |
| 18 | 18 | |
| 19 | +.select2-container .select2-selection.select2-selection--single.select2-selection--clearable .select2-selection__clear { | |
| 20 | + top: 0; | |
| 21 | + right: 25px; | |
| 22 | +} | |
| 23 | + | |
| 19 | 24 | .select2-container .select2-selection .select2-selection__clear { |
| 20 | 25 | color: var(--SmartThemeBodyColor); |
| 21 | 26 | font-size: 20px; |
| @@ -477,14 +477,11 @@ body.expandMessageActions .mes .mes_buttons .extraMesButtonsHint { | ||
| 477 | 477 | display: none !important; |
| 478 | 478 | } |
| 479 | 479 | |
| 480 | 480 | #smooth_streaming_control:has(#smooth_streaming:not(:checked))~#smooth_streaming_speed_control {, |
| 481 | +#smooth_streaming_control:has(#smooth_streaming:not(:checked))~#smooth_streaming_no_think_control { | |
| 481 | 482 | display: none; |
| 482 | 483 | } |
| 483 | 484 | |
| 484 | -#smooth_streaming:checked~#smooth_streaming_speed_control { | |
| 485 | - display: block; | |
| 486 | -} | |
| 487 | - | |
| 488 | 485 | .mdhotkey_icon { |
| 489 | 486 | opacity: 0.6; |
| 490 | 487 | } |
| @@ -532,6 +529,7 @@ label[for="bind_preset_to_connection"]:has(input:checked) { | ||
| 532 | 529 | } |
| 533 | 530 | |
| 534 | 531 | #openai_settings input[type="checkbox"]:not(:checked)~[data-cc-toggle], |
| 532 | +#openai_settings input[type="checkbox"]:not(:checked)~*:has([data-cc-toggle]), | |
| 535 | 533 | #openai_settings [data-cc-toggle="false"]>.icon-supported, |
| 536 | 534 | #openai_settings [data-cc-toggle="true"]>.icon-unsupported { |
| 537 | 535 | display: none; |
| @@ -562,3 +560,7 @@ label[for="bind_preset_to_connection"]:has(input:checked) { | ||
| 562 | 560 | filter: grayscale(0.5); |
| 563 | 561 | cursor: not-allowed; |
| 564 | 562 | } |
| 563 | + | |
| 564 | +#request_images_block:has(#openai_request_images:not(:checked)) #request_images_settings { | |
| 565 | + display: none; | |
| 566 | +} | |
| @@ -6,6 +6,9 @@ import { oai_settings } from './scripts/openai'; | ||
| 6 | 6 | import { textgenerationwebui_settings } from './scripts/textgen-settings'; |
| 7 | 7 | import { FileAttachment } from './scripts/chats'; |
| 8 | 8 | import { 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'; | |
| 9 | 12 | |
| 10 | 13 | declare global { |
| 11 | 14 | // Custom types |
| @@ -14,7 +17,49 @@ declare global { | ||
| 14 | 17 | type ReasoningSettings = typeof power_user.reasoning; |
| 15 | 18 | type ChatCompletionSettings = typeof oai_settings; |
| 16 | 19 | type TextCompletionSettings = typeof textgenerationwebui_settings; |
| 20 | + type WorldInfoSettings = ReturnType<typeof getWorldInfoSettings>; | |
| 17 | 21 | type MessageTimestamp = string | number | Date; |
| 22 | + type Character = import('./scripts/char-data').v1CharData; | |
| 23 | + type ChatMessageExtra = BaseMessageExtra & Partial<ReasoningMessageExtra> & Record<string, any>; | |
| 24 | + | |
| 25 | + interface Group { | |
| 26 | + id: string; | |
| 27 | + name: string; | |
| 28 | + members: string[]; | |
| 29 | + disabled_members: string[]; | |
| 30 | + chat_id: string; | |
| 31 | + chats: string[]; | |
| 32 | + generation_mode?: number; | |
| 33 | + generation_mode_join_prefix?: string; | |
| 34 | + generation_mode_join_suffix?: string; | |
| 35 | + activation_strategy?: number; | |
| 36 | + auto_mode_delay?: number; | |
| 37 | + allow_self_responses?: boolean; | |
| 38 | + avatar_url?: string; | |
| 39 | + hideMutedSprites?: boolean; | |
| 40 | + fav?: boolean; | |
| 41 | + } | |
| 42 | + | |
| 43 | + interface ChatFile extends Array<ChatMessage> { | |
| 44 | + [index: number]: ChatMessage; | |
| 45 | + 0?: ChatHeader; | |
| 46 | + } | |
| 47 | + | |
| 48 | + interface ChatHeader { | |
| 49 | + chat_metadata: ChatMetadata; | |
| 50 | + /** @deprecated For backward compatibility ONLY */ | |
| 51 | + user_name: 'unused'; | |
| 52 | + /** @deprecated For backward compatibility ONLY */ | |
| 53 | + character_name: 'unused'; | |
| 54 | + } | |
| 55 | + | |
| 56 | + interface ChatMetadata { | |
| 57 | + tainted?: boolean; | |
| 58 | + integrity?: string; | |
| 59 | + scenario?: string; | |
| 60 | + persona?: string; | |
| 61 | + [key: string]: any; | |
| 62 | + } | |
| 18 | 63 | |
| 19 | 64 | interface ChatMessage { |
| 20 | 65 | name?: string; |
| @@ -28,21 +73,35 @@ declare global { | ||
| 28 | 73 | force_avatar?: string; |
| 29 | 74 | original_avatar?: string; |
| 30 | 75 | swipes?: string[]; |
| 31 | 76 | swipe_info?: Record<string, any>SwipeInfo[]; |
| 32 | 77 | swipe_id?: number; |
| 33 | - extra?: ChatMessageExtra & Partial<ReasoningMessageExtra> & Record<string, any>; | |
| 78 | + extra?: ChatMessageExtra; | |
| 34 | 79 | }; |
| 35 | 80 | |
| 36 | 81 | 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; | |
| 37 | 93 | bias?: string; |
| 38 | 94 | uses_system_ui?: boolean; |
| 39 | 95 | memory?: string; |
| 40 | 96 | display_text?: string; |
| 41 | 97 | reasoning_display_text?: string; |
| 42 | 98 | tool_invocations?: anyToolInvocation[]; |
| 43 | 99 | title?: string; |
| 44 | 100 | isSmallSys?: boolean; |
| 45 | 101 | token_count?: number; |
| 102 | + /** When false, the message cannot be swiped. */ | |
| 103 | + swipeable?: boolean; | |
| 104 | + overswipe_behavior?: OVERSWIPE_BEHAVIOR; | |
| 46 | 105 | files?: FileAttachment[]; |
| 47 | 106 | inline_image?: boolean; |
| 48 | 107 | media_display?: string; |
| @@ -62,6 +121,8 @@ declare global { | ||
| 62 | 121 | generationType?: number; |
| 63 | 122 | /** @deprecated Use `MediaAttachment.negative` instead */ |
| 64 | 123 | negative?: string; |
| 124 | + /** Will exclude this message from prompt processing */ | |
| 125 | + [IGNORE_SYMBOL]?: boolean; | |
| 65 | 126 | } |
| 66 | 127 | |
| 67 | 128 | type MediaAttachment = MediaAttachmentProps & ImageGenerationAttachmentProps & ImageCaptionAttachmentProps; |
| @@ -93,6 +154,12 @@ declare global { | ||
| 93 | 154 | paused: boolean; |
| 94 | 155 | } |
| 95 | 156 | |
| 157 | + interface ChatCompletionMessage { | |
| 158 | + name?: string; | |
| 159 | + role: string; | |
| 160 | + content: string; | |
| 161 | + } | |
| 162 | + | |
| 96 | 163 | // Global namespace modules |
| 97 | 164 | interface Window { |
| 98 | 165 | ai: any; |
| @@ -165,4 +232,6 @@ declare global { | ||
| 165 | 232 | rgba: string; |
| 166 | 233 | } |
| 167 | 234 | }; |
| 235 | + | |
| 236 | + type SwipeEvent = JQuery.TriggeredEvent<any, any, HTMLElement, HTMLElement>; | |
| 168 | 237 | } |
| @@ -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> | |
| @@ -42,6 +42,7 @@ | ||
| 42 | 42 | <link rel="stylesheet" type="text/css" href="css/extensions-panel.css"> |
| 43 | 43 | <link rel="stylesheet" type="text/css" href="css/select2-overrides.css"> |
| 44 | 44 | <link rel="stylesheet" type="text/css" href="css/mobile-styles.css"> |
| 45 | + <link rel="stylesheet" type="text/css" href="css/macros.css"> | |
| 45 | 46 | <link rel="stylesheet" type="text/css" href="css/user.css"> |
| 46 | 47 | <link rel="icon" type="image/x-icon" href="favicon.ico"> |
| 47 | 48 | <!-- Scripts are loaded at the end of the body to improve page load speed --> |
| @@ -678,6 +679,9 @@ | ||
| 678 | 679 | <div data-source="electronhub"> |
| 679 | 680 | <span data-i18n="Max prompt cost:">Max prompt cost:</span> <span id="electronhub_max_prompt_cost" data-i18n="Unknown">Unknown</span> |
| 680 | 681 | </div> |
| 682 | + <div data-source="chutes"> | |
| 683 | + <span data-i18n="Max prompt cost:">Max prompt cost:</span> <span id="chutes_max_prompt_cost" data-i18n="Unknown">Unknown</span> | |
| 684 | + </div> | |
| 681 | 685 | <hr> |
| 682 | 686 | <div class="range-block"> |
| 683 | 687 | <label for="stream_toggle" title="Enable OpenAI completion streaming" data-i18n="[title]Enable OpenAI completion streaming" class="checkbox_label widthFreeExpand"> |
| @@ -693,7 +697,7 @@ | ||
| 693 | 697 | </span> |
| 694 | 698 | </div> |
| 695 | 699 | </div> |
| 696 | 700 | <div class="range-block" data-source="openai,claude,aimlapi,openrouter,ai21,makersuite,vertexai,mistralai,custom,cohere,perplexity,groq,siliconflow,electronhub,chutes,nanogpt,deepseek,xai,pollinations,moonshot,fireworks,cometapi,azure_openai,zai"> |
| 697 | 701 | <div class="range-block-title" data-i18n="Temperature"> |
| 698 | 702 | Temperature |
| 699 | 703 | </div> |
| @@ -706,7 +710,7 @@ | ||
| 706 | 710 | </div> |
| 707 | 711 | </div> |
| 708 | 712 | </div> |
| 709 | 713 | <div class="range-block" data-source="openai,aimlapi,openrouter,custom,cohere,perplexity,groq,siliconflow,mistralai,electronhub,nanogpt,deepseek,xai,pollinations,moonshot,fireworks,cometapi,azure_openai,chutes"> |
| 710 | 714 | <div class="range-block-title" data-i18n="Frequency Penalty"> |
| 711 | 715 | Frequency Penalty |
| 712 | 716 | </div> |
| @@ -719,7 +723,7 @@ | ||
| 719 | 723 | </div> |
| 720 | 724 | </div> |
| 721 | 725 | </div> |
| 722 | 726 | <div class="range-block" data-source="openai,aimlapi,openrouter,custom,cohere,perplexity,groq,siliconflow,mistralai,electronhub,nanogpt,deepseek,xai,pollinations,moonshot,fireworks,cometapi,azure_openai,chutes"> |
| 723 | 727 | <div class="range-block-title" data-i18n="Presence Penalty"> |
| 724 | 728 | Presence Penalty |
| 725 | 729 | </div> |
| @@ -732,7 +736,7 @@ | ||
| 732 | 736 | </div> |
| 733 | 737 | </div> |
| 734 | 738 | </div> |
| 735 | 739 | <div class="range-block" data-source="claude,aimlapi,openrouter,makersuite,vertexai,cohere,perplexity,electronhub,chutes,nanogpt"> |
| 736 | 740 | <div class="range-block-title" data-i18n="Top K"> |
| 737 | 741 | Top K |
| 738 | 742 | </div> |
| @@ -745,7 +749,7 @@ | ||
| 745 | 749 | </div> |
| 746 | 750 | </div> |
| 747 | 751 | </div> |
| 748 | 752 | <div class="range-block" data-source="openai,claude,aimlapi,openrouter,ai21,makersuite,vertexai,mistralai,custom,cohere,perplexity,groq,siliconflow,electronhub,chutes,nanogpt,deepseek,xai,pollinations,moonshot,fireworks,cometapi,azure_openai,zai"> |
| 749 | 753 | <div class="range-block-title" data-i18n="Top P"> |
| 750 | 754 | Top P |
| 751 | 755 | </div> |
| @@ -758,7 +762,7 @@ | ||
| 758 | 762 | </div> |
| 759 | 763 | </div> |
| 760 | 764 | </div> |
| 761 | 765 | <div class="range-block" data-source="openrouter,nanogpt,chutes"> |
| 762 | 766 | <div class="range-block-title" data-i18n="Repetition Penalty"> |
| 763 | 767 | Repetition Penalty |
| 764 | 768 | </div> |
| @@ -771,7 +775,7 @@ | ||
| 771 | 775 | </div> |
| 772 | 776 | </div> |
| 773 | 777 | </div> |
| 774 | 778 | <div class="range-block" data-source="openrouter,nanogpt,chutes"> |
| 775 | 779 | <div class="range-block-title" data-i18n="Min P"> |
| 776 | 780 | Min P |
| 777 | 781 | </div> |
| @@ -784,7 +788,7 @@ | ||
| 784 | 788 | </div> |
| 785 | 789 | </div> |
| 786 | 790 | </div> |
| 787 | 791 | <div class="range-block" data-source="openrouter,nanogpt"> |
| 788 | 792 | <div class="range-block-title" data-i18n="Top A"> |
| 789 | 793 | Top A |
| 790 | 794 | </div> |
| @@ -982,7 +986,7 @@ | ||
| 982 | 986 | </div> |
| 983 | 987 | </div> |
| 984 | 988 | </div> |
| 985 | 989 | <div class="range-block" data-source="openai,openrouter,mistralai,custom,cohere,groq,electronhub,chutes,nanogpt,xai,pollinations,aimlapi,makersuite,vertexai,azure_openai"> |
| 986 | 990 | <div class="range-block-title justifyLeft" data-i18n="Seed"> |
| 987 | 991 | Seed |
| 988 | 992 | </div> |
| @@ -1123,7 +1127,7 @@ | ||
| 1123 | 1127 | <div id="grammar_block" class="wide100p"> |
| 1124 | 1128 | <hr class="wide100p"> |
| 1125 | 1129 | <h4 class="wide100p textAlignCenter"><span data-i18n="GBNF Grammar">GBNF Grammar</span> |
| 1126 | 1130 | <a href="https://github.com/ggerganovggml-org/llama.cpp/blob/master/grammars/README.md" target="_blank"> |
| 1127 | 1131 | <small> |
| 1128 | 1132 | <div class="fa-solid fa-circle-question note-link-span"></div> |
| 1129 | 1133 | </small> |
| @@ -1246,7 +1250,7 @@ | ||
| 1246 | 1250 | <div class="fa-solid fa-circle-info opacity50p" title="Customize displayed samplers or add custom samplers." data-i18n="[title]Customize displayed samplers or add custom samplers."></div> |
| 1247 | 1251 | </small> |
| 1248 | 1252 | </div> |
| 1249 | 1253 | <div data-tg-type="mancer, vllm, aphrodite, tabby, infermaticai, llamacpp" data-tg-samplers="n" class="flex-container flexFlowColumn alignitemscenter flexBasis100p flexGrow flexShrink gap0"> |
| 1250 | 1254 | <small data-i18n="Multiple swipes per generation">Multiple swipes per generation</small> |
| 1251 | 1255 | <input type="number" id="n_textgenerationwebui" class="text_pole textAlignCenter" min="1" value="1" step="1" /> |
| 1252 | 1256 | </div> |
| @@ -1690,7 +1694,7 @@ | ||
| 1690 | 1694 | <div class="flex-container justifyCenter alignitemscenter"> |
| 1691 | 1695 | <span data-i18n="Grammar String">Grammar String</span> |
| 1692 | 1696 | <div class="margin5 fa-solid fa-circle-info opacity50p " data-i18n="[title]GBNF or EBNF, depends on the backend in use. If you're using this you should know which." title="GBNF or EBNF, depends on the backend in use. If you're using this you should know which."></div> |
| 1693 | 1697 | <a href="https://github.com/ggerganovggml-org/llama.cpp/blob/master/grammars/README.md" target="_blank"> |
| 1694 | 1698 | <div class="fa-solid fa-up-right-from-square note-link-span"></div> |
| 1695 | 1699 | </a> |
| 1696 | 1700 | </div> |
| @@ -1920,18 +1924,6 @@ | ||
| 1920 | 1924 | </div> |
| 1921 | 1925 | </div> |
| 1922 | 1926 | <div class="range-block"> |
| 1923 | - <label for="wrap_in_quotes" title="Wrap user messages in quotes before sending" data-i18n="[title]Wrap user messages in quotes before sending" class="checkbox_label widthFreeExpand"> | |
| 1924 | - <input id="wrap_in_quotes" type="checkbox" /><span data-i18n="Wrap in Quotes"> | |
| 1925 | - Wrap in Quotes</span> | |
| 1926 | - </label> | |
| 1927 | - <div class="toggle-description justifyLeft"> | |
| 1928 | - <span data-i18n="Wrap entire user message in quotes before sending.">Wrap | |
| 1929 | - entire user message in quotes before sending.</span><br> | |
| 1930 | - <span data-i18n="Leave off if you use quotes manually for speech.">Leave off | |
| 1931 | - if you use quotes manually for speech.</span> | |
| 1932 | - </div> | |
| 1933 | - </div> | |
| 1934 | - <div class="range-block"> | |
| 1935 | 1927 | <label for="continue_prefill" class="checkbox_label widthFreeExpand"> |
| 1936 | 1928 | <input id="continue_prefill" type="checkbox" /> |
| 1937 | 1929 | <span data-i18n="Continue prefill">Continue prefill</span> |
| @@ -1955,7 +1947,20 @@ | ||
| 1955 | 1947 | </span> |
| 1956 | 1948 | </div> |
| 1957 | 1949 | </div> |
| 1958 | 1950 | <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"> | |
| 1959 | 1964 | <label for="openai_enable_web_search" class="checkbox_label flexWrap widthFreeExpand"> |
| 1960 | 1965 | <input id="openai_enable_web_search" type="checkbox" /> |
| 1961 | 1966 | <span data-i18n="Enable web search">Enable web search</span> |
| @@ -1969,7 +1974,7 @@ | ||
| 1969 | 1974 | </b> |
| 1970 | 1975 | </div> |
| 1971 | 1976 | </div> |
| 1972 | 1977 | <div class="range-block" data-source="openai,cohere,mistralai,custom,claude,aimlapi,openrouter,groq,siliconflow,deepseek,makersuite,vertexai,ai21,xai,pollinations,moonshot,fireworks,cometapi,electronhub,chutes,azure_openai,zai"> |
| 1973 | 1978 | <label for="openai_function_calling" class="checkbox_label flexWrap widthFreeExpand"> |
| 1974 | 1979 | <input id="openai_function_calling" type="checkbox" /> |
| 1975 | 1980 | <span data-i18n="Enable function calling">Enable function calling</span> |
| @@ -1984,19 +1989,31 @@ | ||
| 1984 | 1989 | <strong data-i18n="enable_functions_desc_4">Not supported when Prompt Post-Processing with "no tools" is used!</strong> |
| 1985 | 1990 | </div> |
| 1986 | 1991 | </div> |
| 1987 | 1992 | <div class="range-block" data-source="openai,aimlapi,openrouter,mistralai,makersuite,vertexai,claude,custom,xai,pollinations,moonshot,cohere,cometapi,nanogpt,electronhub,azure_openai,zai,siliconflow,chutes"> |
| 1988 | 1993 | <label for="openai_image_inliningopenai_media_inlining" class="checkbox_label flexWrap widthFreeExpand"> |
| 1989 | 1994 | <input id="openai_image_inliningopenai_media_inlining" type="checkbox" /> |
| 1990 | 1995 | <span data-i18n="Send inline imagesmedia">Send inline imagesmedia</span> |
| 1991 | 1996 | <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"> | |
| 1993 | 1998 | <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> | |
| 1994 | 2009 | </div> |
| 1995 | 2010 | </label> |
| 1996 | 2011 | <div id="image_inlining_hint" class="flexBasis100p toggle-description justifyLeft"> |
| 1997 | 2012 | <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> | |
| 1998 | 2015 | </div> |
| 1999 | 2016 | <div class="flex-container flexFlowColumn wide100p textAlignCenter marginTop10" data-source="openai,custom,xai,pollinations,cohere,cometapi,nanogpt,moonshot,aimlapi,openrouter,mistralai,electronhub,azure_openai,zai,siliconflow,chutes,makersuite,vertexai"> |
| 2000 | 2017 | <div class="flex-container oneline-dropdown"> |
| 2001 | 2018 | <label for="openai_inline_image_quality" data-i18n="Inline Image Quality"> |
| 2002 | 2019 | Inline Image Quality |
| @@ -2009,40 +2026,12 @@ | ||
| 2009 | 2026 | </div> |
| 2010 | 2027 | </div> |
| 2011 | 2028 | </div> |
| 2012 | 2029 | <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"> | |
| 2041 | 2030 | <label for="openai_request_images" class="checkbox_label widthFreeExpand"> |
| 2042 | 2031 | <input id="openai_request_images" type="checkbox" /> |
| 2043 | 2032 | <span> |
| 2044 | 2033 | <span data-i18n="Request inline images">Request inline images</span> |
| 2045 | 2034 | <i class="opacity50p fa-solid fa-circle-info" title="Gemini 2.05 Flash ExperimentalImage / Gemini 3 Pro Image"></i> |
| 2046 | 2035 | </span> |
| 2047 | 2036 | </label> |
| 2048 | 2037 | <div class="toggle-description justifyLeft marginBot5"> |
| @@ -2053,22 +2042,39 @@ | ||
| 2053 | 2042 | Incompatible with the following features: function calling, web search, system prompt. |
| 2054 | 2043 | </em> |
| 2055 | 2044 | </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> | |
| 2056 | 2074 | </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> | |
| 2069 | 2075 | </div> |
| 2070 | 2076 | </div> |
| 2071 | 2077 | <div class="range-block" data-source="deepseek,aimlapi,openrouter,custom,claude,xai,makersuite,vertexai,pollinations,moonshot,mistralai,fireworks,cometapi,electronhub,chutes,azure_openai,nanogpt,zai"> |
| 2072 | 2078 | <label for="openai_show_thoughts" class="checkbox_label widthFreeExpand"> |
| 2073 | 2079 | <input id="openai_show_thoughts" type="checkbox" /> |
| 2074 | 2080 | <span data-i18n="Request model reasoning">Request model reasoning</span> |
| @@ -2082,7 +2088,7 @@ | ||
| 2082 | 2088 | </strong> |
| 2083 | 2089 | </div> |
| 2084 | 2090 | </div> |
| 2085 | 2091 | <div class="flex-container flexFlowColumn wide100p textAlignCenter marginTop10" data-source="openai,custom,claude,xai,makersuite,vertexai,aimlapi,openrouter,pollinations,perplexity,cometapi,electronhub,azure_openai,chutes"> |
| 2086 | 2092 | <div class="flex-container oneline-dropdown" title="Constrains effort on reasoning for reasoning models. Reducing reasoning effort can result in faster responses and fewer tokens used on reasoning in a response." data-i18n="[title]Constrains effort on reasoning for reasoning models."> |
| 2087 | 2093 | <label for="openai_reasoning_effort"> |
| 2088 | 2094 | <span data-i18n="Reasoning Effort">Reasoning Effort</span> |
| @@ -2096,14 +2102,36 @@ | ||
| 2096 | 2102 | <option data-i18n="openai_reasoning_effort_high" value="high">High</option> |
| 2097 | 2103 | <option data-i18n="openai_reasoning_effort_maximum" value="max">Maximum</option> |
| 2098 | 2104 | </select> |
| 2099 | 2105 | <div class="toggle-description justifyLeft marginBot5" data-source="openai,custom,xai,aimlapi,openrouter,pollinations,perplexity,cometapi,electronhub,azure_openai,chutes" data-i18n="OpenAI-style options: low, medium, high. Minimum and maximum are aliased to low and high. Auto does not send an effort level."> |
| 2100 | 2106 | OpenAI-style options: low, medium, high. Minimum and maximum are aliased to low and high. Auto does not send an effort level. |
| 2101 | 2107 | </div> |
| 2102 | 2108 | <div class="toggle-description justifyLeft marginBot5" data-source="claude" data-i18n="Allocates a portion of the response length for thinking (min: 1024 tokens, low: 10%, medium: 25%, high: 50%, max: 95%), but minimum 1024 tokens. Auto does not request thinking."> |
| 2103 | 2109 | Allocates a portion of the response length for thinking (min: 1024 tokens, low: 10%, medium: 25%, high: 50%, max: 95%), but minimum 1024 tokens. Auto does not request thinking. |
| 2104 | 2110 | </div> |
| 2105 | - <div class="toggle-description justifyLeft marginBot5" data-source="makersuite,vertexai" data-i18n="Allocates a portion of the response length for thinking (Flash 2.5/Pro 2.5) (min: 0/128 tokens, low: 10%, medium: 25%, high: 50%, max: 24576/32768 tokens). Auto lets the model decide."> | |
| 2111 | + <div class="toggle-description justifyLeft marginBot5" data-source="makersuite,vertexai"> | |
| 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."> | |
| 2106 | 2117 | Allocates a portion of the response length for thinking (Flash 2.5/Pro 2.5) (min: 0/128 tokens, low: 10%, medium: 25%, high: 50%, max: 24576/32768 tokens). Auto lets the model decide. |
| 2118 | + </span> | |
| 2119 | + </div> | |
| 2120 | + </div> | |
| 2121 | + </div> | |
| 2122 | + <div class="flex-container flexFlowColumn wide100p textAlignCenter marginTop10" data-source="openai,custom,openrouter,claude"> | |
| 2123 | + <div class="flex-container oneline-dropdown"> | |
| 2124 | + <label for="openai_verbosity"> | |
| 2125 | + <span data-i18n="Verbosity">Verbosity</span> | |
| 2126 | + </label> | |
| 2127 | + <select id="openai_verbosity"> | |
| 2128 | + <option data-i18n="openai_verbosity_auto" value="auto">Auto</option> | |
| 2129 | + <option data-i18n="openai_verbosity_low" value="low">Low</option> | |
| 2130 | + <option data-i18n="openai_verbosity_medium" value="medium">Medium</option> | |
| 2131 | + <option data-i18n="openai_verbosity_high" value="high">High</option> | |
| 2132 | + </select> | |
| 2133 | + <div class="toggle-description justifyLeft marginBot5" data-i18n="Constrains the verbosity of the model's response."> | |
| 2134 | + Constrains the verbosity of the model's response. | |
| 2107 | 2135 | </div> |
| 2108 | 2136 | </div> |
| 2109 | 2137 | </div> |
| @@ -2124,18 +2152,9 @@ | ||
| 2124 | 2152 | <i class="fa-solid fa-circle-info"></i> |
| 2125 | 2153 | <span>Prefills won't work when function calling is enabled and any tools are registered.</span> |
| 2126 | 2154 | </div> |
| 2127 | - <label for="claude_use_sysprompt" class="checkbox_label widthFreeExpand"> | |
| 2128 | - <input id="claude_use_sysprompt" type="checkbox" /> | |
| 2129 | - <span data-i18n="Use system prompt">Use system prompt</span> | |
| 2130 | - </label> | |
| 2131 | - <div class="toggle-description justifyLeft marginBot5"> | |
| 2132 | - <span data-i18n="Send the system prompt for supported models. If disabled, the user message is added to the beginning of the prompt."> | |
| 2133 | - Send the system prompt for supported models. If disabled, the user message is added to the beginning of the prompt. | |
| 2134 | - </span> | |
| 2135 | - </div> | |
| 2136 | 2155 | </div> |
| 2137 | 2156 | </div> |
| 2138 | 2157 | <div class="range-block m-t-1" data-source="openai,aimlapi,openrouter,custom,electronhub,azure_openai,chutes"> |
| 2139 | 2158 | <div id="logit_bias_openai" class="range-block-title openai_restorable" data-i18n="Logit Bias"> |
| 2140 | 2159 | Logit Bias |
| 2141 | 2160 | </div> |
| @@ -2642,8 +2661,8 @@ | ||
| 2642 | 2661 | </div> |
| 2643 | 2662 | <div data-tg-type="llamacpp"> |
| 2644 | 2663 | <div class="flex-container flexFlowColumn"> |
| 2645 | 2664 | <a href="https://github.com/ggerganovggml-org/llama.cpp/blob/master/README.md#llama-server" target="_blank" data-i18n="ggerganovggml-org/llama.cpp"> |
| 2646 | 2665 | ggerganovggml-org/llama.cpp (inference server) |
| 2647 | 2666 | </a> |
| 2648 | 2667 | </div> |
| 2649 | 2668 | <h4 data-i18n="API key (optional)">API key (optional)</h4> |
| @@ -2660,6 +2679,16 @@ | ||
| 2660 | 2679 | <small data-i18n="Example: http://127.0.0.1:8080">Example: http://127.0.0.1:8080</small> |
| 2661 | 2680 | <input id="llamacpp_api_url_text" class="text_pole wide100p" value="" autocomplete="off" data-server-history="llamacpp"> |
| 2662 | 2681 | </div> |
| 2682 | + <div> | |
| 2683 | + <h4> | |
| 2684 | + <span data-i18n="llama.cpp Model">llama.cpp Model</span> | |
| 2685 | + </h4> | |
| 2686 | + <select id="llamacpp_model"> | |
| 2687 | + <option value="" data-i18n="-- Connect to the API --"> | |
| 2688 | + -- Connect to the API -- | |
| 2689 | + </option> | |
| 2690 | + </select> | |
| 2691 | + </div> | |
| 2663 | 2692 | </div> |
| 2664 | 2693 | <div data-tg-type="ollama"> |
| 2665 | 2694 | <div class="flex-container flexFlowColumn"> |
| @@ -2794,6 +2823,7 @@ | ||
| 2794 | 2823 | <option value="ai21">AI21</option> |
| 2795 | 2824 | <option value="aimlapi">AI/ML API</option> |
| 2796 | 2825 | <option value="azure_openai">Azure OpenAI</option> |
| 2826 | + <option value="chutes">Chutes</option> | |
| 2797 | 2827 | <option value="claude">Claude</option> |
| 2798 | 2828 | <option value="cohere">Cohere</option> |
| 2799 | 2829 | <!-- Temporarily disabled. --> |
| @@ -2929,6 +2959,11 @@ | ||
| 2929 | 2959 | <div> |
| 2930 | 2960 | <h4 data-i18n="OpenAI Model">OpenAI Model</h4> |
| 2931 | 2961 | <select id="model_openai_select"> |
| 2962 | + <optgroup label="GPT-5.2"> | |
| 2963 | + <option value="gpt-5.2">gpt-5.2</option> | |
| 2964 | + <option value="gpt-5.2-2025-12-11">gpt-5.2-2025-12-11</option> | |
| 2965 | + <option value="gpt-5.2-chat-latest">gpt-5.2-chat-latest</option> | |
| 2966 | + </optgroup> | |
| 2932 | 2967 | <optgroup label="GPT-5.1"> |
| 2933 | 2968 | <option value="gpt-5.1">gpt-5.1</option> |
| 2934 | 2969 | <option value="gpt-5.1-2025-11-13">gpt-5.1-2025-11-13</option> |
| @@ -3033,6 +3068,8 @@ | ||
| 3033 | 3068 | <h4 data-i18n="Claude Model">Claude Model</h4> |
| 3034 | 3069 | <select id="model_claude_select"> |
| 3035 | 3070 | <optgroup label="Versions"> |
| 3071 | + <option value="claude-opus-4-5">claude-opus-4-5</option> | |
| 3072 | + <option value="claude-opus-4-5-20251101">claude-opus-4-5-20251101</option> | |
| 3036 | 3073 | <option value="claude-sonnet-4-5">claude-sonnet-4-5</option> |
| 3037 | 3074 | <option value="claude-sonnet-4-5-20250929">claude-sonnet-4-5-20250929</option> |
| 3038 | 3075 | <option value="claude-haiku-4-5">claude-haiku-4-5</option> |
| @@ -3174,6 +3211,7 @@ | ||
| 3174 | 3211 | <optgroup label="Gemini 3.0"> |
| 3175 | 3212 | <option value="gemini-3-pro-preview">gemini-3-pro-preview</option> |
| 3176 | 3213 | <option value="gemini-3-pro-image-preview">gemini-3-pro-image-preview</option> |
| 3214 | + <option value="gemini-3-flash-preview">gemini-3-flash-preview</option> | |
| 3177 | 3215 | </optgroup> |
| 3178 | 3216 | <optgroup label="Gemini 2.5"> |
| 3179 | 3217 | <option value="gemini-2.5-pro">gemini-2.5-pro</option> |
| @@ -3346,6 +3384,7 @@ | ||
| 3346 | 3384 | <optgroup label="Gemini 3.0"> |
| 3347 | 3385 | <option value="gemini-3-pro-preview">gemini-3-pro-preview</option> |
| 3348 | 3386 | <option value="gemini-3-pro-image-preview">gemini-3-pro-image-preview</option> |
| 3387 | + <option value="gemini-3-flash-preview">gemini-3-flash-preview</option> | |
| 3349 | 3388 | </optgroup> |
| 3350 | 3389 | <optgroup label="Gemini 2.5"> |
| 3351 | 3390 | <option value="gemini-2.5-pro">gemini-2.5-pro</option> |
| @@ -3468,6 +3507,45 @@ | ||
| 3468 | 3507 | </div> |
| 3469 | 3508 | </div> |
| 3470 | 3509 | </div> |
| 3510 | + <div id="chutes_form" data-source="chutes"> | |
| 3511 | + <h4 data-i18n="Chutes API Key">Chutes API Key</h4> | |
| 3512 | + <div id="chutes_credits_display"> | |
| 3513 | + <a href="https://chutes.ai/app/api/billing-balance" target="_blank" data-i18n="View Billing/Balance">View Billing/Balance</a> | |
| 3514 | + </div> | |
| 3515 | + <div class="flex-container"> | |
| 3516 | + <input id="api_key_chutes" name="api_key_chutes" class="text_pole flex1" value="" type="text" autocomplete="off"> | |
| 3517 | + <div title="Manage API keys" data-i18n="[title]Manage API keys" class="menu_button fa-solid fa-key fa-fw manage-api-keys" data-key="api_key_chutes"></div> | |
| 3518 | + </div> | |
| 3519 | + <div data-for="api_key_chutes" class="neutral_warning" data-i18n="For privacy reasons, your API key will be hidden after you click 'Connect'."> | |
| 3520 | + For privacy reasons, your API key will be hidden after you click 'Connect'. | |
| 3521 | + </div> | |
| 3522 | + <div> | |
| 3523 | + <h4 data-i18n="Chutes Model">Chutes Model</h4> | |
| 3524 | + <select id="model_chutes_select"> | |
| 3525 | + <option value="" data-i18n="-- Connect to the API --">-- Connect to the API --</option> | |
| 3526 | + </select> | |
| 3527 | + </div> | |
| 3528 | + <div class="marginTopBot5"> | |
| 3529 | + <div class="inline-drawer wide100p"> | |
| 3530 | + <div class="inline-drawer-toggle inline-drawer-header"> | |
| 3531 | + <b data-i18n="Chutes Model Sorting">Chutes Model Sorting</b> | |
| 3532 | + <div class="fa-solid fa-circle-chevron-down inline-drawer-icon down"></div> | |
| 3533 | + </div> | |
| 3534 | + <div class="inline-drawer-content m-b-1"> | |
| 3535 | + <div class="marginTopBot5"> | |
| 3536 | + <label for="chutes_sort_models" class="checkbox_label"> | |
| 3537 | + <select id="chutes_sort_models"> | |
| 3538 | + <option data-i18n="Alphabetically" value="alphabetically">Alphabetically</option> | |
| 3539 | + <option data-i18n="Input Price" value="pricing.input">Input Price (cheapest)</option> | |
| 3540 | + <option data-i18n="Output Price" value="pricing.output">Output Price (cheapest)</option> | |
| 3541 | + <option data-i18n="Context Size" value="context_length">Context Size</option> | |
| 3542 | + </select> | |
| 3543 | + </label> | |
| 3544 | + </div> | |
| 3545 | + </div> | |
| 3546 | + </div> | |
| 3547 | + </div> | |
| 3548 | + </div> | |
| 3471 | 3549 | <div id="nanogpt_form" data-source="nanogpt"> |
| 3472 | 3550 | <h4 data-i18n="NanoGPT API Key">NanoGPT API Key</h4> |
| 3473 | 3551 | <div class="flex-container"> |
| @@ -3733,7 +3811,11 @@ | ||
| 3733 | 3811 | </select> |
| 3734 | 3812 | <h4 data-i18n="Z.AI Model">Z.AI Model</h4> |
| 3735 | 3813 | <select id="model_zai_select"> |
| 3814 | + <option value="glm-4.7">glm-4.7</option> | |
| 3736 | 3815 | <option value="glm-4.6">glm-4.6</option> |
| 3816 | + <option value="glm-4.6v">glm-4.6v</option> | |
| 3817 | + <option value="glm-4.6v-flash">glm-4.6v-flash</option> | |
| 3818 | + <option value="glm-4.6v-flashx">glm-4.6v-flashx</option> | |
| 3737 | 3819 | <option value="glm-4.5v">glm-4.5v</option> |
| 3738 | 3820 | <option value="glm-4.5">glm-4.5</option> |
| 3739 | 3821 | <option value="glm-4.5-air">glm-4.5-air</option> |
| @@ -3741,6 +3823,7 @@ | ||
| 3741 | 3823 | <option value="glm-4.5-airx">glm-4.5-airx</option> |
| 3742 | 3824 | <option value="glm-4.5-flash">glm-4.5-flash</option> |
| 3743 | 3825 | <option value="glm-4-32b-0414-128k">glm-4-32b-0414-128k</option> |
| 3826 | + <option value="autoglm-phone-multilingual">autoglm-phone-multilingual</option> | |
| 3744 | 3827 | </select> |
| 3745 | 3828 | </div> |
| 3746 | 3829 | <div id="azure_openai_settings" data-source="azure_openai"> |
| @@ -4697,7 +4780,7 @@ | ||
| 4697 | 4780 | <div name="themeElements" class="flex-container flexFlowColumn flexNoGap"> |
| 4698 | 4781 | <!-- <h4><span data-i18n="UI Colors">Theme Settings</span></h4> --> |
| 4699 | 4782 | <div name="AvatarAndChatDisplay" class="flex-container flexFlowColumn"> |
| 4700 | - <div class="flex-container alignItemsBaseline"> | |
| 4783 | + <div class="flex-container alignItemsBaseline" title="This style applies to all avatars globaly, including your Persona, Character Managment, Account selection, etc." data-i18n="[title]This style applies to all avatars globaly, including your Persona, Character Managment, Account selection, etc."> | |
| 4701 | 4784 | <span data-i18n="Avatar Style:">Avatars:</span> |
| 4702 | 4785 | <select id="avatar_style" class="widthNatural flex1 margin0 text_pole"> |
| 4703 | 4786 | <option value="0" data-i18n="Circle">Circle</option> |
| @@ -4873,7 +4956,7 @@ | ||
| 4873 | 4956 | <input id="mesIDDisplayEnabled" type="checkbox" /> |
| 4874 | 4957 | <small data-i18n="Message IDs">Message IDs</small> |
| 4875 | 4958 | </label> |
| 4876 | 4959 | <label for="hideChatAvatarsEnabled" class="checkbox_label" title="Hide avatars, only in chat messages." data-i18n="[title]Hide avatars, only in chat messages."> |
| 4877 | 4960 | <input id="hideChatAvatarsEnabled" type="checkbox" /> |
| 4878 | 4961 | <small data-i18n="Hide Chat Avatars">Hide Chat Avatars</small> |
| 4879 | 4962 | </label> |
| @@ -4975,14 +5058,21 @@ | ||
| 4975 | 5058 | <small data-i18n="Clean-Up">Clean-Up</small> |
| 4976 | 5059 | </div> |
| 4977 | 5060 | </div> |
| 4978 | 5061 | <label id="smooth_streaming_control" class="checkbox_label flexWrap" for="smooth_streaming"> |
| 4979 | 5062 | <input id="smooth_streaming" type="checkbox" /> |
| 4980 | 5063 | <div class="flex-container alignItemsBaseline"> |
| 4981 | 5064 | <small data-i18n="Smooth Streaming"> |
| 4982 | 5065 | Smooth Streaming |
| 4983 | 5066 | </small> |
| 4984 | 5067 | </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"> | |
| 4986 | 5076 | <input type="range" id="smooth_streaming_speed" name="smooth_streaming_speed" min="0" max="100" step="10" value="50"> |
| 4987 | 5077 | <div class="slider_hint"> |
| 4988 | 5078 | <span data-i18n="Slow">Slow</span> |
| @@ -4990,7 +5080,6 @@ | ||
| 4990 | 5080 | <span data-i18n="Fast">Fast</span> |
| 4991 | 5081 | </div> |
| 4992 | 5082 | </div> |
| 4993 | - </label> | |
| 4994 | 5083 | <label class="checkbox_label" for="stream_fade_in" title="Fade in streamed text when it appears, instead of it just popping in." data-i18n="[title]Fade in streamed text when it appears, instead of it just popping in"> |
| 4995 | 5084 | <input id="stream_fade_in" type="checkbox" /> |
| 4996 | 5085 | <small data-i18n="Stream Fade-In">Stream Fade-In</small> |
| @@ -5191,6 +5280,11 @@ | ||
| 5191 | 5280 | <input id="encode_tags" type="checkbox" /> |
| 5192 | 5281 | <small data-i18n="Show tags in responses">Show <tags> in responses</small> |
| 5193 | 5282 | </label> |
| 5283 | + <label class="checkbox_label" for="experimental_macro_engine" title="Experimental new Macro Engine.

Allows nested macros to be resolved correctly and has a dedicated, logical replacement order.
The new engine is designed to cleanly replace the old regex-based macro system."> | |
| 5284 | + <input id="experimental_macro_engine" type="checkbox" /> | |
| 5285 | + <small data-i18n="Experimental Macro Engine">Experimental Macro Engine</small> | |
| 5286 | + <i class="fa-solid fa-flask" title="Experimental feature. Currently in development to test." data-i18n="[title]Experimental feature. Currently in development to test."></i> | |
| 5287 | + </label> | |
| 5194 | 5288 | <label class="checkbox_label" for="disable_group_trimming" title="Allow AI messages in groups to contain lines spoken by other group members." data-i18n="[title]Allow AI messages in groups to contain lines spoken by other group members"> |
| 5195 | 5289 | <input id="disable_group_trimming" type="checkbox" /> |
| 5196 | 5290 | <small data-i18n="Relax message trim in Groups">Relax message trim in Groups</small> |
| @@ -5383,10 +5477,9 @@ | ||
| 5383 | 5477 | <div id="Backgrounds" class="drawer-content closedDrawer bg-drawer-layout"> |
| 5384 | 5478 | <div id="bg-header-fixed"> |
| 5385 | 5479 | <div class="bg-header-row-1"> |
| 5386 | - <label for="add_bg_button" id="add_background_button_top" class="menu_button menu_button_icon" data-i18n="[title]Add a new background" title="Add a new background"> | |
| 5480 | + <h3 class="margin0"> | |
| 5387 | 5481 | <ispan classdata-i18n="fa-solid fa-plusBackgrounds">Backgrounds</ispan> |
| 5388 | - <span data-i18n="Add Background">Add Background</span> | |
| 5482 | + </h3> | |
| 5389 | - </label> | |
| 5390 | 5483 | <span class="expander"></span> |
| 5391 | 5484 | <select id="background_fitting" class="text_pole" data-i18n="[title]Background Fitting" title="Background Fitting"> |
| 5392 | 5485 | <option value="classic" data-i18n="Classic">Classic</option> |
| @@ -5399,16 +5492,23 @@ | ||
| 5399 | 5492 | <i class="fa-solid fa-wand-magic"></i> |
| 5400 | 5493 | <span data-i18n="Auto-select">Auto-select</span> |
| 5401 | 5494 | </div> |
| 5495 | + <label for="add_bg_button" id="add_background_button_top" class="menu_button menu_button_icon" data-i18n="[title]Add a new background" title="Add a new background"> | |
| 5496 | + <i class="fa-solid fa-upload"></i> | |
| 5497 | + <span data-i18n="Add Background">Add Background</span> | |
| 5498 | + </label> | |
| 5402 | 5499 | </div> |
| 5403 | 5500 | <div class="bg-header-row-2"> |
| 5404 | 5501 | <input id="bg-filter" class="text_pole" type="search" data-i18n="[placeholder]Search..." placeholder="Search..." /> |
| 5405 | 5502 | </div> |
| 5406 | 5503 | </div> |
| 5407 | 5504 | <div id="bgbg_tabs" class="heading-scrollablecontainer-contentwith-controls"> |
| 5408 | 5505 | <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> | |
| 5411 | 5508 | </h3li> |
| 5509 | + <li class="bg_tab_button"> | |
| 5510 | + <a href="#bg_chat_tab" data-i18n="Chat">Chat</a> | |
| 5511 | + </li> | |
| 5412 | 5512 | <div class="heading-controls"> |
| 5413 | 5513 | <button id="bg_thumb_zoom_out" class="menu_button menu_button_icon" title="Make thumbnails smaller" data-i18n="[title]Make thumbnails smaller"> |
| 5414 | 5514 | <i class="fa-solid fa-minus"></i> |
| @@ -5417,20 +5517,19 @@ | ||
| 5417 | 5517 | <i class="fa-solid fa-plus"></i> |
| 5418 | 5518 | </button> |
| 5419 | 5519 | </div> |
| 5520 | + </ul> | |
| 5521 | + <div id="bg_global_tab" class="bg_tab_panel"> | |
| 5522 | + <div id="bg_menu_content" class="bg_list"></div> | |
| 5420 | 5523 | </div> |
| 5421 | 5524 | <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> | |
| 5426 | 5526 | <div id="bg_chat_hint" class="wide100p textAlignCenter"> |
| 5427 | 5527 | <span data-i18n="bg_chat_hint_1">Chat backgrounds generated with the</span> <code><i class="fa-solid fa-paintbrush"></i> Image Generation</code> <span data-i18n="bg_chat_hint_2">extension will appear here.</span> |
| 5428 | 5528 | </div> |
| 5429 | - <div id="bg_custom_content" class="bg_list"> | |
| 5430 | 5529 | </div> |
| 5431 | 5530 | </div> |
| 5432 | 5531 | <form id="form_bg_upload" style="display: none;"hidden> |
| 5433 | 5532 | <input type="file" id="add_bg_button" name="avatar" accept="image/jpeg,image/png,image/gif,image/bmp,image/svg+xml*,video/*" multiple hidden> |
| 5434 | 5533 | </form> |
| 5435 | 5534 | <button id="bg-scroll-top" type="button" class="menu_button menu_button_icon" title="Scroll backgrounds to top" data-i18n="[title]Scroll backgrounds to top"> |
| 5436 | 5535 | <i class="fa-solid fa-chevron-up" aria-hidden="true"></i> |
| @@ -6379,7 +6478,7 @@ | ||
| 6379 | 6478 | <div class="jg-menu"> |
| 6380 | 6479 | <div data-action="lock" class="jg-button jg-lock fa-solid fa-lock fa-fw pointer" data-i18n="[title]Lock" title="Lock"></div> |
| 6381 | 6480 | <div data-action="unlock" class="jg-button jg-unlock fa-solid fa-lock-open fa-fw pointer" data-i18n="[title]Unlock" title="Unlock"></div> |
| 6382 | 6481 | <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> |
| 6383 | 6482 | <div data-action="edit" class="jg-button jg-edit fa-solid fa-pen-to-square fa-fw pointer" data-i18n="[title]Rename Background" title="Rename Background"></div> |
| 6384 | 6483 | <div data-action="delete" class="jg-button jg-delete fa-solid fa-trash-can fa-fw pointer" data-i18n="[title]Delete Background" title="Delete Background"></div> |
| 6385 | 6484 | </div> |
| @@ -7047,7 +7146,7 @@ | ||
| 7047 | 7146 | <div class="mes_timer"></div> |
| 7048 | 7147 | <div class="tokenCounterDisplay"></div> |
| 7049 | 7148 | </div> |
| 7050 | 7149 | <div class="swipe_left fa-solid fa-chevron-left" style="display: none;"></div> |
| 7051 | 7150 | <div class="mes_block"> |
| 7052 | 7151 | <div class="ch_name flex-container justifySpaceBetween"> |
| 7053 | 7152 | <div class="flex-container flex1 alignitemscenter"> |
| @@ -7111,7 +7210,7 @@ | ||
| 7111 | 7210 | <div class="mes_bias"></div> |
| 7112 | 7211 | </div> |
| 7113 | 7212 | <div class="flex-container swipeRightBlock flexFlowColumn flexNoGap"> |
| 7114 | 7213 | <div class="swipe_right fa-solid fa-chevron-right" style="display: none;"></div> |
| 7115 | 7214 | <div class="swipes-counter"></div> |
| 7116 | 7215 | </div> |
| 7117 | 7216 | </div> |
| @@ -7210,9 +7309,8 @@ | ||
| 7210 | 7309 | <div title="Move down" data-action="down" class="right_menu_button fa-solid fa-chevron-down" data-i18n="[title]Move down"></div> |
| 7211 | 7310 | </div> |
| 7212 | 7311 | <div title="View character card" data-action="view" class="right_menu_button fa-solid fa-xl fa-image-portrait" data-i18n="[title]View character card"></div> |
| 7213 | 7312 | <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> | |
| 7216 | 7314 | </div> |
| 7217 | 7315 | </div> |
| 7218 | 7316 | </div> |
| @@ -6,7 +6,13 @@ | ||
| 6 | 6 | "moduleResolution": "node", |
| 7 | 7 | "allowUmdGlobalAccess": true, |
| 8 | 8 | "allowSyntheticDefaultImports": true, |
| 9 | 9 | "strictBindCallApply": true, |
| 10 | + "baseUrl": ".", | |
| 11 | + "paths": { | |
| 12 | + "/*": [ | |
| 13 | + "./*" | |
| 14 | + ] | |
| 15 | + } | |
| 10 | 16 | }, |
| 11 | 17 | "exclude": [ |
| 12 | 18 | "**/node_modules/**", |
| @@ -22,6 +22,7 @@ import morphdom from 'morphdom'; | ||
| 22 | 22 | import { toggle as slideToggle } from 'slidetoggle'; |
| 23 | 23 | import chalk from 'chalk'; |
| 24 | 24 | import yaml from 'yaml'; |
| 25 | +import * as chevrotain from 'chevrotain'; | |
| 25 | 26 | |
| 26 | 27 | /** |
| 27 | 28 | * Expose the libraries to the 'window' object. |
| @@ -100,6 +101,7 @@ export default { | ||
| 100 | 101 | slideToggle, |
| 101 | 102 | chalk, |
| 102 | 103 | yaml, |
| 104 | + chevrotain, | |
| 103 | 105 | }; |
| 104 | 106 | |
| 105 | 107 | export { |
| @@ -124,4 +126,5 @@ export { | ||
| 124 | 126 | slideToggle, |
| 125 | 127 | chalk, |
| 126 | 128 | yaml, |
| 129 | + chevrotain, | |
| 127 | 130 | }; |
| @@ -235,7 +235,7 @@ | ||
| 235 | 235 | "Combines consecutive system messages into one (excluding example dialogues). May improve coherence for some models.": "يجمع الرسائل المتتالية للنظام في رسالة واحدة (باستثناء الحوارات المثالية). قد يحسن التتابع لبعض النماذج.", |
| 236 | 236 | "Enable function calling": "تمكين استدعاء الوظيفة", |
| 237 | 237 | "Send inline images": "إرسال الصور المضمنة", |
| 238 | 238 | "image_inlining_hint_1": "يرسل الصورملفات الوسائط في المطالبات إذا كان النموذج يدعمها .", |
| 239 | 239 | "image_inlining_hint_2": "الإجراء على أي رسالة أو", |
| 240 | 240 | "image_inlining_hint_3": "القائمة لإرفاق ملف صورة للدردشة.", |
| 241 | 241 | "Inline Image Quality": "جودة الصورة المضمنة", |
| @@ -328,7 +328,7 @@ | ||
| 328 | 328 | "PygmalionAI/aphrodite-engine": "PygmalionAI/aphrodite-engine (وضع التغليف لواجهة برمجة التطبيقات OpenAI)", |
| 329 | 329 | "Aphrodite API key": "مفتاح واجهة برمجة التطبيقات Aphrodite", |
| 330 | 330 | "Aphrodite Model": "نموذج أفروديت", |
| 331 | 331 | "ggerganovggml-org/llama.cpp": "ggerganovggml-org/llama.cpp (خادم إخراج)", |
| 332 | 332 | "Example: http://127.0.0.1:8080": "مثال: http://127.0.0.1:8080", |
| 333 | 333 | "Example: http://127.0.0.1:11434": "مثال: http://127.0.0.1:11434", |
| 334 | 334 | "Ollama Model": "نموذج Ollama", |
| @@ -605,7 +605,7 @@ | ||
| 605 | 605 | "Model Icon": "أيقونة النموذج", |
| 606 | 606 | "Show sequential message numbers in the chat log": "إظهار أرقام الرسائل التسلسلية في سجل الدردشة", |
| 607 | 607 | "Message IDs": "معرفات الرسائل", |
| 608 | 608 | "Hide avatars, only in chat messages.": "إخفاء الصور الرمزية في رسائل الدردشة.", |
| 609 | 609 | "Hide Chat Avatars": "إخفاء الصور الرمزية للدردشة", |
| 610 | 610 | "Show the number of tokens in each message in the chat log": "عرض عدد الرموز في كل رسالة في سجل الدردشة", |
| 611 | 611 | "Show Message Token Count": "عرض عدد الرموز في الرسالة", |
| @@ -739,8 +739,8 @@ | ||
| 739 | 739 | "Filter": "تصفية", |
| 740 | 740 | "Automatically select a background based on the chat context": "تحديد خلفية تلقائيًا استنادًا إلى سياق الدردشة", |
| 741 | 741 | "Auto-select": "التحديد التلقائي", |
| 742 | 742 | "System BackgroundsGlobal": "خلفيات النظامعالمي", |
| 743 | 743 | "Chat Backgrounds": "خلفيات الدردشة", |
| 744 | 744 | "bg_chat_hint_1": "خلفيات الدردشة التي تم إنشاؤها باستخدام", |
| 745 | 745 | "bg_chat_hint_2": "سوف يظهر الامتداد هنا", |
| 746 | 746 | "Extensions": "امتدادات", |
| @@ -913,7 +913,7 @@ | ||
| 913 | 913 | "Save": "حفظ", |
| 914 | 914 | "Chat History": "تاريخ الدردشة", |
| 915 | 915 | "Import Chat": "استيراد الدردشة", |
| 916 | 916 | "Copy to systemglobal backgrounds": "نسخ إلى خلفياتالخلفيات النظامالعالمية", |
| 917 | 917 | "Rename background": "إعادة تسمية الخلفية", |
| 918 | 918 | "Lock": "قفل", |
| 919 | 919 | "Unlock": "الغاء القفل", |
| @@ -235,7 +235,7 @@ | ||
| 235 | 235 | "Combines consecutive system messages into one (excluding example dialogues). May improve coherence for some models.": "Kombiniert aufeinanderfolgende Systemnachrichten zu einer (ausschließlich Beispiel-Dialoge ausgeschlossen). Kann die Kohärenz für einige Modelle verbessern.", |
| 236 | 236 | "Enable function calling": "Funktionsaufruf aktivieren", |
| 237 | 237 | "Send inline images": "Inline-Bilder senden", |
| 238 | 238 | "image_inlining_hint_1": "Sendet BilderMediendateien in Eingabeaufforderungen, wenn das Modell dies unterstützt.", |
| 239 | 239 | "image_inlining_hint_2": "Aktion auf eine Nachricht oder die", |
| 240 | 240 | "image_inlining_hint_3": "Menü, um eine Bilddatei an den Chat anzuhängen.", |
| 241 | 241 | "Inline Image Quality": "Inline-Bildqualität", |
| @@ -328,7 +328,7 @@ | ||
| 328 | 328 | "PygmalionAI/aphrodite-engine": "PygmalionAI/aphrodite-engine (Wrappermodus für OpenAI API)", |
| 329 | 329 | "Aphrodite API key": "Aphrodite API-Schlüssel", |
| 330 | 330 | "Aphrodite Model": "Aphrodite-Modell", |
| 331 | 331 | "ggerganovggml-org/llama.cpp": "ggerganovggml-org/llama.cpp (Output-Server)", |
| 332 | 332 | "Example: http://127.0.0.1:8080": "Beispiel: http://127.0.0.1:8080", |
| 333 | 333 | "Example: http://127.0.0.1:11434": "Beispiel: http://127.0.0.1:11434", |
| 334 | 334 | "Ollama Model": "Ollama-Modell", |
| @@ -605,7 +605,7 @@ | ||
| 605 | 605 | "Model Icon": "Modell-Icon", |
| 606 | 606 | "Show sequential message numbers in the chat log": "Zeige aufeinanderfolgende Nachrichtennummern im Chat-Log", |
| 607 | 607 | "Message IDs": "Nachrichten-IDs", |
| 608 | 608 | "Hide avatars, only in chat messages.": "Avatare in Chatnachrichten verbergen.", |
| 609 | 609 | "Hide Chat Avatars": "Chat-Avatare ausblenden", |
| 610 | 610 | "Show the number of tokens in each message in the chat log": "Zeige die Anzahl der Tokens in jeder Nachricht im Chat-Log", |
| 611 | 611 | "Show Message Token Count": "Anzahl der Nachrichten-Token anzeigen", |
| @@ -739,8 +739,8 @@ | ||
| 739 | 739 | "Filter": "Filter", |
| 740 | 740 | "Automatically select a background based on the chat context": "Automatisch einen Hintergrund basierend auf dem Chatkontext auswählen", |
| 741 | 741 | "Auto-select": "Automatische Auswahl", |
| 742 | 742 | "System BackgroundsGlobal": "SystemhintergründeGlobal", |
| 743 | 743 | "Chat Backgrounds": "Chat-Hintergründe", |
| 744 | 744 | "bg_chat_hint_1": "Chat-Hintergründe generiert mit dem", |
| 745 | 745 | "bg_chat_hint_2": "Die Erweiterung wird hier angezeigt.", |
| 746 | 746 | "Extensions": "Erweiterungen", |
| @@ -913,7 +913,7 @@ | ||
| 913 | 913 | "Save": "Speichern", |
| 914 | 914 | "Chat History": "Chat-Verlauf", |
| 915 | 915 | "Import Chat": "Chat importieren", |
| 916 | 916 | "Copy to systemglobal backgrounds": "In Systemhintergründeglobale Hintergründe kopieren", |
| 917 | 917 | "Rename background": "Hintergrund umbenennen", |
| 918 | 918 | "Lock": "Sperren", |
| 919 | 919 | "Unlock": "Freischalten", |
| @@ -235,7 +235,7 @@ | ||
| 235 | 235 | "Combines consecutive system messages into one (excluding example dialogues). May improve coherence for some models.": "Combina mensajes del sistema consecutivos en uno solo (excluyendo diálogos de ejemplo). Puede mejorar la coherencia para algunos modelos.", |
| 236 | 236 | "Enable function calling": "Habilitar llamada a función", |
| 237 | 237 | "Send inline images": "Enviar imágenes en línea", |
| 238 | 238 | "image_inlining_hint_1": "Envía imágenesarchivos multimedia en mensajes si el modelo lo admite.", |
| 239 | 239 | "image_inlining_hint_2": "acción sobre cualquier mensaje o el", |
| 240 | 240 | "image_inlining_hint_3": "menú para adjuntar un archivo de imagen al chat.", |
| 241 | 241 | "Inline Image Quality": "Calidad de imagen en línea", |
| @@ -328,7 +328,7 @@ | ||
| 328 | 328 | "PygmalionAI/aphrodite-engine": "PygmalionAI/aphrodite-engine (Modo envolvente para API de OpenAI)", |
| 329 | 329 | "Aphrodite API key": "Clave de API de Aphrodite", |
| 330 | 330 | "Aphrodite Model": "Modelo Afrodita", |
| 331 | 331 | "ggerganovggml-org/llama.cpp": "ggerganovggml-org/llama.cpp (Servidor de salida)", |
| 332 | 332 | "Example: http://127.0.0.1:8080": "Ejemplo: http://127.0.0.1:8080", |
| 333 | 333 | "Example: http://127.0.0.1:11434": "Ejemplo: http://127.0.0.1:11434", |
| 334 | 334 | "Ollama Model": "Modelo Ollama", |
| @@ -605,7 +605,7 @@ | ||
| 605 | 605 | "Model Icon": "Ícono del Modelo", |
| 606 | 606 | "Show sequential message numbers in the chat log": "Mostrar números de mensaje secuenciales en el registro de chat", |
| 607 | 607 | "Message IDs": "IDs de Mensaje", |
| 608 | 608 | "Hide avatars, only in chat messages.": "Ocultar avatares en los mensajes de chat.", |
| 609 | 609 | "Hide Chat Avatars": "Ocultar avatares de chat", |
| 610 | 610 | "Show the number of tokens in each message in the chat log": "Mostrar el número de tokens en cada mensaje en el registro de chat", |
| 611 | 611 | "Show Message Token Count": "Mostrar Conteo de Tokens en Mensaje", |
| @@ -739,8 +739,8 @@ | ||
| 739 | 739 | "Filter": "Filtro", |
| 740 | 740 | "Automatically select a background based on the chat context": "Seleccionar automáticamente un fondo basado en el contexto del chat", |
| 741 | 741 | "Auto-select": "Auto-seleccionar", |
| 742 | 742 | "System BackgroundsGlobal": "Fondos del SistemaGlobal", |
| 743 | 743 | "Chat Backgrounds": "Fondos de Chat", |
| 744 | 744 | "bg_chat_hint_1": "Fondos de chat generados con el", |
| 745 | 745 | "bg_chat_hint_2": "La extensión aparecerá aquí.", |
| 746 | 746 | "Extensions": "Extensiones", |
| @@ -913,7 +913,7 @@ | ||
| 913 | 913 | "Save": "Guardar", |
| 914 | 914 | "Chat History": "Historial de chat", |
| 915 | 915 | "Import Chat": "Importar chat", |
| 916 | 916 | "Copy to systemglobal backgrounds": "Copiar a fondos del sistemaglobales", |
| 917 | 917 | "Rename background": "Cambiar nombre de fondo", |
| 918 | 918 | "Lock": "Cerrar", |
| 919 | 919 | "Unlock": "desbloquear", |
| @@ -227,7 +227,7 @@ | ||
| 227 | 227 | "Combines consecutive system messages into one (excluding example dialogues). May improve coherence for some models.": "Combine les messages système consécutifs en un seul (à l'exclusion des dialogues d'exemple). Peut améliorer la cohérence pour certains modèles.", |
| 228 | 228 | "Enable function calling": "Activer l'appel de fonction", |
| 229 | 229 | "Send inline images": "Envoyer des images en ligne", |
| 230 | 230 | "image_inlining_hint_1": "Envoie des imagesfichiers multimédias dans les prompts si le modèle le prend en charge.", |
| 231 | 231 | "image_inlining_hint_2": "action sur n'importe quel message ou le", |
| 232 | 232 | "image_inlining_hint_3": "menu pour joindre un fichier image au chat.", |
| 233 | 233 | "Inline Image Quality": "Qualité d'image en ligne", |
| @@ -309,7 +309,7 @@ | ||
| 309 | 309 | "PygmalionAI/aphrodite-engine": "PygmalionAI/aphrodite-engine (mode wrapper pour l'API OpenAI)", |
| 310 | 310 | "Aphrodite API key": "Clé API Aphrodite", |
| 311 | 311 | "Aphrodite Model": "Modèle Aphrodite", |
| 312 | 312 | "ggerganovggml-org/llama.cpp": "ggerganovggml-org/llama.cpp", |
| 313 | 313 | "Example: http://127.0.0.1:8080": "Exemple : http://127.0.0.1:8080", |
| 314 | 314 | "Example: http://127.0.0.1:11434": "Exemple : http://127.0.0.1:11434", |
| 315 | 315 | "Ollama Model": "Modèle Ollama", |
| @@ -554,7 +554,7 @@ | ||
| 554 | 554 | "Model Icon": "Icône du modèle", |
| 555 | 555 | "Show sequential message numbers in the chat log": "Afficher les numéros de message séquentiels dans le journal de discussion", |
| 556 | 556 | "Message IDs": "Identifiants du message", |
| 557 | 557 | "Hide avatars, only in chat messages.": "Masquez les avatars dans les messages de discussion.", |
| 558 | 558 | "Hide Chat Avatars": "Masquer les avatars du chat", |
| 559 | 559 | "Show the number of tokens in each message in the chat log": "Afficher le nombre de tokens dans chaque message dans le journal de discussion", |
| 560 | 560 | "Show Message Token Count": "Afficher le nombre de tokens du message", |
| @@ -684,8 +684,8 @@ | ||
| 684 | 684 | "Filter": "Filtre", |
| 685 | 685 | "Automatically select a background based on the chat context": "Sélectionner automatiquement un arrière-plan en fonction du contexte de la discussion", |
| 686 | 686 | "Auto-select": "Sélection automatique", |
| 687 | 687 | "System BackgroundsGlobal": "Arrière-plans du systèmeGlobal", |
| 688 | 688 | "Chat Backgrounds": "Arrière-plans de la discussionDiscussion", |
| 689 | 689 | "bg_chat_hint_1": "Les arrière-plans générés avec l'extension ", |
| 690 | 690 | "bg_chat_hint_2": " apparaîtrons ici.", |
| 691 | 691 | "Extensions": "Extensions", |
| @@ -856,7 +856,7 @@ | ||
| 856 | 856 | "Save": "Enregistrer", |
| 857 | 857 | "Chat History": "Historique de la conversation", |
| 858 | 858 | "Import Chat": "Importer une discussion", |
| 859 | 859 | "Copy to systemglobal backgrounds": "Copier vers les arrière-plans du systèmeglobaux", |
| 860 | 860 | "Rename background": "Renommer l'arrière-plan", |
| 861 | 861 | "Lock": "Verrouiller", |
| 862 | 862 | "Unlock": "Déverrouiller", |
| @@ -235,7 +235,7 @@ | ||
| 235 | 235 | "Combines consecutive system messages into one (excluding example dialogues). May improve coherence for some models.": "Sameinar samhliða kerfisskilaboð í eitt (sem er utan umsagna dæmum). Getur bætt samfelldni fyrir sumar módel.", |
| 236 | 236 | "Enable function calling": "Virkja aðgerðarkall", |
| 237 | 237 | "Send inline images": "Senda myndir í línu", |
| 238 | 238 | "image_inlining_hint_1": "Sendir myndirmargmiðlunarskrár í skilaboðum ef líkanið styður það.", |
| 239 | 239 | "image_inlining_hint_2": "aðgerð á hvaða skilaboðum sem er eða", |
| 240 | 240 | "image_inlining_hint_3": "valmynd til að hengja myndskrá við spjallið.", |
| 241 | 241 | "Inline Image Quality": "Innbyggð myndgæði", |
| @@ -326,7 +326,7 @@ | ||
| 326 | 326 | "PygmalionAI/aphrodite-engine": "PygmalionAI/aphrodite-engine (OpenAI forritunargrensl)", |
| 327 | 327 | "Aphrodite API key": "Aphrodite API lykill", |
| 328 | 328 | "Aphrodite Model": "Afródíta fyrirmynd", |
| 329 | 329 | "ggerganovggml-org/llama.cpp": "ggerganovggml-org/llama.cpp (úttak þjónn)", |
| 330 | 330 | "Example: http://127.0.0.1:8080": "Dæmi: http://127.0.0.1:8080", |
| 331 | 331 | "Example: http://127.0.0.1:11434": "Dæmi: http://127.0.0.1:11434", |
| 332 | 332 | "Ollama Model": "Ollama módel", |
| @@ -603,7 +603,7 @@ | ||
| 603 | 603 | "Model Icon": "Módel Tákn", |
| 604 | 604 | "Show sequential message numbers in the chat log": "Sýna röðuð skilaboðanúmer í spjall skránni", |
| 605 | 605 | "Message IDs": "Skilaboðaauðkenni", |
| 606 | 606 | "Hide avatars, only in chat messages.": "Fela avatar í spjallskilaboðum.", |
| 607 | 607 | "Hide Chat Avatars": "Fela spjallmyndir", |
| 608 | 608 | "Show the number of tokens in each message in the chat log": "Sýna fjölda tákn í hverju skilaboði í spjall skránni", |
| 609 | 609 | "Show Message Token Count": "Sýna fjölda tákn í skilaboðum", |
| @@ -737,8 +737,8 @@ | ||
| 737 | 737 | "Filter": "Sía", |
| 738 | 738 | "Automatically select a background based on the chat context": "Velja sjálfkrafa bakgrunn út frá samhengi spjallsins", |
| 739 | 739 | "Auto-select": "Sjálfval", |
| 740 | 740 | "System BackgroundsGlobal": "Kerfis bakgrunnsmyndirAlmennt", |
| 741 | 741 | "Chat Backgrounds": "Bakgrunnsmyndir spjallsSpjall", |
| 742 | 742 | "bg_chat_hint_1": "Spjallbakgrunnur myndaður með", |
| 743 | 743 | "bg_chat_hint_2": "viðbót mun birtast hér.", |
| 744 | 744 | "Extensions": "Viðbætur", |
| @@ -911,7 +911,7 @@ | ||
| 911 | 911 | "Save": "Vista", |
| 912 | 912 | "Chat History": "Spjall saga", |
| 913 | 913 | "Import Chat": "Flytja inn spjall", |
| 914 | 914 | "Copy to systemglobal backgrounds": "Afritaðu í kerfisbakgrunnalþjóðlegan bakgrunn", |
| 915 | 915 | "Rename background": "Endurnefna bakgrunn", |
| 916 | 916 | "Lock": "Læsa", |
| 917 | 917 | "Unlock": "Opnaðu", |
| @@ -235,7 +235,7 @@ | ||
| 235 | 235 | "Combines consecutive system messages into one (excluding example dialogues). May improve coherence for some models.": "Combina i messaggi di sistema consecutivi in uno solo (escludendo i dialoghi di esempio). Potrebbe migliorare la coerenza per alcuni modelli.", |
| 236 | 236 | "Enable function calling": "Abilita la chiamata alla funzione", |
| 237 | 237 | "Send inline images": "Invia immagini inline", |
| 238 | 238 | "image_inlining_hint_1": "Invia immaginifile multimediali nei prompt se il modello lo supporta.", |
| 239 | 239 | "image_inlining_hint_2": "azione su qualsiasi messaggio o il", |
| 240 | 240 | "image_inlining_hint_3": "menu per allegare un file immagine alla chat.", |
| 241 | 241 | "Inline Image Quality": "Qualità dell'immagine in linea", |
| @@ -328,7 +328,7 @@ | ||
| 328 | 328 | "PygmalionAI/aphrodite-engine": "PygmalionAI/aphrodite-engine (Modalità wrapper per l'API OpenAI)", |
| 329 | 329 | "Aphrodite API key": "Chiave API di Aphrodite", |
| 330 | 330 | "Aphrodite Model": "Modello di Afrodite", |
| 331 | 331 | "ggerganovggml-org/llama.cpp": "ggerganovggml-org/llama.cpp (Server di output)", |
| 332 | 332 | "Example: http://127.0.0.1:8080": "Esempio: http://127.0.0.1:8080", |
| 333 | 333 | "Example: http://127.0.0.1:11434": "Esempio: http://127.0.0.1:11434", |
| 334 | 334 | "Ollama Model": "Modello Ollama", |
| @@ -605,7 +605,7 @@ | ||
| 605 | 605 | "Model Icon": "Icona del modello", |
| 606 | 606 | "Show sequential message numbers in the chat log": "Mostra numeri di messaggi sequenziali nel registro della chat", |
| 607 | 607 | "Message IDs": "ID Messaggio", |
| 608 | 608 | "Hide avatars, only in chat messages.": "Nascondi gli avatar nei messaggi di chat.", |
| 609 | 609 | "Hide Chat Avatars": "Nascondi avatar di chat", |
| 610 | 610 | "Show the number of tokens in each message in the chat log": "Mostra il numero di token in ogni messaggio nel registro della chat", |
| 611 | 611 | "Show Message Token Count": "Mostra Conteggio Token Messaggio", |
| @@ -739,8 +739,8 @@ | ||
| 739 | 739 | "Filter": "Filtro", |
| 740 | 740 | "Automatically select a background based on the chat context": "Seleziona automaticamente uno sfondo in base al contesto della chat", |
| 741 | 741 | "Auto-select": "Selezione automatica", |
| 742 | 742 | "System BackgroundsGlobal": "Sfondi di sistemaGlobale", |
| 743 | 743 | "Chat Backgrounds": "Sfondi Chat", |
| 744 | 744 | "bg_chat_hint_1": "Sfondi chat generati con", |
| 745 | 745 | "bg_chat_hint_2": "l'estensione apparirà qui.", |
| 746 | 746 | "Extensions": "Estensioni", |
| @@ -913,7 +913,7 @@ | ||
| 913 | 913 | "Save": "Salva", |
| 914 | 914 | "Chat History": "Cronologia Chat", |
| 915 | 915 | "Import Chat": "Importa chat", |
| 916 | 916 | "Copy to systemglobal backgrounds": "Copia negli sfondi di sistemaglobali", |
| 917 | 917 | "Rename background": "Rinominare lo sfondo", |
| 918 | 918 | "Lock": "Serratura", |
| 919 | 919 | "Unlock": "Sbloccare", |
| @@ -235,7 +235,7 @@ | ||
| 235 | 235 | "Combines consecutive system messages into one (excluding example dialogues). May improve coherence for some models.": "連続するシステムメッセージを1つに結合します(例のダイアログを除く)。一部のモデルの一貫性を向上させる可能性があります。", |
| 236 | 236 | "Enable function calling": "関数呼び出しを有効にする", |
| 237 | 237 | "Send inline images": "インライン画像を送信", |
| 238 | 238 | "image_inlining_hint_1": "モデルがサポートしている場合、プロンプトで画像を送信しますプロンプトでメディアファイルを送信します。", |
| 239 | 239 | "image_inlining_hint_2": "メッセージに対するアクションまたは", |
| 240 | 240 | "image_inlining_hint_3": "チャットに画像ファイルを添付するためのメニュー。", |
| 241 | 241 | "Inline Image Quality": "インライン画像品質", |
| @@ -328,7 +328,7 @@ | ||
| 328 | 328 | "PygmalionAI/aphrodite-engine": "PygmalionAI/aphrodite-engine(OpenAI APIエンドポイントのパッケージングモード)", |
| 329 | 329 | "Aphrodite API key": "アフロディーテAPIキー", |
| 330 | 330 | "Aphrodite Model": "アフロディーテモデル", |
| 331 | 331 | "ggerganovggml-org/llama.cpp": "ggerganovggml-org/llama.cpp(出力サーバー)", |
| 332 | 332 | "Example: http://127.0.0.1:8080": "例: http://127.0.0.1:8080", |
| 333 | 333 | "Example: http://127.0.0.1:11434": "例: http://127.0.0.1:11434", |
| 334 | 334 | "Ollama Model": "Ollamaモデル", |
| @@ -605,7 +605,7 @@ | ||
| 605 | 605 | "Model Icon": "モデルアイコン", |
| 606 | 606 | "Show sequential message numbers in the chat log": "チャットログに連続したメッセージ番号を表示する", |
| 607 | 607 | "Message IDs": "メッセージID", |
| 608 | 608 | "Hide avatars, only in chat messages.": "チャットメッセージ内のアバターを非表示にします。", |
| 609 | 609 | "Hide Chat Avatars": "チャットアバターを非表示", |
| 610 | 610 | "Show the number of tokens in each message in the chat log": "チャットログ内の各メッセージのトークン数を表示する", |
| 611 | 611 | "Show Message Token Count": "メッセージトークンの数を表示", |
| @@ -739,8 +739,8 @@ | ||
| 739 | 739 | "Filter": "フィルター", |
| 740 | 740 | "Automatically select a background based on the chat context": "チャットのコンテキストに基づいて背景を自動選択", |
| 741 | 741 | "Auto-select": "自動選択", |
| 742 | 742 | "System BackgroundsGlobal": "システムの背景グローバル", |
| 743 | 743 | "Chat Backgrounds": "チャットの背景チャット", |
| 744 | 744 | "bg_chat_hint_1": "", |
| 745 | 745 | "bg_chat_hint_2": "拡張機能で生成したチャットの背景はここに表示されます。", |
| 746 | 746 | "Extensions": "拡張機能", |
| @@ -913,7 +913,7 @@ | ||
| 913 | 913 | "Save": "保存", |
| 914 | 914 | "Chat History": "チャット履歴", |
| 915 | 915 | "Import Chat": "チャットをインポート", |
| 916 | 916 | "Copy to systemglobal backgrounds": "システム背景にコピーグローバル背景にコピー", |
| 917 | 917 | "Rename background": "背景の名前を変更", |
| 918 | 918 | "Lock": "ロック", |
| 919 | 919 | "Unlock": "ロック解除", |
| @@ -237,7 +237,7 @@ | ||
| 237 | 237 | "Combines consecutive system messages into one (excluding example dialogues). May improve coherence for some models.": "연속된 시스템 메시지를 하나로 결합합니다(예제 대화 제외). 일부 모델의 일관성을 향상시킬 수 있습니다.", |
| 238 | 238 | "Enable function calling": "함수 호출 활성화", |
| 239 | 239 | "Send inline images": "인라인 이미지 전송", |
| 240 | 240 | "image_inlining_hint_1": "모델이 지원하는 경우 메시지로 이미지를미디어 파일을 보냅니다.", |
| 241 | 241 | "Inline Image Quality": "인라인 이미지 품질", |
| 242 | 242 | "openai_inline_image_quality_auto": "자동", |
| 243 | 243 | "openai_inline_image_quality_low": "낮은", |
| @@ -328,7 +328,7 @@ | ||
| 328 | 328 | "PygmalionAI/aphrodite-engine": "PygmalionAI/aphrodite-engine (OpenAI API의 래퍼 모드)", |
| 329 | 329 | "Aphrodite API key": "Aphrodite API 키", |
| 330 | 330 | "Aphrodite Model": "Aphrodite 모델", |
| 331 | 331 | "ggerganovggml-org/llama.cpp": "ggerganovggml-org/llama.cpp (출력 서버)", |
| 332 | 332 | "Example: http://127.0.0.1:8080": "예: http://127.0.0.1:8080", |
| 333 | 333 | "Example: http://127.0.0.1:11434": "예: http://127.0.0.1:11434", |
| 334 | 334 | "Ollama Model": "Ollama 모델", |
| @@ -613,7 +613,7 @@ | ||
| 613 | 613 | "Model Icon": "모델 아이콘 표시", |
| 614 | 614 | "Show sequential message numbers in the chat log": "채팅 로그에 순차적인 메시지 번호 표시", |
| 615 | 615 | "Message IDs": "메시지 ID", |
| 616 | 616 | "Hide avatars, only in chat messages.": "채팅 메시지에서 아바타 숨김.", |
| 617 | 617 | "Hide Chat Avatars": "캐릭터 프로필 숨기기", |
| 618 | 618 | "Show the number of tokens in each message in the chat log": "채팅 로그의 각 메시지에 토큰 수 표시", |
| 619 | 619 | "Show Message Token Count": "메시지 토큰 개수 표시", |
| @@ -752,8 +752,8 @@ | ||
| 752 | 752 | "Filter": "필터", |
| 753 | 753 | "Automatically select a background based on the chat context": "채팅 컨텍스트를 기반으로 자동으로 배경 선택", |
| 754 | 754 | "Auto-select": "자동 선택", |
| 755 | 755 | "System BackgroundsGlobal": "시스템 배경글로벌", |
| 756 | 756 | "Chat Backgrounds": "채팅 배경", |
| 757 | 757 | "bg_chat_hint_1": "다음으로 생성된 채팅 배경", |
| 758 | 758 | "bg_chat_hint_2": "확장 프로그램이 여기에 표시됩니다.", |
| 759 | 759 | "Extensions": "확장", |
| @@ -927,7 +927,7 @@ | ||
| 927 | 927 | "Save": "저장", |
| 928 | 928 | "Chat History": "채팅 기록", |
| 929 | 929 | "Import Chat": "채팅 가져오기", |
| 930 | 930 | "Copy to systemglobal backgrounds": "시스템글로벌 배경에 복사", |
| 931 | 931 | "Rename background": "배경 이름 바꾸기", |
| 932 | 932 | "Lock": "잠금", |
| 933 | 933 | "Unlock": "잠금 해제", |
| @@ -235,7 +235,7 @@ | ||
| 235 | 235 | "Combines consecutive system messages into one (excluding example dialogues). May improve coherence for some models.": "Combineert opeenvolgende systeemberichten tot één (exclusief voorbeeld dialogen). Kan de coherentie verbeteren voor sommige modellen.", |
| 236 | 236 | "Enable function calling": "Schakel functieaanroepen in", |
| 237 | 237 | "Send inline images": "Inline afbeeldingen verzenden", |
| 238 | 238 | "image_inlining_hint_1": "Verzendt afbeeldingenmediabestanden in prompts als het model dit ondersteunt.", |
| 239 | 239 | "Inline Image Quality": "Inline-beeldkwaliteit", |
| 240 | 240 | "openai_inline_image_quality_auto": "Auto", |
| 241 | 241 | "openai_inline_image_quality_low": "Laag", |
| @@ -324,7 +324,7 @@ | ||
| 324 | 324 | "PygmalionAI/aphrodite-engine": "PygmalionAI/aphrodite-engine (Wrappermodus voor OpenAI API)", |
| 325 | 325 | "Aphrodite API key": "Aphrodite API-sleutel", |
| 326 | 326 | "Aphrodite Model": "Aphrodite-model", |
| 327 | 327 | "ggerganovggml-org/llama.cpp": "ggerganovggml-org/llama.cpp (Output-server)", |
| 328 | 328 | "Example: http://127.0.0.1:8080": "Voorbeeld: http://127.0.0.1:8080", |
| 329 | 329 | "Example: http://127.0.0.1:11434": "Voorbeeld: http://127.0.0.1:11434", |
| 330 | 330 | "Ollama Model": "Ollama-model", |
| @@ -601,7 +601,7 @@ | ||
| 601 | 601 | "Model Icon": "Modelpictogram", |
| 602 | 602 | "Show sequential message numbers in the chat log": "Toon opeenvolgende berichtnummers in het chatlogboek", |
| 603 | 603 | "Message IDs": "Bericht-IDs", |
| 604 | 604 | "Hide avatars, only in chat messages.": "Verberg avatars in chatberichten.", |
| 605 | 605 | "Hide Chat Avatars": "Chatavatars verbergen", |
| 606 | 606 | "Show the number of tokens in each message in the chat log": "Toon het aantal tokens in elk bericht in het chatlogboek", |
| 607 | 607 | "Show Message Token Count": "Toon bericht token tellen", |
| @@ -735,8 +735,8 @@ | ||
| 735 | 735 | "Filter": "Filter", |
| 736 | 736 | "Automatically select a background based on the chat context": "Automatisch een achtergrond selecteren op basis van de chatcontext", |
| 737 | 737 | "Auto-select": "Automatisch selecteren", |
| 738 | 738 | "System BackgroundsGlobal": "SysteemachtergrondenGlobaal", |
| 739 | 739 | "Chat Backgrounds": "Achtergronden chatChat", |
| 740 | 740 | "bg_chat_hint_1": "Chatachtergronden gegenereerd met de", |
| 741 | 741 | "bg_chat_hint_2": "extensie zal hier verschijnen.", |
| 742 | 742 | "Extensions": "Uitbreidingen", |
| @@ -909,7 +909,7 @@ | ||
| 909 | 909 | "Save": "Opslaan", |
| 910 | 910 | "Chat History": "Chatgeschiedenis", |
| 911 | 911 | "Import Chat": "Chat importeren", |
| 912 | 912 | "Copy to systemglobal backgrounds": "Kopiëren naar systeemachtergrondenglobale achtergronden", |
| 913 | 913 | "Rename background": "Achtergrond hernoemen", |
| 914 | 914 | "Lock": "Slot", |
| 915 | 915 | "Unlock": "Ontgrendelen", |
| @@ -235,7 +235,7 @@ | ||
| 235 | 235 | "Combines consecutive system messages into one (excluding example dialogues). May improve coherence for some models.": "Combina mensagens do sistema consecutivas em uma (excluindo diálogos de exemplo). Pode melhorar a coerência para alguns modelos.", |
| 236 | 236 | "Enable function calling": "Habilitar chamada de função", |
| 237 | 237 | "Send inline images": "Enviar imagens inline", |
| 238 | 238 | "image_inlining_hint_1": "Envia imagensficheiros multimédia em prompts se o modelo suportar.", |
| 239 | 239 | "Inline Image Quality": "Qualidade de imagem embutida", |
| 240 | 240 | "openai_inline_image_quality_auto": "Auto", |
| 241 | 241 | "openai_inline_image_quality_low": "Baixo", |
| @@ -326,7 +326,7 @@ | ||
| 326 | 326 | "PygmalionAI/aphrodite-engine": "PygmalionAI/aphrodite-engine (Modo Wrapper para API OpenAI)", |
| 327 | 327 | "Aphrodite API key": "Chave da API Aphrodite", |
| 328 | 328 | "Aphrodite Model": "Modelo Afrodite", |
| 329 | 329 | "ggerganovggml-org/llama.cpp": "ggerganovggml-org/llama.cpp (Servidor de Saída)", |
| 330 | 330 | "Example: http://127.0.0.1:8080": "Exemplo: http://127.0.0.1:8080", |
| 331 | 331 | "Example: http://127.0.0.1:11434": "Exemplo: http://127.0.0.1:11434", |
| 332 | 332 | "Ollama Model": "Modelo Ollama", |
| @@ -603,7 +603,7 @@ | ||
| 603 | 603 | "Model Icon": "Ícone do Modelo", |
| 604 | 604 | "Show sequential message numbers in the chat log": "Mostrar números sequenciais de mensagem no registro de chat", |
| 605 | 605 | "Message IDs": "IDs de Mensagem", |
| 606 | 606 | "Hide avatars, only in chat messages.": "Oculte avatares em mensagens de bate-papo.", |
| 607 | 607 | "Hide Chat Avatars": "Ocultar avatares de bate-papo", |
| 608 | 608 | "Show the number of tokens in each message in the chat log": "Mostrar o número de tokens em cada mensagem no registro de chat", |
| 609 | 609 | "Show Message Token Count": "Mostrar Contagem de Tokens da Mensagem", |
| @@ -737,8 +737,8 @@ | ||
| 737 | 737 | "Filter": "Filtro", |
| 738 | 738 | "Automatically select a background based on the chat context": "Selecionar automaticamente um plano de fundo com base no contexto do chat", |
| 739 | 739 | "Auto-select": "Seleção automática", |
| 740 | 740 | "System BackgroundsGlobal": "Fundos do SistemaGlobal", |
| 741 | 741 | "Chat Backgrounds": "Fundos de Chat", |
| 742 | 742 | "bg_chat_hint_1": "Planos de fundo de bate-papo gerados com o", |
| 743 | 743 | "bg_chat_hint_2": "a extensão aparecerá aqui.", |
| 744 | 744 | "Extensions": "Extensões", |
| @@ -911,7 +911,7 @@ | ||
| 911 | 911 | "Save": "Salvar", |
| 912 | 912 | "Chat History": "Histórico de Chat", |
| 913 | 913 | "Import Chat": "Importar bate-papo", |
| 914 | 914 | "Copy to systemglobal backgrounds": "Copiar para planos de fundo do sistemaglobais", |
| 915 | 915 | "Rename background": "Renomear plano de fundo", |
| 916 | 916 | "Lock": "Trancar", |
| 917 | 917 | "Unlock": "Desbloquear", |
| @@ -108,7 +108,7 @@ | ||
| 108 | 108 | "Register": "Зарегистрироваться", |
| 109 | 109 | "TogetherAI Model": "Модель TogetherAI", |
| 110 | 110 | "Example: http://127.0.0.1:5001": "Пример: http://127.0.0.1:5001", |
| 111 | 111 | "ggerganovggml-org/llama.cpp": "ggerganovggml-org/llama.cpp (сервер вывода)", |
| 112 | 112 | "Example: http://127.0.0.1:8080": "Пример: http://127.0.0.1:8080", |
| 113 | 113 | "Example: http://127.0.0.1:11434": "Пример: http://127.0.0.1:11434", |
| 114 | 114 | "Ollama Model": "Модель Ollama", |
| @@ -244,7 +244,6 @@ | ||
| 244 | 244 | "Always include examples": "Всегда применять примеры", |
| 245 | 245 | "Never include examples": "Никогда не применять примеры", |
| 246 | 246 | "Forbid External Media": "Запрет внешних медиа", |
| 247 | - "System Backgrounds": "Системные фоны", | |
| 248 | 247 | "Name": "Имя", |
| 249 | 248 | "Auto-connect": "Подключаться автоматически", |
| 250 | 249 | "First message": "Первое сообщение", |
| @@ -350,7 +349,7 @@ | ||
| 350 | 349 | "Minimum number of blacklisted words detected to trigger an auto-swipe": "Минимальное количество обнаруженных запрещённых слов, при котором срабатывает авто-свайп.", |
| 351 | 350 | "User Message Blur Tint": "Ваши сообщения", |
| 352 | 351 | "AI Message Blur Tint": "Сообщения ИИ", |
| 353 | 352 | "Chat Backgrounds": "Фоны чатаЧат", |
| 354 | 353 | "Chat Background": "Фон чата", |
| 355 | 354 | "UI Background": "Фон UI", |
| 356 | 355 | "Mad Lab Mode": "Режим безумца", |
| @@ -1020,7 +1019,7 @@ | ||
| 1020 | 1019 | "The prompt to be sent.": "Текст промпта.", |
| 1021 | 1020 | "prompt_manager_forbid_overrides": "Запретить перезапись", |
| 1022 | 1021 | "This prompt cannot be overridden by character cards, even if overrides are preferred.": "Карточка персонажа не сможет перезаписать этот промпт, даже если настройки отдают приоритет именно ей.", |
| 1023 | 1022 | "image_inlining_hint_1": "Отправлять картинкимедиафайлы как часть промпта, если позволяет модель.", |
| 1024 | 1023 | "Contest Winners": "Победители конкурса", |
| 1025 | 1024 | "Rename Background": "Переименовать фон", |
| 1026 | 1025 | "Lock": "Закрепить", |
| @@ -1046,7 +1045,7 @@ | ||
| 1046 | 1045 | "openai_inline_image_quality_high": "Высокое", |
| 1047 | 1046 | "Assistant Impersonation Prefill": "Префилл для ассистента при перевоплощении", |
| 1048 | 1047 | "Hide Chat Avatars": "Не показывать аватарки в чате", |
| 1049 | 1048 | "Hide avatars, only in chat messages.": "Скрыть аватарки сбоку от сообщений в чате", |
| 1050 | 1049 | "Flat": "Стандартный", |
| 1051 | 1050 | "Toggle character info panel": "Показать / скрыть инфо-панель", |
| 1052 | 1051 | "(For Chat Completion and Instruct Mode)": "(для Chat Completion и режима Instruct)", |
| @@ -1326,7 +1325,7 @@ | ||
| 1326 | 1325 | "Auto Mode delay": "Auto Mode delay", |
| 1327 | 1326 | "Bulk_edit_characters": "Bulk edit characters\r\rClick to toggle characters\rShift + Click to select/deselect a range of characters\rRight-click for actions", |
| 1328 | 1327 | "Bulk select all characters": "Bulk select all characters", |
| 1329 | 1328 | "Copy to systemglobal backgrounds": "CopyСкопировать toв systemглобальные backgroundsфоны", |
| 1330 | 1329 | "Chat Scenario Override": "Chat Scenario Override", |
| 1331 | 1330 | "Chat Lorebook": "Chat Lorebook for", |
| 1332 | 1331 | "chat_world_template_txt": "A selected World Info will be bound to this chat. When generating an AI reply,\n it will be combined with the entries from global and character lorebooks.", |
| @@ -2443,7 +2442,6 @@ | ||
| 2443 | 2442 | "Allocates a portion of the response length for thinking (min: 1024 tokens, low: 10%, medium: 25%, high: 50%, max: 95%), but minimum 1024 tokens. Auto does not request thinking.": "Отводит часть ответа под рассуждения (Минимальные: 10%, Обычные: 25%, Подробные: 50%, Максимальные: 95%), но минимум 1024 токена. При выборе значение Авто рассуждения не запрашиваются.", |
| 2444 | 2443 | "Use system prompt": "Включить системный промпт", |
| 2445 | 2444 | "Send inline videos": "Отправлять inline-видео", |
| 2446 | - "video_inlining_hint_1": "Отправляет модели видео, если она поддерживает такую возможность.", | |
| 2447 | 2445 | "video_inlining_hint_4": "Видео должно весить не более 20 Мб и длиться не дольше 1 минуты.", |
| 2448 | 2446 | "Allocates a portion of the response length for thinking (Flash 2.5/Pro 2.5) (min: 0/128 tokens, low: 10%, medium: 25%, high: 50%, max: 24576/32768 tokens). Auto lets the model decide.": "Отводит часть ответа под рассуждения (Flash 2.5/Pro 2.5) (Минимальные: 0/128 токенов, Поверхностные: 10%, Обычные: 25%, Подробные: 50%, Максимальные: 24576/32768 токенов). При выборе значения Авто модель определяет объём самостоятельно.", |
| 2449 | 2447 | "Google Vertex AI Configuration": "Настройки Google Vertex AI", |
| @@ -241,7 +241,7 @@ | ||
| 241 | 241 | "Allows the model to return its thinking process.": "อนุญาตให้โมเดลส่งคืนกระบวนการคิดของตัวเอง", |
| 242 | 242 | "This setting affects visibility only.": "การตั้งค่านี้มีผลต่อการมองเห็นเท่านั้น", |
| 243 | 243 | "Send inline images": "ส่งรูปภาพแบบ inline", |
| 244 | 244 | "image_inlining_hint_1": "ส่งรูปภาพในพรอมตงไฟล์สื่อในพรอมต์หากโมเดลรองรับ", |
| 245 | 245 | "Inline Image Quality": "คุณภาพรูปภาพ Inline", |
| 246 | 246 | "openai_inline_image_quality_auto": "อัตโนมัติ", |
| 247 | 247 | "openai_inline_image_quality_low": "ต่ำ", |
| @@ -330,7 +330,7 @@ | ||
| 330 | 330 | "PygmalionAI/aphrodite-engine": "PygmalionAI/aphrodite-engine", |
| 331 | 331 | "Aphrodite API key": "คีย์ API ของ Aphrodite", |
| 332 | 332 | "Aphrodite Model": "โมเดลของ Aphrodite", |
| 333 | 333 | "ggerganovggml-org/llama.cpp": "ggerganovggml-org/llama.cpp", |
| 334 | 334 | "Example: http://127.0.0.1:8080": "ตัวอย่าง: http://127.0.0.1:8080", |
| 335 | 335 | "Example: http://127.0.0.1:11434": "ตัวอย่าง: http://127.0.0.1:11434", |
| 336 | 336 | "Ollama Model": "โมเดลของ Ollama", |
| @@ -611,7 +611,7 @@ | ||
| 611 | 611 | "Model Icon": "แสดงไอคอน Model AI ที่ตอบแชท", |
| 612 | 612 | "Show sequential message numbers in the chat log": "แสดงหมายเลขลำดับข้อความในประวัติแชท", |
| 613 | 613 | "Message IDs": "แสดงเลขจำนวนข้อความ", |
| 614 | 614 | "Hide avatars, only in chat messages.": "ซ่อนภาพ avatar ในแชทข้อความ", |
| 615 | 615 | "Hide Chat Avatars": "ซ่อนรูปโปรไฟล์แชท", |
| 616 | 616 | "Show the number of tokens in each message in the chat log": "แสดงจำนวนโทเค็นในแต่ละข้อความในบันทึกแชท", |
| 617 | 617 | "Show Message Token Count": "แสดงจำนวนโทเค็นที่ Ai ตอบแชท", |
| @@ -745,8 +745,8 @@ | ||
| 745 | 745 | "Filter": "ตัวกรอง", |
| 746 | 746 | "Automatically select a background based on the chat context": "เลือกพื้นหลังโดยอัตโนมัติตามบริบทของแชท", |
| 747 | 747 | "Auto-select": "เลือกอัตโนมัติ", |
| 748 | 748 | "System BackgroundsGlobal": "พื้นหลังของระบบทั่วโลก", |
| 749 | 749 | "Chat Backgrounds": "พื้นหลังแชทแชท", |
| 750 | 750 | "bg_chat_hint_1": "พื้นหลังของแชทที่ถูกสร้างด้วย", |
| 751 | 751 | "bg_chat_hint_2": "จะปรากฏที่นี่", |
| 752 | 752 | "Extensions": "โปรแกรมส่วนขยาย", |
| @@ -921,7 +921,7 @@ | ||
| 921 | 921 | "Save": "บันทึก", |
| 922 | 922 | "Chat History": "ประวัติการแชท", |
| 923 | 923 | "Import Chat": "นำเข้าไฟล์แชท", |
| 924 | 924 | "Copy to systemglobal backgrounds": "คัดลอกไปยังพื้นหลังนหลังทั่วโลก", |
| 925 | 925 | "Rename background": "เปลี่ยนชื่อพื้นหลัง", |
| 926 | 926 | "Lock": "ล็อค", |
| 927 | 927 | "Unlock": "ปลดล็อค", |
| @@ -235,7 +235,7 @@ | ||
| 235 | 235 | "Combines consecutive system messages into one (excluding example dialogues). May improve coherence for some models.": "Об'єднує послідовні системні повідомлення в одне (крім прикладів діалогів). Може покращити співпрацю для деяких моделей.", |
| 236 | 236 | "Enable function calling": "Увімкнути виклик функцій", |
| 237 | 237 | "Send inline images": "Надсилати вбудовані зображення", |
| 238 | 238 | "image_inlining_hint_1": "Надсилає зображеннямедіафайли у підказках, якщо модель це підтримує.", |
| 239 | 239 | "Inline Image Quality": "Якість вбудованого зображення", |
| 240 | 240 | "openai_inline_image_quality_auto": "Авто", |
| 241 | 241 | "openai_inline_image_quality_low": "Низький", |
| @@ -326,7 +326,7 @@ | ||
| 326 | 326 | "PygmalionAI/aphrodite-engine": "PygmalionAI/aphrodite-engine (режим OpenAI API)", |
| 327 | 327 | "Aphrodite API key": "Ключ API для Aphrodite", |
| 328 | 328 | "Aphrodite Model": "Модель Афродіта", |
| 329 | 329 | "ggerganovggml-org/llama.cpp": "ggerganovggml-org/llama.cpp (сервер виведення)", |
| 330 | 330 | "Example: http://127.0.0.1:8080": "Приклад: http://127.0.0.1:8080", |
| 331 | 331 | "Example: http://127.0.0.1:11434": "Приклад: http://127.0.0.1:11434", |
| 332 | 332 | "Ollama Model": "Модель Ollama", |
| @@ -603,7 +603,7 @@ | ||
| 603 | 603 | "Model Icon": "Іконка моделі", |
| 604 | 604 | "Show sequential message numbers in the chat log": "Показувати послідовні номери повідомлень у журналі чату", |
| 605 | 605 | "Message IDs": "Ідентифікатори повідомлень", |
| 606 | 606 | "Hide avatars, only in chat messages.": "Приховати аватари в повідомленнях чату.", |
| 607 | 607 | "Hide Chat Avatars": "Приховати аватари чату", |
| 608 | 608 | "Show the number of tokens in each message in the chat log": "Показувати кількість токенів у кожному повідомленні у журналі чату", |
| 609 | 609 | "Show Message Token Count": "Кількість токенів у повідомленні", |
| @@ -737,8 +737,8 @@ | ||
| 737 | 737 | "Filter": "Фільтр", |
| 738 | 738 | "Automatically select a background based on the chat context": "Автоматичний вибір фону на основі контексту чату", |
| 739 | 739 | "Auto-select": "Автовибір", |
| 740 | 740 | "System BackgroundsGlobal": "Системні фониГлобальні", |
| 741 | 741 | "Chat Backgrounds": "Фони чатуЧат", |
| 742 | 742 | "bg_chat_hint_1": "Фонове зображення чату, створене за допомогою", |
| 743 | 743 | "bg_chat_hint_2": "тут з’явиться розширення.", |
| 744 | 744 | "Extensions": "Розширення", |
| @@ -911,7 +911,7 @@ | ||
| 911 | 911 | "Save": "Зберегти", |
| 912 | 912 | "Chat History": "Історія чату", |
| 913 | 913 | "Import Chat": "Імпорт чату", |
| 914 | 914 | "Copy to systemglobal backgrounds": "Скопіювати в системніглобальні фони", |
| 915 | 915 | "Rename background": "Перейменувати фон", |
| 916 | 916 | "Lock": "Замок", |
| 917 | 917 | "Unlock": "Розблокувати", |
| @@ -235,7 +235,7 @@ | ||
| 235 | 235 | "Combines consecutive system messages into one (excluding example dialogues). May improve coherence for some models.": "Kết hợp các tin nhắn hệ thống liên tiếp thành một (loại bỏ các đoạn hội thoại mẫu). Có thể cải thiện tính nhất quán cho một số model.", |
| 236 | 236 | "Enable function calling": "Sử dụng tính năng gọi hàm (function calling)", |
| 237 | 237 | "Send inline images": "Gửi hình ảnh nội bộ", |
| 238 | 238 | "image_inlining_hint_1": "Gửi hìnhtệp ảnhphương tiện theo Prompt nếu kiểu máy hỗ trợ.", |
| 239 | 239 | "Inline Image Quality": "Chất lượng hình ảnh nội tuyến", |
| 240 | 240 | "openai_inline_image_quality_auto": "Tự động", |
| 241 | 241 | "openai_inline_image_quality_low": "Thấp", |
| @@ -326,7 +326,7 @@ | ||
| 326 | 326 | "PygmalionAI/aphrodite-engine": "PygmalionAI/aphrodite-engine (Chế độ đóng gói cho Giao diện lập trình ứng dụng OpenAI)", |
| 327 | 327 | "Aphrodite API key": "Key API Aphrodite", |
| 328 | 328 | "Aphrodite Model": "Moddel cho Aphrodite", |
| 329 | 329 | "ggerganovggml-org/llama.cpp": "ggerganovggml-org/llama.cpp", |
| 330 | 330 | "Example: http://127.0.0.1:8080": "Ví dụ: http://127.0.0.1:8080", |
| 331 | 331 | "Example: http://127.0.0.1:11434": "Ví dụ: http://127.0.0.1:11434", |
| 332 | 332 | "Ollama Model": "Model Ollama", |
| @@ -603,7 +603,7 @@ | ||
| 603 | 603 | "Model Icon": "Biểu tượng Model", |
| 604 | 604 | "Show sequential message numbers in the chat log": "Hiển thị số tin nhắn tuần tự trong nhật ký trò chuyện", |
| 605 | 605 | "Message IDs": "ID Tin nhắn", |
| 606 | 606 | "Hide avatars, only in chat messages.": "Ẩn hình đại diện trong tin nhắn trò chuyện.", |
| 607 | 607 | "Hide Chat Avatars": "Ẩn hình đại diện trò chuyện", |
| 608 | 608 | "Show the number of tokens in each message in the chat log": "Hiển thị số lượng token trong mỗi tin nhắn trong nhật ký trò chuyện", |
| 609 | 609 | "Show Message Token Count": "Hiển thị Số lượng Token trong Tin nhắn", |
| @@ -737,8 +737,8 @@ | ||
| 737 | 737 | "Filter": "Bộ lọc", |
| 738 | 738 | "Automatically select a background based on the chat context": "Tự động chọn một nền dựa trên context trò chuyện", |
| 739 | 739 | "Auto-select": "Tự động chọn", |
| 740 | 740 | "System BackgroundsGlobal": "Nền HệToàn thốngcầu", |
| 741 | 741 | "Chat Backgrounds": "Hình nềnTrò Chatchuyện", |
| 742 | 742 | "bg_chat_hint_1": "Hình nền trò chuyện được tạo bằng", |
| 743 | 743 | "bg_chat_hint_2": "tiện ích sẽ xuất hiện ở đây.", |
| 744 | 744 | "Extensions": "Tiện ích", |
| @@ -911,7 +911,7 @@ | ||
| 911 | 911 | "Save": "Lưu", |
| 912 | 912 | "Chat History": "Lịch sử Trò chuyện", |
| 913 | 913 | "Import Chat": "Nhập trò chuyện", |
| 914 | 914 | "Copy to systemglobal backgrounds": "Sao chép vào nền hệtoàn thốngcầu", |
| 915 | 915 | "Rename background": "Đổi tên nền background", |
| 916 | 916 | "Lock": "Khóa", |
| 917 | 917 | "Unlock": "Mở khóa", |
| @@ -269,13 +269,12 @@ | ||
| 269 | 269 | "enable_functions_desc_3": "可以被各种扩展利用来提供附加功能。", |
| 270 | 270 | "enable_functions_desc_4": "当提示词后处理没有选择工具时不支持。", |
| 271 | 271 | "Send inline images": "发送图片", |
| 272 | 272 | "image_inlining_hint_1": "如果模型支持,就可以在提示词中发送图片就可以在提示词中发送媒体文件。", |
| 273 | 273 | "Inline Image Quality": "图片画质", |
| 274 | 274 | "openai_inline_image_quality_auto": "自动", |
| 275 | 275 | "openai_inline_image_quality_low": "低", |
| 276 | 276 | "openai_inline_image_quality_high": "高", |
| 277 | 277 | "Send inline videos": "发送视频", |
| 278 | - "video_inlining_hint_1": "当模型支持时,将视频发送给模型。", | |
| 279 | 278 | "video_inlining_hint_4": "视频必须在 20MB 以下且时长不超过1分钟。", |
| 280 | 279 | "Request inline images": "请求图片返回", |
| 281 | 280 | "Allows the model to return image attachments.": "允许模型返回图片附件。", |
| @@ -397,7 +396,7 @@ | ||
| 397 | 396 | "PygmalionAI/aphrodite-engine": "PygmalionAI/aphrodite-engine(用于OpenAI API的包装器)", |
| 398 | 397 | "Aphrodite API key": "Aphrodite API 密钥", |
| 399 | 398 | "Aphrodite Model": "Aphrodite 模型", |
| 400 | 399 | "ggerganovggml-org/llama.cpp": "ggerganovggml-org/llama.cpp", |
| 401 | 400 | "Example: http://127.0.0.1:8080": "示例:http://127.0.0.1:8080", |
| 402 | 401 | "Example: http://127.0.0.1:11434": "示例:http://127.0.0.1:11434", |
| 403 | 402 | "Ollama Model": "Ollama 模型", |
| @@ -787,7 +786,7 @@ | ||
| 787 | 786 | "Model Icon": "模型图标", |
| 788 | 787 | "Show sequential message numbers in the chat log": "在聊天记录中显示消息楼层", |
| 789 | 788 | "Message IDs": "显示消息楼层", |
| 790 | 789 | "Hide avatars, only in chat messages.": "在聊天记录中隐藏头像。", |
| 791 | 790 | "Hide Chat Avatars": "隐藏头像", |
| 792 | 791 | "Show the number of tokens in each message in the chat log": "在聊天记录中显示每条消息的词符数", |
| 793 | 792 | "Show Message Token Count": "显示消息词符数", |
| @@ -962,8 +961,7 @@ | ||
| 962 | 961 | "Automatically select a background based on the chat context": "根据聊天上下文自动选择背景", |
| 963 | 962 | "Auto-select": "自动选择", |
| 964 | 963 | "Add Background": "添加背景", |
| 965 | 964 | "System BackgroundsGlobal": "系统背景全局", |
| 966 | - "Chat Backgrounds": "聊天背景", | |
| 967 | 965 | "bg_chat_hint_1": "使用生成的聊天背景", |
| 968 | 966 | "bg_chat_hint_2": "扩展名将出现在这里。", |
| 969 | 967 | "Extensions": "扩展", |
| @@ -1158,7 +1156,7 @@ | ||
| 1158 | 1156 | "Save": "保存", |
| 1159 | 1157 | "Chat History": "聊天记录", |
| 1160 | 1158 | "Import Chat": "导入聊天", |
| 1161 | 1159 | "Copy to systemglobal backgrounds": "复制到系统背景复制到全局背景", |
| 1162 | 1160 | "Lock": "锁定", |
| 1163 | 1161 | "Unlock": "解锁", |
| 1164 | 1162 | "Rename Background": "重命名背景", |
| @@ -236,7 +236,7 @@ | ||
| 236 | 236 | "Combines consecutive system messages into one (excluding example dialogues). May improve coherence for some models.": "將連續的系統訊息合併為一個(不包括對話範例)。可能會提高某些模型的一致性。", |
| 237 | 237 | "Enable function calling": "啟用函式呼叫", |
| 238 | 238 | "Send inline images": "傳送內嵌圖片", |
| 239 | 239 | "image_inlining_hint_1": "如果模型支援,則在提示詞中傳送圖片則在提示詞中傳送媒體檔案。", |
| 240 | 240 | "Inline Image Quality": "內嵌圖片品質", |
| 241 | 241 | "openai_inline_image_quality_auto": "自動", |
| 242 | 242 | "openai_inline_image_quality_low": "低", |
| @@ -325,7 +325,7 @@ | ||
| 325 | 325 | "PygmalionAI/aphrodite-engine": "PygmalionAI/aphrodite 引擎", |
| 326 | 326 | "Aphrodite API key": "Aphrodite API 金鑰", |
| 327 | 327 | "Aphrodite Model": "Aphrodite 模型", |
| 328 | 328 | "ggerganovggml-org/llama.cpp": "ggerganovggml-org/llama.cpp", |
| 329 | 329 | "Example: http://127.0.0.1:8080": "範例:http://127.0.0.1:8080", |
| 330 | 330 | "Example: http://127.0.0.1:11434": "範例:http://127.0.0.1:11434", |
| 331 | 331 | "Ollama Model": "Ollama 模型", |
| @@ -602,7 +602,7 @@ | ||
| 602 | 602 | "Model Icon": "顯示模型圖示", |
| 603 | 603 | "Show sequential message numbers in the chat log": "在聊天記錄中顯示連續的訊息編號。", |
| 604 | 604 | "Message IDs": "顯示訊息 ID", |
| 605 | 605 | "Hide avatars, only in chat messages.": "在聊天訊息中隱藏頭像", |
| 606 | 606 | "Hide Chat Avatars": "隱藏聊天頭像", |
| 607 | 607 | "Show the number of tokens in each message in the chat log": "在聊天記錄中顯示每條訊息中的符元數量", |
| 608 | 608 | "Show Message Token Count": "顯示訊息符元數", |
| @@ -736,8 +736,7 @@ | ||
| 736 | 736 | "Filter": "篩選", |
| 737 | 737 | "Automatically select a background based on the chat context": "根據聊天上下文自動選擇背景", |
| 738 | 738 | "Auto-select": "自動選擇", |
| 739 | 739 | "System BackgroundsGlobal": "系統背景圖片全域", |
| 740 | - "Chat Backgrounds": "聊天背景圖片", | |
| 741 | 740 | "bg_chat_hint_1": "使用擴充功能", |
| 742 | 741 | "bg_chat_hint_2": "所產生的背景圖片將在此顯示。", |
| 743 | 742 | "Extensions": "擴充功能", |
| @@ -909,7 +908,7 @@ | ||
| 909 | 908 | "(Examples of chat dialog. Begin each example with START on a new line.)": "(聊天對話範例。每個範例以新的行並以「START」開始。)", |
| 910 | 909 | "Chat History": "聊天記錄", |
| 911 | 910 | "Import Chat": "匯入聊天", |
| 912 | 911 | "Copy to systemglobal backgrounds": "複製到系統背景圖片複製到全局背景圖片", |
| 913 | 912 | "Rename background": "重新命名背景圖片", |
| 914 | 913 | "Lock": "上鎖", |
| 915 | 914 | "Unlock": "解鎖", |
| @@ -2648,7 +2647,6 @@ | ||
| 2648 | 2647 | "Min Keep": "最小保留", |
| 2649 | 2648 | "enable_functions_desc_4": "當使用「無工具」提示後處理時不支援!", |
| 2650 | 2649 | "Send inline videos": "傳送內嵌影片", |
| 2651 | - "video_inlining_hint_1": "若模型支援,則在提示中傳送影片。", | |
| 2652 | 2650 | "video_inlining_hint_4": "影片大小必須小於 20 MB 且長度低於 1 分鐘", |
| 2653 | 2651 | "This setting affects visibility only.": "此設定僅影響可見性。", |
| 2654 | 2652 | "Allocates a portion of the response length for thinking (min: 1024 tokens, low: 10%, medium: 25%, high: 50%, max: 95%), but minimum 1024 tokens. Auto does not request thinking.": "為推理部分分配回應長度(最大回應長度至少需 1024 符元,低:10%、中:25%、高:50%、最高:95%)。選擇「自動」將不請求推理。", |
| @@ -65,12 +65,11 @@ import { | ||
| 65 | 65 | renameGroupMember, |
| 66 | 66 | createNewGroupChat, |
| 67 | 67 | getGroupAvatar, |
| 68 | - editGroup, | |
| 69 | 68 | deleteGroupChat, |
| 70 | 69 | renameGroupChat, |
| 71 | 70 | importGroupChat, |
| 72 | 71 | getGroupBlock, |
| 73 | 72 | getGroupCharacterCardsgetGroupCharacterCardsLazy, |
| 74 | 73 | getGroupDepthPrompts, |
| 75 | 74 | } from './scripts/group-chats.js'; |
| 76 | 75 | |
| @@ -182,11 +181,13 @@ import { | ||
| 182 | 181 | canUseNegativeLookbehind, |
| 183 | 182 | trimSpaces, |
| 184 | 183 | clamp, |
| 184 | + shakeElement, | |
| 185 | + createTimeout, | |
| 185 | 186 | } from './scripts/utils.js'; |
| 186 | 187 | import { 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'; |
| 187 | 188 | |
| 188 | 189 | import { cancelDebouncedMetadataSave, doDailyExtensionUpdatesCheck, extension_settings, initExtensions, loadExtensionSettings, runGenerationInterceptors } from './scripts/extensions.js'; |
| 189 | 190 | import { COMMENT_NAME_DEFAULT, CONNECT_API_MAP, executeSlashCommandsOnChatInput, initDefaultSlashCommands, initSlashCommandAutoComplete, isExecutingCommandsFromChatInput, pauseScriptExecution, stopScriptExecution, UNIQUE_APIS } from './scripts/slash-commands.js'; |
| 190 | 191 | import { |
| 191 | 192 | tag_map, |
| 192 | 193 | tags, |
| @@ -266,7 +267,7 @@ import { initServerHistory } from './scripts/server-history.js'; | ||
| 266 | 267 | import { initSettingsSearch } from './scripts/setting-search.js'; |
| 267 | 268 | import { initBulkEdit } from './scripts/bulk-edit.js'; |
| 268 | 269 | import { getContext } from './scripts/st-context.js'; |
| 269 | 270 | import { extractReasoningFromData, extractReasoningSignatureFromData, initReasoning, parseReasoningInSwipes, PromptReasoning, ReasoningHandler, removeReasoningFromString, updateReasoningUI } from './scripts/reasoning.js'; |
| 270 | 271 | import { accountStorage } from './scripts/util/AccountStorage.js'; |
| 271 | 272 | import { initWelcomeScreen, openPermanentAssistantChat, openPermanentAssistantCard, getPermanentAssistantAvatar } from './scripts/welcome-screen.js'; |
| 272 | 273 | import { initDataMaid } from './scripts/data-maid.js'; |
| @@ -278,6 +279,9 @@ import { applyStreamFadeIn } from './scripts/util/stream-fadein.js'; | ||
| 278 | 279 | import { initDomHandlers } from './scripts/dom-handlers.js'; |
| 279 | 280 | import { SimpleMutex } from './scripts/util/SimpleMutex.js'; |
| 280 | 281 | import { 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'; | |
| 281 | 285 | |
| 282 | 286 | // API OBJECT FOR EXTERNAL WIRING |
| 283 | 287 | globalThis.SillyTavern = { |
| @@ -373,18 +377,21 @@ export let name1 = default_user_name; | ||
| 373 | 377 | export let name2 = systemUserName; |
| 374 | 378 | /** @type {ChatMessage[]} */ |
| 375 | 379 | export 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; | |
| 377 | 385 | let chatSaveTimeout; |
| 378 | 386 | let importFlashTimeout; |
| 379 | 387 | export let isChatSaving = false; |
| 380 | -let chat_create_date = ''; | |
| 381 | 388 | let firstRun = false; |
| 382 | 389 | let settingsReady = false; |
| 383 | 390 | let currentVersion = '0.0.0'; |
| 384 | 391 | export let displayVersion = 'SillyTavern'; |
| 385 | 392 | |
| 386 | 393 | let generation_started = new Date(); |
| 387 | 394 | /** @type {import('./scripts/char-data.js').v1CharDataCharacter[]} */ |
| 388 | 395 | export let characters = []; |
| 389 | 396 | /** |
| 390 | 397 | * Stringified index of a currently chosen entity in the characters array. |
| @@ -411,6 +418,7 @@ export const chatElement = $('#chat'); | ||
| 411 | 418 | |
| 412 | 419 | let dialogueResolve = null; |
| 413 | 420 | let dialogueCloseStop = false; |
| 421 | +/** @type {ChatMetadata} */ | |
| 414 | 422 | export let chat_metadata = {}; |
| 415 | 423 | /** @type {StreamingProcessor} */ |
| 416 | 424 | export let streamingProcessor = null; |
| @@ -562,6 +570,7 @@ let chat_file_for_del = ''; | ||
| 562 | 570 | export let online_status = 'no_connection'; |
| 563 | 571 | |
| 564 | 572 | export let is_send_press = false; //Send generation |
| 573 | +export const isGenerating = () => (is_send_press || is_group_generating); | |
| 565 | 574 | |
| 566 | 575 | let this_del_mes = -1; |
| 567 | 576 | |
| @@ -575,7 +584,14 @@ export let settings; | ||
| 575 | 584 | export let amount_gen = 80; //default max length of AI generated responses |
| 576 | 585 | export let max_context = 2048; |
| 577 | 586 | |
| 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 | + | |
| 579 | 595 | export let extension_prompts = {}; |
| 580 | 596 | |
| 581 | 597 | export let main_api;// = "kobold"; |
| @@ -629,7 +645,7 @@ export async function pingServer() { | ||
| 629 | 645 | try { |
| 630 | 646 | const result = await fetch('api/ping', { |
| 631 | 647 | method: 'POST', |
| 632 | 648 | headers: getRequestHeaders({ omitContentType: true }), |
| 633 | 649 | }); |
| 634 | 650 | |
| 635 | 651 | if (!result.ok) { |
| @@ -693,6 +709,7 @@ async function firstLoadInit() { | ||
| 693 | 709 | initBackgrounds(); |
| 694 | 710 | initAuthorsNote(); |
| 695 | 711 | await initPersonas(); |
| 712 | + await initSlashCommandAutoComplete(); | |
| 696 | 713 | initWorldInfo(); |
| 697 | 714 | initHorde(); |
| 698 | 715 | initRossMods(); |
| @@ -730,7 +747,7 @@ function initStandaloneMode() { | ||
| 730 | 747 | } |
| 731 | 748 | } |
| 732 | 749 | |
| 733 | 750 | export function cancelStatusCheck(reason = 'Manually cancelled status check') { |
| 734 | 751 | abortStatusCheck?.abort(new AbortReason(reason)); |
| 735 | 752 | abortStatusCheck = new AbortController(); |
| 736 | 753 | setOnlineStatus('no_connection'); |
| @@ -1029,9 +1046,6 @@ function verifyCharactersSearchSortRule() { | ||
| 1029 | 1046 | } |
| 1030 | 1047 | } |
| 1031 | 1048 | |
| 1032 | -/** @typedef {object} Character - A character */ | |
| 1033 | -/** @typedef {object} Group - A group */ | |
| 1034 | - | |
| 1035 | 1049 | /** |
| 1036 | 1050 | * @typedef {object} Entity - Object representing a display entity |
| 1037 | 1051 | * @property {Character|Group|import('./scripts/tags.js').Tag|*} item - The item |
| @@ -1295,7 +1309,7 @@ export async function deleteCharacterChatByName(characterId, fileName) { | ||
| 1295 | 1309 | // Make sure all the data is loaded. |
| 1296 | 1310 | await unshallowCharacter(characterId); |
| 1297 | 1311 | |
| 1298 | 1312 | /** @type {import('./scripts/char-data.js').v1CharDataCharacter} */ |
| 1299 | 1313 | const character = characters[characterId]; |
| 1300 | 1314 | if (!character) { |
| 1301 | 1315 | console.warn(`Character with ID ${characterId} not found.`); |
| @@ -1380,10 +1394,11 @@ export async function showMoreMessages(messagesToLoad = null) { | ||
| 1380 | 1394 | |
| 1381 | 1395 | while (messageId > 0 && count > 0) { |
| 1382 | 1396 | let newMessageId = messageId - 1; |
| 1383 | 1397 | addOneMessage(chat[newMessageId], { insertBefore: messageId >= chat.length ? null : messageId, scroll: false, forceId: newMessageId, showSwipes: false }); |
| 1384 | 1398 | count--; |
| 1385 | 1399 | messageId--; |
| 1386 | 1400 | } |
| 1401 | + refreshSwipeButtons(); | |
| 1387 | 1402 | |
| 1388 | 1403 | if (messageId == 0) { |
| 1389 | 1404 | $('#show_more_messages').remove(); |
| @@ -1414,13 +1429,13 @@ export async function printMessages() { | ||
| 1414 | 1429 | |
| 1415 | 1430 | chatElement.find('.mes').removeClass('last_mes'); |
| 1416 | 1431 | chatElement.find('.mes').last().addClass('last_mes'); |
| 1417 | 1432 | refreshSwipeButtons(false, false); |
| 1418 | 1433 | applyStylePins(); |
| 1419 | 1434 | scrollChatToBottom({ waitForFrame: true }); |
| 1420 | 1435 | delay(debounce_timeout.short).then(() => scrollOnMediaLoad()); |
| 1421 | 1436 | } |
| 1422 | 1437 | |
| 1423 | 1438 | export function scrollOnMediaLoad() { |
| 1424 | 1439 | const started = Date.now(); |
| 1425 | 1440 | const media = chatElement.find('.mes_block img, .mes_block video, .mes_block audio').toArray(); |
| 1426 | 1441 | let mediaLoaded = 0; |
| @@ -1475,6 +1490,7 @@ export async function clearChat() { | ||
| 1475 | 1490 | if (is_delete_mode) { |
| 1476 | 1491 | $('#dialogue_del_mes_cancel').trigger('click'); |
| 1477 | 1492 | } |
| 1493 | + //This will also remove non '.mes' elements, e.g. '<div id="show_more_messages">Show more messages</div>'. | |
| 1478 | 1494 | chatElement.children().remove(); |
| 1479 | 1495 | if ($('.zoomed_avatar[forChar]').length) { |
| 1480 | 1496 | console.debug('saw avatars to remove'); |
| @@ -1579,9 +1595,17 @@ export async function reloadCurrentChat() { | ||
| 1579 | 1595 | * Send the message currently typed into the chat box. |
| 1580 | 1596 | */ |
| 1581 | 1597 | export async function sendTextareaMessage() { |
| 1598 | + // don't proceed during swipeGenerate() | |
| 1599 | + if (swipeState == SWIPE_STATE.EDITING) { | |
| 1600 | + toastr.warning(t`Confirm the edit to start a generation.`, t`You cannot send a message during a swipe-edit.`); | |
| 1601 | + return; | |
| 1602 | + } | |
| 1603 | + if (swipeState !== SWIPE_STATE.NONE) return; // don't proceed if mid-swipe. | |
| 1582 | 1604 | if (is_send_press) return; |
| 1583 | 1605 | if (isExecutingCommandsFromChatInput) return; |
| 1584 | 1606 | |
| 1607 | + hideSwipeButtons(); //Swipe buttons must be hidden now, otherwise concurrent generations are possible. | |
| 1608 | + | |
| 1585 | 1609 | let generateType = 'normal'; |
| 1586 | 1610 | // "Continue on send" is activated when the user hits "send" (or presses enter) on an empty chat box, and the last |
| 1587 | 1611 | // message was sent from a character (not the user or the system). |
| @@ -1601,7 +1625,9 @@ export async function sendTextareaMessage() { | ||
| 1601 | 1625 | await newAssistantChat({ temporary: false }); |
| 1602 | 1626 | } |
| 1603 | 1627 | |
| 1604 | 1628 | returnlet generation = await Generate(generateType); |
| 1629 | + showSwipeButtons(); | |
| 1630 | + return generation; | |
| 1605 | 1631 | } |
| 1606 | 1632 | |
| 1607 | 1633 | /** |
| @@ -2366,7 +2392,7 @@ export function addCopyToCodeBlocks(messageElement) { | ||
| 2366 | 2392 | * @param {boolean} [options.scroll=true] Whether to scroll to the new message |
| 2367 | 2393 | * @param {number} [options.insertBefore=null] Message ID to insert the new message before |
| 2368 | 2394 | * @param {number} [options.forceId=null] Force the message ID |
| 2369 | 2395 | * @param {boolean} [options.showSwipes=true] Whether to showrefresh the swipe buttons. |
| 2370 | 2396 | * @returns {void} |
| 2371 | 2397 | */ |
| 2372 | 2398 | export function addOneMessage(mes, { type = 'normal', insertAfter = null, scroll = true, insertBefore = null, forceId = null, showSwipes = true } = {}) { |
| @@ -2490,8 +2516,7 @@ export function addOneMessage(mes, { type = 'normal', insertAfter = null, scroll | ||
| 2490 | 2516 | }); |
| 2491 | 2517 | |
| 2492 | 2518 | if (type === 'swipe') { |
| 2493 | - const messageId = forceId ?? chat.length - 1; | |
| 2519 | + const swipeMessage = chatElement.find(`[mesid="${newMessageId}"]`); | |
| 2494 | - const swipeMessage = chatElement.find(`[mesid="${messageId}"]`); | |
| 2495 | 2520 | swipeMessage.attr('swipeid', params.swipeId); |
| 2496 | 2521 | swipeMessage.find('.mes_text').html(messageText).attr('title', title); |
| 2497 | 2522 | swipeMessage.find('.timestamp').text(timestamp).attr('title', `${params.extra.api} - ${params.extra.model}`); |
| @@ -2509,30 +2534,27 @@ export function addOneMessage(mes, { type = 'normal', insertAfter = null, scroll | ||
| 2509 | 2534 | swipeMessage.find('.tokenCounterDisplay').empty(); |
| 2510 | 2535 | } |
| 2511 | 2536 | } else { |
| 2512 | - const messageId = forceId ?? chat.length - 1; | |
| 2537 | + chatElement.find(`[mesid="${newMessageId}"] .mes_text`).append(messageText); | |
| 2513 | - chatElement.find(`[mesid="${messageId}"] .mes_text`).append(messageText); | |
| 2514 | 2538 | appendMediaToMessage(mes, newMessage, scroll ? SCROLL_BEHAVIOR.ADJUST : SCROLL_BEHAVIOR.NONE); |
| 2515 | - showSwipes && hideSwipeButtons(); | |
| 2516 | 2539 | } |
| 2517 | 2540 | |
| 2518 | 2541 | addCopyToCodeBlocks(newMessage); |
| 2519 | 2542 | |
| 2520 | 2543 | // 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)); | |
| 2525 | 2546 | } |
| 2526 | 2547 | |
| 2527 | - if (showSwipes) { | |
| 2548 | + //last_mes should always be updated. | |
| 2549 | + chatElement.find('.mes').removeClass('last_mes'); | |
| 2528 | 2550 | chatElement.find('.mes').last().addClass('last_mes'); |
| 2529 | - chatElement.find('.mes').eq(-2).removeClass('last_mes'); | |
| 2551 | + if (showSwipes) { | |
| 2530 | 2552 | refreshSwipeButtons(); |
| 2531 | 2553 | } |
| 2532 | 2554 | |
| 2533 | 2555 | // Don't scroll if not inserting last |
| 2534 | 2556 | if (!insertAfter && !insertBefore && scroll) { |
| 2535 | 2557 | scrollChatToBottom({ waitForFrame: true }); |
| 2536 | 2558 | } |
| 2537 | 2559 | |
| 2538 | 2560 | applyCharacterTagsToMessageDivs({ mesIds: newMessageId }); |
| @@ -2642,14 +2664,13 @@ export function scrollChatToBottom({ waitForFrame } = {}) { | ||
| 2642 | 2664 | } |
| 2643 | 2665 | |
| 2644 | 2666 | /** |
| 2667 | + * @deprecated Function is not needed anymore, as the new signature of substituteParams is more flexible. | |
| 2668 | + * | |
| 2645 | 2669 | * Substitutes {{macro}} parameters in a string. |
| 2646 | - * @param {string} content - The string to substitute parameters in. | |
| 2647 | - * @param {Record<string,any>} additionalMacro - Additional environment variables for substitution. | |
| 2648 | - * @param {(x: string) => string} [postProcessFn] - Post-processing function for each substituted macro. | |
| 2649 | 2670 | * @returns {string} The string with substituted parameters. |
| 2650 | 2671 | */ |
| 2651 | 2672 | export function substituteParamsExtended(content, additionalMacro = {}, postProcessFn = (x) => x) { |
| 2652 | 2673 | return substituteParams(content, undefined, undefined, undefined, undefined,{ true,dynamicMacros: additionalMacro, postProcessFn }); |
| 2653 | 2674 | } |
| 2654 | 2675 | |
| 2655 | 2676 | /** |
| @@ -2664,11 +2685,24 @@ export function substituteParamsExtended(content, additionalMacro = {}, postProc | ||
| 2664 | 2685 | * @param {(x: string) => string} [postProcessFn] - Post-processing function for each substituted macro. |
| 2665 | 2686 | * @returns {string} The string with substituted parameters. |
| 2666 | 2687 | */ |
| 2667 | 2688 | export function substituteParamssubstituteParamsLegacy(content, _name1, _name2, _original, _group, _replaceCharacterCard = true, additionalMacro = {}, postProcessFn = (x) => x) { |
| 2668 | 2689 | if (!content) { |
| 2669 | 2690 | return ''; |
| 2670 | 2691 | } |
| 2671 | 2692 | |
| 2693 | + // If experimental macro engine is enabled, use it. This code will be cleaned up in the future. | |
| 2694 | + if (power_user?.experimental_macro_engine) { | |
| 2695 | + return substituteParams(content, { | |
| 2696 | + name1Override: _name1, | |
| 2697 | + name2Override: _name2, | |
| 2698 | + original: _original, | |
| 2699 | + groupOverride: _group, | |
| 2700 | + replaceCharacterCard: _replaceCharacterCard ?? true, | |
| 2701 | + dynamicMacros: additionalMacro ?? {}, | |
| 2702 | + postProcessFn: postProcessFn ?? ((x) => x), | |
| 2703 | + }); | |
| 2704 | + } | |
| 2705 | + | |
| 2672 | 2706 | const environment = {}; |
| 2673 | 2707 | |
| 2674 | 2708 | if (typeof _original === 'string') { |
| @@ -2768,6 +2802,55 @@ export function substituteParams(content, _name1, _name2, _original, _group, _re | ||
| 2768 | 2802 | return evaluateMacros(content, environment, postProcessFn); |
| 2769 | 2803 | } |
| 2770 | 2804 | |
| 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 | + | |
| 2771 | 2854 | |
| 2772 | 2855 | /** |
| 2773 | 2856 | * Gets stopping sequences for the prompt. |
| @@ -3076,10 +3159,19 @@ export async function getExtensionPrompt(position = extension_prompt_types.IN_PR | ||
| 3076 | 3159 | return values; |
| 3077 | 3160 | } |
| 3078 | 3161 | |
| 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 }); | |
| 3083 | 3175 | |
| 3084 | 3176 | if (power_user.collapse_newlines) { |
| 3085 | 3177 | value = collapseNewlines(value); |
| @@ -3091,11 +3183,7 @@ export function baseChatReplace(value, name1, name2) { | ||
| 3091 | 3183 | } |
| 3092 | 3184 | |
| 3093 | 3185 | /** |
| 3094 | - * Returns the character card fields for the current character. | |
| 3186 | + * @typedef {Object} CharacterCardFields | |
| 3095 | - * @param {object} [options] | |
| 3096 | - * @param {number} [options.chid] Optional character index | |
| 3097 | - * | |
| 3098 | - * @typedef {object} CharacterCardFields | |
| 3099 | 3187 | * @property {string} system System prompt |
| 3100 | 3188 | * @property {string} mesExamples Message examples |
| 3101 | 3189 | * @property {string} description Description |
| @@ -3106,57 +3194,119 @@ export function baseChatReplace(value, name1, name2) { | ||
| 3106 | 3194 | * @property {string} version Character version |
| 3107 | 3195 | * @property {string} charDepthPrompt Character depth note |
| 3108 | 3196 | * @property {string} creatorNotes Character creator notes |
| 3109 | - * @returns {CharacterCardFields} Character card fields | |
| 3110 | 3197 | */ |
| 3111 | -export function getCharacterCardFields({ chid = null } = {}) { | |
| 3112 | - const currentChid = chid ?? this_chid; | |
| 3113 | 3198 | |
| 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 | +} | |
| 3127 | 3223 | |
| 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; | |
| 3128 | 3233 | const character = characters[currentChid]; |
| 3129 | 3234 | |
| 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; | |
| 3133 | 3238 | |
| 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 ''; | |
| 3134 | 3274 | const scenarioText = chat_metadata['scenario'] || character.scenario || ''; |
| 3275 | + return baseChatReplace(scenarioText.trim()); | |
| 3276 | + }, | |
| 3277 | + mesExamples: () => { | |
| 3278 | + if (groupCardsLazy) return groupCardsLazy.mesExamples; | |
| 3279 | + if (!character) return ''; | |
| 3135 | 3280 | 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)); | |
| 3150 | 3284 | |
| 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 | - } | |
| 3157 | 3286 | } |
| 3158 | 3287 | |
| 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 | + }; | |
| 3160 | 3310 | } |
| 3161 | 3311 | |
| 3162 | 3312 | /** |
| @@ -3247,6 +3397,8 @@ class StreamingProcessor { | ||
| 3247 | 3397 | this.promptReasoning = promptReasoning; |
| 3248 | 3398 | /** @type {string[]} */ |
| 3249 | 3399 | this.images = []; |
| 3400 | + /** @type {string?} */ | |
| 3401 | + this.reasoningSignature = null; | |
| 3250 | 3402 | } |
| 3251 | 3403 | |
| 3252 | 3404 | /** |
| @@ -3279,7 +3431,7 @@ class StreamingProcessor { | ||
| 3279 | 3431 | } |
| 3280 | 3432 | |
| 3281 | 3433 | markUIGenStopped() { |
| 3282 | 3434 | activateSendButtonsunblockGeneration(); |
| 3283 | 3435 | } |
| 3284 | 3436 | |
| 3285 | 3437 | async onStartStreaming(text) { |
| @@ -3408,14 +3560,14 @@ class StreamingProcessor { | ||
| 3408 | 3560 | } |
| 3409 | 3561 | |
| 3410 | 3562 | async onFinishStreaming(messageId, text) { |
| 3411 | - this.markUIGenStopped(); | |
| 3412 | 3563 | await this.onProgressStreaming(messageId, text, true); |
| 3413 | 3564 | addCopyToCodeBlocks(const messageElement = chatElement.find(`.mes[mesid="${messageId}"]`)); |
| 3565 | + const message = chat[messageId]; | |
| 3566 | + addCopyToCodeBlocks(messageElement); | |
| 3414 | 3567 | |
| 3415 | 3568 | await this.reasoningHandler.finish(messageId); |
| 3416 | 3569 | |
| 3417 | 3570 | if (Array.isArray(this.swipes) && this.swipes.length > 0) { |
| 3418 | - const message = chat[messageId]; | |
| 3419 | 3571 | const swipeInfoExtra = structuredClone(message.extra ?? {}); |
| 3420 | 3572 | delete swipeInfoExtra.token_count; |
| 3421 | 3573 | delete swipeInfoExtra.reasoning; |
| @@ -3428,15 +3580,26 @@ class StreamingProcessor { | ||
| 3428 | 3580 | }; |
| 3429 | 3581 | const swipeInfoArray = Array(this.swipes.length).fill().map(() => structuredClone(swipeInfo)); |
| 3430 | 3582 | parseReasoningInSwipes(this.swipes, swipeInfoArray, message.extra?.reasoning_duration); |
| 3431 | 3583 | chat[messageId]message.swipes.push(...this.swipes); |
| 3432 | 3584 | chat[messageId]message.swipe_info.push(...swipeInfoArray); |
| 3433 | 3585 | } |
| 3434 | 3586 | |
| 3587 | + syncMesToSwipe(messageId); | |
| 3588 | + saveLogprobsForActiveMessage(this.messageLogprobs.filter(Boolean), this.continueMessage); | |
| 3589 | + | |
| 3435 | 3590 | if (Array.isArray(this.images) && this.images.length > 0) { |
| 3436 | 3591 | await processImageAttachment(chat[messageId]message, { imageUrls: this.images }); |
| 3437 | 3592 | 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; | |
| 3438 | 3599 | } |
| 3439 | 3600 | |
| 3601 | + this.markUIGenStopped(); | |
| 3602 | + | |
| 3440 | 3603 | if (this.type !== 'impersonate') { |
| 3441 | 3604 | await eventSource.emit(event_types.MESSAGE_RECEIVED, this.messageId, this.type); |
| 3442 | 3605 | await eventSource.emit(event_types.CHARACTER_MESSAGE_RENDERED, this.messageId, this.type); |
| @@ -3444,15 +3607,13 @@ class StreamingProcessor { | ||
| 3444 | 3607 | await eventSource.emit(event_types.IMPERSONATE_READY, text); |
| 3445 | 3608 | } |
| 3446 | 3609 | |
| 3447 | - syncMesToSwipe(messageId); | |
| 3610 | + updateSwipeCounter(messageId, { message, messageElement }); | |
| 3448 | - saveLogprobsForActiveMessage(this.messageLogprobs.filter(Boolean), this.continueMessage); | |
| 3449 | - await saveChatConditional(); | |
| 3450 | - unblockGeneration(); | |
| 3451 | 3611 | |
| 3452 | 3612 | const isAborted = this.abortController.signal.aborted; |
| 3453 | 3613 | if (!isAborted && power_user.auto_swipe && generatedTextFiltered(text)) { |
| 3454 | - return swipe_right(); | |
| 3614 | + return await swipe(null, SWIPE_DIRECTION.RIGHT, { source: SWIPE_SOURCE.AUTO_SWIPE, repeated: true, forceMesId: chat.length - 1 }); | |
| 3455 | 3615 | } |
| 3616 | + await saveChatConditional(); | |
| 3456 | 3617 | |
| 3457 | 3618 | playMessageSound(); |
| 3458 | 3619 | } |
| @@ -3462,7 +3623,6 @@ class StreamingProcessor { | ||
| 3462 | 3623 | this.isStopped = true; |
| 3463 | 3624 | |
| 3464 | 3625 | this.markUIGenStopped(); |
| 3465 | - unblockGeneration(); | |
| 3466 | 3626 | |
| 3467 | 3627 | const noEmitTypes = ['swipe', 'impersonate', 'continue']; |
| 3468 | 3628 | if (!noEmitTypes.includes(this.type)) { |
| @@ -3532,6 +3692,7 @@ class StreamingProcessor { | ||
| 3532 | 3692 | // Get the updated reasoning string into the handler |
| 3533 | 3693 | this.reasoningHandler.updateReasoning(this.messageId, state?.reasoning); |
| 3534 | 3694 | this.images = state?.images ?? []; |
| 3695 | + this.reasoningSignature = state?.signature ?? null; | |
| 3535 | 3696 | await eventSource.emit(event_types.STREAM_TOKEN_RECEIVED, text); |
| 3536 | 3697 | await sw.tick(async () => await this.onProgressStreaming(this.messageId, this.continueMessage + text)); |
| 3537 | 3698 | } |
| @@ -4252,7 +4413,7 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro | ||
| 4252 | 4413 | continue; |
| 4253 | 4414 | } |
| 4254 | 4415 | |
| 4255 | 4416 | const formattedExample = baseChatReplace(exampleMessage, name1, name2); |
| 4256 | 4417 | const cleanedExample = parseMesExamples(formattedExample, isInstruct); |
| 4257 | 4418 | |
| 4258 | 4419 | // Insert depending on before or after position |
| @@ -4296,9 +4457,9 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro | ||
| 4296 | 4457 | if (main_api !== 'openai') { |
| 4297 | 4458 | if (power_user.sysprompt.enabled) { |
| 4298 | 4459 | system = power_user.prefer_character_prompt && system |
| 4299 | 4460 | ? substituteParams(system, name1,{ name2,original: (power_user.sysprompt.content ?? '') }) |
| 4300 | 4461 | : baseChatReplace(power_user.sysprompt.content, name1, name2); |
| 4301 | 4462 | system = isInstruct ? substituteParams(system, name1,{ name2,original: power_user.sysprompt.content ?? '' }) : system; |
| 4302 | 4463 | } else { |
| 4303 | 4464 | // Nullify if it's not enabled |
| 4304 | 4465 | system = ''; |
| @@ -4356,8 +4517,8 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro | ||
| 4356 | 4517 | |
| 4357 | 4518 | if (main_api !== 'openai' && power_user.sysprompt.enabled) { |
| 4358 | 4519 | jailbreak = power_user.prefer_character_jailbreak && jailbreak |
| 4359 | 4520 | ? substituteParams(jailbreak, name1,{ name2,original: (power_user.sysprompt.post_history ?? '') }) |
| 4360 | 4521 | : baseChatReplace(power_user.sysprompt.post_history, name1, name2); |
| 4361 | 4522 | |
| 4362 | 4523 | // Only inject the jb if there is one |
| 4363 | 4524 | if (jailbreak) { |
| @@ -5094,6 +5255,7 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro | ||
| 5094 | 5255 | let title = extractTitleFromData(data); |
| 5095 | 5256 | let reasoning = extractReasoningFromData(data); |
| 5096 | 5257 | let imageUrls = extractImagesFromData(data); |
| 5258 | + const reasoningSignature = extractReasoningSignatureFromData(data); | |
| 5097 | 5259 | kobold_horde_model = title; |
| 5098 | 5260 | |
| 5099 | 5261 | const swipes = extractMultiSwipes(data, type); |
| @@ -5137,10 +5299,10 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro | ||
| 5137 | 5299 | else { |
| 5138 | 5300 | // Without streaming we'll be having a full message on continuation. Treat it as a last chunk. |
| 5139 | 5301 | if (originalType !== 'continue') { |
| 5140 | 5302 | ({ type, getMessage } = await saveReply({ type, getMessage, title, swipes, reasoning, imageUrls, reasoningSignature })); |
| 5141 | 5303 | } |
| 5142 | 5304 | else { |
| 5143 | 5305 | ({ type, getMessage } = await saveReply({ type: 'appendFinal', getMessage, title, swipes, reasoning, imageUrls, reasoningSignature })); |
| 5144 | 5306 | } |
| 5145 | 5307 | |
| 5146 | 5308 | // This relies on `saveReply` having been called to add the message to the chat, so it must be last. |
| @@ -5175,7 +5337,8 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro | ||
| 5175 | 5337 | const isAborted = abortController && abortController.signal.aborted; |
| 5176 | 5338 | if (!isAborted && power_user.auto_swipe && generatedTextFiltered(getMessage)) { |
| 5177 | 5339 | is_send_press = false; |
| 5178 | - return swipe_right(); | |
| 5340 | + return await swipe(null, SWIPE_DIRECTION.RIGHT, { source: SWIPE_SOURCE.AUTO_SWIPE, repeated: true, forceMesId: chat.length - 1 }); | |
| 5341 | + | |
| 5179 | 5342 | } |
| 5180 | 5343 | |
| 5181 | 5344 | console.debug('/api/chats/save called by /Generate'); |
| @@ -5307,7 +5470,6 @@ function unblockGeneration(type) { | ||
| 5307 | 5470 | |
| 5308 | 5471 | is_send_press = false; |
| 5309 | 5472 | activateSendButtons(); |
| 5310 | - showSwipeButtons(); | |
| 5311 | 5473 | setGenerationProgress(0); |
| 5312 | 5474 | flushEphemeralStoppingStrings(); |
| 5313 | 5475 | flushWIInjections(); |
| @@ -5851,7 +6013,7 @@ export function extractMessageFromData(data, activeApi = null) { | ||
| 5851 | 6013 | case 'koboldhorde': |
| 5852 | 6014 | return data.text; |
| 5853 | 6015 | case 'textgenerationwebui': |
| 5854 | 6016 | return data.choices?.[0]?.text ?? data.choices?.[0]?.message?.content ?? data.content ?? data.response ?? data[0]?.content ?? ''; |
| 5855 | 6017 | case 'novel': |
| 5856 | 6018 | return data.output; |
| 5857 | 6019 | case 'openai': |
| @@ -5908,6 +6070,7 @@ export function extractJsonFromData(data, { mainApi = null, chatCompletionSource | ||
| 5908 | 6070 | case chat_completion_sources.COHERE: |
| 5909 | 6071 | case chat_completion_sources.XAI: |
| 5910 | 6072 | case chat_completion_sources.ELECTRONHUB: |
| 6073 | + case chat_completion_sources.CHUTES: | |
| 5911 | 6074 | case chat_completion_sources.AZURE_OPENAI: |
| 5912 | 6075 | case chat_completion_sources.ZAI: |
| 5913 | 6076 | default: |
| @@ -5937,6 +6100,22 @@ function extractMultiSwipes(data, type) { | ||
| 5937 | 6100 | return swipes; |
| 5938 | 6101 | } |
| 5939 | 6102 | |
| 6103 | + if (main_api === 'textgenerationwebui' && textgen_settings.type === textgen_types.LLAMACPP) { | |
| 6104 | + if (!Array.isArray(data)) { | |
| 6105 | + return swipes; | |
| 6106 | + } | |
| 6107 | + | |
| 6108 | + const multiSwipeCount = data.length - 1; | |
| 6109 | + if (multiSwipeCount <= 0) { | |
| 6110 | + return swipes; | |
| 6111 | + } | |
| 6112 | + | |
| 6113 | + for (let i = 1; i < data.length; i++) { | |
| 6114 | + const text = data?.[i]?.content ?? ''; | |
| 6115 | + swipes.push(text); | |
| 6116 | + } | |
| 6117 | + } | |
| 6118 | + | |
| 5940 | 6119 | if (main_api === 'openai' || (main_api === 'textgenerationwebui' && [textgen_types.MANCER, textgen_types.VLLM, textgen_types.APHRODITE, textgen_types.TABBY, textgen_types.INFERMATICAI].includes(textgen_settings.type))) { |
| 5941 | 6120 | if (!Array.isArray(data.choices)) { |
| 5942 | 6121 | return swipes; |
| @@ -5950,18 +6129,18 @@ function extractMultiSwipes(data, type) { | ||
| 5950 | 6129 | |
| 5951 | 6130 | for (let i = 1; i < data.choices.length; i++) { |
| 5952 | 6131 | 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({ | |
| 5954 | 6137 | getMessage: text, |
| 5955 | 6138 | isImpersonate: false, |
| 5956 | 6139 | isContinue: false, |
| 5957 | 6140 | displayIncompleteSentences: false, |
| 5958 | 6141 | })); |
| 5959 | - | |
| 5960 | - swipes.push(cleanedText); | |
| 5961 | - } | |
| 5962 | - } | |
| 5963 | 6142 | |
| 5964 | 6143 | return swipescleanedSwipes; |
| 5965 | 6144 | } |
| 5966 | 6145 | |
| 5967 | 6146 | /** |
| @@ -6172,16 +6351,17 @@ async function processImageAttachment(message, { imageUrls }) { | ||
| 6172 | 6351 | * @property {string[]} [swipes] Extra swipes |
| 6173 | 6352 | * @property {string} [reasoning] Message reasoning |
| 6174 | 6353 | * @property {string[]} [imageUrls] Links to images |
| 6354 | + * @property {string?} [reasoningSignature] Encrypted signature of the reasoning text | |
| 6175 | 6355 | * |
| 6176 | 6356 | * @typedef {object} SaveReplyResult |
| 6177 | 6357 | * @property {string} type Type of generation |
| 6178 | 6358 | * @property {string} getMessage Generated message |
| 6179 | 6359 | */ |
| 6180 | 6360 | export async function saveReply({ type, getMessage, fromStreaming = false, title = '', swipes = [], reasoning = '', imageUrls = [], reasoningSignature = null }) { |
| 6181 | 6361 | // Backward compatibility |
| 6182 | 6362 | if (arguments.length > 1 && typeof arguments[0] !== 'object') { |
| 6183 | 6363 | console.trace('saveReply called with positional arguments. Please use an object instead.'); |
| 6184 | 6364 | [type, getMessage, fromStreaming, title, swipes, reasoning, imageUrls, reasoningSignature] = arguments; |
| 6185 | 6365 | } |
| 6186 | 6366 | |
| 6187 | 6367 | if (type != 'append' && type != 'continue' && type != 'appendFinal' && chat.length && (chat[chat.length - 1]['swipe_id'] === undefined || |
| @@ -6217,6 +6397,7 @@ export async function saveReply({ type, getMessage, fromStreaming = false, title | ||
| 6217 | 6397 | chat[chat.length - 1]['extra']['model'] = getGeneratingModel(); |
| 6218 | 6398 | chat[chat.length - 1]['extra']['reasoning'] = reasoning; |
| 6219 | 6399 | chat[chat.length - 1]['extra']['reasoning_duration'] = null; |
| 6400 | + chat[chat.length - 1]['extra']['reasoning_signature'] = reasoningSignature; | |
| 6220 | 6401 | await processImageAttachment(chat[chat.length - 1], { imageUrls }); |
| 6221 | 6402 | if (power_user.message_token_count_enabled) { |
| 6222 | 6403 | const tokenCountText = (reasoning || '') + chat[chat.length - 1]['mes']; |
| @@ -6241,6 +6422,7 @@ export async function saveReply({ type, getMessage, fromStreaming = false, title | ||
| 6241 | 6422 | chat[chat.length - 1]['extra']['model'] = getGeneratingModel(); |
| 6242 | 6423 | chat[chat.length - 1]['extra']['reasoning'] = reasoning; |
| 6243 | 6424 | chat[chat.length - 1]['extra']['reasoning_duration'] = null; |
| 6425 | + chat[chat.length - 1]['extra']['reasoning_signature'] = reasoningSignature; | |
| 6244 | 6426 | await processImageAttachment(chat[chat.length - 1], { imageUrls }); |
| 6245 | 6427 | if (power_user.message_token_count_enabled) { |
| 6246 | 6428 | const tokenCountText = (reasoning || '') + chat[chat.length - 1]['mes']; |
| @@ -6261,6 +6443,7 @@ export async function saveReply({ type, getMessage, fromStreaming = false, title | ||
| 6261 | 6443 | chat[chat.length - 1]['extra']['api'] = getGeneratingApi(); |
| 6262 | 6444 | chat[chat.length - 1]['extra']['model'] = getGeneratingModel(); |
| 6263 | 6445 | chat[chat.length - 1]['extra']['reasoning'] += reasoning; |
| 6446 | + chat[chat.length - 1]['extra']['reasoning_signature'] = reasoningSignature; | |
| 6264 | 6447 | await processImageAttachment(chat[chat.length - 1], { imageUrls }); |
| 6265 | 6448 | // We don't know if the reasoning duration extended, so we don't update it here on purpose. |
| 6266 | 6449 | if (power_user.message_token_count_enabled) { |
| @@ -6283,6 +6466,7 @@ export async function saveReply({ type, getMessage, fromStreaming = false, title | ||
| 6283 | 6466 | chat[chat.length - 1]['extra']['model'] = getGeneratingModel(); |
| 6284 | 6467 | chat[chat.length - 1]['extra']['reasoning'] = reasoning; |
| 6285 | 6468 | chat[chat.length - 1]['extra']['reasoning_duration'] = null; |
| 6469 | + chat[chat.length - 1]['extra']['reasoning_signature'] = reasoningSignature; | |
| 6286 | 6470 | if (power_user.trim_spaces) { |
| 6287 | 6471 | getMessage = getMessage.trim(); |
| 6288 | 6472 | } |
| @@ -6362,6 +6546,63 @@ export async function saveReply({ type, getMessage, fromStreaming = false, title | ||
| 6362 | 6546 | } |
| 6363 | 6547 | |
| 6364 | 6548 | /** |
| 6549 | + * Creates a message's `swipes`, `swipe_id` and `swipe_info` if necessary. | |
| 6550 | + * @param {ChatMessage} message | |
| 6551 | + * @returns {boolean} true if the message was updated. | |
| 6552 | + */ | |
| 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 | +/** | |
| 6365 | 6606 | * Syncs the current message and all its data into the swipe data at the given message ID (or the last message if no ID is given). |
| 6366 | 6607 | * |
| 6367 | 6608 | * If the swipe data is invalid in some way, this function will exit out without doing anything. |
| @@ -6515,7 +6756,7 @@ export function getGeneratingApi() { | ||
| 6515 | 6756 | } |
| 6516 | 6757 | } |
| 6517 | 6758 | |
| 6518 | 6759 | export function getGeneratingModel(mes) { |
| 6519 | 6760 | let model = ''; |
| 6520 | 6761 | switch (main_api) { |
| 6521 | 6762 | case 'kobold': |
| @@ -6543,6 +6784,7 @@ function getGeneratingModel(mes) { | ||
| 6543 | 6784 | export function activateSendButtons() { |
| 6544 | 6785 | is_send_press = false; |
| 6545 | 6786 | hideStopButton(); |
| 6787 | + showSwipeButtons(); | |
| 6546 | 6788 | delete document.body.dataset.generating; |
| 6547 | 6789 | } |
| 6548 | 6790 | |
| @@ -6551,6 +6793,7 @@ export function activateSendButtons() { | ||
| 6551 | 6793 | */ |
| 6552 | 6794 | export function deactivateSendButtons() { |
| 6553 | 6795 | showStopButton(); |
| 6796 | + hideSwipeButtons(); | |
| 6554 | 6797 | document.body.dataset.generating = 'true'; |
| 6555 | 6798 | } |
| 6556 | 6799 | |
| @@ -6731,7 +6974,7 @@ export async function renameCharacter(name = null, { silent = false, renameChats | ||
| 6731 | 6974 | } |
| 6732 | 6975 | |
| 6733 | 6976 | // Also rename as a group member |
| 6734 | 6977 | await renameGroupMember(oldAvatar, newAvatar, newValue.toString()); |
| 6735 | 6978 | const renamePastChatsConfirm = renameChats !== null |
| 6736 | 6979 | ? renameChats |
| 6737 | 6980 | : silent |
| @@ -6860,6 +7103,11 @@ export function saveChatDebounced() { | ||
| 6860 | 7103 | * @returns {Promise<void>} |
| 6861 | 7104 | */ |
| 6862 | 7105 | export async function saveChat({ chatName, withMetadata, mesId, force = false } = {}) { |
| 7106 | + if (selected_group) { | |
| 7107 | + toastr.error(t`Operation was aborted to prevent data corruption.`, t`saveChat called for a group chat`); | |
| 7108 | + throw new Error('saveChat called for a group chat'); | |
| 7109 | + } | |
| 7110 | + | |
| 6863 | 7111 | if (arguments.length > 0 && typeof arguments[0] !== 'object') { |
| 6864 | 7112 | console.trace('saveChat called with positional arguments. Please use an object instead.'); |
| 6865 | 7113 | [chatName, withMetadata, mesId, force] = arguments; |
| @@ -6879,26 +7127,17 @@ export async function saveChat({ chatName, withMetadata, mesId, force = false } | ||
| 6879 | 7127 | } |
| 6880 | 7128 | |
| 6881 | 7129 | characters[this_chid]['date_last_chat'] = Date.now(); |
| 6882 | - chat.forEach(function (item, i) { | |
| 6883 | - if (item['is_group']) { | |
| 6884 | - toastr.error(t`Trying to save group chat with regular saveChat function. Aborting to prevent corruption.`); | |
| 6885 | - throw new Error('Group chat saved from saveChat'); | |
| 6886 | - } | |
| 6887 | - }); | |
| 6888 | 7130 | |
| 6889 | 7131 | const trimmedChat = (mesId !== undefined && mesId >= 0 && mesId < chat.length) |
| 6890 | 7132 | ? chat.slice(0, Number(mesId) + 1) |
| 6891 | 7133 | : chat.slice(); |
| 6892 | 7134 | |
| 6893 | - const chatToSave = [ | |
| 7135 | + /** @type {ChatHeader} */ | |
| 6894 | - { | |
| 7136 | + const chatHeader = { | |
| 6895 | - user_name: name1, | |
| 6896 | - character_name: name2, | |
| 6897 | - create_date: chat_create_date, | |
| 6898 | 7137 | chat_metadata: metadata, |
| 6899 | - }, | |
| 7138 | + user_name: 'unused', | |
| 6900 | - ...trimmedChat, | |
| 7139 | + character_name: 'unused', | |
| 6901 | 7140 | ]}; |
| 6902 | 7141 | |
| 6903 | 7142 | try { |
| 6904 | 7143 | const result = await fetch('/api/chats/save', { |
| @@ -6908,7 +7147,7 @@ export async function saveChat({ chatName, withMetadata, mesId, force = false } | ||
| 6908 | 7147 | body: JSON.stringify({ |
| 6909 | 7148 | ch_name: characters[this_chid].name, |
| 6910 | 7149 | file_name: fileName, |
| 6911 | 7150 | chat: chatToSave[chatHeader, ...trimmedChat], |
| 6912 | 7151 | avatar_url: characters[this_chid].avatar, |
| 6913 | 7152 | force: force, |
| 6914 | 7153 | }), |
| @@ -7082,7 +7321,7 @@ export async function unshallowCharacter(characterId) { | ||
| 7082 | 7321 | return; |
| 7083 | 7322 | } |
| 7084 | 7323 | |
| 7085 | 7324 | /** @type {import('./scripts/char-data.js').v1CharDataCharacter} */ |
| 7086 | 7325 | const character = characters[characterId]; |
| 7087 | 7326 | if (!character) { |
| 7088 | 7327 | console.debug('Character not found:', characterId); |
| @@ -7121,13 +7360,10 @@ export async function getChat() { | ||
| 7121 | 7360 | }); |
| 7122 | 7361 | if (response[0] !== undefined) { |
| 7123 | 7362 | chat.splice(0, chat.length, ...response); |
| 7124 | - chat_create_date = chat[0]['create_date']; | |
| 7125 | 7363 | chat_metadata = chat[0]['chat_metadata'] ?? {}; |
| 7126 | 7364 | |
| 7127 | 7365 | chat.shift(); |
| 7128 | 7366 | chat.forEach(ensureMessageMediaIsArray); |
| 7129 | - } else { | |
| 7130 | - chat_create_date = humanizedDateTime(); | |
| 7131 | 7367 | } |
| 7132 | 7368 | if (!chat_metadata['integrity']) { |
| 7133 | 7369 | chat_metadata['integrity'] = uuidv4(); |
| @@ -7221,8 +7457,8 @@ export async function openCharacterChat(file_name) { | ||
| 7221 | 7457 | |
| 7222 | 7458 | ////////// OPTIMZED MAIN API CHANGE FUNCTION //////////// |
| 7223 | 7459 | |
| 7224 | 7460 | export function changeMainAPI(api = null) { |
| 7225 | 7461 | const selectedVal = api ?? $('#main_api').val(); |
| 7226 | 7462 | //console.log(selectedVal); |
| 7227 | 7463 | const apiElements = { |
| 7228 | 7464 | 'koboldhorde': { |
| @@ -7596,8 +7832,11 @@ function updateMessage(div) { | ||
| 7596 | 7832 | const mesElement = div.closest('.mes'); |
| 7597 | 7833 | const mes = chat[mesElement.attr('mesid')]; |
| 7598 | 7834 | |
| 7835 | + // editing old messages | |
| 7836 | + mes['extra'] ??= {}; | |
| 7837 | + | |
| 7599 | 7838 | let regexPlacement; |
| 7600 | 7839 | if (mes?.is_user) { |
| 7601 | 7840 | regexPlacement = regex_placement.USER_INPUT; |
| 7602 | 7841 | } else if (mes.extra?.type === 'narrator') { |
| 7603 | 7842 | regexPlacement = regex_placement.SLASH_COMMAND; |
| @@ -7627,15 +7866,11 @@ function updateMessage(div) { | ||
| 7627 | 7866 | } |
| 7628 | 7867 | mes['mes'] = text; |
| 7629 | 7868 | if (mes['swipe_id'] !== undefined) { |
| 7869 | + ensureSwipes(mes); | |
| 7630 | 7870 | mes['swipes'][mes['swipe_id']] = text; |
| 7631 | 7871 | } |
| 7632 | 7872 | |
| 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) { | |
| 7639 | 7874 | mes.extra.bias = bias ?? null; |
| 7640 | 7875 | } else { |
| 7641 | 7876 | mes.extra.bias = null; |
| @@ -7706,8 +7941,7 @@ export async function messageEdit(editMessageId) { | ||
| 7706 | 7941 | this_edit_mes_id = editMessageId; |
| 7707 | 7942 | this_edit_mes_chname = editMessage.name || (editMessage.is_user ? name1 : name2); |
| 7708 | 7943 | |
| 7709 | - const hideCounters = editMessageId < chat.length - 1; | |
| 7944 | + refreshSwipeButtons(); | |
| 7710 | - hideSwipeButtons({ hideCounters }); | |
| 7711 | 7945 | |
| 7712 | 7946 | const chatScrollPosition = chatElement.scrollTop(); |
| 7713 | 7947 | const messageBlock = messageElement.find('.mes_block'); |
| @@ -7762,7 +7996,7 @@ async function messageEditCancel(messageId = this_edit_mes_id) { | ||
| 7762 | 7996 | if (this?.classList?.contains('mes_edit_cancel')) { |
| 7763 | 7997 | thisMesDiv = $(this).closest('.mes'); |
| 7764 | 7998 | } else { |
| 7765 | 7999 | thisMesDiv = chatElement.children('.mes').filter(`[mesid="${messageId}"]`); |
| 7766 | 8000 | } |
| 7767 | 8001 | |
| 7768 | 8002 | const thisMesBlock = thisMesDiv.find('.mes_block'); |
| @@ -7843,11 +8077,17 @@ async function messageEditMove(sourceId, targetId) { | ||
| 7843 | 8077 | } |
| 7844 | 8078 | |
| 7845 | 8079 | updateViewMessageIds(); |
| 8080 | + refreshSwipeButtons(); | |
| 7846 | 8081 | await saveChatConditional(); |
| 7847 | 8082 | return true; |
| 7848 | 8083 | } |
| 7849 | 8084 | |
| 7850 | 8085 | async function messageEditDone(div) { |
| 8086 | + if (!(this_edit_mes_id >= 0)) { | |
| 8087 | + console.trace('this_edit_mes_id cannot be blank when calling messageEditDone.'); | |
| 8088 | + return; | |
| 8089 | + } | |
| 8090 | + | |
| 7851 | 8091 | let { mesBlock, text, mes, bias } = updateMessage(div); |
| 7852 | 8092 | if (this_edit_mes_id == 0) { |
| 7853 | 8093 | text = substituteParams(text); |
| @@ -8016,7 +8256,7 @@ export async function displayPastChats(hightlightNames = []) { | ||
| 8016 | 8256 | }); |
| 8017 | 8257 | |
| 8018 | 8258 | // Define the search input listener |
| 8019 | 8259 | $('#select_chat_search').off('input').on('input', function () { |
| 8020 | 8260 | const searchQuery = $(this).val(); |
| 8021 | 8261 | debouncedDisplay(searchQuery); |
| 8022 | 8262 | }); |
| @@ -8026,6 +8266,8 @@ export async function displayPastChats(hightlightNames = []) { | ||
| 8026 | 8266 | const textSearchElement = $('#select_chat_search'); |
| 8027 | 8267 | textSearchElement.trigger('click').trigger('focus').trigger('select'); |
| 8028 | 8268 | }, 200); |
| 8269 | + | |
| 8270 | + addChatBackupsBrowser(); | |
| 8029 | 8271 | } |
| 8030 | 8272 | |
| 8031 | 8273 | async function displayChats(searchQuery, currentChat, displayName, avatarImg, selected_group, highlightNames) { |
| @@ -8258,7 +8500,7 @@ export function select_selected_character(chid, { switchMenu = true } = {}) { | ||
| 8258 | 8500 | $('#talkativeness_slider').val(characters[chid].talkativeness || talkativeness_default); |
| 8259 | 8501 | $('#mes_example_textarea').val(characters[chid].mes_example); |
| 8260 | 8502 | $('#selected_chat_pole').val(characters[chid].chat); |
| 8261 | 8503 | $('#create_date_pole').val(timestampToMoment(characters[chid].create_date).toISOString()); |
| 8262 | 8504 | $('#avatar_url_pole').val(characters[chid].avatar); |
| 8263 | 8505 | $('#chat_import_avatar_url').val(characters[chid].avatar); |
| 8264 | 8506 | $('#chat_import_character_name').val(characters[chid].name); |
| @@ -8572,99 +8814,191 @@ export function callPopup(text, type, inputValue = '', { okButton, rows, wide, w | ||
| 8572 | 8814 | |
| 8573 | 8815 | /** |
| 8574 | 8816 | * Update the swipe counter for mesId. |
| 8817 | + * By default, the swipe counter's opacity will appear greyed out. The opacity is changed with CSS. | |
| 8575 | 8818 | * @param {Number} mesId |
| 8819 | + * @param {object} [options] Options | |
| 8820 | + * @param {ChatMessage} [options.message=undefined] Swipe numbers from this message will be used instead of mesId. | |
| 8821 | + * @param {JQuery<HTMLElement>} [options.messageElement=undefined] Target Element. Passing in the message's element will save a DOM query. | |
| 8576 | 8822 | */ |
| 8577 | 8823 | export async function updateSwipeCounter(mesId, { message = undefined, messageElement = undefined } = {}) { |
| 8578 | - const swipeCounterText = formatSwipeCounter((chat[mesId]?.['swipe_id'] + 1), chat[mesId]?.['swipes']?.length); | |
| 8824 | + message ??= chat[mesId]; | |
| 8579 | 8825 | 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); | |
| 8582 | 8835 | } |
| 8583 | 8836 | |
| 8584 | 8837 | /** |
| 8585 | 8838 | * Swipe buttons areReturns oftentrue toggledif tomessages updateare theirgenerally positionswipeable. |
| 8586 | - * This should be replaced with a more efficient function. | |
| 8839 | + * @returns {boolean} | |
| 8587 | 8840 | */ |
| 8588 | 8841 | export 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 | + ); | |
| 8591 | 8852 | } |
| 8592 | 8853 | |
| 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]; | |
| 8595 | 8864 | |
| 8596 | - if (chat.length === 0) { | |
| 8865 | + //If the message does not have swipes, create them. | |
| 8597 | - return; | |
| 8866 | + if (ensureSwipes(message)) { | |
| 8867 | + syncMesToSwipe(messageId); | |
| 8598 | 8868 | } |
| 8599 | 8869 | |
| 8600 | 8870 | if ( |
| 8601 | - chat[mesId].is_system || | |
| 8871 | + //Only messages below the currently edited message can be swiped, if it's not mid-swipe edit. | |
| 8602 | - !swipes || | |
| 8872 | + ((messageId > (this_edit_mes_id ?? -1)) && (swipeState != SWIPE_STATE.EDITING)) && | |
| 8603 | - Number($('.mes:last').attr('mesid')) < 0 || | |
| 8873 | + | |
| 8604 | - chat[mesId].is_user || | |
| 8874 | + //If the message is the last message, and it exists. | |
| 8605 | - (selected_group && is_group_generating) | |
| 8875 | + (messageId == chat.length - 1) && | |
| 8606 | - ) { | |
| 8876 | + (message && | |
| 8607 | - return; | |
| 8877 | + //Small system messages cannot be swiped. | |
| 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; } | |
| 8608 | 8889 | } |
| 8609 | 8890 | |
| 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'); | |
| 8612 | 8934 | return; |
| 8935 | + //Don't hide all swipe buttons. | |
| 8936 | + } else { | |
| 8937 | + //CSS will hide all messages. | |
| 8938 | + $('body').removeClass('hideAllSwipeButtons'); | |
| 8613 | 8939 | } |
| 8940 | + //Non-messages can appear in chat. '.mes' is required. | |
| 8941 | + const messageElements = chatElement.children('.mes[mesid]'); | |
| 8614 | 8942 | |
| 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 | - } | |
| 8630 | 8944 | |
| 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'); | |
| 8637 | 8950 | |
| 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'); | |
| 8645 | 8980 | } |
| 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'); | |
| 8651 | 8982 | } |
| 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(); | |
| 8656 | 8989 | } |
| 8657 | 8990 | |
| 8658 | 8991 | /** |
| 8992 | + * This function is misleadingly named. It blocks generation then refreshes the swipe buttons and counters. | |
| 8659 | 8993 | * @param {object} [options] Options |
| 8660 | 8994 | * @param {boolean} [options.hideCounters=false] Also hide the swipes counter. |
| 8661 | 8995 | */ |
| 8662 | 8996 | export function hideSwipeButtons({ hideCounters = false } = {}) { |
| 8663 | 8997 | isSwipingAllowedswipesHidden = falsetrue; |
| 8664 | - chatElement.find('.swipe_right').hide(); | |
| 8998 | + refreshSwipeButtons(); | |
| 8665 | - chatElement.find('.swipe_left').hide(); | |
| 8999 | + | |
| 8666 | 9000 | if (hideCounters === true) { |
| 8667 | 9001 | chatElement.find('.last_mes .swipes-counter').hideprop('hidden', true); |
| 8668 | 9002 | } |
| 8669 | 9003 | } |
| 8670 | 9004 | |
| @@ -8707,25 +9041,23 @@ export async function deleteSwipe(swipeId = null, messageId = chat.length - 1) { | ||
| 8707 | 9041 | |
| 8708 | 9042 | // Select the next swipe, or the one before if it was the last one |
| 8709 | 9043 | const newSwipeId = Math.min(swipeId, message.swipes.length - 1); |
| 8710 | - syncSwipeToMes(messageId, newSwipeId); | |
| 8711 | 9044 | |
| 8712 | 9045 | chat_metadata['tainted'] = true; |
| 8713 | 9046 | |
| 9047 | + messageId = Number(messageId); | |
| 9048 | + swipeId = Number(swipeId); | |
| 8714 | 9049 | await eventSource.emit(event_types.MESSAGE_SWIPE_DELETED, { messageId, swipeId, newSwipeId }); |
| 9050 | + let direction = (swipeId <= newSwipeId) ? SWIPE_DIRECTION.RIGHT : SWIPE_DIRECTION.LEFT; | |
| 9051 | + //Animate swipe and swap dispayed message. | |
| 9052 | + await swipe(null, direction, { source: SWIPE_SOURCE.DELETE, repeated: false, forceMesId: messageId, forceSwipeId: newSwipeId }); | |
| 8715 | 9053 | |
| 8716 | 9054 | await saveChatConditional(); |
| 8717 | - await reloadCurrentChat(); | |
| 8718 | 9055 | |
| 8719 | 9056 | return newSwipeId; |
| 8720 | 9057 | } |
| 8721 | 9058 | |
| 8722 | 9059 | export async function saveMetadata() { |
| 8723 | - if (selected_group) { | |
| 9060 | + return await saveChatConditional(); | |
| 8724 | - await editGroup(selected_group, true, false); | |
| 8725 | - } | |
| 8726 | - else { | |
| 8727 | - await saveChatConditional(); | |
| 8728 | - } | |
| 8729 | 9061 | } |
| 8730 | 9062 | |
| 8731 | 9063 | export async function saveChatConditional() { |
| @@ -8765,7 +9097,7 @@ export async function saveChatConditional() { | ||
| 8765 | 9097 | * @param {boolean} [options.refresh] Whether to refresh the group chat list after import |
| 8766 | 9098 | * @returns {Promise<string[]>} List of imported file names. |
| 8767 | 9099 | */ |
| 8768 | 9100 | export async function importCharacterChat(formData, { refresh = true } = {}) { |
| 8769 | 9101 | const fetchResult = await fetch('/api/chats/import', { |
| 8770 | 9102 | method: 'POST', |
| 8771 | 9103 | body: formData, |
| @@ -8809,7 +9141,7 @@ export function updateEditArrowClasses() { | ||
| 8809 | 9141 | return; |
| 8810 | 9142 | } |
| 8811 | 9143 | |
| 8812 | 9144 | const message = chatElement.findchildren('.mes').filter(`.mes[mesid="${this_edit_mes_id}"]`); |
| 8813 | 9145 | |
| 8814 | 9146 | const downButton = message.find('.mes_edit_down'); |
| 8815 | 9147 | const upButton = message.find('.mes_edit_up'); |
| @@ -9244,6 +9576,23 @@ export async function createOrEditCharacter(e) { | ||
| 9244 | 9576 | } |
| 9245 | 9577 | |
| 9246 | 9578 | /** |
| 9579 | + * Visually updates all chat messages including andd after index by removing them, then adding them. | |
| 9580 | + * @param {ChatMessage[]} chat All messages in chat before index will remain unchanged. | |
| 9581 | + * @param {Number} index The last unchanged messageId. | |
| 9582 | + */ | |
| 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 | +/** | |
| 9247 | 9596 | * Formats a counter for a swipe view. |
| 9248 | 9597 | * @param {number} current The current number of items. |
| 9249 | 9598 | * @param {number} total The total number of items. |
| @@ -9258,41 +9607,61 @@ function formatSwipeCounter(current, total) { | ||
| 9258 | 9607 | |
| 9259 | 9608 | /** |
| 9260 | 9609 | * Handles the swipe event. |
| 9261 | 9610 | * @param {JQuery.EventSwipeEvent} _eventevent Event. |
| 9262 | 9611 | * @param {'left'|'right'SWIPE_DIRECTION} direction The direction to swipe. |
| 9263 | 9612 | * @param {object} params Additional parameters. |
| 9264 | 9613 | * @param {stringimport('./scripts/constants.js').SWIPE_SOURCE} [params.source] The source of the swipe event. null, 'keyboard', 'auto_swipe', 'back' or 'delete'. |
| 9265 | 9614 | * @param {boolean} [params.repeated] Is the swipe event repeated. |
| 9266 | 9615 | * @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. | |
| 9267 | 9619 | */ |
| 9268 | 9620 | export async function swipe(_eventevent, direction, { source, repeated, message = chat[chat.length - 1], forceMesId, forceSwipeId, forceDuration } = {}) { |
| 9269 | 9621 | if (chat.length === 0) { |
| 9270 | 9622 | console.warn('Swipe was called on an empty chat.'); |
| 9271 | 9623 | return; |
| 9272 | 9624 | } |
| 9273 | 9625 | |
| 9274 | - //Only allow one concurrent swipe. | |
| 9275 | - if (!isSwipingAllowed) { | |
| 9276 | - console.info('The swipe has been ignored because another is in progress.'); | |
| 9277 | - return; | |
| 9278 | - } | |
| 9279 | - isSwipingAllowed = false; | |
| 9280 | - | |
| 9281 | - let generation; | |
| 9282 | 9626 | let messageIndex; |
| 9283 | 9627 | |
| 9284 | 9628 | //Only set messageIndex if message exists because -1 is truthy. |
| 9285 | 9629 | if (message) { |
| 9286 | 9630 | messageIndex = chat.indexOf(message); |
| 9287 | 9631 | if (messageIndex === -1 && typeof (forceMesId) != 'number') { |
| 9288 | 9632 | console.error(`The message must exist in chat. ${message};`); |
| 9289 | 9633 | return; |
| 9290 | 9634 | } |
| 9291 | 9635 | } |
| 9292 | 9636 | |
| 9293 | 9637 | 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; | |
| 9294 | 9663 | |
| 9295 | 9664 | const thisMesDiv = chatElement.children('.mes').filter(`.mes[mesid="${mesId}"]`); |
| 9296 | 9665 | const thisMesText = thisMesDiv.find('.mes_block .mes_text'); |
| 9297 | 9666 | const thisMesDivHeight = thisMesDiv[0]?.scrollHeight; |
| 9298 | 9667 | const thisMesTextHeight = thisMesText[0]?.scrollHeight; |
| @@ -9301,43 +9670,108 @@ export async function swipe(_event, direction, { source, repeated, message = cha | ||
| 9301 | 9670 | return; |
| 9302 | 9671 | } |
| 9303 | 9672 | const originalSwipeId = Number(chat[mesId]?.['swipe_id'] ?? 0); |
| 9304 | 9673 | let newSwipeId = Number(forceSwipeId ?? originalSwipeId); |
| 9305 | 9674 | |
| 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; | |
| 9309 | 9700 | |
| 9310 | - async function endSwipe() { | |
| 9701 | + /** | |
| 9702 | + * Waits for the generation to end, reverts the swipe if swipe_id has not changed. | |
| 9703 | + * @param {boolean} revert Attept to revert the swipe without saving. | |
| 9704 | + */ | |
| 9705 | + async function endSwipe(revert = false) { | |
| 9311 | 9706 | //Wait for the generation to end. |
| 9312 | 9707 | try { |
| 9708 | + //`mes_buttons` need to be hidden until the animation completes. | |
| 9709 | + if (generation) { | |
| 9710 | + document.body.dataset.swiping = 'true'; | |
| 9313 | 9711 | await generation; |
| 9314 | 9712 | } |
| 9713 | + } | |
| 9315 | 9714 | catch (error) { |
| 9316 | 9715 | console.warn(`Swipe failed, Swiping back. ${error}`); |
| 9317 | 9716 | } |
| 9318 | - //Allow for another swipe. | |
| 9319 | - showSwipeButtons(); | |
| 9320 | 9717 | |
| 9321 | 9718 | //Clamp Id between swipes. |
| 9322 | 9719 | let clampedId = clamp(chat[mesId]['swipe_id'], 0, Math.max(0, chat[mesId]['swipes'].length - 1)); |
| 9323 | 9720 | |
| 9324 | - //If the id is not within bounds, Swipe back. | |
| 9325 | - if (chat[mesId]['swipe_id'] !== clampedId) { | |
| 9326 | - chat[mesId]['swipe_id'] = clampedId; | |
| 9327 | - syncSwipeToMes(mesId); | |
| 9328 | - addOneMessage(chat[mesId], { type: 'swipe', forceId: mesId, scroll: false }); | |
| 9329 | - } | |
| 9330 | - | |
| 9331 | 9721 | await updateSwipeCounter(mesId); |
| 9332 | 9722 | //Fallback. |
| 9333 | 9723 | if (mesId != chat.length - 1) { |
| 9334 | 9724 | await updateSwipeCounter(chat.length - 1); |
| 9335 | 9725 | } |
| 9726 | + | |
| 9727 | + // If swipe_id has not changed, give the user feedback. | |
| 9728 | + if (clampedId == originalSwipeId && source != SWIPE_SOURCE.DELETE) { | |
| 9729 | + try { | |
| 9730 | + //Shake 700/140=5px | |
| 9731 | + shakeElement(thisMesDiv, -swipeRange / 140, animation_duration, 'ease-in'); | |
| 9732 | + //Flash red. | |
| 9733 | + const flashTime = Math.max(animation_duration * 2, 100); | |
| 9734 | + await Promise.race([thisMesDiv.find('.swipes-counter').animate({ color: 'red' }, flashTime).animate({ color: '' }).promise(), createTimeout(flashTime * 4, `The shake animation did not end within ${flashTime * 4}ms`)].filter(Boolean)); | |
| 9735 | + } catch (error) { | |
| 9736 | + console.warn(error); | |
| 9737 | + } | |
| 9738 | + } | |
| 9739 | + | |
| 9740 | + //If the id is not within bounds, Swipe back. | |
| 9741 | + if (chat[mesId]?.swipe_id !== clampedId || revert) { | |
| 9742 | + // Prevent recursion. | |
| 9743 | + if (source != SWIPE_SOURCE.BACK) { | |
| 9744 | + source = SWIPE_SOURCE.BACK; | |
| 9745 | + chat[mesId].swipe_id = clampedId; | |
| 9746 | + | |
| 9747 | + //Update the chat. | |
| 9748 | + await loadFromSwipeId(mesId, chat[mesId].swipe_id); | |
| 9749 | + await redisplayChat(chat, mesId); | |
| 9750 | + } | |
| 9751 | + else { | |
| 9752 | + await Popup.show.confirm( | |
| 9753 | + t`ERROR: <code>syncSwipeToMes</code> has failed to revert the failed ${direction} swipe on message #${mesId}.`, | |
| 9754 | + t`<p>After you click OK, the chat will be reloaded to prevent data corruption.</p>`, | |
| 9755 | + { okButton: 'OK', cancelButton: false }, | |
| 9756 | + ); | |
| 9757 | + console.trace(`Error! Recursion detected when reverting failed ${direction} swipe on message #${mesId}. Something has broken.`); | |
| 9758 | + await reloadCurrentChat(); | |
| 9759 | + } | |
| 9760 | + //Out of bounds swipes should not be saved. | |
| 9761 | + } else if (source != SWIPE_SOURCE.BACK) { | |
| 9762 | + //Save the chat if swipe_id has changed. | |
| 9763 | + saveChatDebounced(); | |
| 9764 | + } | |
| 9765 | + | |
| 9766 | + //Allow for another swipe. | |
| 9767 | + swipeState = SWIPE_STATE.NONE; | |
| 9768 | + delete document.body.dataset.swiping; | |
| 9769 | + showSwipeButtons(); | |
| 9336 | 9770 | } |
| 9337 | 9771 | |
| 9338 | 9772 | async function standardSwipe(newSwipeId) { |
| 9339 | 9773 | //If swipe_id has changed, or the source is being deleted. |
| 9340 | 9774 | if (newSwipeId !== originalSwipeId || source == 'delete'SWIPE_SOURCE.DELETE || source == SWIPE_SOURCE.BACK) { |
| 9341 | 9775 | //Update the chat. |
| 9342 | 9776 | await loadFromSwipeId(mesId, newSwipeId); |
| 9343 | 9777 | //Transition to the new chat. |
| @@ -9347,6 +9781,27 @@ export async function swipe(_event, direction, { source, repeated, message = cha | ||
| 9347 | 9781 | } |
| 9348 | 9782 | |
| 9349 | 9783 | /** |
| 9784 | + * Removes a message's extra and gen times. | |
| 9785 | + * @param {ChatMessage} message | |
| 9786 | + */ | |
| 9787 | + function clearMessageData(message) { | |
| 9788 | + if (message.extra && typeof message.extra === 'object') { | |
| 9789 | + delete message.extra.memory; | |
| 9790 | + delete message.extra.display_text; | |
| 9791 | + delete message.extra.media; | |
| 9792 | + delete message.extra.inline_image; | |
| 9793 | + delete message.extra.files; | |
| 9794 | + delete message.extra.fileLength; | |
| 9795 | + delete message.extra.generationType; | |
| 9796 | + delete message.extra.negative; | |
| 9797 | + delete message.extra.title; | |
| 9798 | + delete message.extra.append_title; | |
| 9799 | + } | |
| 9800 | + delete message.gen_started; | |
| 9801 | + delete message.gen_finished; | |
| 9802 | + } | |
| 9803 | + | |
| 9804 | + /** | |
| 9350 | 9805 | * Sets the message to the newSwipeId and loads it. |
| 9351 | 9806 | * @param {number} mesId |
| 9352 | 9807 | * @param {number} newSwipeId |
| @@ -9355,55 +9810,89 @@ export async function swipe(_event, direction, { source, repeated, message = cha | ||
| 9355 | 9810 | //Update the swipe_id. |
| 9356 | 9811 | chat[mesId]['swipe_id'] = newSwipeId; |
| 9357 | 9812 | |
| 9358 | - if (chat[mesId].extra && typeof chat[mesId].extra === 'object') { | |
| 9813 | + clearMessageData(chat[mesId]); | |
| 9359 | - delete chat[mesId].extra.memory; | |
| 9814 | + | |
| 9360 | - delete chat[mesId].extra.display_text; | |
| 9815 | + //Load from swipes. | |
| 9361 | - delete chat[mesId].extra.media; | |
| 9816 | + if (syncSwipeToMes(mesId, newSwipeId) == false) { | |
| 9362 | - delete chat[mesId].extra.inline_image; | |
| 9817 | + let errorMessage = t`When swiping ${direction} on message ${mesId}, syncSwipeToMes has returned false. Attempting to swipe back!`; | |
| 9363 | - delete chat[mesId].extra.files; | |
| 9818 | + toastr.error(errorMessage); | |
| 9364 | - delete chat[mesId].extra.fileLength; | |
| 9819 | + | |
| 9365 | 9820 | 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; | |
| 9369 | 9824 | } |
| 9370 | - delete chat[mesId].gen_started; | |
| 9371 | - delete chat[mesId].gen_finished; | |
| 9372 | - //load from swipes. | |
| 9373 | - syncSwipeToMes(mesId, chat[mesId]['swipe_id']); | |
| 9374 | - } | |
| 9375 | - | |
| 9376 | - // Helper function to convert transition to promise | |
| 9377 | - const transitionPromise = (element, properties) => { | |
| 9378 | - return new Promise((resolve) => { | |
| 9379 | - element.transition({ | |
| 9380 | - ...properties, | |
| 9381 | - complete: resolve, | |
| 9382 | - }); | |
| 9383 | - }); | |
| 9384 | - }; | |
| 9385 | 9825 | |
| 9386 | 9826 | /** |
| 9387 | 9827 | * Animates a swipe for all messages >= mesId. |
| 9388 | 9828 | * @param {number} mesId |
| 9389 | 9829 | * @param {numberobject} xparams |
| 9390 | 9830 | * @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 | + } | |
| 9399 | 9889 | |
| 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 | - }); | |
| 9407 | 9896 | } |
| 9408 | 9897 | |
| 9409 | 9898 | function getMessageBottomHeight(thisMesDiv) { |
| @@ -9445,11 +9934,15 @@ export async function swipe(_event, direction, { source, repeated, message = cha | ||
| 9445 | 9934 | /** |
| 9446 | 9935 | * Anime a swipe, optionally running a generation. |
| 9447 | 9936 | * @param {boolean} run_generate |
| 9937 | + * @param {boolean} [skipSwipeOut=false] | |
| 9448 | 9938 | */ |
| 9449 | 9939 | async function animateSwipe(run_generate = false, skipSwipeOut = false) { |
| 9450 | 9940 | |
| 9941 | + if (!skipSwipeOut) { | |
| 9451 | 9942 | //Swipe out. |
| 9452 | 9943 | await animateSwipeTransition(mesId, { xEnd: `${swipeRange}px`, duration: swipeDuration }); |
| 9944 | + } | |
| 9945 | + | |
| 9453 | 9946 | |
| 9454 | 9947 | if (run_generate) { |
| 9455 | 9948 | await updateSwipeCounter(mesId); |
| @@ -9465,7 +9958,8 @@ export async function swipe(_event, direction, { source, repeated, message = cha | ||
| 9465 | 9958 | |
| 9466 | 9959 | //Only scroll when swiping the last message. |
| 9467 | 9960 | const scroll = (mesId == chat.length - 1); |
| 9468 | - addOneMessage(chat[mesId], { type: 'swipe', forceId: mesId, scroll: scroll }); | |
| 9961 | + //The swipe buttons will be refreshed in endSwipe(), refreshing them now will cause flickering. | |
| 9962 | + addOneMessage(chat[mesId], { type: 'swipe', forceId: mesId, scroll: scroll, showSwipes: false }); | |
| 9469 | 9963 | |
| 9470 | 9964 | if (power_user.message_token_count_enabled) { |
| 9471 | 9965 | if (!chat[mesId].extra) { |
| @@ -9483,10 +9977,6 @@ export async function swipe(_event, direction, { source, repeated, message = cha | ||
| 9483 | 9977 | thisMesDiv.css('height', thisMesDivHeight); |
| 9484 | 9978 | expandNewMessage(thisMesDiv); |
| 9485 | 9979 | |
| 9486 | - | |
| 9487 | - //Jump to the opposite side. | |
| 9488 | - await animateSwipeTransition(mesId, -swipeRange, 0); | |
| 9489 | - | |
| 9490 | 9980 | appendMediaToMessage(chat[mesId], thisMesDiv); |
| 9491 | 9981 | |
| 9492 | 9982 | await eventSource.emit(event_types.MESSAGE_SWIPED, (mesId)); |
| @@ -9494,12 +9984,10 @@ export async function swipe(_event, direction, { source, repeated, message = cha | ||
| 9494 | 9984 | if (run_generate && !is_send_press) { |
| 9495 | 9985 | is_send_press = true; |
| 9496 | 9986 | generation = Generate('swipe'); |
| 9497 | - } else if (Number(chat[mesId]['swipe_id']) !== chat[mesId]['swipes'].length) { | |
| 9498 | - saveChatDebounced(); | |
| 9499 | 9987 | } |
| 9500 | 9988 | |
| 9501 | 9989 | //Swipe in from the opposite side. |
| 9502 | - await animateSwipeTransition(mesId, 0, swipeDuration); | |
| 9990 | + await animateSwipeTransition(mesId, { xStart: `${-swipeRange}px`, xEnd: `${0}px`, duration: swipeDuration }); | |
| 9503 | 9991 | } |
| 9504 | 9992 | |
| 9505 | 9993 | if (mesId === Number(this_edit_mes_id)) { |
| @@ -9514,7 +10002,7 @@ export async function swipe(_event, direction, { source, repeated, message = cha | ||
| 9514 | 10002 | } |
| 9515 | 10003 | |
| 9516 | 10004 | //If the swipe is not being deleted. |
| 9517 | 10005 | if (source != 'delete'SWIPE_SOURCE.DELETE && source != SWIPE_SOURCE.BACK) { |
| 9518 | 10006 | |
| 9519 | 10007 | // Make sure ad-hoc changes to extras are saved before swiping away |
| 9520 | 10008 | syncMesToSwipe(mesId); |
| @@ -9533,19 +10021,19 @@ export async function swipe(_event, direction, { source, repeated, message = cha | ||
| 9533 | 10021 | } |
| 9534 | 10022 | // If the user is holding down the key and we're at the last or first swipe, don't do anything. |
| 9535 | 10023 | let isLastSwipe = (direction === SWIPE_DIRECTION.RIGHT) ? (chat[mesId].swipe_id === Math.max(0, chat[mesId]['swipes'].length - 1)) : chat[mesId].swipe_id === 0; |
| 9536 | 10024 | if (source === 'keyboard'SWIPE_SOURCE.KEYBOARD && repeated && isLastSwipe) { |
| 9537 | 10025 | await endSwipe(); |
| 9538 | 10026 | return; |
| 9539 | 10027 | } |
| 9540 | 10028 | } else if (source == 'delete'SWIPE_SOURCE.DELETE || source == SWIPE_SOURCE.BACK) { |
| 9541 | 10029 | //If the swipe is being deleted or reverted. |
| 9542 | 10030 | await standardSwipe(newSwipeId); |
| 9543 | 10031 | return; |
| 9544 | 10032 | } |
| 9545 | 10033 | |
| 9546 | 10034 | //If swiping left. |
| 9547 | 10035 | if (direction === SWIPE_DIRECTION.LEFT) { |
| 9548 | - newSwipeId--; | |
| 10036 | + if (forceSwipeId == null) newSwipeId--; | |
| 9549 | 10037 | //Loop to last swipe if negative. |
| 9550 | 10038 | if (newSwipeId < 0) { |
| 9551 | 10039 | newSwipeId = Math.max(0, chat[mesId]['swipes'].length - 1); |
| @@ -9557,13 +10045,13 @@ export async function swipe(_event, direction, { source, repeated, message = cha | ||
| 9557 | 10045 | await endSwipe(); |
| 9558 | 10046 | return; |
| 9559 | 10047 | } |
| 9560 | 10048 | await standardSwipe(newSwipeId); |
| 9561 | 10049 | return; |
| 9562 | 10050 | } |
| 9563 | 10051 | //If swiping right. |
| 9564 | 10052 | else if (direction === SWIPE_DIRECTION.RIGHT) { |
| 9565 | 10053 | // make new slot in array |
| 9566 | - newSwipeId++; | |
| 10054 | + if (forceSwipeId == null) newSwipeId++; | |
| 9567 | 10055 | |
| 9568 | 10056 | //Minimum of zero. |
| 9569 | 10057 | if (newSwipeId < 0) { |
| @@ -9573,35 +10061,37 @@ export async function swipe(_event, direction, { source, repeated, message = cha | ||
| 9573 | 10061 | return; |
| 9574 | 10062 | } |
| 9575 | 10063 | |
| 9576 | - //if swipe id of last message is the same as the length of the 'swipes' array and not the greeting. | |
| 10064 | + //If overswiping. | |
| 9577 | 10065 | if (newSwipeId >= chat[mesId]['swipes'].length && ((chat.length !== 1 || !isPristine))) { |
| 9578 | 10066 | newSwipeId = chat[mesId]['swipes'].length; |
| 9579 | 10067 | |
| 9580 | 10068 | //Update the swipe_id. |
| 9581 | 10069 | chat[mesId]['swipe_id'] = newSwipeId; |
| 9582 | 10070 | |
| 9583 | - //Cancel the generation if it's a user message or the first message in a pristine chat. | |
| 10071 | + const overswipe = getOverswipeBehavior(mesId); | |
| 9584 | - if (chat[mesId].is_user || (mesId === 0 && isPristine)) { | |
| 10072 | + | |
| 10073 | + //Cancel the generation. | |
| 10074 | + if (overswipe == OVERSWIPE_BEHAVIOR.NONE) { | |
| 9585 | 10075 | //Cancel swipe. |
| 9586 | 10076 | chat[mesId]['swipe_id'] = originalSwipeId; |
| 9587 | 10077 | await endSwipe(); |
| 9588 | 10078 | return; |
| 9589 | 10079 | } else { |
| 9590 | - //Generate. | |
| 10080 | + //Regenerate the message | |
| 9591 | - await loadFromSwipeId(mesId, newSwipeId); | |
| 10081 | + else if (overswipe == OVERSWIPE_BEHAVIOR.REGENERATE) { | |
| 10082 | + clearMessageData(chat[mesId]); | |
| 9592 | 10083 | let run_generate = true; |
| 10084 | + //Generate. | |
| 9593 | 10085 | await animateSwipe(run_generate); |
| 9594 | 10086 | await endSwipe(); |
| 9595 | 10087 | return; |
| 9596 | 10088 | } |
| 9597 | - } | |
| 10089 | + // Loop to the first swipe. | |
| 9598 | - else { | |
| 10090 | + else if (overswipe == OVERSWIPE_BEHAVIOR.LOOP || overswipe == OVERSWIPE_BEHAVIOR.PRISTINE_GREETING) { | |
| 9599 | - // if swipe_right is called on the last alternate greeting in pristine chats, loop back around | |
| 9600 | - if (chat.length === 1 && newSwipeId !== undefined && newSwipeId === chat[0]['swipes'].length && isPristine) { | |
| 9601 | 10091 | newSwipeId = 0; |
| 9602 | 10092 | } |
| 9603 | 10093 | } |
| 9604 | 10094 | await standardSwipe(newSwipeId); |
| 9605 | 10095 | return; |
| 9606 | 10096 | } |
| 9607 | 10097 | } |
| @@ -9609,28 +10099,28 @@ export async function swipe(_event, direction, { source, repeated, message = cha | ||
| 9609 | 10099 | /** |
| 9610 | 10100 | * @deprecated Use `swipe` instead. |
| 9611 | 10101 | * Handles the swipe to the left event. |
| 9612 | 10102 | * @param {JQuery.EventSwipeEvent} _event[event] Event. |
| 9613 | 10103 | * @param {object} params Additional parameters. |
| 9614 | 10104 | * @param {stringimport('./scripts/constants.js').SWIPE_SOURCE} [params.source] The source of the swipe event. null, 'keyboard', 'auto_swipe', 'back' or 'delete'. |
| 9615 | 10105 | * @param {boolean} [params.repeated] Is the swipe event repeated. |
| 9616 | 10106 | * @param {object} [params.message] The chat message to swipe. |
| 9617 | 10107 | */ |
| 9618 | 10108 | export async function swipe_left(_eventevent, { source, repeated, message } = {}) { |
| 9619 | 10109 | await swipe.call(this, _eventevent, SWIPE_DIRECTION.LEFT, { source: source, repeated: repeated, message: message }); |
| 9620 | 10110 | } |
| 9621 | 10111 | |
| 9622 | 10112 | /** |
| 9623 | 10113 | * @deprecated Use `swipe` instead. |
| 9624 | 10114 | * Handles the swipe to the right event. |
| 9625 | 10115 | * @param {JQuery.EventSwipeEvent} [_eventevent] Event. |
| 9626 | 10116 | * @param {object} params Additional parameters. |
| 9627 | 10117 | * @param {stringimport('./scripts/constants.js').SWIPE_SOURCE} [params.source] The source of the swipe event. null, 'keyboard', 'auto_swipe', 'back' or 'delete'. |
| 9628 | 10118 | * @param {boolean} [params.repeated] Is the swipe event repeated. |
| 9629 | 10119 | * @param {object} [params.message] The chat message to swipe. |
| 9630 | 10120 | */ |
| 9631 | 10121 | //MARK: swipe_right |
| 9632 | 10122 | export async function swipe_right(_eventevent = null, { source, repeated, message } = {}) { |
| 9633 | 10123 | await swipe.call(this, _eventevent, SWIPE_DIRECTION.RIGHT, { source: source, repeated: repeated, message: message }); |
| 9634 | 10124 | } |
| 9635 | 10125 | |
| 9636 | 10126 | /** |
| @@ -10356,7 +10846,7 @@ jQuery(async function () { | ||
| 10356 | 10846 | }); |
| 10357 | 10847 | $('#rm_button_selected_ch').on('click', function () { |
| 10358 | 10848 | if (selected_group) { |
| 10359 | 10849 | select_group_chats(selected_group, false); |
| 10360 | 10850 | } else { |
| 10361 | 10851 | selected_button = 'character_edit'; |
| 10362 | 10852 | select_selected_character(this_chid); |
| @@ -10800,10 +11290,12 @@ jQuery(async function () { | ||
| 10800 | 11290 | } |
| 10801 | 11291 | |
| 10802 | 11292 | else if (id == 'option_regenerate') { |
| 10803 | - closeMessageEditor(); | |
| 11293 | + //Attempting to regenerate a user message will instead generate a new message. | |
| 11294 | + if (chat.length && chat.length - 1 === this_edit_mes_id && chat[this_edit_mes_id]?.is_user == false) { | |
| 11295 | + toastr.warning(t`Finish the edit before starting a generation.`, t`You cannot regenerate the message you are editing.`); | |
| 11296 | + return; | |
| 11297 | + } | |
| 10804 | 11298 | if (is_send_press == false) { |
| 10805 | - //hideSwipeButtons(); | |
| 10806 | - | |
| 10807 | 11299 | if (selected_group) { |
| 10808 | 11300 | regenerateGroup(); |
| 10809 | 11301 | } |
| @@ -10822,7 +11314,14 @@ jQuery(async function () { | ||
| 10822 | 11314 | } |
| 10823 | 11315 | |
| 10824 | 11316 | 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 | + } | |
| 10826 | 11325 | |
| 10827 | 11326 | if (is_send_press == false || fromSlashCommand) { |
| 10828 | 11327 | is_send_press = true; |
| @@ -11289,7 +11788,7 @@ jQuery(async function () { | ||
| 11289 | 11788 | |
| 11290 | 11789 | $('#rm_button_group_chats').on('click', function () { |
| 11291 | 11790 | selected_button = 'group_chats'; |
| 11292 | 11791 | select_group_chats(null, false); |
| 11293 | 11792 | }); |
| 11294 | 11793 | |
| 11295 | 11794 | $('#rm_button_back_from_group').on('click', function () { |
| @@ -579,10 +579,6 @@ class PromptManager { | ||
| 579 | 579 | const sourceName = this.promptSources[promptId]; |
| 580 | 580 | entrySource.textContent = sourceName; |
| 581 | 581 | } |
| 582 | - | |
| 583 | - if (!this.systemPrompts.includes(promptId)) { | |
| 584 | - injectionPositionField.removeAttribute('disabled'); | |
| 585 | - } | |
| 586 | 582 | }; |
| 587 | 583 | |
| 588 | 584 | // Append prompt to selected character |
| @@ -1284,10 +1280,10 @@ class PromptManager { | ||
| 1284 | 1280 | const preparedPrompt = new Prompt(prompt); |
| 1285 | 1281 | |
| 1286 | 1282 | if (typeof original === 'string') { |
| 1287 | 1283 | if (0 < groupMembers.length) preparedPrompt.content = substituteParams(prompt.content ?? '', null, null,{ original, groupOverride: groupMembers.join(', ') }); |
| 1288 | 1284 | else preparedPrompt.content = substituteParams(prompt.content, null, null,{ original }); |
| 1289 | 1285 | } else { |
| 1290 | 1286 | if (0 < groupMembers.length) preparedPrompt.content = substituteParams(prompt.content ?? '', null, null,{ null,groupOverride: groupMembers.join(', ') }); |
| 1291 | 1287 | else preparedPrompt.content = substituteParams(prompt.content); |
| 1292 | 1288 | } |
| 1293 | 1289 | |
| @@ -1399,10 +1395,6 @@ class PromptManager { | ||
| 1399 | 1395 | entrySource.textContent = sourceName; |
| 1400 | 1396 | } |
| 1401 | 1397 | |
| 1402 | - if (this.systemPrompts.includes(prompt.identifier)) { | |
| 1403 | - injectionPositionField.setAttribute('disabled', 'disabled'); | |
| 1404 | - } | |
| 1405 | - | |
| 1406 | 1398 | const resetPromptButton = document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_reset'); |
| 1407 | 1399 | if (true === prompt.system_prompt) { |
| 1408 | 1400 | resetPromptButton.style.display = 'block'; |
| @@ -1757,7 +1749,7 @@ class PromptManager { | ||
| 1757 | 1749 | ${isInjectionPrompt ? '<span class="fa-fw fa-solid fa-syringe" title="In-Chat Injection"></span>' : ''} |
| 1758 | 1750 | ${this.isPromptInspectionAllowed(prompt) ? `<a title="${encodedName}" class="prompt-manager-inspect-action">${encodedName}</a>` : `<span title="${encodedName}">${encodedName}</span>`} |
| 1759 | 1751 | ${roleIcon ? `<span data-role="${escapeHtml(prompt.role)}" class="fa-xs fa-solid ${roleIcon}" title="${roleTitle}"></span>` : ''} |
| 1760 | 1752 | ${isInjectionPrompt ? `<small class="prompt-manager-injection-depth">@ ${escapeHtml(prompt.injection_depth.toString())}</small>` : ''} |
| 1761 | 1753 | ${isOverriddenPrompt ? '<small class="fa-solid fa-address-card prompt-manager-overridden" title="Pulled from a character card"></small>' : ''} |
| 1762 | 1754 | </span> |
| 1763 | 1755 | <span> |
| @@ -37,7 +37,7 @@ import { debounce, getStringHash, isValidUrl } from './utils.js'; | ||
| 37 | 37 | import { chat_completion_sources, oai_settings } from './openai.js'; |
| 38 | 38 | import { getTokenCountAsync } from './tokenizers.js'; |
| 39 | 39 | import { textgen_types, textgenerationwebui_settings as textgen_settings, getTextGenServer } from './textgen-settings.js'; |
| 40 | 40 | import { debounce_timeout, SWIPE_SOURCE } from './constants.js'; |
| 41 | 41 | |
| 42 | 42 | import { Popup } from './popup.js'; |
| 43 | 43 | import { accountStorage } from './util/AccountStorage.js'; |
| @@ -162,43 +162,37 @@ export function shouldSendOnEnter() { | ||
| 162 | 162 | } |
| 163 | 163 | } |
| 164 | 164 | |
| 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 | + */ | |
| 170 | 170 | constexport nowfunction =humanizedDateTime(timestamp new= Date(Date.now()); { |
| 171 | + const date = new Date(timestamp); | |
| 171 | 172 | const dt = { |
| 172 | - year: now.getFullYear(), month: now.getMonth() + 1, day: now.getDate(), | |
| 173 | + year: date.getFullYear(), | |
| 173 | - hour: now.getHours(), minute: now.getMinutes(), second: now.getSeconds(), | |
| 174 | + month: date.getMonth() + 1, | |
| 175 | + day: date.getDate(), | |
| 176 | + hour: date.getHours(), | |
| 177 | + minute: date.getMinutes(), | |
| 178 | + second: date.getSeconds(), | |
| 179 | + millisecond: date.getMilliseconds(), | |
| 174 | 180 | }; |
| 175 | 181 | for (const key in dt) { |
| 176 | - dt[key] = dt[key].toString().padStart(2, '0'); | |
| 182 | + const padLength = key === 'millisecond' ? 3 : 2; | |
| 183 | + dt[key] = dt[key].toString().padStart(padLength, '0'); | |
| 177 | 184 | } |
| 178 | 185 | return `${dt.year}-${dt.month}-${dt.day}@${dt.hour}h${dt.minute}m${dt.second}s${dt.millisecond}ms`; |
| 179 | 186 | } |
| 180 | 187 | |
| 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()) { | |
| 187 | 194 | const monthdate = months[d.getMonthnew Date(timestamp)]; |
| 188 | 195 | 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; | |
| 202 | 196 | } |
| 203 | 197 | |
| 204 | 198 | |
| @@ -403,6 +397,7 @@ function RA_autoconnect(PrevApi) { | ||
| 403 | 397 | || (secret_state[SECRET_KEYS.COHERE] && oai_settings.chat_completion_source == chat_completion_sources.COHERE) |
| 404 | 398 | || (secret_state[SECRET_KEYS.PERPLEXITY] && oai_settings.chat_completion_source == chat_completion_sources.PERPLEXITY) |
| 405 | 399 | || (secret_state[SECRET_KEYS.GROQ] && oai_settings.chat_completion_source == chat_completion_sources.GROQ) |
| 400 | + || (secret_state[SECRET_KEYS.CHUTES] && oai_settings.chat_completion_source == chat_completion_sources.CHUTES) | |
| 406 | 401 | || (secret_state[SECRET_KEYS.SILICONFLOW] && oai_settings.chat_completion_source == chat_completion_sources.SILICONFLOW) |
| 407 | 402 | || (secret_state[SECRET_KEYS.ELECTRONHUB] && oai_settings.chat_completion_source == chat_completion_sources.ELECTRONHUB) |
| 408 | 403 | || (secret_state[SECRET_KEYS.NANOGPT] && oai_settings.chat_completion_source == chat_completion_sources.NANOGPT) |
| @@ -930,7 +925,7 @@ export function initRossMods() { | ||
| 930 | 925 | var SwipeButR = $('.swipe_right:last'); |
| 931 | 926 | var SwipeTargetMesClassParent = $(e.target).closest('.last_mes'); |
| 932 | 927 | if (SwipeTargetMesClassParent !== null) { |
| 933 | 928 | if (SwipeButR.cssis('display:visible') === 'flex') { |
| 934 | 929 | SwipeButR.trigger('click'); |
| 935 | 930 | } |
| 936 | 931 | } |
| @@ -954,7 +949,7 @@ export function initRossMods() { | ||
| 954 | 949 | var SwipeButL = $('.swipe_left:last'); |
| 955 | 950 | var SwipeTargetMesClassParent = $(e.target).closest('.last_mes'); |
| 956 | 951 | if (SwipeTargetMesClassParent !== null) { |
| 957 | 952 | if (SwipeButL.cssis('display:visible') === 'flex') { |
| 958 | 953 | SwipeButL.trigger('click'); |
| 959 | 954 | } |
| 960 | 955 | } |
| @@ -975,10 +970,10 @@ export function initRossMods() { | ||
| 975 | 970 | |
| 976 | 971 | function isModifiedKeyboardEvent(event) { |
| 977 | 972 | return (event instanceof KeyboardEvent && |
| 978 | 973 | (event.shiftKey || |
| 979 | 974 | event.ctrlKey || |
| 980 | 975 | event.altKey || |
| 981 | 976 | event.metaKey)); |
| 982 | 977 | } |
| 983 | 978 | |
| 984 | 979 | $(document).on('keydown', async function (event) { |
| @@ -1112,29 +1107,31 @@ export function initRossMods() { | ||
| 1112 | 1107 | |
| 1113 | 1108 | if (event.key == 'ArrowLeft') { //swipes left |
| 1114 | 1109 | if ( |
| 1115 | 1110 | isSwipingAllowed() && |
| 1116 | 1111 | !isNanogallery2LightboxActive() && // Check if lightbox is NOT active |
| 1117 | 1112 | $('#send_textarea').val() === '' && |
| 1118 | 1113 | $('#character_popup').css('display') === 'none' && |
| 1119 | 1114 | $('#shadow_select_chat_popup').css('display') === 'none' && |
| 1120 | 1115 | !isInputElementInFocus() && |
| 1121 | 1116 | !isModifiedKeyboardEvent(event) && |
| 1117 | + !(document.activeElement instanceof HTMLVideoElement) | |
| 1122 | 1118 | ) { |
| 1123 | 1119 | $('.swipe_left:last').trigger('click', { source: 'keyboard'SWIPE_SOURCE.KEYBOARD, repeated: event.repeat }); |
| 1124 | 1120 | return; |
| 1125 | 1121 | } |
| 1126 | 1122 | } |
| 1127 | 1123 | if (event.key == 'ArrowRight') { //swipes right |
| 1128 | 1124 | if ( |
| 1129 | 1125 | isSwipingAllowed() && |
| 1130 | 1126 | !isNanogallery2LightboxActive() && // Check if lightbox is NOT active |
| 1131 | 1127 | $('#send_textarea').val() === '' && |
| 1132 | 1128 | $('#character_popup').css('display') === 'none' && |
| 1133 | 1129 | $('#shadow_select_chat_popup').css('display') === 'none' && |
| 1134 | 1130 | !isInputElementInFocus() && |
| 1135 | 1131 | !isModifiedKeyboardEvent(event) && |
| 1132 | + !(document.activeElement instanceof HTMLVideoElement) | |
| 1136 | 1133 | ) { |
| 1137 | 1134 | $('.swipe_right:last').trigger('click', { source: 'keyboard'SWIPE_SOURCE.KEYBOARD, repeated: event.repeat }); |
| 1138 | 1135 | return; |
| 1139 | 1136 | } |
| 1140 | 1137 | } |
| @@ -50,6 +50,14 @@ const toolbarSelectors = [ | ||
| 50 | 50 | '.jg-menu', |
| 51 | 51 | ].join(', '); |
| 52 | 52 | |
| 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 | + | |
| 53 | 61 | /** @type {Record<string, (element: Element) => void>} */ |
| 54 | 62 | const a11yRules = { |
| 55 | 63 | [buttonSelectors]: (element) => { |
| @@ -64,6 +72,12 @@ const a11yRules = { | ||
| 64 | 72 | [toolbarSelectors]: (element) => { |
| 65 | 73 | element.setAttribute('role', 'toolbar'); |
| 66 | 74 | }, |
| 75 | + [tabListSelectors]: (element) => { | |
| 76 | + element.setAttribute('role', 'tablist'); | |
| 77 | + }, | |
| 78 | + [tabItemSelectors]: (element) => { | |
| 79 | + element.setAttribute('role', 'tab'); | |
| 80 | + }, | |
| 67 | 81 | '#toast-container .toast': (element) => { |
| 68 | 82 | element.setAttribute('role', 'status'); |
| 69 | 83 | }, |
| @@ -19,7 +19,7 @@ import { SlashCommand } from './slash-commands/SlashCommand.js'; | ||
| 19 | 19 | import { ARGUMENT_TYPE, SlashCommandArgument } from './slash-commands/SlashCommandArgument.js'; |
| 20 | 20 | export { MODULE_NAME as NOTE_MODULE_NAME }; |
| 21 | 21 | import { t } from './i18n.js'; |
| 22 | 22 | import { MacrosParsermacros, MacroCategory } from './macros/macro-system.js'; |
| 23 | 23 | |
| 24 | 24 | const MODULE_NAME = '2_floating_prompt'; // <= Deliberate, for sorting lower than memory |
| 25 | 25 | |
| @@ -581,7 +581,24 @@ export function initAuthorsNote() { | ||
| 581 | 581 | })); |
| 582 | 582 | eventSource.on(event_types.CHAT_CHANGED, onChatChanged); |
| 583 | 583 | |
| 584 | - MacrosParser.registerMacro('authorsNote', () => chat_metadata[metadata_keys.prompt] ?? '', t`The contents of the Author's Note`); | |
| 584 | + registerAuthorsNoteMacros(); | |
| 585 | - MacrosParser.registerMacro('charAuthorsNote', () => this_chid !== undefined ? (extension_settings.note.chara.find((e) => e.name === getCharaFilename())?.prompt ?? '') : '', t`The contents of the Character Author's Note`); | |
| 586 | - MacrosParser.registerMacro('defaultAuthorsNote', () => extension_settings.note.default ?? '', t`The contents of the Default Author's Note`); | |
| 587 | 585 | } |
| 586 | + | |
| 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 | + | |
| @@ -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 | +} | |
| @@ -1,12 +1,15 @@ | ||
| 1 | 1 | import { Fuse, localforage } from '../lib.js'; |
| 2 | 2 | import { characters, chat_metadata, eventSource, event_types, generateQuietPrompt, getCurrentChatId, getRequestHeaders, getThumbnailUrl, saveMetadata, saveSettingsDebounced, this_chid } from '../script.js'; |
| 3 | 3 | import { openThirdPartyExtensionMenu, saveMetadataDebounced } from './extensions.js'; |
| 4 | 4 | import { SlashCommand } from './slash-commands/SlashCommand.js'; |
| 5 | 5 | import { SlashCommandParser } from './slash-commands/SlashCommandParser.js'; |
| 6 | 6 | import { createThumbnail, flashHighlight, getBase64Async, stringFormat, debounce, setupScrollToTop, saveBase64AsFile, getFileExtension } from './utils.js'; |
| 7 | 7 | import { debounce_timeout } from './constants.js'; |
| 8 | 8 | import { t } from './i18n.js'; |
| 9 | 9 | import { 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'; | |
| 10 | 13 | |
| 11 | 14 | const BG_METADATA_KEY = 'custom_background'; |
| 12 | 15 | const LIST_METADATA_KEY = 'chat_backgrounds'; |
| @@ -39,6 +42,26 @@ const THUMBNAIL_CONFIG = { | ||
| 39 | 42 | }; |
| 40 | 43 | |
| 41 | 44 | /** |
| 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 | +/** | |
| 42 | 65 | * Global IntersectionObserver instance for lazy loading backgrounds |
| 43 | 66 | * @type {IntersectionObserver|null} |
| 44 | 67 | */ |
| @@ -187,6 +210,7 @@ function onLockBackgroundClick(event = null) { | ||
| 187 | 210 | |
| 188 | 211 | // Update UI states to reflect the new lock. |
| 189 | 212 | highlightLockedBackground(); |
| 213 | + highlightSelectedBackground(); | |
| 190 | 214 | } |
| 191 | 215 | |
| 192 | 216 | /** |
| @@ -219,21 +243,27 @@ function removeBackgroundMetadata() { | ||
| 219 | 243 | saveMetadataDebounced(); |
| 220 | 244 | } |
| 221 | 245 | |
| 222 | -function onSelectBackgroundClick() { | |
| 246 | +/** | |
| 247 | + * Handles the click event for selecting a background. | |
| 248 | + * @param {JQuery.Event} e Event | |
| 249 | + */ | |
| 250 | +function onSelectBackgroundClick(e) { | |
| 223 | 251 | const bgFile = $(this).attr('bgfile'); |
| 252 | + const isCustom = $(this).attr('custom') === 'true'; | |
| 224 | 253 | const backgroundCssUrl = getUrlParameter(this); |
| 254 | + const bypassGlobalLock = !isCustom && e.shiftKey; | |
| 225 | 255 | |
| 226 | 256 | if ((isChatBackgroundLocked() || isCustom) && !bypassGlobalLock) { |
| 227 | 257 | // If a background is locked, update the locked background directly |
| 228 | 258 | saveBackgroundMetadata(backgroundCssUrl); |
| 229 | 259 | $('#bg1').css('background-image', backgroundCssUrl); |
| 230 | - highlightLockedBackground(); | |
| 231 | 260 | } else { |
| 232 | 261 | // Otherwise, update the global background setting |
| 233 | 262 | setBackground(bgFile, backgroundCssUrl); |
| 234 | 263 | } |
| 235 | 264 | |
| 236 | 265 | // Update UI highlights to reflect the changes. |
| 266 | + highlightLockedBackground(); | |
| 237 | 267 | highlightSelectedBackground(); |
| 238 | 268 | } |
| 239 | 269 | |
| @@ -375,20 +405,37 @@ async function onDeleteBackgroundClick(e) { | ||
| 375 | 405 | const bgToDelete = $(this).closest('.bg_example'); |
| 376 | 406 | const url = bgToDelete.data('url'); |
| 377 | 407 | const isCustom = bgToDelete.attr('custom') === 'true'; |
| 378 | - const confirm = await Popup.show.confirm(t`Delete the background?`, null); | |
| 408 | + const deleteFromServerId = 'delete_bg_from_server'; | |
| 409 | + const customInputs = [ | |
| 410 | + { | |
| 411 | + type: 'checkbox', | |
| 412 | + label: t`Also delete file from server`, | |
| 413 | + id: deleteFromServerId, | |
| 414 | + defaultState: true, | |
| 415 | + }, | |
| 416 | + ]; | |
| 417 | + let deleteFromServer = false; | |
| 418 | + const confirm = await Popup.show.confirm(t`Delete the background?`, null, { | |
| 419 | + customInputs: isCustom ? customInputs : [], | |
| 420 | + onClose: (popup) => { | |
| 421 | + if (isCustom) { | |
| 422 | + deleteFromServer = Boolean(popup?.inputResults?.get(deleteFromServerId) ?? false); | |
| 423 | + } | |
| 424 | + }, | |
| 425 | + }); | |
| 379 | 426 | const bg = bgToDelete.attr('bgfile'); |
| 380 | 427 | |
| 381 | 428 | if (confirm) { |
| 382 | 429 | // If it's not custom, it's a built-in background. Delete it from the server |
| 383 | 430 | if (!isCustom) { |
| 384 | 431 | await delBackground(bg); |
| 385 | 432 | } else { |
| 386 | 433 | const list = chat_metadata[LIST_METADATA_KEY] || []; |
| 387 | 434 | const index = list.indexOf(bg); |
| 388 | 435 | list.splice(index, 1); |
| 389 | 436 | } |
| 390 | 437 | |
| 391 | 438 | if (bg === background_settings.name || url === chat_metadata[BG_METADATA_KEY]) { |
| 392 | 439 | const siblingSelector = '.bg_example'; |
| 393 | 440 | const nextBg = bgToDelete.next(siblingSelector); |
| 394 | 441 | const prevBg = bgToDelete.prev(siblingSelector); |
| @@ -409,13 +456,18 @@ async function onDeleteBackgroundClick(e) { | ||
| 409 | 456 | |
| 410 | 457 | if (url === chat_metadata[BG_METADATA_KEY]) { |
| 411 | 458 | removeBackgroundMetadata(); |
| 412 | - highlightLockedBackground(); | |
| 413 | 459 | } |
| 414 | 460 | |
| 415 | 461 | if (isCustom) { |
| 462 | + if (deleteFromServer) { | |
| 463 | + await deleteMediaFromServer(bg); | |
| 464 | + } | |
| 416 | 465 | renderChatBackgrounds(); |
| 417 | 466 | saveMetadataDebouncedawait saveMetadata(); |
| 418 | 467 | } |
| 468 | + | |
| 469 | + highlightLockedBackground(); | |
| 470 | + highlightSelectedBackground(); | |
| 419 | 471 | } |
| 420 | 472 | } |
| 421 | 473 | |
| @@ -607,25 +659,43 @@ async function delBackground(bg) { | ||
| 607 | 659 | } |
| 608 | 660 | } |
| 609 | 661 | |
| 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; | |
| 612 | 669 | |
| 613 | 670 | if (!(forminput instanceof HTMLFormElementHTMLInputElement)) { |
| 614 | 671 | console.error('form_bg_uploadInvalid isinput notelement afor formbackground upload'); |
| 615 | 672 | return; |
| 616 | 673 | } |
| 617 | 674 | |
| 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; | |
| 624 | 678 | } |
| 625 | 679 | |
| 680 | + const formData = new FormData(); | |
| 681 | + formData.append('avatar', file); | |
| 682 | + | |
| 626 | 683 | await convertFileIfVideo(formData); |
| 684 | + switch (getActiveBackgroundTab()) { | |
| 685 | + case BG_SOURCES.GLOBAL: | |
| 627 | 686 | 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 = ''; | |
| 629 | 699 | } |
| 630 | 700 | |
| 631 | 701 | /** |
| @@ -699,6 +769,50 @@ async function uploadBackground(formData) { | ||
| 699 | 769 | } |
| 700 | 770 | |
| 701 | 771 | /** |
| 772 | + * Upload a chat background using a FormData object. | |
| 773 | + * @param {FormData} formData FormData containing the background file | |
| 774 | + * @returns {Promise<void>} | |
| 775 | + */ | |
| 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 | +/** | |
| 702 | 816 | * @param {string} bg |
| 703 | 817 | */ |
| 704 | 818 | function highlightNewBackground(bg) { |
| @@ -746,6 +860,14 @@ function onBackgroundFilterInput() { | ||
| 746 | 860 | |
| 747 | 861 | const debouncedOnBackgroundFilterInput = debounce(onBackgroundFilterInput, debounce_timeout.standard); |
| 748 | 862 | |
| 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 | + | |
| 749 | 871 | export function initBackgrounds() { |
| 750 | 872 | eventSource.on(event_types.CHAT_CHANGED, onChatChanged); |
| 751 | 873 | eventSource.on(event_types.FORCE_SET_BACKGROUND, forceSetBackground); |
| @@ -797,7 +919,7 @@ export function initBackgrounds() { | ||
| 797 | 919 | applyThumbnailColumns(background_settings.thumbnailColumns + 1); |
| 798 | 920 | }); |
| 799 | 921 | $('#auto_background').on('click', autoBackgroundCommand); |
| 800 | 922 | $('#add_bg_button').on('change', (e) => onBackgroundUploadSelected(e.originalEvent)); |
| 801 | 923 | $('#bg-filter').on('input', () => debouncedOnBackgroundFilterInput()); |
| 802 | 924 | SlashCommandParser.addCommandObject(SlashCommand.fromProps({ |
| 803 | 925 | name: 'lockbg', |
| @@ -839,9 +961,13 @@ export function initBackgrounds() { | ||
| 839 | 961 | await onChatChanged(); |
| 840 | 962 | }); |
| 841 | 963 | |
| 964 | + Object.values(BG_TABS).forEach(tabId => { | |
| 842 | 965 | setupScrollToTop({ |
| 843 | - scrollContainerId: 'bg-scrollable-content', | |
| 966 | + scrollContainerId: tabId, | |
| 844 | 967 | buttonId: 'bg-scroll-top', |
| 845 | 968 | drawerId: 'Backgrounds', |
| 846 | 969 | }); |
| 970 | + }); | |
| 971 | + | |
| 972 | + $('#bg_tabs').tabs(); | |
| 847 | 973 | } |
| @@ -11,6 +11,7 @@ import { | ||
| 11 | 11 | chat, |
| 12 | 12 | saveChatConditional, |
| 13 | 13 | saveItemizedPrompts, |
| 14 | + setActiveGroup, | |
| 14 | 15 | } from '../script.js'; |
| 15 | 16 | import { humanizedDateTime } from './RossAscends-mods.js'; |
| 16 | 17 | import { |
| @@ -298,28 +299,35 @@ export async function convertSoloToGroupChat() { | ||
| 298 | 299 | const chats = [chatName]; |
| 299 | 300 | const members = [character.avatar]; |
| 300 | 301 | const favChecked = character.fav || character.fav == 'true'; |
| 301 | 302 | /** @type {anyChatMetadata} */ |
| 302 | 303 | const metadata = Object.assign({}, chat_metadata); |
| 303 | 304 | delete metadata.main_chat; |
| 304 | - | |
| 305 | + /** @type {ChatHeader} */ | |
| 305 | - const createGroupResponse = await fetch('/api/groups/create', { | |
| 306 | + const chatHeader = { | |
| 306 | 307 | methodchat_metadata: 'POST'metadata, |
| 307 | 308 | headersuser_name: getRequestHeaders()'unused', |
| 308 | - body: JSON.stringify({ | |
| 309 | + character_name: 'unused', | |
| 310 | + }; | |
| 311 | + /** @type {Omit<Group, 'id'>} */ | |
| 312 | + const groupCreateModel = { | |
| 309 | 313 | name: name, |
| 310 | 314 | members: members, |
| 311 | 315 | avatar_url: avatar, |
| 312 | 316 | allow_self_responses: false, |
| 313 | 317 | activation_strategy: group_activation_strategy.NATURAL, |
| 314 | 318 | disabled_members: [], |
| 315 | - chat_metadata: metadata, | |
| 316 | 319 | fav: favChecked, |
| 317 | 320 | chat_id: chatName, |
| 318 | 321 | chats: chats, |
| 319 | 322 | hideMutedSprites: false, |
| 320 | 323 | generation_mode: group_generation_mode.SWAP, |
| 321 | 324 | 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), | |
| 323 | 331 | }); |
| 324 | 332 | |
| 325 | 333 | if (!createGroupResponse.ok) { |
| @@ -327,6 +335,7 @@ export async function convertSoloToGroupChat() { | ||
| 327 | 335 | return; |
| 328 | 336 | } |
| 329 | 337 | |
| 338 | + /** @type {Group} */ | |
| 330 | 339 | const group = await createGroupResponse.json(); |
| 331 | 340 | |
| 332 | 341 | // Convert tags list and assign to group |
| @@ -336,39 +345,34 @@ export async function convertSoloToGroupChat() { | ||
| 336 | 345 | await getCharacters(); |
| 337 | 346 | |
| 338 | 347 | // Convert chat to group format |
| 339 | 348 | const groupChat = [...chat].slicemap(m => structuredClone(m)); |
| 340 | 349 | const genIdFirst = Date.now(); |
| 341 | 350 | |
| 342 | 351 | for (let index = 0; index < groupChat.length; index++) { |
| 343 | 352 | const message = groupChat[index]; |
| 344 | 353 | |
| 345 | - // Save group-chat marker | |
| 346 | - if (index == 0) { | |
| 347 | - // @ts-ignore | |
| 348 | - message.is_group = true; | |
| 349 | - } | |
| 350 | - | |
| 351 | 354 | // Skip messages we don't care about |
| 352 | 355 | if (message.is_user || message.is_system || message.extra?.type === system_message_types.NARRATOR || message.force_avatar !== undefined) { |
| 353 | 356 | continue; |
| 354 | 357 | } |
| 355 | 358 | |
| 359 | + if (!message.extra || typeof message.extra !== 'object') { | |
| 360 | + message.extra = {}; | |
| 361 | + } | |
| 362 | + | |
| 356 | 363 | // Set force fields for solo character |
| 357 | 364 | message.name = character.name; |
| 358 | 365 | message.original_avatar = character.avatar; |
| 359 | 366 | message.force_avatar = getThumbnailUrl('avatar', character.avatar); |
| 360 | - | |
| 361 | 367 | // Allow regens of a single message in group |
| 362 | - if (typeof message.extra !== 'object') { | |
| 368 | + message.extra.gen_id = genIdFirst + index; | |
| 363 | - message.extra = { gen_id: genIdFirst + index }; | |
| 364 | - } | |
| 365 | 369 | } |
| 366 | 370 | |
| 367 | 371 | // Save group chat |
| 368 | 372 | const createChatResponse = await fetch('/api/chats/group/save', { |
| 369 | 373 | method: 'POST', |
| 370 | 374 | headers: getRequestHeaders(), |
| 371 | 375 | body: JSON.stringify({ id: chatName, chat: [chatHeader, ...groupChat] }), |
| 372 | 376 | }); |
| 373 | 377 | |
| 374 | 378 | if (!createChatResponse.ok) { |
| @@ -378,6 +382,7 @@ export async function convertSoloToGroupChat() { | ||
| 378 | 382 | } |
| 379 | 383 | |
| 380 | 384 | // Click on the freshly selected group to open it |
| 385 | + setActiveGroup(group.id); | |
| 381 | 386 | await openGroupById(group.id); |
| 382 | 387 | |
| 383 | 388 | toastr.success(t`The chat has been successfully converted!`); |
| @@ -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 | +} | |
| @@ -10,7 +10,6 @@ import { | ||
| 10 | 10 | event_types, |
| 11 | 11 | getCurrentChatId, |
| 12 | 12 | getRequestHeaders, |
| 13 | - name1, | |
| 14 | 13 | name2, |
| 15 | 14 | reloadCurrentChat, |
| 16 | 15 | saveSettingsDebounced, |
| @@ -349,8 +348,7 @@ async function validateFile(file) { | ||
| 349 | 348 | export function hasPendingFileAttachment() { |
| 350 | 349 | const fileInput = document.getElementById('file_form_input'); |
| 351 | 350 | if (!(fileInput instanceof HTMLInputElement)) return false; |
| 352 | 351 | const file =return fileInput.files[.length > 0]; |
| 353 | - return !!file; | |
| 354 | 352 | } |
| 355 | 353 | |
| 356 | 354 | /** |
| @@ -2140,13 +2138,15 @@ export function initChatUtilities() { | ||
| 2140 | 2138 | await viewMessageFile(messageId, fileIndex); |
| 2141 | 2139 | }); |
| 2142 | 2140 | |
| 2143 | 2141 | $(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, | |
| 2148 | 2144 | chat_metadata: chat_metadata, |
| 2149 | - }, | |
| 2145 | + user_name: 'unused', | |
| 2146 | + character_name: 'unused', | |
| 2147 | + }; | |
| 2148 | + const chatToSave = [ | |
| 2149 | + chatHeader, | |
| 2150 | 2150 | ...chat.filter(x => x?.extra?.type !== system_message_types.ASSISTANT_NOTE), |
| 2151 | 2151 | ]; |
| 2152 | 2152 | |
| @@ -2182,9 +2182,31 @@ export function initChatUtilities() { | ||
| 2182 | 2182 | fileInput.click(); |
| 2183 | 2183 | }); |
| 2184 | 2184 | |
| 2185 | + const fileInput = document.getElementById('file_form_input'); | |
| 2186 | + | |
| 2185 | 2187 | // Do not change. #attachFile is added by extension. |
| 2186 | 2188 | $(document).on('click', '#attachFile', function () { |
| 2187 | - $('#file_form_input').trigger('click'); | |
| 2189 | + if (!(fileInput instanceof HTMLInputElement)) return; | |
| 2190 | + const $fileInput = $(fileInput); | |
| 2191 | + | |
| 2192 | + // Preserve existing files in DataTransfer | |
| 2193 | + const dataTransfer = new DataTransfer(); | |
| 2194 | + for (const file of fileInput.files) { | |
| 2195 | + dataTransfer.items.add(file); | |
| 2196 | + } | |
| 2197 | + | |
| 2198 | + $fileInput.off('change').on('change', async () => { | |
| 2199 | + for (const file of fileInput.files) { | |
| 2200 | + if (!Array.from(dataTransfer.files).some(f => isSameFile(f, file))) { | |
| 2201 | + dataTransfer.items.add(file); | |
| 2202 | + } | |
| 2203 | + } | |
| 2204 | + | |
| 2205 | + fileInput.files = dataTransfer.files; | |
| 2206 | + await onFileAttach(fileInput.files); | |
| 2207 | + }); | |
| 2208 | + | |
| 2209 | + $fileInput.trigger('click'); | |
| 2188 | 2210 | }); |
| 2189 | 2211 | |
| 2190 | 2212 | // Do not change. #manageAttachments is added by extension. |
| @@ -2347,11 +2369,6 @@ export function initChatUtilities() { | ||
| 2347 | 2369 | await onImageSwiped(messageId, messageBlock, SWIPE_DIRECTION.RIGHT); |
| 2348 | 2370 | }); |
| 2349 | 2371 | |
| 2350 | - $('#file_form_input').on('change', async () => { | |
| 2351 | - const fileInput = document.getElementById('file_form_input'); | |
| 2352 | - if (!(fileInput instanceof HTMLInputElement)) return; | |
| 2353 | - await onFileAttach(fileInput.files); | |
| 2354 | - }); | |
| 2355 | 2372 | $('#file_form').on('reset', function () { |
| 2356 | 2373 | $('#file_form').addClass('displayNone'); |
| 2357 | 2374 | }); |
| @@ -2377,17 +2394,16 @@ export function initChatUtilities() { | ||
| 2377 | 2394 | * @returns {Promise<void>} |
| 2378 | 2395 | */ |
| 2379 | 2396 | async function handleFileAttach(files) { |
| 2380 | - const fileInput = document.getElementById('file_form_input'); | |
| 2381 | 2397 | if (!(fileInput instanceof HTMLInputElement)) return; |
| 2382 | 2398 | |
| 2383 | 2399 | // Workaround for Firefox: Use a DataTransfer object to indirectly set fileInput.files |
| 2384 | 2400 | const dataTransfer = new DataTransfer(); |
| 2385 | 2401 | for (let i = 0;const ifile <of filesfileInput.length; i++files) { |
| 2386 | 2402 | dataTransfer.items.add(files[i]file); |
| 2387 | 2403 | } |
| 2388 | 2404 | |
| 2389 | 2405 | // Preserve existing non-duplicate files in the input |
| 2390 | 2406 | for (const file of fileInput.files) { |
| 2391 | 2407 | if (!Array.from(dataTransfer.files).some(f => isSameFile(f, file))) { |
| 2392 | 2408 | dataTransfer.items.add(file); |
| 2393 | 2409 | } |
| @@ -1,5 +1,6 @@ | ||
| 1 | 1 | /** |
| 2 | 2 | * Common debounce timeout values to use with `debounce` calls. |
| 3 | + * @readonly | |
| 3 | 4 | * @enum {number} |
| 4 | 5 | */ |
| 5 | 6 | export const debounce_timeout = { |
| @@ -68,8 +69,8 @@ export const COMETAPI_IGNORE_PATTERNS = [ | ||
| 68 | 69 | ]; |
| 69 | 70 | |
| 70 | 71 | /** |
| 71 | - * @enum {string} | |
| 72 | 72 | * @readonly |
| 73 | + * @enum {string} | |
| 73 | 74 | */ |
| 74 | 75 | export const MEDIA_SOURCE = { |
| 75 | 76 | API: 'api', |
| @@ -79,8 +80,8 @@ export const MEDIA_SOURCE = { | ||
| 79 | 80 | }; |
| 80 | 81 | |
| 81 | 82 | /** |
| 82 | - * @enum {string} | |
| 83 | 83 | * @readonly |
| 84 | + * @enum {string} | |
| 84 | 85 | */ |
| 85 | 86 | export const MEDIA_DISPLAY = { |
| 86 | 87 | LIST: 'list', |
| @@ -88,8 +89,8 @@ export const MEDIA_DISPLAY = { | ||
| 88 | 89 | }; |
| 89 | 90 | |
| 90 | 91 | /** |
| 91 | - * @enum {string} | |
| 92 | 92 | * @readonly |
| 93 | + * @enum {string} | |
| 93 | 94 | */ |
| 94 | 95 | export const IMAGE_OVERSWIPE = { |
| 95 | 96 | GENERATE: 'generate', |
| @@ -118,9 +119,20 @@ export const MEDIA_TYPE = { | ||
| 118 | 119 | }; |
| 119 | 120 | |
| 120 | 121 | /** |
| 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 | +/** | |
| 121 | 133 | * Scroll behavior options when appending media to messages. |
| 122 | - * @enum {string} | |
| 123 | 134 | * @readonly |
| 135 | + * @enum {string} | |
| 124 | 136 | */ |
| 125 | 137 | export const SCROLL_BEHAVIOR = { |
| 126 | 138 | NONE: 'none', |
| @@ -129,9 +141,48 @@ export const SCROLL_BEHAVIOR = { | ||
| 129 | 141 | }; |
| 130 | 142 | |
| 131 | 143 | /** |
| 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} | |
| 133 | 163 | */ |
| 134 | 164 | export const SWIPE_DIRECTION = { |
| 135 | 165 | LEFT: 'left', |
| 136 | 166 | RIGHT: 'right', |
| 137 | 167 | }; |
| 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 | +}; | |
| @@ -1,9 +1,9 @@ | ||
| 1 | 1 | import { getPresetManager } from './preset-manager.js'; |
| 2 | 2 | import { extractJsonFromData, extractMessageFromData, getGenerateUrl, getRequestHeaders, name1, name2 } from '../script.js'; |
| 3 | 3 | import { getTextGenServer, createTextGenGenerationData, setting_names, textgenerationwebui_settings } from './textgen-settings.js'; |
| 4 | 4 | import { extractReasoningFromData } from './reasoning.js'; |
| 5 | 5 | import { formatInstructModeChat, formatInstructModePrompt, getInstructStoppingSequences, names_behavior_types } from './instruct-mode.js'; |
| 6 | 6 | import { getStreamingReply, tryParseStreamingError, createGenerationParameters, settingsToUpdate, oai_settings } from './openai.js'; |
| 7 | 7 | import EventSourceStream from './sse-stream.js'; |
| 8 | 8 | |
| 9 | 9 | // #region Type Definitions |
| @@ -34,6 +34,7 @@ import EventSourceStream from './sse-stream.js'; | ||
| 34 | 34 | |
| 35 | 35 | /** |
| 36 | 36 | * @typedef {Object} ChatCompletionMessage |
| 37 | + * @property {string} [name] - The name of the message author (optional) | |
| 37 | 38 | * @property {string} role - The role of the message author (e.g., "user", "assistant", "system") |
| 38 | 39 | * @property {string} content - The content of the message |
| 39 | 40 | */ |
| @@ -125,7 +126,7 @@ export class TextCompletionService { | ||
| 125 | 126 | |
| 126 | 127 | const json = await response.json(); |
| 127 | 128 | if (!response.ok || json.error) { |
| 128 | - throw json; | |
| 129 | + throw new Error(String(json.error?.message || 'Response not OK')); | |
| 129 | 130 | } |
| 130 | 131 | |
| 131 | 132 | if (!extractData) { |
| @@ -188,57 +189,27 @@ export class TextCompletionService { | ||
| 188 | 189 | } |
| 189 | 190 | |
| 190 | 191 | /** |
| 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. | |
| 192 | 193 | * @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} | |
| 201 | 196 | */ |
| 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 | - } | |
| 227 | 202 | } |
| 228 | 203 | |
| 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) { | |
| 237 | 204 | // Clone the preset to avoid modifying the original |
| 238 | 205 | 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; | |
| 242 | 213 | } |
| 243 | 214 | |
| 244 | 215 | // Format messages using instruct formatting |
| @@ -254,52 +225,76 @@ export class TextCompletionService { | ||
| 254 | 225 | // 2. If prefill is active, format all messages except the last one |
| 255 | 226 | if (!isLastMessage || !prefillActive) { |
| 256 | 227 | messageContent = formatInstructModeChat( |
| 257 | - message.role, | |
| 228 | + message.name ?? message.role, | |
| 258 | 229 | message.content, |
| 259 | 230 | message.role === 'user', |
| 260 | 231 | message.role === 'system', |
| 261 | 232 | undefined, |
| 262 | - undefined, | |
| 233 | + name1, // for macros | |
| 263 | - undefined, | |
| 234 | + name2, // for macros | |
| 264 | 235 | undefined, |
| 265 | 236 | instructPreset, |
| 266 | 237 | ); |
| 267 | 238 | } |
| 268 | 239 | |
| 269 | 240 | // Add prompt formatting for the last message. |
| 241 | + // e.g. "<|im_start|>assistant" | |
| 270 | 242 | 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, | |
| 279 | 250 | false, |
| 280 | 251 | instructPreset, |
| 281 | 252 | ); |
| 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 | - ); | |
| 297 | 261 | } |
| 298 | 262 | } |
| 299 | 263 | } |
| 300 | 264 | formattedMessages.push(messageContent); |
| 301 | 265 | } |
| 302 | 266 | 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); | |
| 303 | 298 | const stoppingStrings = getInstructStoppingSequences({ customInstruct: instructPreset, useStopStrings: false }); |
| 304 | 299 | requestData.stop = stoppingStrings; |
| 305 | 300 | requestData.stopping_strings = stoppingStrings; |
| @@ -307,18 +302,33 @@ export class TextCompletionService { | ||
| 307 | 302 | console.warn(`Instruct preset "${instructName}" not found, using basic formatting`); |
| 308 | 303 | requestData.prompt = prompt.map(x => x.content).join('\n\n'); |
| 309 | 304 | } |
| 305 | + } else { | |
| 306 | + requestData.prompt = prompt.map(x => x.content).join('\n\n'); | |
| 307 | + } | |
| 310 | 308 | } else if (typeof prompt === 'string') { |
| 311 | 309 | 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); | |
| 312 | 320 | } 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 | + } | |
| 314 | 326 | } |
| 315 | 327 | |
| 316 | - // @ts-ignore | |
| 328 | + const response = await this.sendRequest(requestData, extractData, signal); | |
| 317 | - const data = this.createRequestData(requestData); | |
| 318 | 329 | |
| 319 | - const response = await this.sendRequest(data, extractData, signal); | |
| 320 | 330 | // Remove stopping strings from the end |
| 321 | 331 | if (!datarequestData.stream && extractData) { |
| 322 | 332 | /** @type {ExtractedData} */ |
| 323 | 333 | // @ts-ignore |
| 324 | 334 | const extractedData = response; |
| @@ -377,31 +387,30 @@ export class TextCompletionService { | ||
| 377 | 387 | * Converts a preset to a valid text completion payload. |
| 378 | 388 | * Only supports temperature. |
| 379 | 389 | * @param {Object} preset - The preset configuration |
| 380 | 390 | * @param {Object} customPresetoverridePreset - Additional parameters to override preset values |
| 391 | + * @param {Object} overridePayload - Additional parameters to override payload values | |
| 381 | 392 | * @returns {Object} - Formatted payload for text completion API |
| 382 | 393 | */ |
| 383 | 394 | static presetToGeneratePayload(preset, customPresetoverridePreset = {}, overridePayload = {}) { |
| 384 | 395 | if (!preset || typeof preset !== 'object') { |
| 385 | 396 | throw new Error('Invalid preset: must be an object'); |
| 386 | 397 | } |
| 387 | 398 | |
| 388 | 399 | // Mergeapply preset with custom parametersoverrides |
| 389 | 400 | 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 | - }; | |
| 396 | 401 | |
| 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; | |
| 401 | 407 | } |
| 402 | - }); | |
| 403 | 408 | |
| 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 }); | |
| 405 | 414 | } |
| 406 | 415 | } |
| 407 | 416 | |
| @@ -427,8 +436,7 @@ export class ChatCompletionService { | ||
| 427 | 436 | reverse_proxy, |
| 428 | 437 | proxy_password, |
| 429 | 438 | custom_prompt_post_processing, |
| 430 | 439 | use_makersuite_syspromptuse_sysprompt: true, |
| 431 | - claude_use_sysprompt: true, | |
| 432 | 440 | ...props, |
| 433 | 441 | }; |
| 434 | 442 | |
| @@ -462,7 +470,7 @@ export class ChatCompletionService { | ||
| 462 | 470 | if (!data.stream) { |
| 463 | 471 | const json = await response.json(); |
| 464 | 472 | if (!response.ok || json.error) { |
| 465 | - throw json; | |
| 473 | + throw new Error(String(json.error?.message || 'Response not OK')); | |
| 466 | 474 | } |
| 467 | 475 | |
| 468 | 476 | if (!extractData) { |
| @@ -524,7 +532,7 @@ export class ChatCompletionService { | ||
| 524 | 532 | |
| 525 | 533 | /** |
| 526 | 534 | * Process and send a chat completion request with optional preset |
| 527 | 535 | * @param {ChatCompletionPayload} customrequestData - payload data, overriding preset if given |
| 528 | 536 | * @param {Object} options - Configuration options |
| 529 | 537 | * @param {string?} [options.presetName] - Name of the preset to use for generation settings |
| 530 | 538 | * @param {boolean} [extractData=true] - Whether to extract structured data from response |
| @@ -532,9 +540,9 @@ export class ChatCompletionService { | ||
| 532 | 540 | * @returns {Promise<ExtractedData | (() => AsyncGenerator<StreamResponse>)>} If not streaming, returns extracted data; if streaming, returns a function that creates an AsyncGenerator |
| 533 | 541 | * @throws {Error} |
| 534 | 542 | */ |
| 535 | 543 | static async processRequest(customrequestData, options, extractData = true, signal = null) { |
| 536 | 544 | const { presetName } = options; |
| 537 | 545 | let requestData = { ..this.custom }createRequestData(requestData); |
| 538 | 546 | |
| 539 | 547 | // Apply generation preset if specified |
| 540 | 548 | if (presetName) { |
| @@ -543,8 +551,7 @@ export class ChatCompletionService { | ||
| 543 | 551 | const preset = presetManager.getCompletionPresetByName(presetName); |
| 544 | 552 | if (preset) { |
| 545 | 553 | // Convert preset to payload and merge with custom parameters |
| 546 | 554 | const presetPayloadrequestData = await this.presetToGeneratePayload(preset, {}, requestData); |
| 547 | - requestData = { ...presetPayload, ...requestData }; | |
| 548 | 555 | } else { |
| 549 | 556 | console.warn(`Preset "${presetName}" not found, continuing with default settings`); |
| 550 | 557 | } |
| @@ -553,38 +560,47 @@ export class ChatCompletionService { | ||
| 553 | 560 | } |
| 554 | 561 | } |
| 555 | 562 | |
| 556 | 563 | const datareturn =await this.createRequestDatasendRequest(requestData, extractData, signal); |
| 557 | - | |
| 558 | - return await this.sendRequest(data, extractData, signal); | |
| 559 | 564 | } |
| 560 | 565 | |
| 561 | 566 | /** |
| 562 | 567 | * Converts a preset to a valid chat completion payload |
| 563 | 568 | * Only supports temperature. |
| 564 | 569 | * @param {Object} preset - The preset configuration |
| 565 | 570 | * @param {Object} customParamsoverridePreset - Additional parameters to override preset values |
| 566 | 571 | * @returnsparam {Object} overridePayload - FormattedAdditional payloadparameters forto chatoverride completionpayload APIvalues |
| 572 | + * @returns {Promise<any>} - Formatted payload for chat completion API | |
| 567 | 573 | */ |
| 568 | 574 | static async presetToGeneratePayload(preset, customParamsoverridePreset = {}, overridePayload = {}) { |
| 569 | 575 | if (!preset || typeof preset !== 'object') { |
| 570 | 576 | throw new Error('Invalid preset: must be an object'); |
| 571 | 577 | } |
| 572 | 578 | |
| 573 | 579 | // Mergeapply preset with custom parametersoverrides |
| 574 | 580 | const settingspreset = { ...preset, ...customParamsoverridePreset }; |
| 575 | 581 | |
| 576 | 582 | // 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 | - }; | |
| 580 | 584 | |
| 581 | 585 | // 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)) { | |
| 584 | 588 | delete const payloadsettingToUpdate = settingsToUpdate[key]; |
| 589 | + if (!settingToUpdate) continue; | |
| 590 | + settings[settingToUpdate[1]] = value; | |
| 585 | 591 | } |
| 592 | + | |
| 593 | + // Ensure api-url is properly applied for all sources that accept it | |
| 594 | + ['custom_url', 'vertexai_region', 'zai_endpoint'].forEach(field => { | |
| 595 | + // The order is: connection profile => CC preset => CC settings | |
| 596 | + overridePayload[field] = overridePayload[field] || settings[field] || oai_settings[field]; | |
| 586 | 597 | }); |
| 587 | 598 | |
| 588 | - return payload; | |
| 599 | + // Convert from settings to generation payload | |
| 600 | + const data = await createGenerationParameters(settings, overridePayload.model, 'quiet', overridePayload.messages); | |
| 601 | + const payload = data.generate_data; | |
| 602 | + | |
| 603 | + // apply overrides | |
| 604 | + return this.createRequestData({ ...payload, ...overridePayload }); | |
| 589 | 605 | } |
| 590 | 606 | } |
| @@ -68,7 +68,7 @@ class DataMaidDialog { | ||
| 68 | 68 | async getReport() { |
| 69 | 69 | const response = await fetch('/api/data-maid/report', { |
| 70 | 70 | method: 'POST', |
| 71 | 71 | headers: getRequestHeaders({ omitContentType: true }), |
| 72 | 72 | }); |
| 73 | 73 | |
| 74 | 74 | if (!response.ok) { |
| @@ -91,6 +91,7 @@ export const event_types = { | ||
| 91 | 91 | PRESET_RENAMED_BEFORE: 'preset_renamed_before', |
| 92 | 92 | MAIN_API_CHANGED: 'main_api_changed', |
| 93 | 93 | WORLDINFO_ENTRIES_LOADED: 'worldinfo_entries_loaded', |
| 94 | + WORLDINFO_SCAN_DONE: 'worldinfo_scan_done', | |
| 94 | 95 | MEDIA_ATTACHMENT_DELETED: 'media_attachment_deleted', |
| 95 | 96 | }; |
| 96 | 97 | |
| @@ -139,8 +139,8 @@ async function downloadAssetsList(url) { | ||
| 139 | 139 | assetTypeMenu.append(await renderExtensionTemplateAsync('assets', 'installation')); |
| 140 | 140 | } |
| 141 | 141 | |
| 142 | 142 | for (const iasset inof availableAssets[assetType].sort((a, b) => a?.name && b?.name && a['name'].localeCompare(b['name']))) { |
| 143 | 143 | const asseti = availableAssets[assetType][i].indexOf(asset); |
| 144 | 144 | const elemId = `assets_install_${assetType}_${i}`; |
| 145 | 145 | let element = $('<div />', { id: elemId, class: 'asset-download-button right_menu_button' }); |
| 146 | 146 | const label = $('<i class="fa-fw fa-solid fa-download fa-lg"></i>'); |
| @@ -425,7 +425,7 @@ async function updateCurrentAssets() { | ||
| 425 | 425 | try { |
| 426 | 426 | const result = await fetch('/api/assets/get', { |
| 427 | 427 | method: 'POST', |
| 428 | 428 | headers: getRequestHeaders({ omitContentType: true }), |
| 429 | 429 | }); |
| 430 | 430 | currentAssets = result.ok ? (await result.json()) : {}; |
| 431 | 431 | } |
| @@ -218,7 +218,7 @@ function cleanUpAttachments() { | ||
| 218 | 218 | |
| 219 | 219 | /** |
| 220 | 220 | * Clean up character attachments when a character is deleted. |
| 221 | 221 | * @param {{character: import('../../char-data.js').v1CharDataCharacter}} data Event data |
| 222 | 222 | */ |
| 223 | 223 | function cleanUpCharacterAttachments(data) { |
| 224 | 224 | const avatar = data?.character?.avatar; |
| @@ -10,7 +10,7 @@ import { SlashCommand } from '../../slash-commands/SlashCommand.js'; | ||
| 10 | 10 | import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from '../../slash-commands/SlashCommandArgument.js'; |
| 11 | 11 | import { commonEnumProviders } from '../../slash-commands/SlashCommandCommonEnumsProvider.js'; |
| 12 | 12 | import { callGenericPopup, Popup, POPUP_TYPE } from '../../popup.js'; |
| 13 | 13 | import { debounce_timeout, MEDIA_DISPLAY, MEDIA_SOURCE, MEDIA_TYPE, SCROLL_BEHAVIOR } from '../../constants.js'; |
| 14 | 14 | export { MODULE_NAME }; |
| 15 | 15 | |
| 16 | 16 | const MODULE_NAME = 'caption'; |
| @@ -108,7 +108,7 @@ async function wrapCaptionTemplate(caption) { | ||
| 108 | 108 | 'Review and edit the generated caption:', |
| 109 | 109 | 'Press "Cancel" to abort the caption sending.', |
| 110 | 110 | messageText, |
| 111 | 111 | { rows: 58, okButton: 'Send' }); |
| 112 | 112 | |
| 113 | 113 | if (!messageText) { |
| 114 | 114 | throw new Error('User aborted the caption sending.'); |
| @@ -211,6 +211,7 @@ async function sendCaptionedMessage(caption, image, mimeType) { | ||
| 211 | 211 | context.addOneMessage(message); |
| 212 | 212 | await eventSource.emit(event_types.USER_MESSAGE_RENDERED, messageId); |
| 213 | 213 | await context.saveChat(); |
| 214 | + setTimeout(() => context.scrollOnMediaLoad(), debounce_timeout.short); | |
| 214 | 215 | } |
| 215 | 216 | |
| 216 | 217 | /** |
| @@ -454,7 +455,7 @@ function isVideoCaptioningAvailable() { | ||
| 454 | 455 | return false; |
| 455 | 456 | } |
| 456 | 457 | |
| 457 | 458 | return ['google', 'vertexai', 'zai'].includes(extension_settings.caption.multimodal_api); |
| 458 | 459 | } |
| 459 | 460 | |
| 460 | 461 | jQuery(async function () { |
| @@ -503,6 +504,7 @@ jQuery(async function () { | ||
| 503 | 504 | 'aimlapi': SECRET_KEYS.AIMLAPI, |
| 504 | 505 | 'moonshot': SECRET_KEYS.MOONSHOT, |
| 505 | 506 | 'nanogpt': SECRET_KEYS.NANOGPT, |
| 507 | + 'chutes': SECRET_KEYS.CHUTES, | |
| 506 | 508 | 'electronhub': SECRET_KEYS.ELECTRONHUB, |
| 507 | 509 | 'zai': SECRET_KEYS.ZAI, |
| 508 | 510 | }; |
| @@ -592,7 +594,7 @@ jQuery(async function () { | ||
| 592 | 594 | const options = Array.from(dropdown.options); |
| 593 | 595 | const response = await fetch(url, { |
| 594 | 596 | method: 'POST', |
| 595 | 597 | headers: getRequestHeaders({ omitContentType: true }), |
| 596 | 598 | }); |
| 597 | 599 | if (!response.ok) { |
| 598 | 600 | return; |
| @@ -616,6 +618,7 @@ jQuery(async function () { | ||
| 616 | 618 | await processEndpoint('aimlapi', '/api/backends/chat-completions/multimodal-models/aimlapi'); |
| 617 | 619 | await processEndpoint('pollinations', '/api/backends/chat-completions/multimodal-models/pollinations'); |
| 618 | 620 | await processEndpoint('nanogpt', '/api/backends/chat-completions/multimodal-models/nanogpt'); |
| 621 | + await processEndpoint('chutes', '/api/backends/chat-completions/multimodal-models/chutes'); | |
| 619 | 622 | await processEndpoint('electronhub', '/api/backends/chat-completions/multimodal-models/electronhub'); |
| 620 | 623 | await processEndpoint('mistral', '/api/backends/chat-completions/multimodal-models/mistral'); |
| 621 | 624 | await processEndpoint('xai', '/api/backends/chat-completions/multimodal-models/xai'); |
| @@ -18,6 +18,7 @@ | ||
| 18 | 18 | <label for="caption_multimodal_api" data-i18n="API">API</label> |
| 19 | 19 | <select id="caption_multimodal_api" class="flex1 text_pole"> |
| 20 | 20 | <option value="aimlapi">AI/ML API</option> |
| 21 | + <option value="chutes">Chutes</option> | |
| 21 | 22 | <option value="anthropic">Claude</option> |
| 22 | 23 | <option value="cohere">Cohere</option> |
| 23 | 24 | <option value="custom" data-i18n="Custom (OpenAI-compatible)">Custom (OpenAI-compatible)</option> |
| @@ -55,6 +56,9 @@ | ||
| 55 | 56 | <option data-type="moonshot" value="moonshot-v1-8k-vision-preview">moonshot-v1-8k-vision-preview</option> |
| 56 | 57 | <option data-type="moonshot" value="moonshot-v1-32k-vision-preview">moonshot-v1-32k-vision-preview</option> |
| 57 | 58 | <option data-type="moonshot" value="moonshot-v1-128k-vision-preview">moonshot-v1-128k-vision-preview</option> |
| 59 | + <option data-type="openai" value="gpt-5.2">gpt-5.2</option> | |
| 60 | + <option data-type="openai" value="gpt-5.2-2025-12-11">gpt-5.2-2025-12-11</option> | |
| 61 | + <option data-type="openai" value="gpt-5.2-chat-latest">gpt-5.2-chat-latest</option> | |
| 58 | 62 | <option data-type="openai" value="gpt-5.1">gpt-5.1</option> |
| 59 | 63 | <option data-type="openai" value="gpt-5.1-2025-11-13">gpt-5.1-2025-11-13</option> |
| 60 | 64 | <option data-type="openai" value="gpt-5.1-chat-latest">gpt-5.1-chat-latest</option> |
| @@ -85,6 +89,8 @@ | ||
| 85 | 89 | <option data-type="openai" value="o4-mini-2025-04-16">o4-mini-2025-04-16</option> |
| 86 | 90 | <option data-type="openai" value="gpt-4.5-preview">gpt-4.5-preview</option> |
| 87 | 91 | <option data-type="openai" value="gpt-4.5-preview-2025-02-27">gpt-4.5-preview-2025-02-27</option> |
| 92 | + <option data-type="anthropic" value="claude-opus-4-5">claude-opus-4-5</option> | |
| 93 | + <option data-type="anthropic" value="claude-opus-4-5-20251101">claude-opus-4-5-20251101</option> | |
| 88 | 94 | <option data-type="anthropic" value="claude-sonnet-4-5">claude-sonnet-4-5</option> |
| 89 | 95 | <option data-type="anthropic" value="claude-sonnet-4-5-20250929">claude-sonnet-4-5-20250929</option> |
| 90 | 96 | <option data-type="anthropic" value="claude-haiku-4-5">claude-haiku-4-5</option> |
| @@ -106,6 +112,7 @@ | ||
| 106 | 112 | <option data-type="anthropic" value="claude-3-haiku-20240307">claude-3-haiku-20240307</option> |
| 107 | 113 | <option data-type="google" value="gemini-3-pro-preview">gemini-3-pro-preview</option> |
| 108 | 114 | <option data-type="google" value="gemini-3-pro-image-preview">gemini-3-pro-image-preview</option> |
| 115 | + <option data-type="google" value="gemini-3-flash-preview">gemini-3-flash-preview</option> | |
| 109 | 116 | <option data-type="google" value="gemini-2.5-pro">gemini-2.5-pro</option> |
| 110 | 117 | <option data-type="google" value="gemini-2.5-pro-preview-06-05">gemini-2.5-pro-preview-06-05</option> |
| 111 | 118 | <option data-type="google" value="gemini-2.5-pro-preview-05-06">gemini-2.5-pro-preview-05-06</option> |
| @@ -135,6 +142,7 @@ | ||
| 135 | 142 | <option data-type="google" value="gemini-robotics-er-1.5-preview">gemini-robotics-er-1.5-preview</option> |
| 136 | 143 | <option data-type="vertexai" value="gemini-3-pro-preview">gemini-3-pro-preview</option> |
| 137 | 144 | <option data-type="vertexai" value="gemini-3-pro-image-preview">gemini-3-pro-image-preview</option> |
| 145 | + <option data-type="vertexai" value="gemini-3-flash-preview">gemini-3-flash-preview</option> | |
| 138 | 146 | <option data-type="vertexai" value="gemini-2.5-pro">gemini-2.5-pro</option> |
| 139 | 147 | <option data-type="vertexai" value="gemini-2.5-flash">gemini-2.5-flash</option> |
| 140 | 148 | <option data-type="vertexai" value="gemini-2.5-flash-lite">gemini-2.5-flash-lite</option> |
| @@ -159,7 +167,11 @@ | ||
| 159 | 167 | <option data-type="ollama" value="mistral-small3.2">mistral-small3.2</option> |
| 160 | 168 | <option data-type="ollama" value="llama3.2-vision">llama3.2-vision</option> |
| 161 | 169 | <option data-type="ollama" value="llama4">llama4</option> |
| 170 | + <option data-type="zai" value="glm-4.6v">glm-4.6v</option> | |
| 171 | + <option data-type="zai" value="glm-4.6v-flashx">glm-4.6v-flashx</option> | |
| 172 | + <option data-type="zai" value="glm-4.6v-flash">glm-4.6v-flash</option> | |
| 162 | 173 | <option data-type="zai" value="glm-4.5v">glm-4.5v</option> |
| 174 | + <option data-type="zai" value="autoglm-phone-multilingual">autoglm-phone-multilingual</option> | |
| 163 | 175 | <option data-type="llamacpp" value="llamacpp_current" data-i18n="currently_loaded">[Currently loaded]</option> |
| 164 | 176 | <option data-type="ooba" value="ooba_current" data-i18n="currently_loaded">[Currently loaded]</option> |
| 165 | 177 | <option data-type="koboldcpp" value="koboldcpp_current" data-i18n="currently_loaded">[Currently loaded]</option> |
| @@ -167,9 +179,6 @@ | ||
| 167 | 179 | <option data-type="custom" value="custom_current" data-i18n="currently_selected">[Currently selected]</option> |
| 168 | 180 | </select> |
| 169 | 181 | </div> |
| 170 | - <div data-type="zai"> | |
| 171 | - <b>Will use Common API. Coding API is not supported!</b> | |
| 172 | - </div> | |
| 173 | 182 | <div data-type="ollama"> |
| 174 | 183 | <div> |
| 175 | 184 | The model must be downloaded first! Do it with the <code>ollama pull</code> command or <a href="#" id="caption_ollama_pull">click here</a>. |
| @@ -667,7 +667,14 @@ export async function sendExpressionCall(spriteFolderName, expression, { force = | ||
| 667 | 667 | } |
| 668 | 668 | } |
| 669 | 669 | |
| 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) { | |
| 671 | 678 | if (!folder) { |
| 672 | 679 | console.log('Clearing sprite set'); |
| 673 | 680 | folder = ''; |
| @@ -675,8 +682,12 @@ async function setSpriteFolderCommand(_, folder) { | ||
| 675 | 682 | |
| 676 | 683 | if (folder.startsWith('/') || folder.startsWith('\\')) { |
| 677 | 684 | const currentLastMessage = getLastCharacterMessage(); |
| 685 | + if (currentLastMessage.name === null && !name) { | |
| 686 | + toastr.error('At least one character message is required to set a sprites subfolder.', 'Provide the name with "name=" argument.'); | |
| 687 | + return ''; | |
| 688 | + } | |
| 678 | 689 | folder = folder.slice(1); |
| 679 | 690 | folder = `${name || currentLastMessage.name}/${folder}`; |
| 680 | 691 | } |
| 681 | 692 | |
| 682 | 693 | $('#expression_override').val(folder.trim()); |
| @@ -1414,7 +1425,7 @@ export async function getExpressionsList({ filterAvailable = false } = {}) { | ||
| 1414 | 1425 | if (extension_settings.expressions.api == EXPRESSION_API.local) { |
| 1415 | 1426 | const apiResult = await fetch('/api/extra/classify/labels', { |
| 1416 | 1427 | method: 'POST', |
| 1417 | 1428 | headers: getRequestHeaders({ omitContentType: true }), |
| 1418 | 1429 | }); |
| 1419 | 1430 | |
| 1420 | 1431 | if (apiResult.ok) { |
| @@ -2322,6 +2333,16 @@ function migrateSettings() { | ||
| 2322 | 2333 | name: 'expression-folder-override', |
| 2323 | 2334 | aliases: ['spriteoverride', 'costume'], |
| 2324 | 2335 | callback: setSpriteFolderCommand, |
| 2336 | + namedArgumentList:[ | |
| 2337 | + SlashCommandNamedArgument.fromProps({ | |
| 2338 | + name: 'name', | |
| 2339 | + description: 'Character name to set a subfolder for. If not provided, the character who last sent a message will be used.', | |
| 2340 | + typeList: [ARGUMENT_TYPE.STRING], | |
| 2341 | + enumProvider: commonEnumProviders.characters('character'), | |
| 2342 | + isRequired: false, | |
| 2343 | + acceptsMultiple: false, | |
| 2344 | + }), | |
| 2345 | + ], | |
| 2325 | 2346 | unnamedArgumentList: [ |
| 2326 | 2347 | new SlashCommandArgument( |
| 2327 | 2348 | 'optional folder', [ARGUMENT_TYPE.STRING], false, |
| @@ -8,7 +8,7 @@ import { | ||
| 8 | 8 | animation_easing, |
| 9 | 9 | } from '../../../script.js'; |
| 10 | 10 | import { groups, selected_group } from '../../group-chats.js'; |
| 11 | 11 | import { loadFileToDocument, delay, getBase64Async, getSanitizedFilename, saveBase64AsFile, getFileExtension, getVideoThumbnail, clamp } from '../../utils.js'; |
| 12 | 12 | import { loadMovingUIState } from '../../power-user.js'; |
| 13 | 13 | import { dragElement } from '../../RossAscends-mods.js'; |
| 14 | 14 | import { SlashCommandParser } from '../../slash-commands/SlashCommandParser.js'; |
| @@ -19,17 +19,14 @@ import { commonEnumProviders } from '../../slash-commands/SlashCommandCommonEnum | ||
| 19 | 19 | import { t, translate } from '../../i18n.js'; |
| 20 | 20 | import { Popup } from '../../popup.js'; |
| 21 | 21 | import { deleteMediaFromServer } from '../../chats.js'; |
| 22 | +import { MEDIA_REQUEST_TYPE, VIDEO_EXTENSIONS } from '../../constants.js'; | |
| 22 | 23 | |
| 24 | +const isVideo = (/** @type {string} */ url) => VIDEO_EXTENSIONS.some(ext => new RegExp(`.${ext}$`, 'i').test(url)); | |
| 23 | 25 | const extensionName = 'gallery'; |
| 24 | 26 | const extensionFolderPath = `scripts/extensions/${extensionName}/`; |
| 25 | 27 | let firstTime = true; |
| 26 | 28 | let deleteModeActive = false; |
| 27 | 29 | |
| 28 | -// Exposed defaults for future tweaking | |
| 29 | -let thumbnailHeight = 150; | |
| 30 | -let paginationVisiblePages = 10; | |
| 31 | -let paginationMaxLinesPerPage = 2; | |
| 32 | -let galleryMaxRows = 3; | |
| 33 | 30 | |
| 34 | 31 | // Remove all draggables associated with the gallery |
| 35 | 32 | $('#movingDivs').on('click', '.dragClose', function () { |
| @@ -98,7 +95,7 @@ function initSettings() { | ||
| 98 | 95 | |
| 99 | 96 | /** |
| 100 | 97 | * Retrieves the gallery folder for a given character. |
| 101 | 98 | * @param {import('../../char-data.js').v1CharDataCharacter} char Character data |
| 102 | 99 | * @returns {string} The gallery folder for the character |
| 103 | 100 | */ |
| 104 | 101 | function getGalleryFolder(char) { |
| @@ -122,17 +119,34 @@ async function getGalleryItems(url) { | ||
| 122 | 119 | folder: url, |
| 123 | 120 | sortField: sortObj.field, |
| 124 | 121 | sortOrder: sortObj.order, |
| 122 | + type: MEDIA_REQUEST_TYPE.IMAGE | MEDIA_REQUEST_TYPE.VIDEO, | |
| 125 | 123 | }), |
| 126 | 124 | }); |
| 127 | 125 | |
| 128 | 126 | url = await getSanitizedFilename(url); |
| 129 | 127 | |
| 130 | 128 | const data = await response.json(); |
| 131 | - const items = data.map((file) => ({ | |
| 129 | + const items = []; | |
| 130 | + | |
| 131 | + for (const file of data) { | |
| 132 | + const item = { | |
| 132 | 133 | src: `user/images/${url}/${file}`, |
| 133 | 134 | srct: `user/images/${url}/${file}`, |
| 134 | 135 | title: '', // Optional title for each item |
| 135 | 136 | })); |
| 137 | + | |
| 138 | + if (isVideo(file)) { | |
| 139 | + try { | |
| 140 | + // 150px of max height with some allowance for various aspect ratios | |
| 141 | + const maxSide = Math.round(150 * 1.5); | |
| 142 | + item.srct = await getVideoThumbnail(item.src, maxSide, maxSide); | |
| 143 | + } catch (error) { | |
| 144 | + console.error('Failed to generate video thumbnail for gallery:', error); | |
| 145 | + } | |
| 146 | + } | |
| 147 | + | |
| 148 | + items.push(item); | |
| 149 | + } | |
| 136 | 150 | |
| 137 | 151 | return items; |
| 138 | 152 | } |
| @@ -145,7 +159,7 @@ async function getGalleryFolders() { | ||
| 145 | 159 | try { |
| 146 | 160 | const response = await fetch('/api/images/folders', { |
| 147 | 161 | method: 'POST', |
| 148 | 162 | headers: getRequestHeaders({ omitContentType: true }), |
| 149 | 163 | }); |
| 150 | 164 | |
| 151 | 165 | if (!response.ok) { |
| @@ -198,6 +212,12 @@ function getSortOrder() { | ||
| 198 | 212 | * @returns {Promise<void>} - Promise representing the completion of the gallery initialization. |
| 199 | 213 | */ |
| 200 | 214 | async function initGallery(items, url) { |
| 215 | + // Exposed defaults for future tweaking | |
| 216 | + const thumbnailHeight = 150; | |
| 217 | + const paginationVisiblePages = 5; | |
| 218 | + const paginationMaxLinesPerPage = 2; | |
| 219 | + const galleryMaxRows = clamp(Math.floor((window.innerHeight * 0.9 - 75) / thumbnailHeight), 1, 10); | |
| 220 | + | |
| 201 | 221 | const nonce = `nonce-${Math.random().toString(36).substring(2, 15)}`; |
| 202 | 222 | const gallery = $('#dragGallery'); |
| 203 | 223 | gallery.addClass(nonce); |
| @@ -210,6 +230,7 @@ async function initGallery(items, url) { | ||
| 210 | 230 | galleryMaxRows: galleryMaxRows, |
| 211 | 231 | galleryPaginationTopButtons: false, |
| 212 | 232 | galleryNavigationOverlayButtons: true, |
| 233 | + galleryPaginationMode: 'rectangles', | |
| 213 | 234 | galleryTheme: { |
| 214 | 235 | navigationBar: { background: 'none', borderTop: '', borderBottom: '', borderRight: '', borderLeft: '' }, |
| 215 | 236 | navigationBreadcrumb: { background: '#111', color: '#fff', colorHover: '#ccc', borderRadius: '4px' }, |
| @@ -399,7 +420,7 @@ async function makeMovable(url) { | ||
| 399 | 420 | // Create a hidden file input |
| 400 | 421 | const fileInput = document.createElement('input'); |
| 401 | 422 | fileInput.type = 'file'; |
| 402 | 423 | fileInput.accept = 'image/*,video/*'; |
| 403 | 424 | fileInput.multiple = true; |
| 404 | 425 | fileInput.style.display = 'none'; |
| 405 | 426 | |
| @@ -617,12 +638,19 @@ function makeDragImg(id, url) { | ||
| 617 | 638 | const newElement = document.importNode(template.content, true); |
| 618 | 639 | |
| 619 | 640 | // Step 2: Append the given image |
| 620 | 641 | 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 | + | |
| 622 | 650 | let uniqueId = `draggable_${id}`; |
| 623 | 651 | const draggableElem = /** @type {HTMLElement} */ (newElement.querySelector('.draggable')); |
| 624 | 652 | if (draggableElem) { |
| 625 | 653 | draggableElem.appendChild(imgElemmediaElement); |
| 626 | 654 | |
| 627 | 655 | // Find a unique id for the draggable element |
| 628 | 656 | |
| @@ -49,3 +49,7 @@ | ||
| 49 | 49 | opacity: 1; |
| 50 | 50 | filter: unset; |
| 51 | 51 | } |
| 52 | + | |
| 53 | +.galleryImageDraggable video { | |
| 54 | + width: 100%; | |
| 55 | +} | |
| @@ -19,17 +19,18 @@ import { | ||
| 19 | 19 | animation_easing, |
| 20 | 20 | } from '../../../script.js'; |
| 21 | 21 | import { is_group_generating, selected_group } from '../../group-chats.js'; |
| 22 | 22 | import { loadMovingUIState, power_user } from '../../power-user.js'; |
| 23 | 23 | import { dragElement } from '../../RossAscends-mods.js'; |
| 24 | 24 | import { getTextTokens, getTokenCountAsync, tokenizers } from '../../tokenizers.js'; |
| 25 | 25 | import { debounce_timeout } from '../../constants.js'; |
| 26 | 26 | import { SlashCommandParser } from '../../slash-commands/SlashCommandParser.js'; |
| 27 | 27 | import { SlashCommand } from '../../slash-commands/SlashCommand.js'; |
| 28 | 28 | import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from '../../slash-commands/SlashCommandArgument.js'; |
| 29 | 29 | import { MacrosParsermacros, MacroCategory } from '../../macros/macro-system.js'; |
| 30 | 30 | import { countWebLlmTokens, generateWebLlmChatPrompt, getWebLlmContextSize, isWebLlmSupported } from '../shared.js'; |
| 31 | 31 | import { commonEnumProviders } from '../../slash-commands/SlashCommandCommonEnumsProvider.js'; |
| 32 | 32 | import { removeReasoningFromString } from '../../reasoning.js'; |
| 33 | +import { MacrosParser } from '/scripts/macros.js'; | |
| 33 | 34 | export { MODULE_NAME }; |
| 34 | 35 | |
| 35 | 36 | const MODULE_NAME = '1_memory'; |
| @@ -1094,5 +1095,16 @@ jQuery(async function () { | ||
| 1094 | 1095 | returns: ARGUMENT_TYPE.STRING, |
| 1095 | 1096 | })); |
| 1096 | 1097 | |
| 1097 | - MacrosParser.registerMacro('summary', () => getLatestMemoryFromChat(getContext().chat)); | |
| 1098 | + if (power_user.experimental_macro_engine) { | |
| 1099 | + macros.register('summary', { | |
| 1100 | + category: MacroCategory.CHAT, | |
| 1101 | + description: 'Returns the latest memory/summary from the current chat.', | |
| 1102 | + handler: () => getLatestMemoryFromChat(getContext().chat), | |
| 1103 | + }); | |
| 1104 | + } else { | |
| 1105 | + // TODO: Remove this when the experimental macro engine is replacing the old macro engine | |
| 1106 | + MacrosParser.registerMacro('summary', | |
| 1107 | + () => getLatestMemoryFromChat(getContext().chat), | |
| 1108 | + 'Returns the latest memory/summary from the current chat.'); | |
| 1109 | + } | |
| 1098 | 1110 | }); |
| @@ -152,7 +152,7 @@ const handleCharChange = () => { | ||
| 152 | 152 | lastCharId = this_chid; |
| 153 | 153 | |
| 154 | 154 | // If no character is loaded, there's nothing more to do. |
| 155 | 155 | /** @type {import('../../char-data.js').v1CharDataCharacter} */ |
| 156 | 156 | const character = characters[this_chid]; |
| 157 | 157 | if (!character || selected_group) { |
| 158 | 158 | return; |
| @@ -5,8 +5,8 @@ import { regexFromString } from '../../utils.js'; | ||
| 5 | 5 | import { lodash } from '../../../lib.js'; |
| 6 | 6 | |
| 7 | 7 | /** |
| 8 | - * @enum {number} Regex scripts types | |
| 9 | 8 | * @readonly |
| 9 | + * @enum {number} Regex scripts types | |
| 10 | 10 | */ |
| 11 | 11 | export const SCRIPT_TYPES = { |
| 12 | 12 | // ORDER MATTERS: defines the regex script priority |
| @@ -35,6 +35,61 @@ export const SCRIPT_TYPE_UNKNOWN = -1; | ||
| 35 | 35 | const DEFAULT_GET_REGEX_SCRIPTS_OPTIONS = Object.freeze({ allowedOnly: false }); |
| 36 | 36 | |
| 37 | 37 | /** |
| 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 | +/** | |
| 38 | 93 | * Retrieves the list of regex scripts by combining the scripts from the extension settings and the character data |
| 39 | 94 | * |
| 40 | 95 | * @param {GetRegexScriptsOptions} options Options for retrieving the regex scripts |
| @@ -105,7 +160,7 @@ export async function saveScriptsByType(scripts, scriptType) { | ||
| 105 | 160 | |
| 106 | 161 | /** |
| 107 | 162 | * Check if character's regexes are allowed to be used; if character is undefined, returns false |
| 108 | 163 | * @param {import('../../char-data.js').v1CharDataCharacter|undefined} character |
| 109 | 164 | * @returns {boolean} |
| 110 | 165 | */ |
| 111 | 166 | export function isScopedScriptsAllowed(character) { |
| @@ -114,7 +169,7 @@ export function isScopedScriptsAllowed(character) { | ||
| 114 | 169 | |
| 115 | 170 | /** |
| 116 | 171 | * Allow character's regexes to be used; if character is undefined, do nothing |
| 117 | 172 | * @param {import('../../char-data.js').v1CharDataCharacter|undefined} character |
| 118 | 173 | * @returns {void} |
| 119 | 174 | */ |
| 120 | 175 | export function allowScopedScripts(character) { |
| @@ -133,7 +188,7 @@ export function allowScopedScripts(character) { | ||
| 133 | 188 | |
| 134 | 189 | /** |
| 135 | 190 | * Disallow character's regexes to be used; if character is undefined, do nothing |
| 136 | 191 | * @param {import('../../char-data.js').v1CharDataCharacter|undefined} character |
| 137 | 192 | * @returns {void} |
| 138 | 193 | */ |
| 139 | 194 | export function disallowScopedScripts(character) { |
| @@ -220,8 +275,8 @@ export function getCurrentPresetName() { | ||
| 220 | 275 | } |
| 221 | 276 | |
| 222 | 277 | /** |
| 223 | - * @enum {number} Where the regex script should be applied | |
| 224 | 278 | * @readonly |
| 279 | + * @enum {number} Where the regex script should be applied | |
| 225 | 280 | */ |
| 226 | 281 | export const regex_placement = { |
| 227 | 282 | /** |
| @@ -237,8 +292,8 @@ export const regex_placement = { | ||
| 237 | 292 | }; |
| 238 | 293 | |
| 239 | 294 | /** |
| 240 | - * @enum {number} How to substitute parameters in the find regex | |
| 241 | 295 | * @readonly |
| 296 | + * @enum {number} How to substitute parameters in the find regex | |
| 242 | 297 | */ |
| 243 | 298 | export const substitute_find_regex = { |
| 244 | 299 | NONE: 0, |
| @@ -353,7 +408,7 @@ export function runRegexScript(regexScript, rawString, { characterOverride } = { | ||
| 353 | 408 | } |
| 354 | 409 | }; |
| 355 | 410 | const regexString = getRegexString(); |
| 356 | 411 | const findRegex = regexFromStringRegexProvider.instance.get(regexString); |
| 357 | 412 | |
| 358 | 413 | // The user skill issued. Return with nothing. |
| 359 | 414 | if (!findRegex) { |
| @@ -402,7 +457,7 @@ export function runRegexScript(regexScript, rawString, { characterOverride } = { | ||
| 402 | 457 | function filterString(rawString, trimStrings, { characterOverride } = {}) { |
| 403 | 458 | let finalString = rawString; |
| 404 | 459 | trimStrings.forEach((trimString) => { |
| 405 | 460 | const subTrimString = substituteParams(trimString, undefined,{ name2Override: characterOverride }); |
| 406 | 461 | finalString = finalString.replaceAll(subTrimString, ''); |
| 407 | 462 | }); |
| 408 | 463 | |
| @@ -8,7 +8,7 @@ import { commonEnumProviders, enumIcons } from '../../slash-commands/SlashComman | ||
| 8 | 8 | import { SlashCommandEnumValue, enumTypes } from '../../slash-commands/SlashCommandEnumValue.js'; |
| 9 | 9 | import { SlashCommandParser } from '../../slash-commands/SlashCommandParser.js'; |
| 10 | 10 | import { download, equalsIgnoreCaseAndAccents, escapeHtml, getFileText, getSortableDelay, isFalseBoolean, isTrueBoolean, regexFromString, setInfoBlock, uuidv4 } from '../../utils.js'; |
| 11 | 11 | import { 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'; |
| 12 | 12 | import { t } from '../../i18n.js'; |
| 13 | 13 | import { accountStorage } from '../../util/AccountStorage.js'; |
| 14 | 14 | import { getPresetManager } from '../../preset-manager.js'; |
| @@ -1628,6 +1628,8 @@ async function checkCharEmbeddedRegexScripts() { | ||
| 1628 | 1628 | } |
| 1629 | 1629 | } |
| 1630 | 1630 | |
| 1631 | + // Clear cache and reload scripts | |
| 1632 | + RegexProvider.instance.clear(); | |
| 1631 | 1633 | await loadRegexScripts(); |
| 1632 | 1634 | } |
| 1633 | 1635 | |
| @@ -1,7 +1,7 @@ | ||
| 1 | 1 | import { CONNECT_API_MAP, getRequestHeaders } from '../../script.js'; |
| 2 | 2 | import { extension_settings, openThirdPartyExtensionMenu } from '../extensions.js'; |
| 3 | 3 | import { t } from '../i18n.js'; |
| 4 | 4 | import { oai_settings, proxies, ZAI_ENDPOINT } from '../openai.js'; |
| 5 | 5 | import { SECRET_KEYS, secret_state } from '../secrets.js'; |
| 6 | 6 | import { textgen_types, textgenerationwebui_settings } from '../textgen-settings.js'; |
| 7 | 7 | import { getTokenCountAsync } from '../tokenizers.js'; |
| @@ -115,6 +115,10 @@ export async function getMultimodalCaption(base64Img, prompt) { | ||
| 115 | 115 | requestBody.custom_exclude_body = oai_settings.custom_exclude_body; |
| 116 | 116 | } |
| 117 | 117 | |
| 118 | + if (extension_settings.caption.multimodal_api === 'zai') { | |
| 119 | + requestBody.zai_endpoint = oai_settings.zai_endpoint || ZAI_ENDPOINT.COMMON; | |
| 120 | + } | |
| 121 | + | |
| 118 | 122 | function getEndpointUrl() { |
| 119 | 123 | switch (extension_settings.caption.multimodal_api) { |
| 120 | 124 | case 'google': |
| @@ -257,6 +261,10 @@ function throwIfInvalidModel(useReverseProxy) { | ||
| 257 | 261 | throw new Error('Electron Hub API key is not set.'); |
| 258 | 262 | } |
| 259 | 263 | |
| 264 | + if (multimodalApi === 'chutes' && !secret_state[SECRET_KEYS.CHUTES]) { | |
| 265 | + throw new Error('Chutes API key is not set.'); | |
| 266 | + } | |
| 267 | + | |
| 260 | 268 | if (multimodalApi === 'zai' && !secret_state[SECRET_KEYS.ZAI]) { |
| 261 | 269 | throw new Error('Z.AI API key is not set.'); |
| 262 | 270 | } |
| @@ -393,7 +401,7 @@ export class ConnectionManagerRequestService { | ||
| 393 | 401 | throw new Error('Connection Manager is not available'); |
| 394 | 402 | } |
| 395 | 403 | |
| 396 | - const profile = context.extensionSettings.connectionManager.profiles.find((p) => p.id === profileId); | |
| 404 | + const profile = this.getProfile(profileId); | |
| 397 | 405 | const selectedApiMap = this.validateProfile(profile); |
| 398 | 406 | |
| 399 | 407 | try { |
| @@ -413,6 +421,8 @@ export class ConnectionManagerRequestService { | ||
| 413 | 421 | model: profile.model, |
| 414 | 422 | chat_completion_source: selectedApiMap.source, |
| 415 | 423 | custom_url: profile['api-url'], |
| 424 | + vertexai_region: profile['api-url'], | |
| 425 | + zai_endpoint: profile['api-url'], | |
| 416 | 426 | reverse_proxy: proxyPreset?.url, |
| 417 | 427 | proxy_password: proxyPreset?.password, |
| 418 | 428 | custom_prompt_post_processing: profile['prompt-post-processing'], |
| @@ -450,6 +460,38 @@ export class ConnectionManagerRequestService { | ||
| 450 | 460 | } |
| 451 | 461 | |
| 452 | 462 | /** |
| 463 | + * If using text completion, return a formatted prompt string given an array of messages, a given profile ID, and optional instruct settings. | |
| 464 | + * If using chat completion, simply return the given prompt as-is. | |
| 465 | + * @param {ChatCompletionMessage[]} prompt An array of prompt messages. | |
| 466 | + * @param {string} profileId ID of a given connection profile (from which to infer a completion preset). | |
| 467 | + * @param {InstructSettings} instructSettings optional instruct settings | |
| 468 | + */ | |
| 469 | + static constructPrompt(prompt, profileId, instructSettings = null) { | |
| 470 | + const context = SillyTavern.getContext(); | |
| 471 | + const profile = this.getProfile(profileId); | |
| 472 | + const selectedApiMap = this.validateProfile(profile); | |
| 473 | + const instructName = profile.instruct; | |
| 474 | + | |
| 475 | + switch (selectedApiMap.selected) { | |
| 476 | + case 'openai': { | |
| 477 | + if (!selectedApiMap.source) { | |
| 478 | + throw new Error(`API type ${selectedApiMap.selected} does not support chat completions`); | |
| 479 | + } | |
| 480 | + return prompt; | |
| 481 | + } | |
| 482 | + case 'textgenerationwebui': { | |
| 483 | + if (!selectedApiMap.type) { | |
| 484 | + throw new Error(`API type ${selectedApiMap.selected} does not support text completions`); | |
| 485 | + } | |
| 486 | + return context.TextCompletionService.constructPrompt(prompt, instructName, instructSettings); | |
| 487 | + } | |
| 488 | + default: { | |
| 489 | + throw new Error(`Unknown API type ${selectedApiMap.selected}`); | |
| 490 | + } | |
| 491 | + } | |
| 492 | + } | |
| 493 | + | |
| 494 | + /** | |
| 453 | 495 | * Respects allowed types. |
| 454 | 496 | * @returns {import('./connection-manager/index.js').ConnectionProfile[]} |
| 455 | 497 | */ |
| @@ -464,6 +506,18 @@ export class ConnectionManagerRequestService { | ||
| 464 | 506 | } |
| 465 | 507 | |
| 466 | 508 | /** |
| 509 | + * Return profile data given the profile ID | |
| 510 | + * @param {string} profileId | |
| 511 | + * @returns {import('./connection-manager/index.js').ConnectionProfile?} [profile] | |
| 512 | + * @throws {Error} | |
| 513 | + */ | |
| 514 | + static getProfile(profileId) { | |
| 515 | + const profile = SillyTavern.getContext().extensionSettings.connectionManager.profiles.find((p) => p.id === profileId); | |
| 516 | + if (!profile) throw new Error(`Profile not found (ID: ${profileId})`); | |
| 517 | + return profile; | |
| 518 | + } | |
| 519 | + | |
| 520 | + /** | |
| 467 | 521 | * @param {import('./connection-manager/index.js').ConnectionProfile?} [profile] |
| 468 | 522 | * @returns {boolean} |
| 469 | 523 | */ |
| @@ -57,9 +57,11 @@ import { SlashCommandEnumValue } from '../../slash-commands/SlashCommandEnumValu | ||
| 57 | 57 | import { callGenericPopup, Popup, POPUP_TYPE } from '../../popup.js'; |
| 58 | 58 | import { commonEnumProviders } from '../../slash-commands/SlashCommandCommonEnumsProvider.js'; |
| 59 | 59 | import { ToolManager } from '../../tool-calling.js'; |
| 60 | 60 | import { MacrosParsermacros, MacroCategory } from '../../macros/macro-system.js'; |
| 61 | 61 | import { t, translate } from '../../i18n.js'; |
| 62 | 62 | import { oai_settings } from '../../openai.js'; |
| 63 | +import { power_user } from '/scripts/power-user.js'; | |
| 64 | +import { MacrosParser } from '/scripts/macros.js'; | |
| 63 | 65 | |
| 64 | 66 | export { MODULE_NAME }; |
| 65 | 67 | |
| @@ -81,12 +83,19 @@ const sources = { | ||
| 81 | 83 | pollinations: 'pollinations', |
| 82 | 84 | stability: 'stability', |
| 83 | 85 | huggingface: 'huggingface', |
| 86 | + chutes: 'chutes', | |
| 84 | 87 | electronhub: 'electronhub', |
| 85 | 88 | nanogpt: 'nanogpt', |
| 86 | 89 | bfl: 'bfl', |
| 87 | 90 | falai: 'falai', |
| 88 | 91 | xai: 'xai', |
| 89 | 92 | google: 'google', |
| 93 | + zai: 'zai', | |
| 94 | + openrouter: 'openrouter', | |
| 95 | +}; | |
| 96 | +const comfyTypes = { | |
| 97 | + standard: 'standard', | |
| 98 | + runpod_serverless: 'runpod_serverless', | |
| 90 | 99 | }; |
| 91 | 100 | |
| 92 | 101 | const initiators = { |
| @@ -317,9 +326,13 @@ const defaultSettings = { | ||
| 317 | 326 | styles: defaultStyles, |
| 318 | 327 | |
| 319 | 328 | // ComyUI settings |
| 329 | + comfy_type: 'standard', | |
| 330 | + | |
| 320 | 331 | comfy_url: 'http://127.0.0.1:8188', |
| 321 | 332 | comfy_workflow: 'Default_Comfy_Workflow.json', |
| 322 | 333 | |
| 334 | + comfy_runpod_url: '', | |
| 335 | + | |
| 323 | 336 | // Pollinations settings |
| 324 | 337 | pollinations_enhance: false, |
| 325 | 338 | |
| @@ -432,6 +445,10 @@ function toggleSourceControls() { | ||
| 432 | 445 | const source = $(this).data('sd-source').split(','); |
| 433 | 446 | $(this).toggle(source.includes(extension_settings.sd.source)); |
| 434 | 447 | }); |
| 448 | + $('.sd_settings [data-sd-comfy-type]').each(function () { | |
| 449 | + const source = $(this).data('sd-comfy-type').split(','); | |
| 450 | + $(this).toggle(source.includes(extension_settings.sd.comfy_type)); | |
| 451 | + }); | |
| 435 | 452 | } |
| 436 | 453 | |
| 437 | 454 | async function loadSettings() { |
| @@ -512,8 +529,10 @@ async function loadSettings() { | ||
| 512 | 529 | $('#sd_openai_style').val(extension_settings.sd.openai_style); |
| 513 | 530 | $('#sd_openai_quality').val(extension_settings.sd.openai_quality); |
| 514 | 531 | $('#sd_openai_duration').val(extension_settings.sd.openai_duration); |
| 532 | + $('#sd_comfy_type').val(extension_settings.sd.comfy_type); | |
| 515 | 533 | $('#sd_comfy_url').val(extension_settings.sd.comfy_url); |
| 516 | 534 | $('#sd_comfy_prompt').val(extension_settings.sd.comfy_prompt); |
| 535 | + $('#sd_comfy_runpod_url').val(extension_settings.sd.comfy_runpod_url); | |
| 517 | 536 | $('#sd_snap').prop('checked', extension_settings.sd.snap); |
| 518 | 537 | $('#sd_clip_skip').val(extension_settings.sd.clip_skip); |
| 519 | 538 | $('#sd_clip_skip_value').val(extension_settings.sd.clip_skip); |
| @@ -734,7 +753,7 @@ async function onSaveStyleClick() { | ||
| 734 | 753 | async function refinePrompt(prompt, isNegative) { |
| 735 | 754 | if (extension_settings.sd.refine_mode) { |
| 736 | 755 | const text = isNegative ? '<h3>Review and edit the <i>negative</i> prompt:</h3>' : '<h3>Review and edit the prompt:</h3>'; |
| 737 | 756 | const refinedPrompt = await callGenericPopup(text + 'Press "Cancel" to abort the image generation.', POPUP_TYPE.INPUT, prompt.trim(), { rows: 58, okButton: 'Continue' }); |
| 738 | 757 | |
| 739 | 758 | if (refinedPrompt) { |
| 740 | 759 | return String(refinedPrompt); |
| @@ -1011,6 +1030,11 @@ async function onSourceChange() { | ||
| 1011 | 1030 | await loadSettingOptions(); |
| 1012 | 1031 | } |
| 1013 | 1032 | |
| 1033 | +async function onComfyTypeChange() { | |
| 1034 | + extension_settings.sd.comfy_type = $('#sd_comfy_type').find(':selected').val(); | |
| 1035 | + await onSourceChange(); | |
| 1036 | +} | |
| 1037 | + | |
| 1014 | 1038 | function onFunctionToolInput() { |
| 1015 | 1039 | extension_settings.sd.function_tool = !!$(this).prop('checked'); |
| 1016 | 1040 | saveSettingsDebounced(); |
| @@ -1161,11 +1185,15 @@ function onHrSecondPassStepsInput() { | ||
| 1161 | 1185 | } |
| 1162 | 1186 | |
| 1163 | 1187 | function onComfyUrlInput() { |
| 1164 | - // Remove trailing slashes | |
| 1165 | 1188 | extension_settings.sd.comfy_url = String($('#sd_comfy_url').val()); |
| 1166 | 1189 | saveSettingsDebounced(); |
| 1167 | 1190 | } |
| 1168 | 1191 | |
| 1192 | +function onComfyRunPodUrlInput() { | |
| 1193 | + extension_settings.sd.comfy_runpod_url = String($('#sd_comfy_runpod_url').val()); | |
| 1194 | + saveSettingsDebounced(); | |
| 1195 | +} | |
| 1196 | + | |
| 1169 | 1197 | function onHFModelInput() { |
| 1170 | 1198 | extension_settings.sd.huggingface_model_id = $('#sd_huggingface_model_id').val(); |
| 1171 | 1199 | saveSettingsDebounced(); |
| @@ -1291,6 +1319,30 @@ async function validateComfyUrl() { | ||
| 1291 | 1319 | } |
| 1292 | 1320 | } |
| 1293 | 1321 | |
| 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 | + | |
| 1294 | 1346 | async function onModelChange() { |
| 1295 | 1347 | const selectedModel = $('#sd_model').find(':selected'); |
| 1296 | 1348 | extension_settings.sd.model = selectedModel.val(); |
| @@ -1304,24 +1356,13 @@ async function onModelChange() { | ||
| 1304 | 1356 | |
| 1305 | 1357 | switchModelSpecificControls(extension_settings.sd.model); |
| 1306 | 1358 | |
| 1307 | 1359 | const cloudSourcesupdateRemoteModelSources = [ |
| 1308 | 1360 | sources.hordeauto, |
| 1309 | 1361 | sources.novelvlad, |
| 1310 | 1362 | 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, | |
| 1322 | 1363 | ]; |
| 1323 | 1364 | |
| 1324 | 1365 | if (cloudSources!updateRemoteModelSources.includes(extension_settings.sd.source)) { |
| 1325 | 1366 | return; |
| 1326 | 1367 | } |
| 1327 | 1368 | |
| @@ -1531,6 +1572,9 @@ async function loadSamplers() { | ||
| 1531 | 1572 | case sources.huggingface: |
| 1532 | 1573 | samplers = ['N/A']; |
| 1533 | 1574 | break; |
| 1575 | + case sources.chutes: | |
| 1576 | + samplers = ['N/A']; | |
| 1577 | + break; | |
| 1534 | 1578 | case sources.electronhub: |
| 1535 | 1579 | samplers = ['N/A']; |
| 1536 | 1580 | break; |
| @@ -1540,12 +1584,21 @@ async function loadSamplers() { | ||
| 1540 | 1584 | case sources.bfl: |
| 1541 | 1585 | samplers = ['N/A']; |
| 1542 | 1586 | break; |
| 1587 | + case sources.falai: | |
| 1588 | + samplers = ['N/A']; | |
| 1589 | + break; | |
| 1543 | 1590 | case sources.xai: |
| 1544 | 1591 | samplers = ['N/A']; |
| 1545 | 1592 | break; |
| 1546 | 1593 | case sources.google: |
| 1547 | 1594 | samplers = ['N/A']; |
| 1548 | 1595 | break; |
| 1596 | + case sources.zai: | |
| 1597 | + samplers = ['N/A']; | |
| 1598 | + break; | |
| 1599 | + case sources.openrouter: | |
| 1600 | + samplers = ['N/A']; | |
| 1601 | + break; | |
| 1549 | 1602 | } |
| 1550 | 1603 | |
| 1551 | 1604 | for (const sampler of samplers) { |
| @@ -1565,7 +1618,7 @@ async function loadSamplers() { | ||
| 1565 | 1618 | async function loadHordeSamplers() { |
| 1566 | 1619 | const result = await fetch('/api/horde/sd-samplers', { |
| 1567 | 1620 | method: 'POST', |
| 1568 | 1621 | headers: getRequestHeaders({ omitContentType: true }), |
| 1569 | 1622 | }); |
| 1570 | 1623 | |
| 1571 | 1624 | if (result.ok) { |
| @@ -1665,6 +1718,9 @@ async function loadNovelSamplers() { | ||
| 1665 | 1718 | } |
| 1666 | 1719 | |
| 1667 | 1720 | async function loadComfySamplers() { |
| 1721 | + if (extension_settings.sd.comfy_type === comfyTypes.runpod_serverless) { | |
| 1722 | + return ['N/A']; | |
| 1723 | + } | |
| 1668 | 1724 | if (!extension_settings.sd.comfy_url) { |
| 1669 | 1725 | return []; |
| 1670 | 1726 | } |
| @@ -1730,6 +1786,9 @@ async function loadModels() { | ||
| 1730 | 1786 | case sources.huggingface: |
| 1731 | 1787 | models = [{ value: '', text: t`<Enter Model ID above>` }]; |
| 1732 | 1788 | break; |
| 1789 | + case sources.chutes: | |
| 1790 | + models = await loadChutesModels(); | |
| 1791 | + break; | |
| 1733 | 1792 | case sources.electronhub: |
| 1734 | 1793 | models = await loadElectronHubModels(); |
| 1735 | 1794 | break; |
| @@ -1748,6 +1807,12 @@ async function loadModels() { | ||
| 1748 | 1807 | case sources.google: |
| 1749 | 1808 | models = await loadGoogleModels(); |
| 1750 | 1809 | break; |
| 1810 | + case sources.zai: | |
| 1811 | + models = await loadZaiModels(); | |
| 1812 | + break; | |
| 1813 | + case sources.openrouter: | |
| 1814 | + models = await loadOpenRouterModels(); | |
| 1815 | + break; | |
| 1751 | 1816 | } |
| 1752 | 1817 | |
| 1753 | 1818 | if (extension_settings.sd.source === sources.electronhub) { |
| @@ -1858,7 +1923,7 @@ async function loadFalaiModels() { | ||
| 1858 | 1923 | |
| 1859 | 1924 | const result = await fetch('/api/sd/falai/models', { |
| 1860 | 1925 | method: 'POST', |
| 1861 | 1926 | headers: getRequestHeaders({ omitContentType: true }), |
| 1862 | 1927 | }); |
| 1863 | 1928 | |
| 1864 | 1929 | if (result.ok) { |
| @@ -1877,7 +1942,7 @@ async function loadXAIModels() { | ||
| 1877 | 1942 | async function loadPollinationsModels() { |
| 1878 | 1943 | const result = await fetch('/api/sd/pollinations/models', { |
| 1879 | 1944 | method: 'POST', |
| 1880 | 1945 | headers: getRequestHeaders({ omitContentType: true }), |
| 1881 | 1946 | }); |
| 1882 | 1947 | |
| 1883 | 1948 | if (result.ok) { |
| @@ -1895,7 +1960,7 @@ async function loadTogetherAIModels() { | ||
| 1895 | 1960 | |
| 1896 | 1961 | const result = await fetch('/api/sd/together/models', { |
| 1897 | 1962 | method: 'POST', |
| 1898 | 1963 | headers: getRequestHeaders({ omitContentType: true }), |
| 1899 | 1964 | }); |
| 1900 | 1965 | |
| 1901 | 1966 | if (result.ok) { |
| @@ -1905,6 +1970,27 @@ async function loadTogetherAIModels() { | ||
| 1905 | 1970 | return []; |
| 1906 | 1971 | } |
| 1907 | 1972 | |
| 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 | + | |
| 1908 | 1994 | async function loadElectronHubModels() { |
| 1909 | 1995 | if (!secret_state[SECRET_KEYS.ELECTRONHUB]) { |
| 1910 | 1996 | console.debug('Electron Hub API key is not set.'); |
| @@ -1913,7 +1999,7 @@ async function loadElectronHubModels() { | ||
| 1913 | 1999 | |
| 1914 | 2000 | const result = await fetch('/api/sd/electronhub/models', { |
| 1915 | 2001 | method: 'POST', |
| 1916 | 2002 | headers: getRequestHeaders({ omitContentType: true }), |
| 1917 | 2003 | }); |
| 1918 | 2004 | |
| 1919 | 2005 | function getModelName(model) { |
| @@ -1946,7 +2032,7 @@ async function loadNanoGPTModels() { | ||
| 1946 | 2032 | |
| 1947 | 2033 | const result = await fetch('/api/sd/nanogpt/models', { |
| 1948 | 2034 | method: 'POST', |
| 1949 | 2035 | headers: getRequestHeaders({ omitContentType: true }), |
| 1950 | 2036 | }); |
| 1951 | 2037 | |
| 1952 | 2038 | if (result.ok) { |
| @@ -1959,7 +2045,7 @@ async function loadNanoGPTModels() { | ||
| 1959 | 2045 | async function loadHordeModels() { |
| 1960 | 2046 | const result = await fetch('/api/horde/sd-models', { |
| 1961 | 2047 | method: 'POST', |
| 1962 | 2048 | headers: getRequestHeaders({ omitContentType: true }), |
| 1963 | 2049 | }); |
| 1964 | 2050 | |
| 1965 | 2051 | |
| @@ -2080,6 +2166,8 @@ async function loadDrawthingsModels() { | ||
| 2080 | 2166 | |
| 2081 | 2167 | async function loadOpenAiModels() { |
| 2082 | 2168 | return [ |
| 2169 | + { value: 'gpt-image-1.5', text: 'gpt-image-1.5' }, | |
| 2170 | + { value: 'gpt-image-1-mini', text: 'gpt-image-1-mini' }, | |
| 2083 | 2171 | { value: 'gpt-image-1', text: 'gpt-image-1' }, |
| 2084 | 2172 | { value: 'dall-e-3', text: 'dall-e-3' }, |
| 2085 | 2173 | { value: 'dall-e-2', text: 'dall-e-2' }, |
| @@ -2093,7 +2181,7 @@ async function loadAimlapiModels() { | ||
| 2093 | 2181 | |
| 2094 | 2182 | const result = await fetch('/api/sd/aimlapi/models', { |
| 2095 | 2183 | method: 'POST', |
| 2096 | 2184 | headers: getRequestHeaders({ omitContentType: true }), |
| 2097 | 2185 | }); |
| 2098 | 2186 | |
| 2099 | 2187 | if (!result.ok) { |
| @@ -2205,11 +2293,34 @@ async function loadGoogleModels() { | ||
| 2205 | 2293 | ].map(name => ({ value: name, text: name })); |
| 2206 | 2294 | } |
| 2207 | 2295 | |
| 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 | + | |
| 2208 | 2313 | function loadNovelSchedulers() { |
| 2209 | 2314 | return ['karras', 'native', 'exponential', 'polyexponential']; |
| 2210 | 2315 | } |
| 2211 | 2316 | |
| 2212 | 2317 | async function loadComfyModels() { |
| 2318 | + if (extension_settings.sd.comfy_type === comfyTypes.runpod_serverless) { | |
| 2319 | + $('#sd_runpod_key').toggleClass('success', !!secret_state[SECRET_KEYS.COMFY_RUNPOD]); | |
| 2320 | + return [ | |
| 2321 | + { value: '', text: 'N/A' }, | |
| 2322 | + ]; | |
| 2323 | + } | |
| 2213 | 2324 | if (!extension_settings.sd.comfy_url) { |
| 2214 | 2325 | return []; |
| 2215 | 2326 | } |
| @@ -2275,6 +2386,9 @@ async function loadSchedulers() { | ||
| 2275 | 2386 | case sources.huggingface: |
| 2276 | 2387 | schedulers = ['N/A']; |
| 2277 | 2388 | break; |
| 2389 | + case sources.chutes: | |
| 2390 | + schedulers = ['N/A']; | |
| 2391 | + break; | |
| 2278 | 2392 | case sources.electronhub: |
| 2279 | 2393 | schedulers = ['N/A']; |
| 2280 | 2394 | break; |
| @@ -2293,6 +2407,12 @@ async function loadSchedulers() { | ||
| 2293 | 2407 | case sources.google: |
| 2294 | 2408 | schedulers = ['N/A']; |
| 2295 | 2409 | break; |
| 2410 | + case sources.zai: | |
| 2411 | + schedulers = ['N/A']; | |
| 2412 | + break; | |
| 2413 | + case sources.openrouter: | |
| 2414 | + schedulers = ['N/A']; | |
| 2415 | + break; | |
| 2296 | 2416 | } |
| 2297 | 2417 | |
| 2298 | 2418 | for (const scheduler of schedulers) { |
| @@ -2310,6 +2430,9 @@ async function loadSchedulers() { | ||
| 2310 | 2430 | } |
| 2311 | 2431 | |
| 2312 | 2432 | async function loadComfySchedulers() { |
| 2433 | + if (extension_settings.sd.comfy_type === comfyTypes.runpod_serverless) { | |
| 2434 | + return ['N/A']; | |
| 2435 | + } | |
| 2313 | 2436 | if (!extension_settings.sd.comfy_url) { |
| 2314 | 2437 | return []; |
| 2315 | 2438 | } |
| @@ -2375,6 +2498,9 @@ async function loadVaes() { | ||
| 2375 | 2498 | case sources.huggingface: |
| 2376 | 2499 | vaes = ['N/A']; |
| 2377 | 2500 | break; |
| 2501 | + case sources.chutes: | |
| 2502 | + vaes = ['N/A']; | |
| 2503 | + break; | |
| 2378 | 2504 | case sources.electronhub: |
| 2379 | 2505 | vaes = ['N/A']; |
| 2380 | 2506 | break; |
| @@ -2393,6 +2519,12 @@ async function loadVaes() { | ||
| 2393 | 2519 | case sources.google: |
| 2394 | 2520 | vaes = ['N/A']; |
| 2395 | 2521 | break; |
| 2522 | + case sources.zai: | |
| 2523 | + vaes = ['N/A']; | |
| 2524 | + break; | |
| 2525 | + case sources.openrouter: | |
| 2526 | + vaes = ['N/A']; | |
| 2527 | + break; | |
| 2396 | 2528 | } |
| 2397 | 2529 | |
| 2398 | 2530 | for (const vae of vaes) { |
| @@ -2434,6 +2566,9 @@ async function loadAutoVaes() { | ||
| 2434 | 2566 | } |
| 2435 | 2567 | |
| 2436 | 2568 | async function loadComfyVaes() { |
| 2569 | + if (extension_settings.sd.comfy_type === comfyTypes.runpod_serverless) { | |
| 2570 | + return ['N/A']; | |
| 2571 | + } | |
| 2437 | 2572 | if (!extension_settings.sd.comfy_url) { |
| 2438 | 2573 | return []; |
| 2439 | 2574 | } |
| @@ -2456,10 +2591,6 @@ async function loadComfyVaes() { | ||
| 2456 | 2591 | } |
| 2457 | 2592 | |
| 2458 | 2593 | async function loadComfyWorkflows() { |
| 2459 | - if (!extension_settings.sd.comfy_url) { | |
| 2460 | - return; | |
| 2461 | - } | |
| 2462 | - | |
| 2463 | 2594 | try { |
| 2464 | 2595 | $('#sd_comfy_workflow').empty(); |
| 2465 | 2596 | const result = await fetch('/api/sd/comfy/workflows', { |
| @@ -2532,7 +2663,7 @@ function processReply(str) { | ||
| 2532 | 2663 | str = str.normalize('NFD'); |
| 2533 | 2664 | |
| 2534 | 2665 | // Strip out non-alphanumeric characters barring model syntax exceptions |
| 2535 | 2666 | str = str.replace(/[^a-zA-Z0-9.,:_(){}<>[\]/\-'|#]+/g, ' '); |
| 2536 | 2667 | |
| 2537 | 2668 | str = str.replace(/\s+/g, ' '); // Collapse multiple whitespaces into one |
| 2538 | 2669 | str = str.trim(); |
| @@ -2865,7 +2996,7 @@ function getCharacterAvatarUrl() { | ||
| 2865 | 2996 | if (context.groupId) { |
| 2866 | 2997 | const groupMembers = context.groups.find(x => x.id === context.groupId)?.members; |
| 2867 | 2998 | const lastMessageAvatar = context.chat?.filter(x => !x.is_system && !x.is_user)?.slice(-1)[0]?.original_avatar; |
| 2868 | 2999 | const randomMemberAvatar = Array.isArray(groupMembers) ? groupMembers[Math.floor(Math.random() * groupMembers.length)]?.avatar : null; |
| 2869 | 3000 | const avatarToUse = lastMessageAvatar || randomMemberAvatar; |
| 2870 | 3001 | return formatCharacterAvatar(avatarToUse); |
| 2871 | 3002 | } else { |
| @@ -2953,8 +3084,17 @@ async function sendGenerationRequest(generationType, prompt, additionalNegativeP | ||
| 2953 | 3084 | result = await generateAimlapiImage(prefixedPrompt, signal); |
| 2954 | 3085 | break; |
| 2955 | 3086 | 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: | |
| 2956 | 3092 | result = await generateComfyImage(prefixedPrompt, negativePrompt, signal); |
| 2957 | 3093 | break; |
| 3094 | + default: | |
| 3095 | + throw new Error('Unknown comfyUI server type.'); | |
| 3096 | + } | |
| 3097 | + break; | |
| 2958 | 3098 | case sources.togetherai: |
| 2959 | 3099 | result = await generateTogetherAIImage(prefixedPrompt, negativePrompt, signal); |
| 2960 | 3100 | break; |
| @@ -2967,6 +3107,9 @@ async function sendGenerationRequest(generationType, prompt, additionalNegativeP | ||
| 2967 | 3107 | case sources.huggingface: |
| 2968 | 3108 | result = await generateHuggingFaceImage(prefixedPrompt, signal); |
| 2969 | 3109 | break; |
| 3110 | + case sources.chutes: | |
| 3111 | + result = await generateChutesImage(prefixedPrompt, negativePrompt, signal); | |
| 3112 | + break; | |
| 2970 | 3113 | case sources.electronhub: |
| 2971 | 3114 | result = await generateElectronHubImage(prefixedPrompt, signal); |
| 2972 | 3115 | break; |
| @@ -2985,6 +3128,12 @@ async function sendGenerationRequest(generationType, prompt, additionalNegativeP | ||
| 2985 | 3128 | case sources.google: |
| 2986 | 3129 | result = await generateGoogleImage(prefixedPrompt, negativePrompt, signal); |
| 2987 | 3130 | break; |
| 3131 | + case sources.zai: | |
| 3132 | + result = await generateZaiImage(prefixedPrompt, signal); | |
| 3133 | + break; | |
| 3134 | + case sources.openrouter: | |
| 3135 | + result = await generateOpenRouterImage(prefixedPrompt, signal); | |
| 3136 | + break; | |
| 2988 | 3137 | } |
| 2989 | 3138 | |
| 2990 | 3139 | if (!result.data) { |
| @@ -3546,9 +3695,9 @@ async function generateOpenAiImage(prompt, signal) { | ||
| 3546 | 3695 | const dalle3PromptLimit = 4000; |
| 3547 | 3696 | const gptImgPromptLimit = 32000; |
| 3548 | 3697 | |
| 3549 | 3698 | const isDalle2 = extension_settings.sd.model === '/dall-e-2'/.test(extension_settings.sd.model); |
| 3550 | 3699 | const isDalle3 = extension_settings.sd.model === '/dall-e-3'/.test(extension_settings.sd.model); |
| 3551 | 3700 | const isGptImg = extension_settings.sd.model === '/gpt-image-1'/.test(extension_settings.sd.model); |
| 3552 | 3701 | const isSora2 = /sora-2/.test(extension_settings.sd.model); |
| 3553 | 3702 | |
| 3554 | 3703 | if (isDalle2 && prompt.length > dalle2PromptLimit) { |
| @@ -3679,25 +3828,17 @@ async function generateAimlapiImage(prompt, signal) { | ||
| 3679 | 3828 | } |
| 3680 | 3829 | |
| 3681 | 3830 | /** |
| 3682 | 3831 | * Generates an image in local ComfyUI or serverless runpod using the provided prompt and configuration settings. |
| 3683 | 3832 | * |
| 3684 | 3833 | * @param {string} prompt - The main instruction used to guide the image generation. |
| 3685 | 3834 | * @param {string} negativePrompt - The instruction used to restrict the image generation. |
| 3686 | 3835 | * @param {AbortSignal} signal - An AbortSignal object that can be used to cancel the request. |
| 3836 | + * @param {string} basePath - ST server endpoint for the service. '/api/sd/comfy' for local, '/api/sd/comfyrunpod' for serverless. | |
| 3837 | + * @param {string[]} placeholders - Array of substitutions to apply to the workflow. | |
| 3838 | + * @param {string} url - The url of the service to call. Passed to ST server. | |
| 3687 | 3839 | * @returns {Promise<{format: string, data: string}>} - A promise that resolves when the image generation and processing are complete. |
| 3688 | 3840 | */ |
| 3689 | 3841 | async 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 | - | |
| 3701 | 3842 | const workflowResponse = await fetch('/api/sd/comfy/workflow', { |
| 3702 | 3843 | method: 'POST', |
| 3703 | 3844 | headers: getRequestHeaders(), |
| @@ -3752,12 +3893,12 @@ async function generateComfyImage(prompt, negativePrompt, signal) { | ||
| 3752 | 3893 | console.log(`{ |
| 3753 | 3894 | "prompt": ${workflow} |
| 3754 | 3895 | }`); |
| 3755 | 3896 | const promptResult = await fetch('/api/sd/comfy`${basePath}/generate'`, { |
| 3756 | 3897 | method: 'POST', |
| 3757 | 3898 | headers: getRequestHeaders(), |
| 3758 | 3899 | signal: signal, |
| 3759 | 3900 | body: JSON.stringify({ |
| 3760 | - url: extension_settings.sd.comfy_url, | |
| 3901 | + url, | |
| 3761 | 3902 | prompt: `{ |
| 3762 | 3903 | "prompt": ${workflow} |
| 3763 | 3904 | }`, |
| @@ -3771,6 +3912,46 @@ async function generateComfyImage(prompt, negativePrompt, signal) { | ||
| 3771 | 3912 | return { format, data }; |
| 3772 | 3913 | } |
| 3773 | 3914 | |
| 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 | +} | |
| 3774 | 3955 | |
| 3775 | 3956 | /** |
| 3776 | 3957 | * Generates an image in Hugging Face Inference API using the provided prompt and configuration settings (model selected). |
| @@ -3799,6 +3980,38 @@ async function generateHuggingFaceImage(prompt, signal) { | ||
| 3799 | 3980 | } |
| 3800 | 3981 | |
| 3801 | 3982 | /** |
| 3983 | + * Generates an image using the Chutes API. | |
| 3984 | + * @param {string} prompt - The main instruction used to guide the image generation. | |
| 3985 | + * @param {string} negativePrompt - The instruction used to restrict the image generation. | |
| 3986 | + * @param {AbortSignal} signal - An AbortSignal object that can be used to cancel the request. | |
| 3987 | + * @returns {Promise<{format: string, data: string}>} - A promise that resolves when the image generation and processing are complete. | |
| 3988 | + */ | |
| 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 | +/** | |
| 3802 | 4015 | * Generates an image using the Electron Hub API. |
| 3803 | 4016 | * @param {string} prompt - The main instruction used to guide the image generation. |
| 3804 | 4017 | * @param {AbortSignal} signal - An AbortSignal object that can be used to cancel the request. |
| @@ -4022,6 +4235,74 @@ async function generateGoogleImage(prompt, negativePrompt, signal) { | ||
| 4022 | 4235 | } |
| 4023 | 4236 | } |
| 4024 | 4237 | |
| 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 | + | |
| 4025 | 4306 | async function onComfyOpenWorkflowEditorClick() { |
| 4026 | 4307 | let workflow = await (await fetch('/api/sd/comfy/workflow', { |
| 4027 | 4308 | method: 'POST', |
| @@ -4207,6 +4488,7 @@ async function sendMessage(prompt, image, generationType, additionalNegativePref | ||
| 4207 | 4488 | context.addOneMessage(message); |
| 4208 | 4489 | await eventSource.emit(event_types.CHARACTER_MESSAGE_RENDERED, messageId, 'extension'); |
| 4209 | 4490 | await context.saveChat(); |
| 4491 | + setTimeout(() => context.scrollOnMediaLoad(), debounce_timeout.short); | |
| 4210 | 4492 | } |
| 4211 | 4493 | |
| 4212 | 4494 | /** |
| @@ -4304,7 +4586,15 @@ function isValidState() { | ||
| 4304 | 4586 | case sources.aimlapi: |
| 4305 | 4587 | return secret_state[SECRET_KEYS.AIMLAPI]; |
| 4306 | 4588 | case sources.comfy: |
| 4307 | - return true; | |
| 4589 | + switch (extension_settings.sd.comfy_type) { | |
| 4590 | + case comfyTypes.runpod_serverless: | |
| 4591 | + return !!extension_settings.sd.comfy_runpod_url && | |
| 4592 | + secret_state[SECRET_KEYS.COMFY_RUNPOD]; | |
| 4593 | + case comfyTypes.standard: | |
| 4594 | + return !!extension_settings.sd.comfy_url; | |
| 4595 | + default: | |
| 4596 | + return false; | |
| 4597 | + } | |
| 4308 | 4598 | case sources.togetherai: |
| 4309 | 4599 | return secret_state[SECRET_KEYS.TOGETHERAI]; |
| 4310 | 4600 | case sources.pollinations: |
| @@ -4313,6 +4603,8 @@ function isValidState() { | ||
| 4313 | 4603 | return secret_state[SECRET_KEYS.STABILITY]; |
| 4314 | 4604 | case sources.huggingface: |
| 4315 | 4605 | return secret_state[SECRET_KEYS.HUGGINGFACE]; |
| 4606 | + case sources.chutes: | |
| 4607 | + return secret_state[SECRET_KEYS.CHUTES]; | |
| 4316 | 4608 | case sources.electronhub: |
| 4317 | 4609 | return secret_state[SECRET_KEYS.ELECTRONHUB]; |
| 4318 | 4610 | case sources.nanogpt: |
| @@ -4325,6 +4617,12 @@ function isValidState() { | ||
| 4325 | 4617 | return secret_state[SECRET_KEYS.XAI]; |
| 4326 | 4618 | case sources.google: |
| 4327 | 4619 | return secret_state[SECRET_KEYS.MAKERSUITE] || secret_state[SECRET_KEYS.VERTEXAI] || secret_state[SECRET_KEYS.VERTEXAI_SERVICE_ACCOUNT]; |
| 4620 | + case sources.zai: | |
| 4621 | + return secret_state[SECRET_KEYS.ZAI]; | |
| 4622 | + case sources.openrouter: | |
| 4623 | + return secret_state[SECRET_KEYS.OPENROUTER]; | |
| 4624 | + default: | |
| 4625 | + return false; | |
| 4328 | 4626 | } |
| 4329 | 4627 | } |
| 4330 | 4628 | |
| @@ -4378,6 +4676,10 @@ async function sdMessageButton($icon, { animate } = {}) { | ||
| 4378 | 4676 | message.extra.media = []; |
| 4379 | 4677 | } |
| 4380 | 4678 | |
| 4679 | + if (!message.extra.media.length && !message.extra.media_display) { | |
| 4680 | + message.extra.media_display = MEDIA_DISPLAY.GALLERY; | |
| 4681 | + } | |
| 4682 | + | |
| 4381 | 4683 | /** @type {MediaAttachment} */ |
| 4382 | 4684 | const selectedMedia = message.extra.media.length > 0 |
| 4383 | 4685 | ? (message.extra.media[message.extra.media_index] ?? message.extra.media[message.extra.media.length - 1]) |
| @@ -4506,6 +4808,10 @@ async function generateMediaSwipe(mediaAttachment, message, onStart, onComplete, | ||
| 4506 | 4808 | async function onImageSwiped({ message, element, direction }) { |
| 4507 | 4809 | const { powerUserSettings, accountStorage } = getContext(); |
| 4508 | 4810 | |
| 4811 | + if (!isValidState()) { | |
| 4812 | + return; | |
| 4813 | + } | |
| 4814 | + | |
| 4509 | 4815 | if (!message || direction !== SWIPE_DIRECTION.RIGHT || powerUserSettings.image_overswipe !== IMAGE_OVERSWIPE.GENERATE) { |
| 4510 | 4816 | return; |
| 4511 | 4817 | } |
| @@ -4954,8 +5260,11 @@ jQuery(async () => { | ||
| 4954 | 5260 | $('#sd_novel_decrisper').on('input', onNovelDecrisperInput); |
| 4955 | 5261 | $('#sd_novel_variety_boost').on('input', onNovelVarietyBoostInput); |
| 4956 | 5262 | $('#sd_pollinations_enhance').on('input', onPollinationsEnhanceInput); |
| 5263 | + $('#sd_comfy_type').on('change', onComfyTypeChange); | |
| 4957 | 5264 | $('#sd_comfy_validate').on('click', validateComfyUrl); |
| 5265 | + $('#sd_comfy_runpod_validate').on('click', validateComfyRunPodUrl); | |
| 4958 | 5266 | $('#sd_comfy_url').on('input', onComfyUrlInput); |
| 5267 | + $('#sd_comfy_runpod_url').on('input', onComfyRunPodUrlInput); | |
| 4959 | 5268 | $('#sd_comfy_workflow').on('change', onComfyWorkflowChange); |
| 4960 | 5269 | $('#sd_comfy_open_workflow_editor').on('click', onComfyOpenWorkflowEditorClick); |
| 4961 | 5270 | $('#sd_comfy_new_workflow').on('click', onComfyNewWorkflowClick); |
| @@ -5033,6 +5342,7 @@ jQuery(async () => { | ||
| 5033 | 5342 | case SECRET_KEYS.FALAI: |
| 5034 | 5343 | case SECRET_KEYS.STABILITY: |
| 5035 | 5344 | case SECRET_KEYS.AIMLAPI: |
| 5345 | + case SECRET_KEYS.COMFY_RUNPOD: | |
| 5036 | 5346 | await loadSettingOptions(); |
| 5037 | 5347 | break; |
| 5038 | 5348 | } |
| @@ -5064,6 +5374,25 @@ jQuery(async () => { | ||
| 5064 | 5374 | return isNegative ? negativePrompt : characterPrompt; |
| 5065 | 5375 | }; |
| 5066 | 5376 | |
| 5067 | - MacrosParser.registerMacro('charPrefix', () => getMacroValue({ isNegative: false }), t`Character's positive positive Image Generation prompt prefix`); | |
| 5377 | + if (power_user.experimental_macro_engine) { | |
| 5068 | - MacrosParser.registerMacro('charNegativePrefix', () => getMacroValue({ isNegative: true }), t`Character's negative Image Generation prompt prefix`); | |
| 5378 | + macros.register('charPrefix', { | |
| 5379 | + category: MacroCategory.PROMPTS, | |
| 5380 | + description: t`Character's positive Image Generation prompt prefix`, | |
| 5381 | + handler: () => getMacroValue({ isNegative: false }), | |
| 5382 | + }); | |
| 5383 | + macros.register('charNegativePrefix', { | |
| 5384 | + category: MacroCategory.PROMPTS, | |
| 5385 | + description: t`Character's negative Image Generation prompt prefix`, | |
| 5386 | + handler: () => getMacroValue({ isNegative: true }), | |
| 5387 | + }); | |
| 5388 | + } else { | |
| 5389 | + MacrosParser.registerMacro('charPrefix', | |
| 5390 | + () => getMacroValue({ isNegative: false }), | |
| 5391 | + t`Character's positive Image Generation prompt prefix`, | |
| 5392 | + ); | |
| 5393 | + MacrosParser.registerMacro('charNegativePrefix', | |
| 5394 | + () => getMacroValue({ isNegative: true }), | |
| 5395 | + t`Character's negative Image Generation prompt prefix`, | |
| 5396 | + ); | |
| 5397 | + } | |
| 5069 | 5398 | }); |
| @@ -39,6 +39,7 @@ | ||
| 39 | 39 | <select id="sd_source"> |
| 40 | 40 | <option value="aimlapi">AI/ML API</option> |
| 41 | 41 | <option value="bfl">BFL (Black Forest Labs)</option> |
| 42 | + <option value="chutes">Chutes</option> | |
| 42 | 43 | <option value="comfy">ComfyUI</option> |
| 43 | 44 | <option value="drawthings">DrawThings HTTP API</option> |
| 44 | 45 | <option value="electronhub">Electron Hub</option> |
| @@ -49,6 +50,7 @@ | ||
| 49 | 50 | <option value="nanogpt">NanoGPT</option> |
| 50 | 51 | <option value="novel">NovelAI Diffusion</option> |
| 51 | 52 | <option value="openai">OpenAI</option> |
| 53 | + <option value="openrouter">OpenRouter</option> | |
| 52 | 54 | <option value="pollinations">Pollinations</option> |
| 53 | 55 | <option value="vlad">SD.Next (vladmandic)</option> |
| 54 | 56 | <option value="stability">Stability AI</option> |
| @@ -56,6 +58,7 @@ | ||
| 56 | 58 | <option value="horde">Stable Horde</option> |
| 57 | 59 | <option value="togetherai">TogetherAI</option> |
| 58 | 60 | <option value="xai">xAI (Grok)</option> |
| 61 | + <option value="zai">Z.AI (CogView)</option> | |
| 59 | 62 | </select> |
| 60 | 63 | <div data-sd-source="auto"> |
| 61 | 64 | <label for="sd_auto_url">SD Web UI URL</label> |
| @@ -94,6 +97,9 @@ | ||
| 94 | 97 | <label for="sd_huggingface_model_id" data-i18n="Model ID">Model ID</label> |
| 95 | 98 | <input id="sd_huggingface_model_id" type="text" class="text_pole" data-i18n="[placeholder]e.g. black-forest-labs/FLUX.1-dev" placeholder="e.g. black-forest-labs/FLUX.1-dev" value="" /> |
| 96 | 99 | </div> |
| 100 | + <div data-sd-source="chutes"> | |
| 101 | + <i data-i18n="Hint: Save an API key in the Chutes (Chat Completion) API settings to use it here.">Hint: Save an API key in the Chutes (Chat Completion) API settings to use it here.</i> | |
| 102 | + </div> | |
| 97 | 103 | <div data-sd-source="electronhub"> |
| 98 | 104 | <i data-i18n="Hint: Save an API key in the Electron Hub (Chat Completion) API settings to use it here.">Hint: Save an API key in the Electron Hub (Chat Completion) API settings to use it here.</i> |
| 99 | 105 | <div class="flex-container" id="sd_electronhub_quality_row"> |
| @@ -160,16 +166,19 @@ | ||
| 160 | 166 | </div> |
| 161 | 167 | </div> |
| 162 | 168 | </div> |
| 163 | 169 | <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"> | |
| 166 | 175 | <label for="sd_openai_style" data-i18n="Image Style">Image Style</label> |
| 167 | 176 | <select id="sd_openai_style"> |
| 168 | 177 | <option value="vivid">Vivid</option> |
| 169 | 178 | <option value="natural">Natural</option> |
| 170 | 179 | </select> |
| 171 | 180 | </div> |
| 172 | 181 | <div data-sd-model="dall-e-3,cogview-4-250304" class="flex1"> |
| 173 | 182 | <label for="sd_openai_quality" data-i18n="Image Quality">Image Quality</label> |
| 174 | 183 | <select id="sd_openai_quality"> |
| 175 | 184 | <option value="standard" data-i18n="Standard">Standard</option> |
| @@ -189,6 +198,12 @@ | ||
| 189 | 198 | </div> |
| 190 | 199 | </div> |
| 191 | 200 | <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"> | |
| 192 | 207 | <label for="sd_comfy_url">ComfyUI URL</label> |
| 193 | 208 | <div class="flex-container flexnowrap"> |
| 194 | 209 | <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 @@ | ||
| 199 | 214 | </span> |
| 200 | 215 | </div> |
| 201 | 216 | </div> |
| 217 | + </div> | |
| 218 | + <div data-sd-comfy-type="runpod_serverless"> | |
| 219 | + <label for="sd_comfy_runpod_url">ComfyUI RunPod URL</label> | |
| 220 | + <div class="flex-container flexnowrap"> | |
| 221 | + <input id="sd_comfy_runpod_url" type="text" class="text_pole" data-i18n="[placeholder]sd_comfy_runpod_url" placeholder="eg: https://api.runpod.ai/v2/<your endpoint id>" value="{{comfy_runpod_url}}" /> | |
| 222 | + <div id="sd_comfy_runpod_validate" class="menu_button menu_button_icon"> | |
| 223 | + <i class="fa-solid fa-check"></i> | |
| 224 | + <span data-i18n="Connect"> | |
| 225 | + Connect | |
| 226 | + </span> | |
| 227 | + </div> | |
| 228 | + </div> | |
| 229 | + <div class="flex-container flexnowrap alignItemsBaseline marginBot5"> | |
| 230 | + <a href="https://console.runpod.io/user/settings" target="_blank" rel="noopener noreferrer"> | |
| 231 | + <strong data-i18n="API Key">API Key</strong> | |
| 232 | + <i class="fa-solid fa-share-from-square"></i> | |
| 233 | + </a> | |
| 234 | + <span class="expander"></span> | |
| 235 | + <div id="sd_runpod_key" class="menu_button menu_button_icon manage-api-keys" data-key="api_key_comfy_runpod"> | |
| 236 | + <i class="fa-fw fa-solid fa-key"></i> | |
| 237 | + <span data-i18n="Click to set">Click to set</span> | |
| 238 | + </div> | |
| 239 | + </div> | |
| 240 | + </div> | |
| 202 | 241 | <p><i><b data-i18n="Important:">Important:</b></i><i data-i18n="The server must be accessible from the SillyTavern host machine."> The server must be accessible from the SillyTavern host machine.</i></p> |
| 203 | 242 | <label for="sd_comfy_workflow">ComfyUI Workflow</label> |
| 204 | 243 | <div class="flex-container flexnowrap"> |
| @@ -210,7 +210,7 @@ async function translateIncomingMessage(messageId) { | ||
| 210 | 210 | return; |
| 211 | 211 | } |
| 212 | 212 | |
| 213 | 213 | const textToTranslate = substituteParams(message.mes, context.name1,{ name2Override: message.name }); |
| 214 | 214 | const translation = await translate(textToTranslate, extension_settings.translate.target_language); |
| 215 | 215 | message.extra.display_text = translation; |
| 216 | 216 | |
| @@ -238,7 +238,7 @@ async function translateIncomingMessageReasoning(messageId) { | ||
| 238 | 238 | return false; |
| 239 | 239 | } |
| 240 | 240 | |
| 241 | 241 | const textToTranslate = substituteParams(message.extra.reasoning, context.name1,{ name2Override: message.name }); |
| 242 | 242 | const translation = await translate(textToTranslate, extension_settings.translate.target_language); |
| 243 | 243 | message.extra.reasoning_display_text = translation; |
| 244 | 244 | |
| @@ -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 | +} | |
| @@ -259,7 +259,7 @@ class EdgeTtsProvider { | ||
| 259 | 259 | try { |
| 260 | 260 | const result = await fetch('/api/plugins/edge-tts/probe', { |
| 261 | 261 | method: 'POST', |
| 262 | 262 | headers: getRequestHeaders({ omitContentType: true }), |
| 263 | 263 | }); |
| 264 | 264 | return result.ok; |
| 265 | 265 | } catch (e) { |
| @@ -187,7 +187,7 @@ class ElectronHubTtsProvider { | ||
| 187 | 187 | try { |
| 188 | 188 | const response = await fetch('/api/openai/electronhub/models', { |
| 189 | 189 | method: 'POST', |
| 190 | 190 | headers: getRequestHeaders({ omitContentType: true }), |
| 191 | 191 | }); |
| 192 | 192 | if (!response.ok) { |
| 193 | 193 | throw new Error(`HTTP ${response.status}: ${await response.text()}`); |
| @@ -1,23 +1,20 @@ | ||
| 1 | 1 | import { 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'; | |
| 2 | 5 | export { ElevenLabsTtsProvider }; |
| 3 | 6 | |
| 4 | 7 | class ElevenLabsTtsProvider { |
| 5 | - //########// | |
| 6 | - // Config // | |
| 7 | - //########// | |
| 8 | - | |
| 9 | 8 | settings; |
| 10 | 9 | voices = []; |
| 11 | 10 | separator = ' ... ... ... '; |
| 12 | 11 | |
| 13 | - | |
| 14 | 12 | defaultSettings = { |
| 15 | 13 | stability: 0.75, |
| 16 | 14 | similarity_boost: 0.75, |
| 17 | 15 | style_exaggeration: 0.00, |
| 18 | 16 | speaker_boost: true, |
| 19 | 17 | speed: 1.0, |
| 20 | - apiKey: '', | |
| 21 | 18 | model: 'eleven_turbo_v2_5', |
| 22 | 19 | voiceMap: {}, |
| 23 | 20 | }; |
| @@ -25,8 +22,15 @@ class ElevenLabsTtsProvider { | ||
| 25 | 22 | get settingsHtml() { |
| 26 | 23 | let html = ` |
| 27 | 24 | <div class="elevenlabs_tts_settings"> |
| 28 | - <label for="elevenlabs_tts_api_key">API Key</label> | |
| 25 | + <div class="flex-container alignItemsBaseline"> | |
| 29 | - <input id="elevenlabs_tts_api_key" type="text" class="text_pole" placeholder="<API Key>"/> | |
| 26 | + <h4 for="elevenlabs_tts_key" class="flex1 margin0"> | |
| 27 | + <a href="https://elevenlabs.io/app/developers/api-keys" target="_blank">ElevenLabs TTS Key</a> | |
| 28 | + </h4> | |
| 29 | + <div id="elevenlabs_tts_key" class="menu_button menu_button_icon manage-api-keys" data-key="api_key_elevenlabs"> | |
| 30 | + <i class="fa-solid fa-key"></i> | |
| 31 | + <span>Click to set</span> | |
| 32 | + </div> | |
| 33 | + </div> | |
| 30 | 34 | <label for="elevenlabs_tts_model">Model</label> |
| 31 | 35 | <select id="elevenlabs_tts_model" class="text_pole"> |
| 32 | 36 | <option value="eleven_v3">Eleven v3</option> |
| @@ -39,7 +43,6 @@ class ElevenLabsTtsProvider { | ||
| 39 | 43 | <option value="eleven_multilingual_v1">Multilingual v1 (Old)</option> |
| 40 | 44 | <option value="eleven_turbo_v2">Turbo v2 (Old)</option> |
| 41 | 45 | </select> |
| 42 | - <input id="eleven_labs_connect" class="menu_button" type="button" value="Connect" /> | |
| 43 | 46 | <label for="elevenlabs_tts_stability">Stability: <span id="elevenlabs_tts_stability_output"></span></label> |
| 44 | 47 | <input id="elevenlabs_tts_stability" type="range" value="${this.defaultSettings.stability}" min="0" max="1" step="0.01" /> |
| 45 | 48 | <label for="elevenlabs_tts_similarity_boost">Similarity Boost: <span id="elevenlabs_tts_similarity_boost_output"></span></label> |
| @@ -72,6 +75,20 @@ class ElevenLabsTtsProvider { | ||
| 72 | 75 | return html; |
| 73 | 76 | } |
| 74 | 77 | |
| 78 | + constructor() { | |
| 79 | + this.handler = async function (/** @type {string} */ key) { | |
| 80 | + if (key !== SECRET_KEYS.ELEVENLABS) return; | |
| 81 | + $('#elevenlabs_tts_key').toggleClass('success', !!secret_state[SECRET_KEYS.ELEVENLABS]); | |
| 82 | + await this.fetchTtsVoiceObjects(); | |
| 83 | + }.bind(this); | |
| 84 | + } | |
| 85 | + | |
| 86 | + dispose() { | |
| 87 | + [event_types.SECRET_WRITTEN, event_types.SECRET_DELETED, event_types.SECRET_ROTATED].forEach(event => { | |
| 88 | + eventSource.removeListener(event, this.handler); | |
| 89 | + }); | |
| 90 | + } | |
| 91 | + | |
| 75 | 92 | shouldInvolveExtendedSettings() { |
| 76 | 93 | // Models that support extended settings (style_exaggeration, speaker_boost) |
| 77 | 94 | const modelsWithExtendedSettings = [ |
| @@ -114,6 +131,18 @@ class ElevenLabsTtsProvider { | ||
| 114 | 131 | delete settings['multilingual']; |
| 115 | 132 | } |
| 116 | 133 | |
| 134 | + if (Object.hasOwn(settings, 'apiKey')) { | |
| 135 | + if (settings.apiKey && !secret_state[SECRET_KEYS.ELEVENLABS]){ | |
| 136 | + await writeSecret(SECRET_KEYS.ELEVENLABS, settings.apiKey); | |
| 137 | + } | |
| 138 | + delete settings['apiKey']; | |
| 139 | + } | |
| 140 | + | |
| 141 | + $('#elevenlabs_tts_key').toggleClass('success', !!secret_state[SECRET_KEYS.ELEVENLABS]); | |
| 142 | + [event_types.SECRET_WRITTEN, event_types.SECRET_DELETED, event_types.SECRET_ROTATED].forEach(event => { | |
| 143 | + eventSource.on(event, this.handler); | |
| 144 | + }); | |
| 145 | + | |
| 117 | 146 | for (const key in settings) { |
| 118 | 147 | if (key in this.settings) { |
| 119 | 148 | this.settings[key] = settings[key]; |
| @@ -127,9 +156,7 @@ class ElevenLabsTtsProvider { | ||
| 127 | 156 | $('#elevenlabs_tts_style_exaggeration').val(this.settings.style_exaggeration); |
| 128 | 157 | $('#elevenlabs_tts_speaker_boost').prop('checked', this.settings.speaker_boost); |
| 129 | 158 | $('#elevenlabs_tts_speed').val(this.settings.speed); |
| 130 | - $('#elevenlabs_tts_api_key').val(this.settings.apiKey); | |
| 131 | 159 | $('#elevenlabs_tts_model').val(this.settings.model); |
| 132 | - $('#eleven_labs_connect').on('click', () => { this.onConnectClick(); }); | |
| 133 | 160 | $('#elevenlabs_tts_similarity_boost').on('input', this.onSettingsChange.bind(this)); |
| 134 | 161 | $('#elevenlabs_tts_stability').on('input', this.onSettingsChange.bind(this)); |
| 135 | 162 | $('#elevenlabs_tts_style_exaggeration').on('input', this.onSettingsChange.bind(this)); |
| @@ -157,23 +184,17 @@ class ElevenLabsTtsProvider { | ||
| 157 | 184 | } |
| 158 | 185 | |
| 159 | 186 | async onRefreshClick() { |
| 160 | - } | |
| 187 | + await this.fetchTtsVoiceObjects(); | |
| 161 | - | |
| 162 | - async onConnectClick() { | |
| 163 | - // Update on Apply click | |
| 164 | - return await this.updateApiKey().catch((error) => { | |
| 165 | - toastr.error(`ElevenLabs: ${error}`); | |
| 166 | - }); | |
| 167 | 188 | } |
| 168 | 189 | |
| 169 | 190 | setupVoiceCloningMenu() { |
| 170 | 191 | const audioFilesInput = /** @type {HTMLInputElement} */ (document.getElementById('elevenlabs_tts_audio_files')); |
| 171 | 192 | const selectedFilesListElement = document.getElementById('elevenlabs_tts_selected_files_list'); |
| 172 | 193 | const cloneVoiceButton = document.getElementById('elevenlabs_tts_clone_voice_button'); |
| 173 | 194 | const uploadAudioFileButton = document.getElementById('upload_audio_file'); |
| 174 | 195 | const voiceCloningNameInput = /** @type {HTMLInputElement} */ (document.getElementById('elevenlabs_tts_voice_cloning_name')); |
| 175 | 196 | const voiceCloningDescriptionInput = /** @type {HTMLInputElement} */ (document.getElementById('elevenlabs_tts_voice_cloning_description')); |
| 176 | 197 | const voiceCloningLabelsInput = /** @type {HTMLInputElement} */ (document.getElementById('elevenlabs_tts_voice_cloning_labels')); |
| 177 | 198 | |
| 178 | 199 | const updateCloneVoiceButtonVisibility = () => { |
| 179 | 200 | cloneVoiceButton.style.display = audioFilesInput.files.length > 0 ? 'inline-block' : 'none'; |
| @@ -224,22 +245,11 @@ class ElevenLabsTtsProvider { | ||
| 224 | 245 | updateCloneVoiceButtonVisibility(); |
| 225 | 246 | } |
| 226 | 247 | |
| 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 | - | |
| 243 | 253 | async getVoice(voiceName) { |
| 244 | 254 | if (this.voices.length == 0) { |
| 245 | 255 | this.voices = await this.fetchTtsVoiceObjects(); |
| @@ -253,25 +263,30 @@ class ElevenLabsTtsProvider { | ||
| 253 | 263 | return match; |
| 254 | 264 | } |
| 255 | 265 | |
| 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 | + */ | |
| 257 | 272 | async generateTts(text, voiceId) { |
| 258 | 273 | const historyId = await this.findTtsGenerationInHistory(text, voiceId); |
| 259 | 274 | |
| 260 | - let response; | |
| 261 | 275 | if (historyId) { |
| 262 | 276 | console.debug(`Found existing TTS generation with id ${historyId}`); |
| 263 | 277 | response =return await this.fetchTtsFromHistory(historyId); |
| 264 | 278 | } else { |
| 265 | 279 | console.debug('No existing TTS generation found, requesting new generation'); |
| 266 | 280 | response =return await this.fetchTtsGeneration(text, voiceId); |
| 267 | 281 | } |
| 268 | - return response; | |
| 269 | 282 | } |
| 270 | 283 | |
| 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 | + */ | |
| 275 | 290 | async findTtsGenerationInHistory(message, voiceId) { |
| 276 | 291 | const ttsHistory = await this.fetchTtsHistory(); |
| 277 | 292 | for (const history of ttsHistory) { |
| @@ -285,40 +300,35 @@ class ElevenLabsTtsProvider { | ||
| 285 | 300 | return ''; |
| 286 | 301 | } |
| 287 | 302 | |
| 288 | - | |
| 289 | - //###########// | |
| 290 | - // API CALLS // | |
| 291 | - //###########// | |
| 292 | 303 | async fetchTtsVoiceObjects() { |
| 293 | - const headers = { | |
| 304 | + const response = await fetch('/api/speech/elevenlabs/voices', { | |
| 294 | - 'xi-api-key': this.settings.apiKey, | |
| 305 | + method: 'POST', | |
| 295 | - }; | |
| 306 | + headers: getRequestHeaders({ omitContentType: true }), | |
| 296 | - const response = await fetch('https://api.elevenlabs.io/v1/voices', { | |
| 297 | - headers: headers, | |
| 298 | 307 | }); |
| 299 | 308 | if (!response.ok) { |
| 300 | 309 | throw new Error(`HTTP ${response.status}:. ${awaitSee responseserver console for details.text()}`); |
| 301 | 310 | } |
| 302 | 311 | const responseJson = await response.json(); |
| 303 | 312 | return responseJson.voices; |
| 304 | 313 | } |
| 305 | 314 | |
| 306 | 315 | async fetchTtsVoiceSettings() { |
| 307 | - const headers = { | |
| 316 | + const response = await fetch('/api/speech/elevenlabs/voice-settings', { | |
| 308 | - 'xi-api-key': this.settings.apiKey, | |
| 317 | + method: 'POST', | |
| 309 | - }; | |
| 318 | + headers: getRequestHeaders({ omitContentType: true }), | |
| 310 | - const response = await fetch( | |
| 319 | + }); | |
| 311 | - 'https://api.elevenlabs.io/v1/voices/settings/default', | |
| 312 | - { | |
| 313 | - headers: headers, | |
| 314 | - }, | |
| 315 | - ); | |
| 316 | 320 | if (!response.ok) { |
| 317 | 321 | throw new Error(`HTTP ${response.status}:. ${awaitSee responseserver console for details.text()}`); |
| 318 | 322 | } |
| 319 | 323 | return response.json(); |
| 320 | 324 | } |
| 321 | 325 | |
| 326 | + /** | |
| 327 | + * Fetch new TTS generation from ElevenLabs API | |
| 328 | + * @param {string} text Text to synthesize | |
| 329 | + * @param {string} voiceId Voice ID to use for synthesis | |
| 330 | + * @returns {Promise<Response>} Response object containing audio data | |
| 331 | + */ | |
| 322 | 332 | async fetchTtsGeneration(text, voiceId) { |
| 323 | 333 | let model = this.settings.model ?? 'eleven_monolingual_v1'; |
| 324 | 334 | console.info(`Generating new TTS for voice_id ${voiceId}, model ${model}`); |
| @@ -335,75 +345,90 @@ class ElevenLabsTtsProvider { | ||
| 335 | 345 | request.voice_settings.style = Number(this.settings.style_exaggeration); |
| 336 | 346 | request.voice_settings.use_speaker_boost = Boolean(this.settings.speaker_boost); |
| 337 | 347 | } |
| 338 | 348 | const response = await fetch(`https:/'/api.elevenlabs.io/v1/text-to-speech/${voiceId}`elevenlabs/synthesize', { |
| 339 | 349 | method: 'POST', |
| 340 | 350 | headers: {getRequestHeaders(), |
| 341 | - 'xi-api-key': this.settings.apiKey, | |
| 351 | + body: JSON.stringify({ | |
| 342 | - 'Content-Type': 'application/json', | |
| 352 | + voiceId: voiceId, | |
| 343 | - }, | |
| 353 | + request: request, | |
| 344 | - body: JSON.stringify(request), | |
| 354 | + }), | |
| 345 | 355 | }); |
| 346 | 356 | if (!response.ok) { |
| 347 | 357 | toastr.error(response.statusText, 'TTS Generation Failed'); |
| 348 | 358 | throw new Error(`HTTP ${response.status}:. ${awaitSee responseserver console for details.text()}`); |
| 349 | 359 | } |
| 350 | 360 | return response; |
| 351 | 361 | } |
| 352 | 362 | |
| 353 | - async fetchTtsFromHistory(history_item_id) { | |
| 363 | + /** | |
| 354 | - console.info(`Fetched existing TTS with history_item_id ${history_item_id}`); | |
| 364 | + * Fetch existing TTS audio from history | |
| 355 | - const response = await fetch( | |
| 365 | + * @param {string} historyItemId History item ID to fetch audio for | |
| 356 | - `https://api.elevenlabs.io/v1/history/${history_item_id}/audio`, | |
| 366 | + * @returns {Promise<Response>} Response object containing audio data | |
| 357 | - { | |
| 367 | + */ | |
| 358 | - headers: { | |
| 368 | + async fetchTtsFromHistory(historyItemId) { | |
| 359 | - 'xi-api-key': this.settings.apiKey, | |
| 369 | + console.info(`Fetched existing TTS with history_item_id ${historyItemId}`); | |
| 360 | - }, | |
| 370 | + const response = await fetch('/api/speech/elevenlabs/history-audio', { | |
| 361 | - }, | |
| 371 | + method: 'POST', | |
| 362 | - ); | |
| 372 | + headers: getRequestHeaders(), | |
| 373 | + body: JSON.stringify({ | |
| 374 | + historyItemId: historyItemId, | |
| 375 | + }), | |
| 376 | + }); | |
| 363 | 377 | if (!response.ok) { |
| 364 | 378 | throw new Error(`HTTP ${response.status}:. ${awaitSee responseserver console for details.text()}`); |
| 365 | 379 | } |
| 366 | 380 | return response; |
| 367 | 381 | } |
| 368 | 382 | |
| 383 | + /** | |
| 384 | + * Fetch TTS generation history | |
| 385 | + * @returns {Promise<Array>} Array of TTS history items | |
| 386 | + */ | |
| 369 | 387 | async fetchTtsHistory() { |
| 370 | - const headers = { | |
| 388 | + const response = await fetch('/api/speech/elevenlabs/history', { | |
| 371 | - 'xi-api-key': this.settings.apiKey, | |
| 389 | + method: 'POST', | |
| 372 | - }; | |
| 390 | + headers: getRequestHeaders({ omitContentType: true }), | |
| 373 | - const response = await fetch('https://api.elevenlabs.io/v1/history', { | |
| 374 | - headers: headers, | |
| 375 | 391 | }); |
| 376 | 392 | if (!response.ok) { |
| 377 | 393 | throw new Error(`HTTP ${response.status}:. ${awaitSee responseserver console for details.text()}`); |
| 378 | 394 | } |
| 379 | 395 | const responseJson = await response.json(); |
| 380 | 396 | return responseJson.history; |
| 381 | 397 | } |
| 382 | 398 | |
| 399 | + /** | |
| 400 | + * Add a new voice via ElevenLabs API | |
| 401 | + * @param {string} name Voice name | |
| 402 | + * @param {string} description Voice description | |
| 403 | + * @param {string} labels Voice labels | |
| 404 | + * @returns {Promise<Object>} Newly created voice object | |
| 405 | + */ | |
| 383 | 406 | async addVoice(name, description, labels) { |
| 384 | - const selected_files = document.querySelectorAll('input[type="file"][name="audio_files"]'); | |
| 407 | + const audioFilesInput = /** @type {HTMLInputElement} */ (document.getElementById('elevenlabs_tts_audio_files')); | |
| 385 | - const formData = new FormData(); | |
| 408 | + if (!(audioFilesInput instanceof HTMLInputElement) || audioFilesInput.files.length === 0) { | |
| 409 | + throw new Error('No audio files selected for voice cloning.'); | |
| 410 | + } | |
| 386 | 411 | |
| 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 | + }; | |
| 390 | 418 | |
| 391 | 419 | 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 | - } | |
| 395 | 422 | } |
| 396 | 423 | |
| 397 | 424 | const response = await fetch('https://api.elevenlabs.io/v1speech/elevenlabs/voices/add', { |
| 398 | 425 | method: 'POST', |
| 399 | 426 | headers: {getRequestHeaders(), |
| 400 | - 'xi-api-key': this.settings.apiKey, | |
| 427 | + body: JSON.stringify(data), | |
| 401 | - }, | |
| 402 | - body: formData, | |
| 403 | 428 | }); |
| 404 | 429 | |
| 405 | 430 | if (!response.ok) { |
| 406 | 431 | throw new Error(`HTTP ${response.status}:. ${awaitSee responseserver console for details.text()}`); |
| 407 | 432 | } |
| 408 | 433 | |
| 409 | 434 | return await response.json(); |
| @@ -34,6 +34,7 @@ import { TtsWebuiProvider } from './tts-webui.js'; | ||
| 34 | 34 | import { PollinationsTtsProvider } from './pollinations.js'; |
| 35 | 35 | import { MiniMaxTtsProvider } from './minimax.js'; |
| 36 | 36 | import { ElectronHubTtsProvider } from './electronhub.js'; |
| 37 | +import { ChutesTtsProvider } from './chutes.js'; | |
| 37 | 38 | |
| 38 | 39 | const UPDATE_INTERVAL = 1000; |
| 39 | 40 | const wrapper = new ModuleWorkerWrapper(moduleWorker); |
| @@ -120,6 +121,7 @@ const ttsProviders = { | ||
| 120 | 121 | AllTalk: AllTalkTtsProvider, |
| 121 | 122 | Azure: AzureTtsProvider, |
| 122 | 123 | Chatterbox: ChatterboxTtsProvider, |
| 124 | + Chutes: ChutesTtsProvider, | |
| 123 | 125 | Coqui: CoquiTtsProvider, |
| 124 | 126 | 'CosyVoice (Unofficial)': CosyVoiceProvider, |
| 125 | 127 | Edge: EdgeTtsProvider, |
| @@ -239,9 +241,7 @@ function isTtsProcessing() { | ||
| 239 | 241 | |
| 240 | 242 | /** |
| 241 | 243 | * Splits a message into lines and adds each non-empty line to the TTS job queue. |
| 242 | 244 | * @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. | |
| 245 | 245 | * @returns {void} |
| 246 | 246 | */ |
| 247 | 247 | function processAndQueueTtsMessage(message) { |
| @@ -69,6 +69,7 @@ const settings = { | ||
| 69 | 69 | vllm_model: '', |
| 70 | 70 | webllm_model: '', |
| 71 | 71 | google_model: 'text-embedding-005', |
| 72 | + chutes_model: 'chutes-qwen-qwen3-embedding-8b', | |
| 72 | 73 | summarize: false, |
| 73 | 74 | summarize_sent: false, |
| 74 | 75 | summary_source: 'main', |
| @@ -829,6 +830,9 @@ function getVectorsRequestBody(args = {}) { | ||
| 829 | 830 | body.vertexai_region = oai_settings.vertexai_region; |
| 830 | 831 | body.vertexai_express_project_id = oai_settings.vertexai_express_project_id; |
| 831 | 832 | break; |
| 833 | + case 'chutes': | |
| 834 | + body.model = extension_settings.vectors.chutes_model; | |
| 835 | + break; | |
| 832 | 836 | default: |
| 833 | 837 | break; |
| 834 | 838 | } |
| @@ -913,6 +917,7 @@ async function insertVectorItems(collectionId, items) { | ||
| 913 | 917 | function throwIfSourceInvalid() { |
| 914 | 918 | if (settings.source === 'openai' && !secret_state[SECRET_KEYS.OPENAI] || |
| 915 | 919 | settings.source === 'electronhub' && !secret_state[SECRET_KEYS.ELECTRONHUB] || |
| 920 | + settings.source === 'chutes' && !secret_state[SECRET_KEYS.CHUTES] || | |
| 916 | 921 | settings.source === 'openrouter' && !secret_state[SECRET_KEYS.OPENROUTER] || |
| 917 | 922 | settings.source === 'palm' && !secret_state[SECRET_KEYS.MAKERSUITE] || |
| 918 | 923 | settings.source === 'vertexai' && !secret_state[SECRET_KEYS.VERTEXAI] && !secret_state[SECRET_KEYS.VERTEXAI_SERVICE_ACCOUNT] || |
| @@ -1127,6 +1132,7 @@ function toggleSettings() { | ||
| 1127 | 1132 | $('#together_vectorsModel').toggle(settings.source === 'togetherai'); |
| 1128 | 1133 | $('#openai_vectorsModel').toggle(settings.source === 'openai'); |
| 1129 | 1134 | $('#electronhub_vectorsModel').toggle(settings.source === 'electronhub'); |
| 1135 | + $('#chutes_vectorsModel').toggle(settings.source === 'chutes'); | |
| 1130 | 1136 | $('#openrouter_vectorsModel').toggle(settings.source === 'openrouter'); |
| 1131 | 1137 | $('#cohere_vectorsModel').toggle(settings.source === 'cohere'); |
| 1132 | 1138 | $('#ollama_vectorsModel').toggle(settings.source === 'ollama'); |
| @@ -1147,14 +1153,51 @@ function toggleSettings() { | ||
| 1147 | 1153 | case 'openrouter': |
| 1148 | 1154 | loadOpenRouterModels(); |
| 1149 | 1155 | 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; | |
| 1150 | 1192 | } |
| 1193 | + $('#vectors_chutes_model').val(settings.chutes_model); | |
| 1151 | 1194 | } |
| 1152 | 1195 | |
| 1153 | 1196 | async function loadElectronHubModels() { |
| 1154 | 1197 | try { |
| 1155 | 1198 | const response = await fetch('/api/openai/electronhub/models', { |
| 1156 | 1199 | method: 'POST', |
| 1157 | 1200 | headers: getRequestHeaders({ omitContentType: true }), |
| 1158 | 1201 | }); |
| 1159 | 1202 | if (!response.ok) { |
| 1160 | 1203 | throw new Error(`HTTP ${response.status}`); |
| @@ -1193,7 +1236,7 @@ async function loadOpenRouterModels() { | ||
| 1193 | 1236 | try { |
| 1194 | 1237 | const response = await fetch('/api/openrouter/models/embedding', { |
| 1195 | 1238 | method: 'POST', |
| 1196 | 1239 | headers: getRequestHeaders({ omitContentType: true }), |
| 1197 | 1240 | }); |
| 1198 | 1241 | if (!response.ok) { |
| 1199 | 1242 | throw new Error(`HTTP ${response.status}`); |
| @@ -1630,6 +1673,11 @@ jQuery(async () => { | ||
| 1630 | 1673 | Object.assign(extension_settings.vectors, settings); |
| 1631 | 1674 | saveSettingsDebounced(); |
| 1632 | 1675 | }); |
| 1676 | + $('#vectors_chutes_model').val(settings.chutes_model).on('change', () => { | |
| 1677 | + settings.chutes_model = String($('#vectors_chutes_model').val()); | |
| 1678 | + Object.assign(extension_settings.vectors, settings); | |
| 1679 | + saveSettingsDebounced(); | |
| 1680 | + }); | |
| 1633 | 1681 | $('#vectors_openrouter_model').val(settings.openrouter_model).on('change', () => { |
| 1634 | 1682 | settings.openrouter_model = String($('#vectors_openrouter_model').val()); |
| 1635 | 1683 | Object.assign(extension_settings.vectors, settings); |
| @@ -10,6 +10,7 @@ | ||
| 10 | 10 | Vectorization Source |
| 11 | 11 | </label> |
| 12 | 12 | <select id="vectors_source" class="text_pole"> |
| 13 | + <option value="chutes">Chutes</option> | |
| 13 | 14 | <option value="cohere">Cohere</option> |
| 14 | 15 | <option value="electronhub">Electron Hub</option> |
| 15 | 16 | <option value="extras">Extras (deprecated)</option> |
| @@ -28,6 +29,15 @@ | ||
| 28 | 29 | <option value="webllm" data-i18n="WebLLM Extension">WebLLM Extension</option> |
| 29 | 30 | </select> |
| 30 | 31 | </div> |
| 32 | + <div class="flex-container flexFlowColumn" id="chutes_vectorsModel"> | |
| 33 | + <label for="vectors_chutes_model" data-i18n="Vectorization Model"> | |
| 34 | + Vectorization Model | |
| 35 | + </label> | |
| 36 | + <select id="vectors_chutes_model" class="text_pole"></select> | |
| 37 | + <i data-i18n="Hint: Set your Chutes API key in API Connections."> | |
| 38 | + Hint: Set your Chutes API key in API Connections. | |
| 39 | + </i> | |
| 40 | + </div> | |
| 31 | 41 | <div class="flex-container flexFlowColumn" id="electronhub_vectorsModel"> |
| 32 | 42 | <label for="vectors_electronhub_model" data-i18n="Vectorization Model"> |
| 33 | 43 | Vectorization Model |
| @@ -17,6 +17,8 @@ import { | ||
| 17 | 17 | renderPaginationDropdown, |
| 18 | 18 | paginationDropdownChangeHandler, |
| 19 | 19 | waitUntilCondition, |
| 20 | + uuidv4, | |
| 21 | + humanFileSize, | |
| 20 | 22 | } from './utils.js'; |
| 21 | 23 | import { RA_CountCharTokens, humanizedDateTime, dragElement, favsToHotswap, getMessageTimeStamp } from './RossAscends-mods.js'; |
| 22 | 24 | import { power_user, loadMovingUIState, sortEntitiesList } from './power-user.js'; |
| @@ -37,8 +39,6 @@ import { | ||
| 37 | 39 | setCharacterName, |
| 38 | 40 | setEditedMessageId, |
| 39 | 41 | is_send_press, |
| 40 | - name1, | |
| 41 | - name2, | |
| 42 | 42 | resetChatState, |
| 43 | 43 | setSendButtonState, |
| 44 | 44 | getCharacters, |
| @@ -71,6 +71,7 @@ import { | ||
| 71 | 71 | isChatSaving, |
| 72 | 72 | setExternalAbortController, |
| 73 | 73 | baseChatReplace, |
| 74 | + createLazyFields, | |
| 74 | 75 | depth_prompt_depth_default, |
| 75 | 76 | loadItemizedPrompts, |
| 76 | 77 | animation_duration, |
| @@ -109,7 +110,9 @@ export { | ||
| 109 | 110 | let is_group_generating = false; // Group generation flag |
| 110 | 111 | let is_group_automode_enabled = false; |
| 111 | 112 | let hideMutedSprites = false; |
| 113 | +/** @type {Group[]} */ | |
| 112 | 114 | let groups = []; |
| 115 | +/** @type {string|null} */ | |
| 113 | 116 | let selected_group = null; |
| 114 | 117 | let group_generation_id = null; |
| 115 | 118 | let fav_grp_checked = false; |
| @@ -143,6 +146,11 @@ function setAutoModeWorker() { | ||
| 143 | 146 | autoModeWorker = setInterval(groupChatAutoModeWorker, autoModeDelay * 1000); |
| 144 | 147 | } |
| 145 | 148 | |
| 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 | + */ | |
| 146 | 154 | async function _save(group, reload = true) { |
| 147 | 155 | await fetch('/api/groups/edit', { |
| 148 | 156 | method: 'POST', |
| @@ -179,6 +187,11 @@ async function regenerateGroup() { | ||
| 179 | 187 | generateGroupWrapper(false, 'normal', { signal: abortController.signal }); |
| 180 | 188 | } |
| 181 | 189 | |
| 190 | +/** | |
| 191 | + * Loads group chat messages from the server. | |
| 192 | + * @param {string} chatId Chat ID | |
| 193 | + * @returns {Promise<ChatFile>} Array of chat messages | |
| 194 | + */ | |
| 182 | 195 | async function loadGroupChat(chatId) { |
| 183 | 196 | const response = await fetch('/api/chats/group/get', { |
| 184 | 197 | method: 'POST', |
| @@ -188,12 +201,20 @@ async function loadGroupChat(chatId) { | ||
| 188 | 201 | |
| 189 | 202 | if (response.ok) { |
| 190 | 203 | const data = await response.json(); |
| 204 | + if (!Array.isArray(data)) { | |
| 205 | + return []; | |
| 206 | + } | |
| 191 | 207 | return data; |
| 192 | 208 | } |
| 193 | 209 | |
| 194 | 210 | return []; |
| 195 | 211 | } |
| 196 | 212 | |
| 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 | + */ | |
| 197 | 218 | async function validateGroup(group) { |
| 198 | 219 | if (!group) return; |
| 199 | 220 | |
| @@ -225,6 +246,12 @@ async function validateGroup(group) { | ||
| 225 | 246 | } |
| 226 | 247 | } |
| 227 | 248 | |
| 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 | + */ | |
| 228 | 255 | export async function getGroupChat(groupId, reload = false) { |
| 229 | 256 | const group = groups.find((x) => x.id === groupId); |
| 230 | 257 | if (!group) { |
| @@ -238,9 +265,19 @@ export async function getGroupChat(groupId, reload = false) { | ||
| 238 | 265 | |
| 239 | 266 | const chat_id = group.chat_id; |
| 240 | 267 | const data = await loadGroupChat(chat_id); |
| 241 | 268 | const metadata = groupdata?.[0]?.chat_metadata ?? {}; |
| 242 | 269 | const freshChat = !metadata.tainted && (!Array.isArray(data) || !data.length); |
| 243 | 270 | |
| 271 | + // Remove chat file header if present | |
| 272 | + if (Array.isArray(data) && data.length && Object.hasOwn(data[0], 'chat_metadata')) { | |
| 273 | + data.shift(); | |
| 274 | + } | |
| 275 | + | |
| 276 | + // Add integrity slug if missing | |
| 277 | + if (!metadata['integrity']) { | |
| 278 | + metadata['integrity'] = uuidv4(); | |
| 279 | + } | |
| 280 | + | |
| 244 | 281 | await loadItemizedPrompts(getCurrentChatId()); |
| 245 | 282 | |
| 246 | 283 | if (group && Array.isArray(group.members) && freshChat) { |
| @@ -266,7 +303,6 @@ export async function getGroupChat(groupId, reload = false) { | ||
| 266 | 303 | } |
| 267 | 304 | await saveGroupChat(groupId, false); |
| 268 | 305 | } else if (Array.isArray(data) && data.length) { |
| 269 | - data[0].is_group = true; | |
| 270 | 306 | chat.splice(0, chat.length, ...data); |
| 271 | 307 | chat.forEach(ensureMessageMediaIsArray); |
| 272 | 308 | chatElement.find('.mes').remove(); |
| @@ -287,7 +323,7 @@ export async function getGroupChat(groupId, reload = false) { | ||
| 287 | 323 | * Retrieves the members of a group |
| 288 | 324 | * |
| 289 | 325 | * @param {string} [groupId=selected_group] - The ID of the group to retrieve members from. Defaults to the currently selected group. |
| 290 | 326 | * @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. |
| 291 | 327 | */ |
| 292 | 328 | export function getGroupMembers(groupId = selected_group) { |
| 293 | 329 | const group = groups.find((x) => x.id === groupId); |
| @@ -421,7 +457,7 @@ export function getGroupDepthPrompts(groupId, characterId) { | ||
| 421 | 457 | continue; |
| 422 | 458 | } |
| 423 | 459 | |
| 424 | 460 | const depthPromptText = baseChatReplace(character.data?.extensions?.depth_prompt?.prompt?.trim(), name1null, character.name) || ''; |
| 425 | 461 | const depthPromptDepth = character.data?.extensions?.depth_prompt?.depth ?? depth_prompt_depth_default; |
| 426 | 462 | const depthPromptRole = character.data?.extensions?.depth_prompt?.role ?? depth_prompt_role_default; |
| 427 | 463 | |
| @@ -440,29 +476,46 @@ export function getGroupDepthPrompts(groupId, characterId) { | ||
| 440 | 476 | * @returns {{description: string, personality: string, scenario: string, mesExamples: string}} Group character cards combined |
| 441 | 477 | */ |
| 442 | 478 | export 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) { | |
| 443 | 499 | const group = groups.find(x => x.id === groupId); |
| 444 | 500 | |
| 501 | + // If no group cards should be generated, return null so caller knows to fall back | |
| 445 | 502 | if (!group || !group?.generation_mode || !Array.isArray(group.members) || !group.members.length) { |
| 446 | 503 | return null; |
| 447 | 504 | } |
| 448 | 505 | |
| 449 | 506 | /** |
| 450 | 507 | * Runs the macro enginebaseChatReplace on a text, with custom <FIELDNAME> replace |
| 451 | 508 | * @param {string} value Value to replace |
| 452 | 509 | * @param {string} fieldName Name of the field |
| 453 | 510 | * @param {string} characterName Name of the character |
| 454 | 511 | * @param {boolean} trim Whether to trim the value |
| 455 | 512 | * @returns {string} Replaced text |
| 456 | 513 | * */ |
| 457 | 514 | function customBaseChatReplacecustomTransform(value, fieldName, characterName, trim) { |
| 458 | 515 | if (!value) {return ''; |
| 459 | - return ''; | |
| 460 | - } | |
| 461 | - | |
| 462 | - // We should do the custom field name replacement first, and then run it through the normal macro engine with provided names | |
| 463 | 516 | value = value.replace(/<FIELDNAME>/gi, fieldName); |
| 464 | 517 | value = trim ? value.trim() : value; |
| 465 | 518 | return baseChatReplace(value, name1null, characterName); |
| 466 | 519 | } |
| 467 | 520 | |
| 468 | 521 | /** |
| @@ -472,62 +525,57 @@ export function getGroupCharacterCards(groupId, characterId) { | ||
| 472 | 525 | * @param {string} fieldName Name of the field |
| 473 | 526 | * @param {function(string): string} [preprocess] Preprocess function |
| 474 | 527 | * @returns {string} Prepared text |
| 475 | 528 | * */ |
| 476 | 529 | function replaceAndPrepareForJoin(value, characterName, fieldName, preprocess = null) { |
| 477 | 530 | value = value?.trim() ?? ''; |
| 478 | 531 | if (!value) {return ''; |
| 479 | - return ''; | |
| 480 | - } | |
| 481 | - | |
| 482 | - // Run preprocess function | |
| 483 | 532 | if (typeof preprocess === 'function') { |
| 484 | 533 | value = preprocess(value); |
| 485 | 534 | } |
| 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); | |
| 488 | 537 | 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 | - | |
| 493 | 538 | return `${prefix}${value}${suffix}`; |
| 494 | 539 | } |
| 495 | 540 | |
| 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 = []; | |
| 504 | 550 | for (const member of group.members) { |
| 505 | 551 | const index = characters.findIndex(x => x.avatar === member); |
| 506 | 552 | 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 | - | |
| 513 | 554 | if (group.disabled_members.includes(member) && characterId !== index && group.generation_mode !== group_generation_mode.APPEND_DISABLED) { |
| 514 | 555 | continue; |
| 515 | 556 | } |
| 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)); | |
| 521 | 560 | } |
| 522 | 561 | |
| 523 | 562 | const descriptionscenarioOverride = descriptions.filterString(xchat_metadata['scenario'] =>|| x.length).join('\n'); |
| 524 | 563 | 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'); | |
| 527 | 564 | |
| 528 | - return { description, personality, scenario, mesExamples }; | |
| 565 | + return createLazyFields({ | |
| 566 | + description: () => collectField('Description', c => c.description), | |
| 567 | + personality: () => collectField('Personality', c => c.personality), | |
| 568 | + scenario: () => baseChatReplace(scenarioOverride?.trim()) || collectField('Scenario', c => c.scenario), | |
| 569 | + mesExamples: () => baseChatReplace(mesExamplesOverride?.trim()) || | |
| 570 | + collectField('Example Messages', c => c.mes_example, x => !x.startsWith('<START>') ? `<START>\n${x}` : x), | |
| 571 | + }); | |
| 529 | 572 | } |
| 530 | 573 | |
| 574 | +/** | |
| 575 | + * Gets the first message for a character. | |
| 576 | + * @param {Character} character Character object | |
| 577 | + * @returns {Promise<ChatMessage>} First message object | |
| 578 | + */ | |
| 531 | 579 | async function getFirstCharacterMessage(character) { |
| 532 | 580 | let messageText = character.first_mes; |
| 533 | 581 | |
| @@ -552,7 +600,7 @@ async function getFirstCharacterMessage(character) { | ||
| 552 | 600 | mes['original_avatar'] = character.avatar; |
| 553 | 601 | mes['extra'] = { 'gen_id': Date.now() * Math.random() * 1000000 }; |
| 554 | 602 | mes['mes'] = messageText |
| 555 | 603 | ? substituteParams(messageText.trim(), name1,{ name2Override: character.name }) |
| 556 | 604 | : ''; |
| 557 | 605 | mes['force_avatar'] = |
| 558 | 606 | character.avatar != 'none' |
| @@ -566,27 +614,72 @@ function resetSelectedGroup() { | ||
| 566 | 614 | is_group_generating = false; |
| 567 | 615 | } |
| 568 | 616 | |
| 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) { | |
| 570 | 625 | const group = groups.find(x => x.id == groupId); |
| 571 | - const chat_id = group.chat_id; | |
| 626 | + if (!group) { | |
| 627 | + console.warn('Group not found', groupId); | |
| 628 | + return; | |
| 629 | + } | |
| 630 | + const chatId = group.chat_id; | |
| 572 | 631 | group['date_last_chat'] = Date.now(); |
| 632 | + /** @type {ChatHeader} */ | |
| 633 | + const chatHeader = { | |
| 634 | + chat_metadata: { ...chat_metadata }, | |
| 635 | + user_name: 'unused', | |
| 636 | + character_name: 'unused', | |
| 637 | + }; | |
| 573 | 638 | const response = await fetch('/api/chats/group/save', { |
| 574 | 639 | method: 'POST', |
| 575 | 640 | headers: getRequestHeaders(), |
| 576 | 641 | body: JSON.stringify({ id: chat_idchatId, chat: [chatHeader, ...chat], force: force }), |
| 577 | 642 | }); |
| 578 | 643 | |
| 579 | 644 | if (!response.ok) { |
| 645 | + const errorData = await response.json(); | |
| 646 | + const isIntegrityError = errorData?.error === 'integrity' && !force; | |
| 647 | + if (!isIntegrityError) { | |
| 580 | 648 | toastr.error(t`Check the server connection and reload the page to prevent data loss.`, t`Group Chat could not be saved`); |
| 581 | 649 | console.error('Group chat could not be saved', response); |
| 582 | 650 | return; |
| 583 | 651 | } |
| 584 | 652 | |
| 653 | + const popupResult = await Popup.show.input( | |
| 654 | + t`ERROR: Chat integrity check failed while saving the file.`, | |
| 655 | + t`<p>After you click OK, the page will be reloaded to prevent data corruption.</p> | |
| 656 | + <p>To confirm an overwrite (and potentially <b>LOSE YOUR DATA</b>), enter <code>OVERWRITE</code> (in all caps) in the box below before clicking OK.</p>`, | |
| 657 | + '', | |
| 658 | + { okButton: 'OK', cancelButton: false }, | |
| 659 | + ); | |
| 660 | + | |
| 661 | + const forceSaveConfirmed = popupResult === 'OVERWRITE'; | |
| 662 | + | |
| 663 | + if (!forceSaveConfirmed) { | |
| 664 | + console.warn('Chat integrity check failed, and user did not confirm the overwrite. Reloading the page.'); | |
| 665 | + window.location.reload(); | |
| 666 | + return; | |
| 667 | + } | |
| 668 | + | |
| 669 | + await saveGroupChat(groupId, shouldSaveGroup, true); | |
| 670 | + } | |
| 671 | + | |
| 585 | 672 | if (shouldSaveGroup) { |
| 586 | 673 | await editGroup(groupId, false, false); |
| 587 | 674 | } |
| 588 | 675 | } |
| 589 | 676 | |
| 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 | + */ | |
| 590 | 683 | export async function renameGroupMember(oldAvatar, newAvatar, newName) { |
| 591 | 684 | // Scan every group for our renamed character |
| 592 | 685 | for (const group of groups) { |
| @@ -614,6 +707,11 @@ export async function renameGroupMember(oldAvatar, newAvatar, newName) { | ||
| 614 | 707 | if (Array.isArray(messages) && messages.length) { |
| 615 | 708 | // Iterate over every chat message |
| 616 | 709 | for (const message of messages) { |
| 710 | + // Skip the chat header | |
| 711 | + if (Object.hasOwn(message, 'chat_metadata')) { | |
| 712 | + continue; | |
| 713 | + } | |
| 714 | + | |
| 617 | 715 | // Only look at character messages |
| 618 | 716 | if (message.is_user || message.is_system) { |
| 619 | 717 | continue; |
| @@ -630,6 +728,8 @@ export async function renameGroupMember(oldAvatar, newAvatar, newName) { | ||
| 630 | 728 | } |
| 631 | 729 | |
| 632 | 730 | if (hadChanges) { |
| 731 | + await eventSource.emit(event_types.CHARACTER_RENAMED_IN_PAST_CHAT, messages, oldAvatar, newAvatar); | |
| 732 | + | |
| 633 | 733 | const saveChatResponse = await fetch('/api/chats/group/save', { |
| 634 | 734 | method: 'POST', |
| 635 | 735 | headers: getRequestHeaders(), |
| @@ -652,15 +752,19 @@ export async function renameGroupMember(oldAvatar, newAvatar, newName) { | ||
| 652 | 752 | } |
| 653 | 753 | } |
| 654 | 754 | |
| 755 | +/** | |
| 756 | + * Fetches all groups from the server and processes them. | |
| 757 | + */ | |
| 655 | 758 | async function getGroups() { |
| 656 | 759 | const response = await fetch('/api/groups/all', { |
| 657 | 760 | method: 'POST', |
| 658 | 761 | headers: getRequestHeaders({ omitContentType: true }), |
| 659 | 762 | }); |
| 660 | 763 | |
| 661 | 764 | if (response.ok) { |
| 765 | + /** @type {Group[]} */ | |
| 662 | 766 | const data = await response.json(); |
| 663 | - groups = data.sort((a, b) => a.id - b.id); | |
| 767 | + groups = data.slice(); | |
| 664 | 768 | |
| 665 | 769 | // Convert groups to new format |
| 666 | 770 | for (const group of groups) { |
| @@ -678,9 +782,6 @@ async function getGroups() { | ||
| 678 | 782 | .filter(x => x) |
| 679 | 783 | .filter(onlyUnique); |
| 680 | 784 | } |
| 681 | - if (group.past_metadata == undefined) { | |
| 682 | - group.past_metadata = {}; | |
| 683 | - } | |
| 684 | 785 | if (typeof group.chat_id === 'number') { |
| 685 | 786 | group.chat_id = String(group.chat_id); |
| 686 | 787 | } |
| @@ -691,6 +792,11 @@ async function getGroups() { | ||
| 691 | 792 | } |
| 692 | 793 | } |
| 693 | 794 | |
| 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 | + */ | |
| 694 | 800 | export function getGroupBlock(group) { |
| 695 | 801 | let count = 0; |
| 696 | 802 | let namesList = []; |
| @@ -712,7 +818,7 @@ export function getGroupBlock(group) { | ||
| 712 | 818 | template.find('.ch_name').text(group.name).attr('title', `[Group] ${group.name}`); |
| 713 | 819 | template.find('.group_fav_icon').css('display', 'none'); |
| 714 | 820 | template.addClass(group.fav ? 'is_fav' : ''); |
| 715 | 821 | template.find('.ch_fav').val(String(group.fav)); |
| 716 | 822 | template.find('.group_select_counter').text(count + ' ' + (count != 1 ? t`characters` : t`character`)); |
| 717 | 823 | template.find('.group_select_block_list').text(namesList.join(', ')); |
| 718 | 824 | |
| @@ -728,6 +834,10 @@ export function getGroupBlock(group) { | ||
| 728 | 834 | return template; |
| 729 | 835 | } |
| 730 | 836 | |
| 837 | +/** | |
| 838 | + * Updates the avatar display for a given group. | |
| 839 | + * @param {Group} group Group object | |
| 840 | + */ | |
| 731 | 841 | function updateGroupAvatar(group) { |
| 732 | 842 | $('#group_avatar_preview').empty().append(getGroupAvatar(group)); |
| 733 | 843 | |
| @@ -740,7 +850,11 @@ function updateGroupAvatar(group) { | ||
| 740 | 850 | favsToHotswap(); |
| 741 | 851 | } |
| 742 | 852 | |
| 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 | + */ | |
| 744 | 858 | function isValidImageUrl(url) { |
| 745 | 859 | // check if empty dict |
| 746 | 860 | if (Object.keys(url).length === 0) { |
| @@ -749,6 +863,11 @@ function isValidImageUrl(url) { | ||
| 749 | 863 | return isDataURL(url) || (url && (url.startsWith('user') || url.startsWith('/user'))); |
| 750 | 864 | } |
| 751 | 865 | |
| 866 | +/** | |
| 867 | + * Gets a group avatar element. | |
| 868 | + * @param {Group} group Group object | |
| 869 | + * @returns {JQuery<HTMLElement>} Group avatar element | |
| 870 | + */ | |
| 752 | 871 | function getGroupAvatar(group) { |
| 753 | 872 | if (!group) { |
| 754 | 873 | return $(`<div class="avatar"><img src="${default_avatar}"></div>`); |
| @@ -797,6 +916,11 @@ function getGroupAvatar(group) { | ||
| 797 | 916 | return groupAvatar; |
| 798 | 917 | } |
| 799 | 918 | |
| 919 | +/** | |
| 920 | + * Gets chat IDs for a group. | |
| 921 | + * @param {string} groupId Group ID | |
| 922 | + * @returns {string[]} Array of chat IDs | |
| 923 | + */ | |
| 800 | 924 | function getGroupChatNames(groupId) { |
| 801 | 925 | const group = groups.find(x => x.id === groupId); |
| 802 | 926 | |
| @@ -811,7 +935,14 @@ function getGroupChatNames(groupId) { | ||
| 811 | 935 | return names; |
| 812 | 936 | } |
| 813 | 937 | |
| 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 = {}) { | |
| 815 | 946 | function throwIfAborted() { |
| 816 | 947 | if (params.signal instanceof AbortSignal && params.signal.aborted) { |
| 817 | 948 | throw new Error('AbortSignal was fired. Group generation stopped'); |
| @@ -830,7 +961,7 @@ async function generateGroupWrapper(by_auto_mode, type = null, params = {}) { | ||
| 830 | 961 | |
| 831 | 962 | // Auto-navigate back to group menu |
| 832 | 963 | if (menu_type !== 'group_edit') { |
| 833 | 964 | select_group_chats(selected_group, false); |
| 834 | 965 | await delay(1); |
| 835 | 966 | } |
| 836 | 967 | |
| @@ -859,7 +990,7 @@ async function generateGroupWrapper(by_auto_mode, type = null, params = {}) { | ||
| 859 | 990 | let activationText = ''; |
| 860 | 991 | let isUserInput = false; |
| 861 | 992 | |
| 862 | 993 | if (userInput?.length && !by_auto_modebyAutoMode) { |
| 863 | 994 | isUserInput = true; |
| 864 | 995 | activationText = userInput; |
| 865 | 996 | } else { |
| @@ -935,12 +1066,12 @@ async function generateGroupWrapper(by_auto_mode, type = null, params = {}) { | ||
| 935 | 1066 | |
| 936 | 1067 | // Wait for generation to finish |
| 937 | 1068 | const generateType = ['swipe', 'impersonate', 'quiet', 'continue'].includes(type) ? type : 'normal'; |
| 938 | 1069 | textResult = await Generate(generateType, { automatic_trigger: by_auto_modebyAutoMode, ...(params || {}) }); |
| 939 | 1070 | let messageChunk = textResult?.messageChunk; |
| 940 | 1071 | |
| 941 | 1072 | if (messageChunk) { |
| 942 | 1073 | while (shouldAutoContinue(messageChunk, type === 'impersonate')) { |
| 943 | 1074 | textResult = await Generate('continue', { automatic_trigger: by_auto_modebyAutoMode, ...(params || {}) }); |
| 944 | 1075 | messageChunk = textResult?.messageChunk; |
| 945 | 1076 | } |
| 946 | 1077 | } |
| @@ -966,6 +1097,10 @@ async function generateGroupWrapper(by_auto_mode, type = null, params = {}) { | ||
| 966 | 1097 | return Promise.resolve(textResult); |
| 967 | 1098 | } |
| 968 | 1099 | |
| 1100 | +/** | |
| 1101 | + * Gets the generation ID of the last chat message. | |
| 1102 | + * @returns {number|null} Generation ID or null | |
| 1103 | + */ | |
| 969 | 1104 | function getLastMessageGenerationId() { |
| 970 | 1105 | let generationId = null; |
| 971 | 1106 | if (chat.length > 0) { |
| @@ -977,6 +1112,11 @@ function getLastMessageGenerationId() { | ||
| 977 | 1112 | return generationId; |
| 978 | 1113 | } |
| 979 | 1114 | |
| 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 | + */ | |
| 980 | 1120 | function activateImpersonate(members) { |
| 981 | 1121 | const randomIndex = Math.floor(Math.random() * members.length); |
| 982 | 1122 | const activatedMembers = [members[randomIndex]]; |
| @@ -1039,6 +1179,11 @@ function activateSwipe(members, { allowSystem = false } = {}) { | ||
| 1039 | 1179 | return memberIds; |
| 1040 | 1180 | } |
| 1041 | 1181 | |
| 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 | + */ | |
| 1042 | 1187 | function activateListOrder(members) { |
| 1043 | 1188 | let activatedMembers = members.filter(onlyUnique); |
| 1044 | 1189 | |
| @@ -1092,6 +1237,15 @@ function activatePooledOrder(members, lastMessage, isUserInput) { | ||
| 1092 | 1237 | return memberId !== -1 ? [memberId] : []; |
| 1093 | 1238 | } |
| 1094 | 1239 | |
| 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 | + */ | |
| 1095 | 1249 | function activateNaturalOrder(members, input, lastMessage, allowSelfResponses, isUserInput) { |
| 1096 | 1250 | let activatedMembers = []; |
| 1097 | 1251 | |
| @@ -1168,6 +1322,11 @@ function activateNaturalOrder(members, input, lastMessage, allowSelfResponses, i | ||
| 1168 | 1322 | return memberIds; |
| 1169 | 1323 | } |
| 1170 | 1324 | |
| 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 | + */ | |
| 1171 | 1330 | async function deleteGroup(id) { |
| 1172 | 1331 | const group = groups.find((x) => x.id === id); |
| 1173 | 1332 | |
| @@ -1197,6 +1356,13 @@ async function deleteGroup(id) { | ||
| 1197 | 1356 | } |
| 1198 | 1357 | } |
| 1199 | 1358 | |
| 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 | + */ | |
| 1200 | 1366 | export async function editGroup(id, immediately, reload = true) { |
| 1201 | 1367 | let group = groups.find((x) => x.id === id); |
| 1202 | 1368 | |
| @@ -1204,11 +1370,6 @@ export async function editGroup(id, immediately, reload = true) { | ||
| 1204 | 1370 | return; |
| 1205 | 1371 | } |
| 1206 | 1372 | |
| 1207 | - if (id === selected_group) { | |
| 1208 | - // structuredClone may cause issues if metadata has non-cloneable references | |
| 1209 | - group['chat_metadata'] = JSON.parse(JSON.stringify(chat_metadata)); | |
| 1210 | - } | |
| 1211 | - | |
| 1212 | 1373 | if (immediately) { |
| 1213 | 1374 | return await _save(group, reload); |
| 1214 | 1375 | } |
| @@ -1260,6 +1421,12 @@ async function groupChatAutoModeWorker() { | ||
| 1260 | 1421 | await generateGroupWrapper(true, 'auto', { signal: groupAutoModeAbortController.signal }); |
| 1261 | 1422 | } |
| 1262 | 1423 | |
| 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 | + */ | |
| 1263 | 1430 | async function modifyGroupMember(groupId, groupMember, isDelete) { |
| 1264 | 1431 | const id = groupMember.data('id'); |
| 1265 | 1432 | const thisGroup = groups.find((x) => x.id == groupId); |
| @@ -1287,9 +1454,16 @@ async function modifyGroupMember(groupId, groupMember, isDelete) { | ||
| 1287 | 1454 | $('#rm_group_submit').prop('disabled', !groupHasMembers); |
| 1288 | 1455 | } |
| 1289 | 1456 | |
| 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) { | |
| 1291 | 1465 | const id = groupMember.data('id'); |
| 1292 | 1466 | const thisGroup = groups.find((x) => x.id == chat_idgroupId); |
| 1293 | 1467 | const memberArray = thisGroup?.members ?? newGroupMembers; |
| 1294 | 1468 | |
| 1295 | 1469 | const indexOf = memberArray.indexOf(id); |
| @@ -1312,7 +1486,7 @@ async function reorderGroupMember(chat_id, groupMember, direction) { | ||
| 1312 | 1486 | |
| 1313 | 1487 | // Existing groups need to modify members list |
| 1314 | 1488 | if (openGroupId) { |
| 1315 | 1489 | await editGroup(chat_idgroupId, false, false); |
| 1316 | 1490 | updateGroupAvatar(thisGroup); |
| 1317 | 1491 | } |
| 1318 | 1492 | } |
| @@ -1358,10 +1532,16 @@ async function onGroupNameInput() { | ||
| 1358 | 1532 | let _thisGroup = groups.find((x) => x.id == openGroupId); |
| 1359 | 1533 | _thisGroup.name = $(this).val(); |
| 1360 | 1534 | $('#rm_button_selected_ch').children('h2').text(_thisGroup.name); |
| 1361 | 1535 | await editGroup(openGroupId, false); |
| 1362 | 1536 | } |
| 1363 | 1537 | } |
| 1364 | 1538 | |
| 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 | + */ | |
| 1365 | 1545 | function isGroupMember(group, avatarId) { |
| 1366 | 1546 | if (group && Array.isArray(group.members)) { |
| 1367 | 1547 | return group.members.includes(avatarId); |
| @@ -1370,6 +1550,13 @@ function isGroupMember(group, avatarId) { | ||
| 1370 | 1550 | } |
| 1371 | 1551 | } |
| 1372 | 1552 | |
| 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 | + */ | |
| 1373 | 1560 | function getGroupCharacters({ doFilter = false, onlyMembers = false } = {}) { |
| 1374 | 1561 | function sortMembersFn(a, b) { |
| 1375 | 1562 | const membersArray = thisGroup?.members ?? newGroupMembers; |
| @@ -1461,15 +1648,20 @@ function printGroupMembers() { | ||
| 1461 | 1648 | }); |
| 1462 | 1649 | } |
| 1463 | 1650 | |
| 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 | + */ | |
| 1464 | 1656 | function getGroupCharacterBlock(character) { |
| 1465 | 1657 | const avatar = getThumbnailUrl('avatar', character.avatar); |
| 1466 | 1658 | const template = $('#group_member_template .group_member').clone(); |
| 1467 | 1659 | const isFav = !!character.fav || character.fav == 'true'; |
| 1468 | 1660 | template.data('id', character.avatar); |
| 1469 | 1661 | template.find('.avatar img').attr({ 'src': avatar, 'title': character.avatar }); |
| 1470 | 1662 | template.find('.ch_name').text(character.name); |
| 1471 | 1663 | template.attr('data-chid', characters.indexOf(character)); |
| 1472 | 1664 | template.find('.ch_fav').val(String(isFav)); |
| 1473 | 1665 | template.toggleClass('is_fav', isFav); |
| 1474 | 1666 | |
| 1475 | 1667 | const auxFieldName = power_user.aux_field || 'character_version'; |
| @@ -1503,6 +1695,11 @@ function getGroupCharacterBlock(character) { | ||
| 1503 | 1695 | return template; |
| 1504 | 1696 | } |
| 1505 | 1697 | |
| 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 | + */ | |
| 1506 | 1703 | function isGroupMemberDisabled(avatarId) { |
| 1507 | 1704 | const thisGroup = openGroupId && groups.find((x) => x.id == openGroupId); |
| 1508 | 1705 | return Boolean(thisGroup && thisGroup.disabled_members.includes(avatarId)); |
| @@ -1553,6 +1750,11 @@ async function onHideMutedSpritesClick(value) { | ||
| 1553 | 1750 | } |
| 1554 | 1751 | } |
| 1555 | 1752 | |
| 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 | + */ | |
| 1556 | 1758 | function toggleHiddenControls(group, generationMode = null) { |
| 1557 | 1759 | const isJoin = [group_generation_mode.APPEND, group_generation_mode.APPEND_DISABLED].includes(generationMode ?? group?.generation_mode); |
| 1558 | 1760 | $('#rm_group_generation_mode_join_prefix').parent().toggle(isJoin); |
| @@ -1564,6 +1766,11 @@ function toggleHiddenControls(group, generationMode = null) { | ||
| 1564 | 1766 | } |
| 1565 | 1767 | } |
| 1566 | 1768 | |
| 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 | + */ | |
| 1567 | 1774 | function select_group_chats(groupId, skipAnimation) { |
| 1568 | 1775 | openGroupId = groupId; |
| 1569 | 1776 | newGroupMembers = []; |
| @@ -1774,6 +1981,11 @@ function updateFavButtonState(state) { | ||
| 1774 | 1981 | $('#group_favorite_button').toggleClass('fav_off', !fav_grp_checked); |
| 1775 | 1982 | } |
| 1776 | 1983 | |
| 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 | + */ | |
| 1777 | 1989 | export async function openGroupById(groupId) { |
| 1778 | 1990 | if (isChatSaving) { |
| 1779 | 1991 | toastr.info(t`Please wait until the chat is saved before switching characters.`, t`Your chat is still saving...`); |
| @@ -1786,7 +1998,7 @@ export async function openGroupById(groupId) { | ||
| 1786 | 1998 | } |
| 1787 | 1999 | |
| 1788 | 2000 | if (!is_send_press && !is_group_generating) { |
| 1789 | 2001 | select_group_chats(groupId, false); |
| 1790 | 2002 | |
| 1791 | 2003 | if (selected_group !== groupId) { |
| 1792 | 2004 | groupChatQueueOrder = new Map(); |
| @@ -1806,6 +2018,11 @@ export async function openGroupById(groupId) { | ||
| 1806 | 2018 | return false; |
| 1807 | 2019 | } |
| 1808 | 2020 | |
| 2021 | +/** | |
| 2022 | + * Peeks the character definition from a group member element. | |
| 2023 | + * @param {JQuery<HTMLElement>} characterSelect Character select element | |
| 2024 | + * @returns {Promise<void>} | |
| 2025 | + */ | |
| 1809 | 2026 | async function openCharacterDefinition(characterSelect) { |
| 1810 | 2027 | if (is_group_generating) { |
| 1811 | 2028 | toastr.warning(t`Can't peek a character while group reply is being generated`); |
| @@ -1834,7 +2051,7 @@ function filterGroupMembers() { | ||
| 1834 | 2051 | } |
| 1835 | 2052 | |
| 1836 | 2053 | async function createGroup() { |
| 1837 | 2054 | let name = $('#rm_group_chat_name').val().toString(); |
| 1838 | 2055 | let allowSelfResponses = !!$('#rm_group_allow_self_responses').prop('checked'); |
| 1839 | 2056 | let activationStrategy = Number($('#rm_group_activation_strategy').find(':selected').val()) ?? group_activation_strategy.NATURAL; |
| 1840 | 2057 | let generationMode = Number($('#rm_group_generation_mode').find(':selected').val()) ?? group_generation_mode.SWAP; |
| @@ -1846,29 +2063,30 @@ async function createGroup() { | ||
| 1846 | 2063 | name = t`Group: ${memberNames}`; |
| 1847 | 2064 | } |
| 1848 | 2065 | |
| 1849 | 2066 | const avatar_urlavatarUrl = $('#group_avatar_preview img').attr('src'); |
| 1850 | - | |
| 1851 | 2067 | const chatName = humanizedDateTime(); |
| 1852 | 2068 | const chats = [chatName]; |
| 1853 | 2069 | |
| 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({ | |
| 1858 | 2072 | name: name, |
| 1859 | 2073 | members: members, |
| 1860 | 2074 | avatar_url: isValidImageUrl(avatar_urlavatarUrl) ? avatar_urlavatarUrl : default_avatar, |
| 1861 | 2075 | allow_self_responses: allowSelfResponses, |
| 1862 | 2076 | hideMutedSprites: hideMutedSprites, |
| 1863 | 2077 | activation_strategy: activationStrategy, |
| 1864 | 2078 | generation_mode: generationMode, |
| 1865 | 2079 | disabled_members: [], |
| 1866 | - chat_metadata: {}, | |
| 1867 | 2080 | fav: fav_grp_checked, |
| 1868 | 2081 | chat_id: chatName, |
| 1869 | 2082 | chats: chats, |
| 1870 | 2083 | 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), | |
| 1872 | 2090 | }); |
| 1873 | 2091 | |
| 1874 | 2092 | if (createGroupResponse.ok) { |
| @@ -1880,6 +2098,11 @@ async function createGroup() { | ||
| 1880 | 2098 | } |
| 1881 | 2099 | } |
| 1882 | 2100 | |
| 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 | + */ | |
| 1883 | 2106 | export async function createNewGroupChat(groupId) { |
| 1884 | 2107 | const group = groups.find(x => x.id === groupId); |
| 1885 | 2108 | |
| @@ -1887,27 +2110,22 @@ export async function createNewGroupChat(groupId) { | ||
| 1887 | 2110 | return; |
| 1888 | 2111 | } |
| 1889 | 2112 | |
| 1890 | - const oldChatName = group.chat_id; | |
| 1891 | - const newChatName = humanizedDateTime(); | |
| 1892 | - | |
| 1893 | - if (typeof group.past_metadata !== 'object') { | |
| 1894 | - group.past_metadata = {}; | |
| 1895 | - } | |
| 1896 | - | |
| 1897 | 2113 | await clearChat(); |
| 1898 | 2114 | chat.length = 0; |
| 1899 | - if (oldChatName) { | |
| 2115 | + const newChatName = humanizedDateTime(); | |
| 1900 | - group.past_metadata[oldChatName] = Object.assign({}, chat_metadata); | |
| 1901 | - } | |
| 1902 | 2116 | group.chats.push(newChatName); |
| 1903 | 2117 | group.chat_id = newChatName; |
| 1904 | - group.chat_metadata = {}; | |
| 2118 | + updateChatMetadata({}, true); | |
| 1905 | - updateChatMetadata(group.chat_metadata, true); | |
| 1906 | 2119 | |
| 1907 | 2120 | await editGroup(group.id, true, false); |
| 1908 | 2121 | await getGroupChat(group.id); |
| 1909 | 2122 | } |
| 1910 | 2123 | |
| 2124 | +/** | |
| 2125 | + * Retrieves past chats for a specified group. | |
| 2126 | + * @param {string} groupId Group ID | |
| 2127 | + * @returns {Promise<Array>} Array of past chats | |
| 2128 | + */ | |
| 1911 | 2129 | export async function getGroupPastChats(groupId) { |
| 1912 | 2130 | const group = groups.find(x => x.id === groupId); |
| 1913 | 2131 | |
| @@ -1920,16 +2138,22 @@ export async function getGroupPastChats(groupId) { | ||
| 1920 | 2138 | try { |
| 1921 | 2139 | for (const chatId of group.chats) { |
| 1922 | 2140 | const messages = await loadGroupChat(chatId); |
| 1923 | - let this_chat_file_size = (JSON.stringify(messages).length / 1024).toFixed(2) + 'kb'; | |
| 2141 | + if (!Array.isArray(messages)) { | |
| 1924 | - let chat_items = messages.length; | |
| 2142 | + continue; | |
| 2143 | + } | |
| 2144 | + const fileSize = humanFileSize(JSON.stringify(messages).length); | |
| 2145 | + if (messages.length > 0 && Object.hasOwn(messages[0], 'chat_metadata')) { | |
| 2146 | + messages.shift(); | |
| 2147 | + } | |
| 2148 | + const chatItems = messages.length; | |
| 1925 | 2149 | const lastMessage = messages.length ? messages[messages.length - 1].mes : '[The chat is empty]'; |
| 1926 | 2150 | const lastMessageDate = messages.length ? (messages[messages.length - 1].send_date || Date.now()) : Date.now(); |
| 1927 | 2151 | chats.push({ |
| 1928 | 2152 | 'file_name': chatId, |
| 1929 | 2153 | 'mes': lastMessage, |
| 1930 | 2154 | 'last_mes': lastMessageDate, |
| 1931 | 2155 | 'file_size': this_chat_file_sizefileSize, |
| 1932 | 2156 | 'chat_items': chat_itemschatItems, |
| 1933 | 2157 | }); |
| 1934 | 2158 | } |
| 1935 | 2159 | } catch (err) { |
| @@ -1938,6 +2162,12 @@ export async function getGroupPastChats(groupId) { | ||
| 1938 | 2162 | return chats; |
| 1939 | 2163 | } |
| 1940 | 2164 | |
| 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 | + */ | |
| 1941 | 2171 | export async function openGroupChat(groupId, chatId) { |
| 1942 | 2172 | await waitUntilCondition(() => !isChatSaving, debounce_timeout.extended, 10); |
| 1943 | 2173 | const group = groups.find(x => x.id === groupId); |
| @@ -1948,17 +2178,21 @@ export async function openGroupChat(groupId, chatId) { | ||
| 1948 | 2178 | |
| 1949 | 2179 | await clearChat(); |
| 1950 | 2180 | chat.length = 0; |
| 1951 | - const previousChat = group.chat_id; | |
| 1952 | - group.past_metadata[previousChat] = Object.assign({}, chat_metadata); | |
| 1953 | 2181 | group.chat_id = chatId; |
| 1954 | - group.chat_metadata = group.past_metadata[chatId] || {}; | |
| 1955 | 2182 | group['date_last_chat'] = Date.now(); |
| 1956 | 2183 | updateChatMetadata(group.chat_metadata{}, true); |
| 1957 | 2184 | |
| 1958 | 2185 | await editGroup(groupId, true, false); |
| 1959 | 2186 | await getGroupChat(groupId); |
| 1960 | 2187 | } |
| 1961 | 2188 | |
| 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 | + */ | |
| 1962 | 2196 | export async function renameGroupChat(groupId, oldChatId, newChatId) { |
| 1963 | 2197 | const group = groups.find(x => x.id === groupId); |
| 1964 | 2198 | |
| @@ -1972,8 +2206,6 @@ export async function renameGroupChat(groupId, oldChatId, newChatId) { | ||
| 1972 | 2206 | |
| 1973 | 2207 | group.chats.splice(group.chats.indexOf(oldChatId), 1); |
| 1974 | 2208 | group.chats.push(newChatId); |
| 1975 | - group.past_metadata[newChatId] = (group.past_metadata[oldChatId] || {}); | |
| 1976 | - delete group.past_metadata[oldChatId]; | |
| 1977 | 2209 | |
| 1978 | 2210 | await editGroup(groupId, true, true); |
| 1979 | 2211 | } |
| @@ -1990,12 +2222,7 @@ export async function deleteGroupChatByName(groupId, chatName) { | ||
| 1990 | 2222 | return; |
| 1991 | 2223 | } |
| 1992 | 2224 | |
| 1993 | - if (typeof group.past_metadata !== 'object') { | |
| 1994 | - group.past_metadata = {}; | |
| 1995 | - } | |
| 1996 | - | |
| 1997 | 2225 | group.chats.splice(group.chats.indexOf(chatName), 1); |
| 1998 | - delete group.past_metadata[chatName]; | |
| 1999 | 2226 | |
| 2000 | 2227 | const response = await fetch('/api/chats/group/delete', { |
| 2001 | 2228 | method: 'POST', |
| @@ -2011,12 +2238,8 @@ export async function deleteGroupChatByName(groupId, chatName) { | ||
| 2011 | 2238 | |
| 2012 | 2239 | // If the deleted chat was the current chat, switch to the last chat in the group |
| 2013 | 2240 | if (group.chat_id === chatName) { |
| 2014 | - group.chat_id = ''; | |
| 2015 | - group.chat_metadata = {}; | |
| 2016 | - | |
| 2017 | 2241 | const newChatName = group.chats.length ? group.chats[group.chats.length - 1] : humanizedDateTime(); |
| 2018 | 2242 | group.chat_id = newChatName; |
| 2019 | - group.chat_metadata = group.past_metadata[newChatName] || {}; | |
| 2020 | 2243 | } |
| 2021 | 2244 | |
| 2022 | 2245 | await editGroup(groupId, true, true); |
| @@ -2038,12 +2261,10 @@ export async function deleteGroupChat(groupId, chatId, { jumpToNewChat = true } | ||
| 2038 | 2261 | } |
| 2039 | 2262 | |
| 2040 | 2263 | group.chats.splice(group.chats.indexOf(chatId), 1); |
| 2041 | - delete group.past_metadata[chatId]; | |
| 2042 | 2264 | |
| 2043 | 2265 | if (group.chat_id === chatId) { |
| 2044 | 2266 | group.chat_id = ''; |
| 2045 | - group.chat_metadata = {}; | |
| 2267 | + updateChatMetadata({}, true); | |
| 2046 | - updateChatMetadata(group.chat_metadata, true); | |
| 2047 | 2268 | } |
| 2048 | 2269 | |
| 2049 | 2270 | const response = await fetch('/api/chats/group/delete', { |
| @@ -2093,6 +2314,8 @@ export async function importGroupChat(formData, { refresh = true } = {}) { | ||
| 2093 | 2314 | await displayPastChats(); |
| 2094 | 2315 | } |
| 2095 | 2316 | } |
| 2317 | + | |
| 2318 | + return [data.res]; | |
| 2096 | 2319 | } |
| 2097 | 2320 | |
| 2098 | 2321 | return data?.fileNames || []; |
| @@ -2101,6 +2324,14 @@ export async function importGroupChat(formData, { refresh = true } = {}) { | ||
| 2101 | 2324 | return []; |
| 2102 | 2325 | } |
| 2103 | 2326 | |
| 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 | + */ | |
| 2104 | 2335 | export async function saveGroupBookmarkChat(groupId, name, metadata, mesId) { |
| 2105 | 2336 | const group = groups.find(x => x.id === groupId); |
| 2106 | 2337 | |
| @@ -2108,11 +2339,18 @@ export async function saveGroupBookmarkChat(groupId, name, metadata, mesId) { | ||
| 2108 | 2339 | return; |
| 2109 | 2340 | } |
| 2110 | 2341 | |
| 2111 | - group.past_metadata[name] = { ...chat_metadata, ...(metadata || {}) }; | |
| 2112 | 2342 | group.chats.push(name); |
| 2113 | 2343 | |
| 2114 | - const trimmed_chat = (mesId !== undefined && mesId >= 0 && mesId < chat.length) | |
| 2344 | + /** @type {ChatHeader} */ | |
| 2115 | - ? chat.slice(0, parseInt(mesId) + 1) | |
| 2345 | + const chatHeader = { | |
| 2346 | + chat_metadata: { ...chat_metadata, ...(metadata || {}) }, | |
| 2347 | + user_name: 'unused', | |
| 2348 | + character_name: 'unused', | |
| 2349 | + }; | |
| 2350 | + | |
| 2351 | + /** @type {ChatMessage[]} */ | |
| 2352 | + const trimmedChat = (mesId !== undefined && mesId >= 0 && mesId < chat.length) | |
| 2353 | + ? chat.slice(0, Number(mesId) + 1) | |
| 2116 | 2354 | : chat; |
| 2117 | 2355 | |
| 2118 | 2356 | await editGroup(groupId, true, false); |
| @@ -2120,7 +2358,7 @@ export async function saveGroupBookmarkChat(groupId, name, metadata, mesId) { | ||
| 2120 | 2358 | const response = await fetch('/api/chats/group/save', { |
| 2121 | 2359 | method: 'POST', |
| 2122 | 2360 | headers: getRequestHeaders(), |
| 2123 | 2361 | body: JSON.stringify({ id: name, chat: [chatHeader, ...trimmed_chattrimmedChat] }), |
| 2124 | 2362 | }); |
| 2125 | 2363 | |
| 2126 | 2364 | if (!response.ok) { |
| @@ -107,7 +107,7 @@ export async function checkHordeStatus() { | ||
| 107 | 107 | try { |
| 108 | 108 | const response = await fetch('/api/horde/status', { |
| 109 | 109 | method: 'POST', |
| 110 | 110 | headers: getRequestHeaders({ omitContentType: true }), |
| 111 | 111 | }); |
| 112 | 112 | |
| 113 | 113 | if (!response.ok) { |
| @@ -328,7 +328,7 @@ export function loadHordeSettings(settings) { | ||
| 328 | 328 | async function showKudos() { |
| 329 | 329 | const response = await fetch('/api/horde/user-info', { |
| 330 | 330 | method: 'POST', |
| 331 | 331 | headers: getRequestHeaders({ omitContentType: true }), |
| 332 | 332 | }); |
| 333 | 333 | |
| 334 | 334 | if (!response.ok) { |
| @@ -436,10 +436,10 @@ export function formatInstructModeChat(name, mes, isUser, isNarrator, forceAvata | ||
| 436 | 436 | let suffix = getSuffix() || ''; |
| 437 | 437 | |
| 438 | 438 | if (instruct.macro) { |
| 439 | 439 | prefix = substituteParams(prefix, { name1Override: name1, name2Override: name2 }); |
| 440 | 440 | prefix = prefix.replace(/{{name}}/gi, name || 'System'); |
| 441 | 441 | |
| 442 | 442 | suffix = substituteParams(suffix, { name1Override: name1, name2Override: name2 }); |
| 443 | 443 | suffix = suffix.replace(/{{name}}/gi, name || 'System'); |
| 444 | 444 | } |
| 445 | 445 | |
| @@ -524,10 +524,10 @@ export function formatInstructModeExamples(mesExamplesArray, name1, name2) { | ||
| 524 | 524 | let outputSuffix = power_user.instruct.output_suffix || ''; |
| 525 | 525 | |
| 526 | 526 | if (power_user.instruct.macro) { |
| 527 | 527 | inputPrefix = substituteParams(inputPrefix, { name1Override: name1, name2Override: name2 }); |
| 528 | 528 | outputPrefix = substituteParams(outputPrefix, { name1Override: name1, name2Override: name2 }); |
| 529 | 529 | inputSuffix = substituteParams(inputSuffix, { name1Override: name1, name2Override: name2 }); |
| 530 | 530 | outputSuffix = substituteParams(outputSuffix, { name1Override: name1, name2Override: name2 }); |
| 531 | 531 | |
| 532 | 532 | inputPrefix = inputPrefix.replace(/{{name}}/gi, name1); |
| 533 | 533 | outputPrefix = outputPrefix.replace(/{{name}}/gi, name2); |
| @@ -631,7 +631,7 @@ export function formatInstructModePrompt(name, isImpersonate, promptBias, name1, | ||
| 631 | 631 | } |
| 632 | 632 | |
| 633 | 633 | if (instruct.macro) { |
| 634 | 634 | sequence = substituteParams(sequence, { name1Override: name1, name2Override: name2 }); |
| 635 | 635 | sequence = sequence.replace(/{{name}}/gi, name || 'System'); |
| 636 | 636 | } |
| 637 | 637 | |
| @@ -19,6 +19,7 @@ const interactableSelectors = [ | ||
| 19 | 19 | '.stscript_btn', // STscript buttons in the chat bar |
| 20 | 20 | '.select2_choice_clickable+span.select2-container .select2-selection__choice__display', // select2 control elements if they are meant to be clickable |
| 21 | 21 | '.avatar_load_preview', // Char display avatar selection |
| 22 | + '.bg_tabs_list .bg_tab_button', // Background tabs | |
| 22 | 23 | ]; |
| 23 | 24 | |
| 24 | 25 | if (CSS.supports('selector(:has(*))')) { |
| @@ -417,6 +417,7 @@ function createSwipe(messageId, prompt) { | ||
| 417 | 417 | |
| 418 | 418 | console.debug('cleanedPrompt: ', cleanedPrompt); |
| 419 | 419 | |
| 420 | + /** @type {SwipeInfo} */ | |
| 420 | 421 | const newSwipeInfo = { |
| 421 | 422 | send_date: msg.send_date, |
| 422 | 423 | gen_started: msg.gen_started, |
| @@ -6,6 +6,8 @@ import { getInstructMacros } from './instruct-mode.js'; | ||
| 6 | 6 | import { getVariableMacros } from './variables.js'; |
| 7 | 7 | import { isMobile } from './RossAscends-mods.js'; |
| 8 | 8 | import { inject_ids } from './constants.js'; |
| 9 | +import { initRegisterMacros, macros as macroSystem } from './macros/macro-system.js'; | |
| 10 | +import { power_user } from './power-user.js'; | |
| 9 | 11 | |
| 10 | 12 | /** |
| 11 | 13 | * @typedef Macro |
| @@ -33,6 +35,10 @@ Handlebars.registerHelper('helperMissing', function () { | ||
| 33 | 35 | * @property {string} description - Optional description of the macro |
| 34 | 36 | */ |
| 35 | 37 | |
| 38 | +/** | |
| 39 | + * @deprecated Use macros.registry.registerMacro (from scripts/macros/macro-system.js) | |
| 40 | + * or substituteParams({ dynamicMacros }) with the new macro engine. | |
| 41 | + */ | |
| 36 | 42 | export class MacrosParser { |
| 37 | 43 | /** |
| 38 | 44 | * A map of registered macros. |
| @@ -47,10 +53,97 @@ export class MacrosParser { | ||
| 47 | 53 | static #descriptions = new Map(); |
| 48 | 54 | |
| 49 | 55 | /** |
| 56 | + * Logs a deprecation warning for MacrosParser APIs, pointing callers to | |
| 57 | + * the new macro engine registration surface. | |
| 58 | + * | |
| 59 | + * @param {string} method | |
| 60 | + * @param {string} replacement | |
| 61 | + * @returns {void} | |
| 62 | + */ | |
| 63 | + static #logDeprecated(method, replacement) { | |
| 64 | + console.warn(`[DEPRECATED] MacrosParser.${method} is deprecated and will be removed in a future version. Use ${replacement} instead.`); | |
| 65 | + } | |
| 66 | + | |
| 67 | + /** | |
| 68 | + * Bridges a legacy MacrosParser macro registration into the new macro | |
| 69 | + * engine when the experimental macro engine flag is enabled. | |
| 70 | + * | |
| 71 | + * This mirrors the simple "{{key}}" replacement behavior by registering | |
| 72 | + * a 0-arg macro in MacroRegistry that does not take arguments and returns | |
| 73 | + * the sanitized value from the legacy registry. | |
| 74 | + * | |
| 75 | + * @param {string} key | |
| 76 | + * @param {string|MacroFunction} value | |
| 77 | + * @param {string} description | |
| 78 | + * @returns {void} | |
| 79 | + */ | |
| 80 | + static #registerMacroInNewEngine(key, value, description) { | |
| 81 | + if (!power_user.experimental_macro_engine) { | |
| 82 | + return; | |
| 83 | + } | |
| 84 | + | |
| 85 | + // Like the old MacrosParser, we explicitly allow overriding macros, and only warn | |
| 86 | + if (macroSystem.registry.hasMacro(key)) { | |
| 87 | + console.warn(`Macro ${key} is already registered`); | |
| 88 | + } | |
| 89 | + | |
| 90 | + const legacyValue = value; | |
| 91 | + | |
| 92 | + macroSystem.registry.registerMacro(key, { | |
| 93 | + // Legacy MacrosParser macros never took arguments; keep the | |
| 94 | + // contract that only {{key}} without arguments is valid. | |
| 95 | + category: 'legacy', | |
| 96 | + description: typeof description === 'string' ? description : 'Automatically registered macro from MacrosParser', | |
| 97 | + handler: () => { | |
| 98 | + /** @type {string|MacroFunction|undefined} */ | |
| 99 | + let stored = legacyValue; | |
| 100 | + | |
| 101 | + if (typeof stored === 'function') { | |
| 102 | + try { | |
| 103 | + const nonce = uuidv4(); | |
| 104 | + stored = stored(nonce); | |
| 105 | + } catch (e) { | |
| 106 | + console.warn(`Macro "${key}" function threw an error.`, e); | |
| 107 | + stored = ''; | |
| 108 | + } | |
| 109 | + } | |
| 110 | + | |
| 111 | + // Let the new macro engine's normalizeMacroResult handle type | |
| 112 | + // normalization for the returned value. | |
| 113 | + return stored; | |
| 114 | + }, | |
| 115 | + }); | |
| 116 | + } | |
| 117 | + | |
| 118 | + /** | |
| 119 | + * Bridges a legacy MacrosParser macro unregistration into the new macro | |
| 120 | + * engine when the experimental macro engine flag is enabled. | |
| 121 | + * | |
| 122 | + * @param {string} key | |
| 123 | + * @returns {void} | |
| 124 | + */ | |
| 125 | + static #unregisterMacroInNewEngine(key) { | |
| 126 | + if (!power_user.experimental_macro_engine) { | |
| 127 | + return; | |
| 128 | + } | |
| 129 | + | |
| 130 | + macroSystem.registry.unregisterMacro(key); | |
| 131 | + } | |
| 132 | + | |
| 133 | + /** | |
| 50 | 134 | * Returns an iterator over all registered macros. |
| 51 | 135 | * @returns {IterableIterator<CustomMacro>} |
| 52 | 136 | */ |
| 53 | 137 | 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 | + | |
| 54 | 147 | for (const macro of MacrosParser.#macros.keys()) { |
| 55 | 148 | yield { key: macro, description: MacrosParser.#descriptions.get(macro) }; |
| 56 | 149 | } |
| @@ -62,6 +155,7 @@ export class MacrosParser { | ||
| 62 | 155 | * @returns {string|MacroFunction|undefined} The macro value |
| 63 | 156 | */ |
| 64 | 157 | static get(key) { |
| 158 | + MacrosParser.#logDeprecated('get', 'macros.registry.getMacro (from scripts/macros/macro-system.js)'); | |
| 65 | 159 | return MacrosParser.#macros.get(key); |
| 66 | 160 | } |
| 67 | 161 | |
| @@ -71,6 +165,11 @@ export class MacrosParser { | ||
| 71 | 165 | * @returns {boolean} True if the macro is registered, false otherwise |
| 72 | 166 | */ |
| 73 | 167 | 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 | + | |
| 74 | 173 | return MacrosParser.#macros.has(key); |
| 75 | 174 | } |
| 76 | 175 | |
| @@ -81,6 +180,7 @@ export class MacrosParser { | ||
| 81 | 180 | * @param {string} [description] Optional description of the macro |
| 82 | 181 | */ |
| 83 | 182 | static registerMacro(key, value, description = '') { |
| 183 | + MacrosParser.#logDeprecated('registerMacro', 'macros.registry.registerMacro (from scripts/macros/macro-system.js) or substituteParams({ dynamicMacros })'); | |
| 84 | 184 | if (typeof key !== 'string') { |
| 85 | 185 | throw new Error('Macro key must be a string'); |
| 86 | 186 | } |
| @@ -101,6 +201,11 @@ export class MacrosParser { | ||
| 101 | 201 | value = this.sanitizeMacroValue(value); |
| 102 | 202 | } |
| 103 | 203 | |
| 204 | + MacrosParser.#registerMacroInNewEngine(key, value, description); | |
| 205 | + if (power_user.experimental_macro_engine) { | |
| 206 | + return; | |
| 207 | + } | |
| 208 | + | |
| 104 | 209 | if (this.#macros.has(key)) { |
| 105 | 210 | console.warn(`Macro ${key} is already registered`); |
| 106 | 211 | } |
| @@ -118,6 +223,7 @@ export class MacrosParser { | ||
| 118 | 223 | * @param {string} key Macro name (key) |
| 119 | 224 | */ |
| 120 | 225 | static unregisterMacro(key) { |
| 226 | + MacrosParser.#logDeprecated('unregisterMacro', 'macros.registry.unregisterMacro (from scripts/macros/macro-system.js)'); | |
| 121 | 227 | if (typeof key !== 'string') { |
| 122 | 228 | throw new Error('Macro key must be a string'); |
| 123 | 229 | } |
| @@ -129,6 +235,11 @@ export class MacrosParser { | ||
| 129 | 235 | throw new Error('Macro key must not be empty or whitespace only'); |
| 130 | 236 | } |
| 131 | 237 | |
| 238 | + if (power_user.experimental_macro_engine) { | |
| 239 | + MacrosParser.#unregisterMacroInNewEngine(key); | |
| 240 | + return; | |
| 241 | + } | |
| 242 | + | |
| 132 | 243 | const deleted = this.#macros.delete(key); |
| 133 | 244 | |
| 134 | 245 | if (!deleted) { |
| @@ -597,10 +708,15 @@ export function evaluateMacros(content, env, postProcessFn) { | ||
| 597 | 708 | } |
| 598 | 709 | |
| 599 | 710 | export 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) { | |
| 600 | 713 | function initLastGenerationType() { |
| 601 | 714 | let lastGenerationType = ''; |
| 602 | 715 | |
| 603 | 716 | MacrosParser.registerMacro('lastGenerationType', () => lastGenerationType); |
| 717 | + () => lastGenerationType, | |
| 718 | + 'Returns the type of the last generation (e.g., "normal", "swipe", "continue", "impersonate", "quiet").', | |
| 719 | + ); | |
| 604 | 720 | |
| 605 | 721 | eventSource.on(event_types.GENERATION_STARTED, (type, _params, isDryRun) => { |
| 606 | 722 | if (isDryRun) return; |
| @@ -612,6 +728,13 @@ export function initMacros() { | ||
| 612 | 728 | }); |
| 613 | 729 | } |
| 614 | 730 | |
| 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 | + ); | |
| 616 | 735 | initLastGenerationType(); |
| 617 | 736 | } |
| 737 | + | |
| 738 | + // TODO: Needs to be moved once old macros are deprecated and removed | |
| 739 | + initRegisterMacros(); | |
| 740 | +} | |
| @@ -0,0 +1,680 @@ | ||
| 1 | +/** | |
| 2 | + * MacroBrowser - Dynamic documentation browser for macros. | |
| 3 | + * Similar to SlashCommandBrowser but for the macro system. | |
| 4 | + */ | |
Diff truncated