Merge branch 'staging' into persona-improvements
| @@ -80,6 +80,8 @@ body: | ||
| 80 | 80 | required: true |
| 81 | 81 | - label: I have checked the [docs](https://docs.sillytavern.app/)  |
| 82 | 82 | required: true |
| 83 | + - label: I confirm that my issue is not related to third-party content, unofficial extension or patch. If in doubt, check with a new [user account](https://docs.sillytavern.app/administration/multi-user/) and with extensions disabled | |
| 84 | + required: true | |
| 83 | 85 | |
| 84 | 86 | - type: markdown |
| 85 | 87 | attributes: |
| @@ -0,0 +1,13 @@ | ||
| 1 | +These are master copies of the default content files and are managed by SillyTavern. | |
| 2 | + | |
| 3 | +Editing any of these files would not only have no effect, but will also cause merge conflicts during update pulls. | |
| 4 | + | |
| 5 | +You should edit their respective copies instead, for example: | |
| 6 | + | |
| 7 | +1. /default/config.yaml => /config.yaml | |
| 8 | +2. /default/public/css/user.css => /public/css/user.css | |
| 9 | +etc. | |
| 10 | + | |
| 11 | +Any questions? You're always welcome at our official documentation website: | |
| 12 | + | |
| 13 | +https://docs.sillytavern.app/ | |
| @@ -6,7 +6,13 @@ cardsCacheCapacity: 100 | ||
| 6 | 6 | # -- SERVER CONFIGURATION -- |
| 7 | 7 | # Listen for incoming connections |
| 8 | 8 | listen: false |
| 9 | +# Listen on a specific address, supports IPv4 and IPv6 | |
| 10 | +listenAddress: | |
| 11 | + ipv4: 0.0.0.0 | |
| 12 | + ipv6: '[::]' | |
| 9 | 13 | # Enables IPv6 and/or IPv4 protocols. Need to have at least one enabled! |
| 14 | +# - Use option "auto" to automatically detect support | |
| 15 | +# - Use true or false (no qoutes) to enable or disable each protocol | |
| 10 | 16 | protocol: |
| 11 | 17 | ipv4: true |
| 12 | 18 | ipv6: false |
| @@ -77,6 +83,18 @@ cookieSecret: '' | ||
| 77 | 83 | disableCsrfProtection: false |
| 78 | 84 | # Disable startup security checks - NOT RECOMMENDED |
| 79 | 85 | securityOverride: false |
| 86 | +# -- LOGGING CONFIGURATION -- | |
| 87 | +logging: | |
| 88 | + # Enable access logging to access.log file | |
| 89 | + # Records new connections with timestamp, IP address and user agent | |
| 90 | + enableAccessLog: true | |
| 91 | + # Minimum log level to display in the terminal (DEBUG = 0, INFO = 1, WARN = 2, ERROR = 3) | |
| 92 | + minLogLevel: 0 | |
| 93 | +# -- RATE LIMITING CONFIGURATION -- | |
| 94 | +rateLimiting: | |
| 95 | + # Use X-Real-IP header instead of socket IP for rate limiting | |
| 96 | + # Only enable this if you are using a properly configured reverse proxy (like Nginx/traefik/Caddy) | |
| 97 | + preferRealIpHeader: false | |
| 80 | 98 | # -- ADVANCED CONFIGURATION -- |
| 81 | 99 | # Open the browser automatically |
| 82 | 100 | autorun: true |
| @@ -179,6 +197,10 @@ ollama: | ||
| 179 | 197 | # * 0: Unload the model immediately after the request |
| 180 | 198 | # * N (any positive number): Keep the model loaded for N seconds after the request. |
| 181 | 199 | keepAlive: -1 |
| 200 | + # Controls the "num_batch" (batch size) parameter of the generation request | |
| 201 | + # * -1: Use the default value of the model | |
| 202 | + # * N (positive number): Use the specified value. Must be a power of 2, e.g. 128, 256, 512, etc. | |
| 203 | + batchSize: -1 | |
| 182 | 204 | # -- ANTHROPIC CLAUDE API CONFIGURATION -- |
| 183 | 205 | claude: |
| 184 | 206 | # Enables caching of the system prompt (if supported). |
| @@ -198,3 +220,5 @@ claude: | ||
| 198 | 220 | cachingAtDepth: -1 |
| 199 | 221 | # -- SERVER PLUGIN CONFIGURATION -- |
| 200 | 222 | enableServerPlugins: false |
| 223 | +# Attempt to automatically update server plugins on startup | |
| 224 | +enableServerPluginsAutoUpdate: true | |
| @@ -672,10 +672,6 @@ | ||
| 672 | 672 | "type": "moving_ui" |
| 673 | 673 | }, |
| 674 | 674 | { |
| 675 | - "filename": "presets/moving-ui/Black Magic Time.json", | |
| 676 | - "type": "moving_ui" | |
| 677 | - }, | |
| 678 | - { | |
| 679 | 675 | "filename": "presets/quick-replies/Default.json", |
| 680 | 676 | "type": "quick_replies" |
| 681 | 677 | }, |
| @@ -1,45 +0,0 @@ | ||
| 1 | -{ | |
| 2 | - "name": "Black Magic Time", | |
| 3 | - "movingUIState": { | |
| 4 | - "sheld": { | |
| 5 | - "top": 488, | |
| 6 | - "left": 1407, | |
| 7 | - "right": 1, | |
| 8 | - "bottom": 4, | |
| 9 | - "margin": "unset", | |
| 10 | - "width": 471, | |
| 11 | - "height": 439 | |
| 12 | - }, | |
| 13 | - "floatingPrompt": { | |
| 14 | - "width": 369, | |
| 15 | - "height": 441 | |
| 16 | - }, | |
| 17 | - "right-nav-panel": { | |
| 18 | - "top": 0, | |
| 19 | - "left": 1400, | |
| 20 | - "right": 111, | |
| 21 | - "bottom": 446, | |
| 22 | - "margin": "unset", | |
| 23 | - "width": 479, | |
| 24 | - "height": 487 | |
| 25 | - }, | |
| 26 | - "WorldInfo": { | |
| 27 | - "top": 41, | |
| 28 | - "left": 369, | |
| 29 | - "right": 642, | |
| 30 | - "bottom": 51, | |
| 31 | - "margin": "unset", | |
| 32 | - "width": 1034, | |
| 33 | - "height": 858 | |
| 34 | - }, | |
| 35 | - "left-nav-panel": { | |
| 36 | - "top": 442, | |
| 37 | - "left": 0, | |
| 38 | - "right": 1546, | |
| 39 | - "bottom": 25, | |
| 40 | - "margin": "unset", | |
| 41 | - "width": 368, | |
| 42 | - "height": 483 | |
| 43 | - } | |
| 44 | - } | |
| 45 | -} | |
| 45 | \ No newline at end of file | |
| @@ -1,4 +1,4 @@ | ||
| 1 | 1 | import getWebpackServeMiddleware from '../src/middleware/webpack-serve.js'; |
| 2 | 2 | |
| 3 | 3 | const middleware = getWebpackServeMiddleware(); |
| 4 | 4 | await middleware.runWebpackCompiler({ forceDist: true }); |
| @@ -1,12 +1,12 @@ | ||
| 1 | 1 | { |
| 2 | 2 | "name": "sillytavern", |
| 3 | 3 | "version": "1.12.1112", |
| 4 | 4 | "lockfileVersion": 3, |
| 5 | 5 | "requires": true, |
| 6 | 6 | "packages": { |
| 7 | 7 | "": { |
| 8 | 8 | "name": "sillytavern", |
| 9 | 9 | "version": "1.12.1112", |
| 10 | 10 | "hasInstallScript": true, |
| 11 | 11 | "license": "AGPL-3.0", |
| 12 | 12 | "dependencies": { |
| @@ -28,7 +28,7 @@ | ||
| 28 | 28 | "cors": "^2.8.5", |
| 29 | 29 | "csrf-sync": "^4.0.3", |
| 30 | 30 | "diff-match-patch": "^1.0.5", |
| 31 | 31 | "dompurify": "^3.12.74", |
| 32 | 32 | "droll": "^0.2.1", |
| 33 | 33 | "express": "^4.21.0", |
| 34 | 34 | "form-data": "^4.0.0", |
| @@ -41,7 +41,9 @@ | ||
| 41 | 41 | "html-entities": "^2.5.2", |
| 42 | 42 | "iconv-lite": "^0.6.3", |
| 43 | 43 | "ip-matching": "^2.1.2", |
| 44 | + "ip-regex": "^5.0.0", | |
| 44 | 45 | "ipaddr.js": "^2.0.1", |
| 46 | + "is-docker": "^3.0.0", | |
| 45 | 47 | "jimp": "^0.22.10", |
| 46 | 48 | "localforage": "^1.10.0", |
| 47 | 49 | "lodash": "^4.17.21", |
| @@ -1462,6 +1464,13 @@ | ||
| 1462 | 1464 | "@types/jquery": "*" |
| 1463 | 1465 | } |
| 1464 | 1466 | }, |
| 1467 | + "node_modules/@types/trusted-types": { | |
| 1468 | + "version": "2.0.7", | |
| 1469 | + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", | |
| 1470 | + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", | |
| 1471 | + "license": "MIT", | |
| 1472 | + "optional": true | |
| 1473 | + }, | |
| 1465 | 1474 | "node_modules/@types/write-file-atomic": { |
| 1466 | 1475 | "version": "4.0.3", |
| 1467 | 1476 | "resolved": "https://registry.npmjs.org/@types/write-file-atomic/-/write-file-atomic-4.0.3.tgz", |
| @@ -3217,10 +3226,13 @@ | ||
| 3217 | 3226 | } |
| 3218 | 3227 | }, |
| 3219 | 3228 | "node_modules/dompurify": { |
| 3220 | 3229 | "version": "3.12.74", |
| 3221 | 3230 | "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.12.74.tgz", |
| 3222 | 3231 | "integrity": "sha512-VaTstWtsneJY8xzy7DekmYWEOZcmzIe3Qb3zPd4STve1OBTaysFSFEDVduQpyhzAob/kkuJjf5zWkZD8/A9ywSp1byueyuCfHamrCBa14/Oc2iiB0e51B+eNpxSl5gmzn+WmS1ITQec1fZYXI3HCsOZZiSMpG6oxoWMWQMs/mg==", |
| 3223 | 3232 | "license": "(MPL-2.0 OR Apache-2.0)", |
| 3233 | + "optionalDependencies": { | |
| 3234 | + "@types/trusted-types": "^2.0.7" | |
| 3235 | + } | |
| 3224 | 3236 | }, |
| 3225 | 3237 | "node_modules/domutils": { |
| 3226 | 3238 | "version": "3.1.0", |
| @@ -4610,6 +4622,18 @@ | ||
| 4610 | 4622 | "integrity": "sha512-/ok+VhKMasgR5gvTRViwRFQfc0qYt9Vdowg6TO4/pFlDCob5ZjGPkwuOoQVCd5OrMm20zqh+1vA8KLJZTeWudg==", |
| 4611 | 4623 | "license": "LGPL-3.0-only" |
| 4612 | 4624 | }, |
| 4625 | + "node_modules/ip-regex": { | |
| 4626 | + "version": "5.0.0", | |
| 4627 | + "resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-5.0.0.tgz", | |
| 4628 | + "integrity": "sha512-fOCG6lhoKKakwv+C6KdsOnGvgXnmgfmp0myi3bcNwj3qfwPAxRKWEuFhvEFF7ceYIz6+1jRZ+yguLFAmUNPEfw==", | |
| 4629 | + "license": "MIT", | |
| 4630 | + "engines": { | |
| 4631 | + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" | |
| 4632 | + }, | |
| 4633 | + "funding": { | |
| 4634 | + "url": "https://github.com/sponsors/sindresorhus" | |
| 4635 | + } | |
| 4636 | + }, | |
| 4613 | 4637 | "node_modules/ipaddr.js": { |
| 4614 | 4638 | "version": "2.1.0", |
| 4615 | 4639 | "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.1.0.tgz", |
| @@ -4626,15 +4650,15 @@ | ||
| 4626 | 4650 | "license": "MIT" |
| 4627 | 4651 | }, |
| 4628 | 4652 | "node_modules/is-docker": { |
| 4629 | 4653 | "version": "23.20.10", |
| 4630 | 4654 | "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-23.20.10.tgz", |
| 4631 | 4655 | "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQeljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", |
| 4632 | 4656 | "license": "MIT", |
| 4633 | 4657 | "bin": { |
| 4634 | 4658 | "is-docker": "cli.js" |
| 4635 | 4659 | }, |
| 4636 | 4660 | "engines": { |
| 4637 | - "node": ">=8" | |
| 4661 | + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" | |
| 4638 | 4662 | }, |
| 4639 | 4663 | "funding": { |
| 4640 | 4664 | "url": "https://github.com/sponsors/sindresorhus" |
| @@ -4711,6 +4735,21 @@ | ||
| 4711 | 4735 | "node": ">=8" |
| 4712 | 4736 | } |
| 4713 | 4737 | }, |
| 4738 | + "node_modules/is-wsl/node_modules/is-docker": { | |
| 4739 | + "version": "2.2.1", | |
| 4740 | + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", | |
| 4741 | + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", | |
| 4742 | + "license": "MIT", | |
| 4743 | + "bin": { | |
| 4744 | + "is-docker": "cli.js" | |
| 4745 | + }, | |
| 4746 | + "engines": { | |
| 4747 | + "node": ">=8" | |
| 4748 | + }, | |
| 4749 | + "funding": { | |
| 4750 | + "url": "https://github.com/sponsors/sindresorhus" | |
| 4751 | + } | |
| 4752 | + }, | |
| 4714 | 4753 | "node_modules/isarray": { |
| 4715 | 4754 | "version": "1.0.0", |
| 4716 | 4755 | "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", |
| @@ -5495,6 +5534,21 @@ | ||
| 5495 | 5534 | "url": "https://github.com/sponsors/sindresorhus" |
| 5496 | 5535 | } |
| 5497 | 5536 | }, |
| 5537 | + "node_modules/open/node_modules/is-docker": { | |
| 5538 | + "version": "2.2.1", | |
| 5539 | + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", | |
| 5540 | + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", | |
| 5541 | + "license": "MIT", | |
| 5542 | + "bin": { | |
| 5543 | + "is-docker": "cli.js" | |
| 5544 | + }, | |
| 5545 | + "engines": { | |
| 5546 | + "node": ">=8" | |
| 5547 | + }, | |
| 5548 | + "funding": { | |
| 5549 | + "url": "https://github.com/sponsors/sindresorhus" | |
| 5550 | + } | |
| 5551 | + }, | |
| 5498 | 5552 | "node_modules/openai": { |
| 5499 | 5553 | "version": "4.17.4", |
| 5500 | 5554 | "resolved": "https://registry.npmjs.org/openai/-/openai-4.17.4.tgz", |
| @@ -18,7 +18,7 @@ | ||
| 18 | 18 | "cors": "^2.8.5", |
| 19 | 19 | "csrf-sync": "^4.0.3", |
| 20 | 20 | "diff-match-patch": "^1.0.5", |
| 21 | 21 | "dompurify": "^3.12.74", |
| 22 | 22 | "droll": "^0.2.1", |
| 23 | 23 | "express": "^4.21.0", |
| 24 | 24 | "form-data": "^4.0.0", |
| @@ -31,7 +31,9 @@ | ||
| 31 | 31 | "html-entities": "^2.5.2", |
| 32 | 32 | "iconv-lite": "^0.6.3", |
| 33 | 33 | "ip-matching": "^2.1.2", |
| 34 | + "ip-regex": "^5.0.0", | |
| 34 | 35 | "ipaddr.js": "^2.0.1", |
| 36 | + "is-docker": "^3.0.0", | |
| 35 | 37 | "jimp": "^0.22.10", |
| 36 | 38 | "localforage": "^1.10.0", |
| 37 | 39 | "lodash": "^4.17.21", |
| @@ -86,9 +88,10 @@ | ||
| 86 | 88 | "type": "git", |
| 87 | 89 | "url": "https://github.com/SillyTavern/SillyTavern.git" |
| 88 | 90 | }, |
| 89 | 91 | "version": "1.12.1112", |
| 90 | 92 | "scripts": { |
| 91 | 93 | "start": "node server.js", |
| 94 | + "debug": "node server.js --inspect", | |
| 92 | 95 | "start:deno": "deno run --allow-run --allow-net --allow-read --allow-write --allow-sys --allow-env server.js", |
| 93 | 96 | "start:bun": "bun server.js", |
| 94 | 97 | "start:no-csrf": "node server.js --disableCsrf", |
| @@ -8,7 +8,7 @@ import path from 'node:path'; | ||
| 8 | 8 | import process from 'node:process'; |
| 9 | 9 | import { fileURLToPath } from 'node:url'; |
| 10 | 10 | |
| 11 | 11 | import { default as git, CheckRepoActions } from 'simple-git'; |
| 12 | 12 | import { color } from './src/util.js'; |
| 13 | 13 | |
| 14 | 14 | const __dirname = import.meta.dirname ?? path.dirname(fileURLToPath(import.meta.url)); |
| @@ -48,6 +48,13 @@ async function updatePlugins() { | ||
| 48 | 48 | console.log(`Updating plugin ${color.green(directory)}...`); |
| 49 | 49 | const pluginPath = path.join(pluginsPath, directory); |
| 50 | 50 | const pluginRepo = git(pluginPath); |
| 51 | + | |
| 52 | + const isRepo = await pluginRepo.checkIsRepo(CheckRepoActions.IS_REPO_ROOT); | |
| 53 | + if (!isRepo) { | |
| 54 | + console.log(`Directory ${color.yellow(directory)} is not a Git repository`); | |
| 55 | + continue; | |
| 56 | + } | |
| 57 | + | |
| 51 | 58 | await pluginRepo.fetch(); |
| 52 | 59 | const commitHash = await pluginRepo.revparse(['HEAD']); |
| 53 | 60 | const trackingBranch = await pluginRepo.revparse(['--abbrev-ref', '@{u}']); |
| @@ -104,6 +104,11 @@ const keyMigrationMap = [ | ||
| 104 | 104 | newKey: 'extensions.models.textToSpeech', |
| 105 | 105 | migrate: (value) => value, |
| 106 | 106 | }, |
| 107 | + { | |
| 108 | + oldKey: 'minLogLevel', | |
| 109 | + newKey: 'logging.minLogLevel', | |
| 110 | + migrate: (value) => value, | |
| 111 | + }, | |
| 107 | 112 | ]; |
| 108 | 113 | |
| 109 | 114 | /** |
| @@ -216,8 +216,6 @@ | ||
| 216 | 216 | |
| 217 | 217 | } |
| 218 | 218 | |
| 219 | - #showRawPrompt, | |
| 220 | - #copyPromptToClipboard, | |
| 221 | 219 | #groupCurrentMemberPopoutButton, |
| 222 | 220 | #summaryExtensionPopoutButton { |
| 223 | 221 | display: none; |
| @@ -72,6 +72,10 @@ dialog { | ||
| 72 | 72 | overflow-x: auto; |
| 73 | 73 | } |
| 74 | 74 | |
| 75 | +.popup.left_aligned_dialogue_popup .popup-content { | |
| 76 | + text-align: start; | |
| 77 | +} | |
| 78 | + | |
| 75 | 79 | /* Opening animation */ |
| 76 | 80 | .popup[opening] { |
| 77 | 81 | animation: pop-in var(--popup-animation-speed) ease-in-out; |
| @@ -100,6 +100,13 @@ | ||
| 100 | 100 | border: 1px solid var(--SmartThemeBorderColor); |
| 101 | 101 | } |
| 102 | 102 | |
| 103 | +.select2-container .select2-results .select2-results__option--disabled { | |
| 104 | + color: inherit; | |
| 105 | + background-color: inherit; | |
| 106 | + cursor: not-allowed; | |
| 107 | + filter: brightness(0.5); | |
| 108 | +} | |
| 109 | + | |
| 103 | 110 | .select2-container .select2-selection--multiple .select2-selection__choice, |
| 104 | 111 | .select2-container .select2-selection--single .select2-selection__choice { |
| 105 | 112 | border-radius: 5px; |
| @@ -473,6 +473,11 @@ label[for="trim_spaces"]:has(input:checked) i.warning { | ||
| 473 | 473 | display: none; |
| 474 | 474 | } |
| 475 | 475 | |
| 476 | +label[for="trim_spaces"]:not(:has(input:checked)) small { | |
| 477 | + color: var(--warning); | |
| 478 | + opacity: 1; | |
| 479 | +} | |
| 480 | + | |
| 476 | 481 | #claude_function_prefill_warning { |
| 477 | 482 | display: none; |
| 478 | 483 | color: red; |
| @@ -489,3 +494,7 @@ label[for="trim_spaces"]:has(input:checked) i.warning { | ||
| 489 | 494 | #mistralai_other_models:empty { |
| 490 | 495 | display: none; |
| 491 | 496 | } |
| 497 | + | |
| 498 | +#banned_tokens_block_ooba:not(:has(#send_banned_tokens_textgenerationwebui:checked)) #banned_tokens_controls_ooba { | |
| 499 | + filter: brightness(0.5); | |
| 500 | +} | |
| @@ -40,4 +40,12 @@ declare global { | ||
| 40 | 40 | searchInputCssClass?: string; |
| 41 | 41 | } |
| 42 | 42 | } |
| 43 | + | |
| 44 | + /** | |
| 45 | + * Translates a text to a target language using a translation provider. | |
| 46 | + * @param text Text to translate | |
| 47 | + * @param lang Target language | |
| 48 | + * @param provider Translation provider | |
| 49 | + */ | |
| 50 | + async function translate(text: string, lang: string, provider: string = null): Promise<string>; | |
| 43 | 51 | } |
| @@ -730,7 +730,7 @@ | ||
| 730 | 730 | <input type="range" id="top_k_openai" name="volume" min="0" max="500" step="1"> |
| 731 | 731 | </div> |
| 732 | 732 | <div class="range-block-counter"> |
| 733 | 733 | <input type="number" min="0" max="200500" step="1" data-for="top_k_openai" id="top_k_counter_openai"> |
| 734 | 734 | </div> |
| 735 | 735 | </div> |
| 736 | 736 | </div> |
| @@ -1587,6 +1587,10 @@ | ||
| 1587 | 1587 | <input type="checkbox" id="skip_special_tokens_textgenerationwebui" /> |
| 1588 | 1588 | <small data-i18n="Skip Special Tokens">Skip Special Tokens</small> |
| 1589 | 1589 | </label> |
| 1590 | + <label data-tg-type="openrouter" class="checkbox_label flexGrow flexShrink" for="include_reasoning_textgenerationwebui"> | |
| 1591 | + <input type="checkbox" id="include_reasoning_textgenerationwebui" /> | |
| 1592 | + <small data-i18n="Request Model Reasoning">Request Model Reasoning</small> | |
| 1593 | + </label> | |
| 1590 | 1594 | <label data-tg-type="ooba, aphrodite, tabby" class="checkbox_label flexGrow flexShrink" for="temperature_last_textgenerationwebui"> |
| 1591 | 1595 | <input type="checkbox" id="temperature_last_textgenerationwebui" /> |
| 1592 | 1596 | <label> |
| @@ -1617,17 +1621,34 @@ | ||
| 1617 | 1621 | </div> |
| 1618 | 1622 | <div data-tg-type-mode="except" data-tg-type="generic" id="banned_tokens_block_ooba" class="wide100p"> |
| 1619 | 1623 | <hr class="width100p"> |
| 1620 | 1624 | <h4div class="range-block-title justifyCentertitle_restorable"> |
| 1621 | - <span data-i18n="Banned Tokens">Banned Tokens/Strings</span> | |
| 1625 | + <div> | |
| 1626 | + <strong data-i18n="Banned Tokens">Banned Tokens/Strings</strong> | |
| 1622 | 1627 | <div class="margin5 fa-solid fa-circle-info opacity50p " data-i18n="[title]LLaMA / Mistral / Yi models only" title="Enter sequences you don't want to appear in the output. Unquoted text will be tokenized in the back end and banned as tokens. [token ids] will be banned as-is. Most tokens have a leading space. Use token counter (with the correct tokenizer selected first!) if you are unsure. Enclose text in double quotes to ban the entire string as a set. Quoted Strings and [Token ids] must be on their own line."></div> |
| 1623 | 1628 | </h4div> |
| 1629 | + <label id="send_banned_tokens_label" for="send_banned_tokens_textgenerationwebui" class="checkbox_label"> | |
| 1630 | + <input id="send_banned_tokens_textgenerationwebui" type="checkbox" style="display:none;" /> | |
| 1631 | + <small><i class="fa-solid fa-power-off menu_button togglable margin0"></i></small> | |
| 1632 | + </label> | |
| 1633 | + </div> | |
| 1634 | + <div id="banned_tokens_controls_ooba"> | |
| 1635 | + <div class="textAlignCenter"> | |
| 1636 | + <small data-i18n="Global list">Global list</small> | |
| 1637 | + </div> | |
| 1638 | + <div class="wide100p marginBot10"> | |
| 1639 | + <textarea id="global_banned_tokens_textgenerationwebui" class="text_pole textarea_compact" name="global_banned_tokens_textgenerationwebui" rows="3" data-i18n="[placeholder]Example: some text [42, 69, 1337]" placeholder='some text as tokens [420, 69, 1337] "Some verbatim string"'></textarea> | |
| 1640 | + </div> | |
| 1641 | + <div class="textAlignCenter"> | |
| 1642 | + <small data-i18n="Preset-specific list">Preset-specific list</small> | |
| 1643 | + </div> | |
| 1624 | 1644 | <div class="wide100p"> |
| 1625 | 1645 | <textarea id="banned_tokens_textgenerationwebui" class="text_pole textarea_compact" name="banned_tokens_textgenerationwebui" rows="3" data-i18n="[placeholder]Example: some text [42, 69, 1337]" placeholder='some text as tokens [420, 69, 1337] "Some verbatim string"'></textarea> |
| 1626 | 1646 | </div> |
| 1627 | 1647 | </div> |
| 1648 | + </div> | |
| 1628 | 1649 | <div class="range-block wide100p"> |
| 1629 | 1650 | <div id="logit_bias_textgenerationwebui" class="range-block-title title_restorable"> |
| 1630 | 1651 | <spanstrong data-i18n="Logit Bias">Logit Bias</spanstrong> |
| 1631 | 1652 | <div id="textgen_logit_bias_new_entry" class="menu_button menu_button_icon"> |
| 1632 | 1653 | <i class="fa-xs fa-solid fa-plus"></i> |
| 1633 | 1654 | <small data-i18n="Add">Add</small> |
| @@ -1930,7 +1951,7 @@ | ||
| 1930 | 1951 | </span> |
| 1931 | 1952 | </div> |
| 1932 | 1953 | </div> |
| 1933 | 1954 | <div class="range-block" data-source="openai,cohere,mistralai,custom,claude,openrouter,groq,deepseek"> |
| 1934 | 1955 | <label for="openai_function_calling" class="checkbox_label flexWrap widthFreeExpand"> |
| 1935 | 1956 | <input id="openai_function_calling" type="checkbox" /> |
| 1936 | 1957 | <span data-i18n="Enable function calling">Enable function calling</span> |
| @@ -1953,6 +1974,7 @@ | ||
| 1953 | 1974 | <span data-i18n="image_inlining_hint_3">menu to attach an image file to the chat.</span> |
| 1954 | 1975 | </div> |
| 1955 | 1976 | <div class="flex-container flexFlowColumn wide100p textAlignCenter marginTop10" data-source="openai,custom"> |
| 1977 | + <div class="flex-container oneline-dropdown"> | |
| 1956 | 1978 | <label for="openai_inline_image_quality" data-i18n="Inline Image Quality"> |
| 1957 | 1979 | Inline Image Quality |
| 1958 | 1980 | </label> |
| @@ -1963,6 +1985,7 @@ | ||
| 1963 | 1985 | </select> |
| 1964 | 1986 | </div> |
| 1965 | 1987 | </div> |
| 1988 | + </div> | |
| 1966 | 1989 | <div class="range-block" data-source="makersuite"> |
| 1967 | 1990 | <label for="use_makersuite_sysprompt" class="checkbox_label widthFreeExpand"> |
| 1968 | 1991 | <input id="use_makersuite_sysprompt" type="checkbox" /> |
| @@ -1977,20 +2000,32 @@ | ||
| 1977 | 2000 | </span> |
| 1978 | 2001 | </div> |
| 1979 | 2002 | </div> |
| 1980 | 2003 | <div class="range-block" data-source="makersuite,deepseek,openrouter,custom"> |
| 1981 | 2004 | <label for="openai_show_thoughts" class="checkbox_label widthFreeExpand"> |
| 1982 | 2005 | <input id="openai_show_thoughts" type="checkbox" /> |
| 1983 | 2006 | <span> |
| 1984 | 2007 | <span data-i18n="ShowRequest model reasoning">ShowRequest model reasoning</span> |
| 1985 | 2008 | <i class="opacity50p fa-solid fa-circle-info" title="Gemini 2.0 Thinking / DeepSeek Reasoner"></i> |
| 1986 | 2009 | </span> |
| 1987 | 2010 | </label> |
| 1988 | 2011 | <div class="toggle-description justifyLeft marginBot5"> |
| 1989 | 2012 | <span data-i18n="DisplayAllows the model's internalto thoughtsreturn inits thethinking responseprocess."> |
| 1990 | 2013 | DisplayAllows the model's internalto thoughtsreturn inits thethinking responseprocess. |
| 1991 | 2014 | </span> |
| 1992 | 2015 | </div> |
| 1993 | 2016 | </div> |
| 2017 | + <div class="flex-container flexFlowColumn wide100p textAlignCenter marginTop10" data-source="openai,custom"> | |
| 2018 | + <div class="flex-container oneline-dropdown" title="Constrains effort on reasoning for reasoning models. Currently supported values are low, medium, and high. Reducing reasoning effort can result in faster responses and fewer tokens used on reasoning in a response." data-i18n="[title]Constrains effort on reasoning for reasoning models."> | |
| 2019 | + <label for="openai_reasoning_effort" data-i18n="Reasoning Effort"> | |
| 2020 | + Reasoning Effort | |
| 2021 | + </label> | |
| 2022 | + <select id="openai_reasoning_effort"> | |
| 2023 | + <option data-i18n="openai_reasoning_effort_low" value="low">Low</option> | |
| 2024 | + <option data-i18n="openai_reasoning_effort_medium" value="medium">Medium</option> | |
| 2025 | + <option data-i18n="openai_reasoning_effort_high" value="high">High</option> | |
| 2026 | + </select> | |
| 2027 | + </div> | |
| 2028 | + </div> | |
| 1994 | 2029 | <div class="range-block" data-source="claude"> |
| 1995 | 2030 | <div class="wide100p"> |
| 1996 | 2031 | <div class="flex-container alignItemsCenter"> |
| @@ -2805,27 +2840,6 @@ | ||
| 2805 | 2840 | <div> |
| 2806 | 2841 | <h4 data-i18n="OpenAI Model">OpenAI Model</h4> |
| 2807 | 2842 | <select id="model_openai_select"> |
| 2808 | - <optgroup label="GPT-3.5 Turbo"> | |
| 2809 | - <option value="gpt-3.5-turbo">gpt-3.5-turbo</option> | |
| 2810 | - <option value="gpt-3.5-turbo-0125">gpt-3.5-turbo-0125 (2024)</option> | |
| 2811 | - <option value="gpt-3.5-turbo-1106">gpt-3.5-turbo-1106 (2023)</option> | |
| 2812 | - <option value="gpt-3.5-turbo-0613">gpt-3.5-turbo-0613 (2023)</option> | |
| 2813 | - <option value="gpt-3.5-turbo-0301">gpt-3.5-turbo-0301 (2023)</option> | |
| 2814 | - <option value="gpt-3.5-turbo-16k">gpt-3.5-turbo-16k</option> | |
| 2815 | - <option value="gpt-3.5-turbo-16k-0613">gpt-3.5-turbo-16k-0613 (2023)</option> | |
| 2816 | - </optgroup> | |
| 2817 | - <optgroup label="GPT-3.5 Turbo Instruct"> | |
| 2818 | - <option value="gpt-3.5-turbo-instruct">gpt-3.5-turbo-instruct</option> | |
| 2819 | - <option value="gpt-3.5-turbo-instruct-0914">gpt-3.5-turbo-instruct-0914</option> | |
| 2820 | - </optgroup> | |
| 2821 | - <optgroup label="GPT-4"> | |
| 2822 | - <option value="gpt-4">gpt-4</option> | |
| 2823 | - <option value="gpt-4-0613">gpt-4-0613 (2023)</option> | |
| 2824 | - <option value="gpt-4-0314">gpt-4-0314 (2023)</option> | |
| 2825 | - <option value="gpt-4-32k">gpt-4-32k</option> | |
| 2826 | - <option value="gpt-4-32k-0613">gpt-4-32k-0613 (2023)</option> | |
| 2827 | - <option value="gpt-4-32k-0314">gpt-4-32k-0314 (2023)</option> | |
| 2828 | - </optgroup> | |
| 2829 | 2843 | <optgroup label="GPT-4o"> |
| 2830 | 2844 | <option value="gpt-4o">gpt-4o</option> |
| 2831 | 2845 | <option value="gpt-4o-2024-11-20">gpt-4o-2024-11-20</option> |
| @@ -2833,29 +2847,44 @@ | ||
| 2833 | 2847 | <option value="gpt-4o-2024-05-13">gpt-4o-2024-05-13</option> |
| 2834 | 2848 | <option value="chatgpt-4o-latest">chatgpt-4o-latest</option> |
| 2835 | 2849 | </optgroup> |
| 2836 | 2850 | <optgroup label="gptGPT-4o- mini"> |
| 2837 | 2851 | <option value="gpt-4o-mini">gpt-4o-mini</option> |
| 2838 | 2852 | <option value="gpt-4o-mini-2024-0711-1820">gpt-4o-mini-2024-0711-1820</option> |
| 2853 | + <option value="gpt-4o-2024-08-06">gpt-4o-2024-08-06</option> | |
| 2854 | + <option value="gpt-4o-2024-05-13">gpt-4o-2024-05-13</option> | |
| 2855 | + <option value="chatgpt-4o-latest">chatgpt-4o-latest</option> | |
| 2839 | 2856 | </optgroup> |
| 2840 | 2857 | <optgroup label="GPT-4o1 Turboand o1-mini"> |
| 2858 | + <option value="o1">o1</option> | |
| 2859 | + <option value="o1-2024-12-17">o1-2024-12-17</option> | |
| 2860 | + <option value="o1-mini">o1-mini</option> | |
| 2861 | + <option value="o1-mini-2024-09-12">o1-mini-2024-09-12</option> | |
| 2862 | + <option value="o1-preview">o1-preview</option> | |
| 2863 | + <option value="o1-preview-2024-09-12">o1-preview-2024-09-12</option> | |
| 2864 | + </optgroup> | |
| 2865 | + <optgroup label="o3"> | |
| 2866 | + <option value="o3-mini">o3-mini</option> | |
| 2867 | + <option value="o3-mini-2025-01-31">o3-mini-2025-01-31</option> | |
| 2868 | + </optgroup> | |
| 2869 | + <optgroup label="GPT-4 Turbo and GPT-4"> | |
| 2841 | 2870 | <option value="gpt-4-turbo">gpt-4-turbo</option> |
| 2842 | 2871 | <option value="gpt-4-turbo-2024-04-09">gpt-4-turbo-2024-04-09</option> |
| 2843 | 2872 | <option value="gpt-4-turbo-preview">gpt-4-turbo-preview</option> |
| 2844 | - <option value="gpt-4-vision-preview">gpt-4-vision-preview</option> | |
| 2845 | 2873 | <option value="gpt-4-0125-preview">gpt-4-0125-preview (2024)</option> |
| 2846 | 2874 | <option value="gpt-4-1106-preview">gpt-4-1106-preview (2023)</option> |
| 2875 | + <option value="gpt-4">gpt-4</option> | |
| 2876 | + <option value="gpt-4-0613">gpt-4-0613 (2023)</option> | |
| 2877 | + <option value="gpt-4-0314">gpt-4-0314 (2023)</option> | |
| 2847 | 2878 | </optgroup> |
| 2848 | 2879 | <optgroup label="o1GPT-3.5 Turbo"> |
| 2849 | 2880 | <option value="o1gpt-preview3.5-turbo">o1gpt-preview3.5-turbo</option> |
| 2850 | 2881 | <option value="o1gpt-mini3.5-turbo-0125">o1gpt-mini3.5-turbo-0125 (2024)</option> |
| 2882 | + <option value="gpt-3.5-turbo-1106">gpt-3.5-turbo-1106 (2023)</option> | |
| 2883 | + <option value="gpt-3.5-turbo-instruct">gpt-3.5-turbo-instruct</option> | |
| 2851 | 2884 | </optgroup> |
| 2852 | 2885 | <optgroup label="Other"> |
| 2853 | 2886 | <option value="text-davincibabbage-003002">text-davincibabbage-003002</option> |
| 2854 | 2887 | <option value="text-davinci-002">text-davinci-002</option> |
| 2855 | - <option value="text-curie-001">text-curie-001</option> | |
| 2856 | - <option value="text-babbage-001">text-babbage-001</option> | |
| 2857 | - <option value="text-ada-001">text-ada-001</option> | |
| 2858 | - <option value="code-davinci-002">code-davinci-002</option> | |
| 2859 | 2888 | </optgroup> |
| 2860 | 2889 | <optgroup id="openai_external_category" label="External"> |
| 2861 | 2890 | </optgroup> |
| @@ -3054,6 +3083,7 @@ | ||
| 3054 | 3083 | <h4 data-i18n="Google Model">Google Model</h4> |
| 3055 | 3084 | <select id="model_google_select"> |
| 3056 | 3085 | <optgroup label="Primary"> |
| 3086 | + <option value="gemini-2.0-flash">Gemini 2.0 Flash</option> | |
| 3057 | 3087 | <option value="gemini-1.5-pro">Gemini 1.5 Pro</option> |
| 3058 | 3088 | <option value="gemini-1.5-flash">Gemini 1.5 Flash</option> |
| 3059 | 3089 | <option value="gemini-1.0-pro">Gemini 1.0 Pro (Deprecated)</option> |
| @@ -3062,6 +3092,11 @@ | ||
| 3062 | 3092 | <option value="gemini-1.0-ultra-latest">Gemini 1.0 Ultra</option> |
| 3063 | 3093 | </optgroup> |
| 3064 | 3094 | <optgroup label="Subversions"> |
| 3095 | + <option value="gemini-2.0-pro-exp">Gemini 2.0 Pro Experimental</option> | |
| 3096 | + <option value="gemini-2.0-pro-exp-02-05">Gemini 2.0 Pro Experimental 2025-02-05</option> | |
| 3097 | + <option value="gemini-2.0-flash-lite-preview">Gemini 2.0 Flash-Lite Preview</option> | |
| 3098 | + <option value="gemini-2.0-flash-lite-preview-02-05">Gemini 2.0 Flash-Lite Preview 2025-02-05</option> | |
| 3099 | + <option value="gemini-2.0-flash-001">Gemini 2.0 Flash [001]</option> | |
| 3065 | 3100 | <option value="gemini-2.0-flash-thinking-exp">Gemini 2.0 Flash Thinking Experimental</option> |
| 3066 | 3101 | <option value="gemini-2.0-flash-thinking-exp-01-21">Gemini 2.0 Flash Thinking Experimental 2025-01-21</option> |
| 3067 | 3102 | <option value="gemini-2.0-flash-thinking-exp-1219">Gemini 2.0 Flash Thinking Experimental 2024-12-19</option> |
| @@ -3151,33 +3186,33 @@ | ||
| 3151 | 3186 | </div> |
| 3152 | 3187 | <h4 data-i18n="Groq Model">Groq Model</h4> |
| 3153 | 3188 | <select id="model_groq_select"> |
| 3154 | 3189 | <optgroup label="LlamaAlibaba 3.3Cloud"> |
| 3155 | 3190 | <option value="llamaqwen-32.3-70b5-versatile32b">llamaqwen-32.3-70b5-versatile32b</option> |
| 3191 | + <option value="qwen-2.5-coder-32b">qwen-2.5-coder-32b</option> | |
| 3192 | + </optgroup> | |
| 3193 | + <optgroup label="DeepSeek / Alibaba Cloud"> | |
| 3194 | + <option value="deepseek-r1-distill-qwen-32b">deepseek-r1-distill-qwen-32b</option> | |
| 3195 | + </optgroup> | |
| 3196 | + <optgroup label="DeepSeek / Meta"> | |
| 3197 | + <option value="deepseek-r1-distill-llama-70b">deepseek-r1-distill-llama-70b</option> | |
| 3198 | + </optgroup> | |
| 3199 | + <optgroup label="Google"> | |
| 3200 | + <option value="gemma2-9b-it">gemma2-9b-it</option> | |
| 3156 | 3201 | </optgroup> |
| 3157 | 3202 | <optgroup label="Llama 3.2Meta"> |
| 3203 | + <option value="llama-3.1-8b-instant">llama-3.1-8b-instant </option> | |
| 3204 | + <option value="llama-3.2-11b-vision-preview">llama-3.2-11b-vision-preview </option> | |
| 3158 | 3205 | <option value="llama-3.2-1b-preview">llama-3.2-1b-preview </option> |
| 3159 | 3206 | <option value="llama-3.2-3b-preview">llama-3.2-3b-preview </option> |
| 3160 | - <option value="llama-3.2-11b-vision-preview">llama-3.2-11b-vision-preview</option> | |
| 3161 | 3207 | <option value="llama-3.2-90b-vision-preview">llama-3.2-90b-vision-preview </option> |
| 3162 | - </optgroup> | |
| 3208 | + <option value="llama-3.3-70b-specdec">llama-3.3-70b-specdec </option> | |
| 3163 | - <optgroup label="Llama 3.1"> | |
| 3209 | + <option value="llama-3.3-70b-versatile">llama-3.3-70b-versatile </option> | |
| 3164 | 3210 | <option value="llama-guard-3.1-8b-instant">llama-guard-3.1-8b-instant </option> |
| 3165 | - <option value="llama-3.1-70b-versatile">llama-3.1-70b-versatile</option> | |
| 3166 | - <option value="llama-3.1-405b-reasoning">llama-3.1-405b-reasoning</option> | |
| 3167 | - </optgroup> | |
| 3168 | - <optgroup label="Llama 3"> | |
| 3169 | - <option value="llama3-groq-8b-8192-tool-use-preview">llama3-groq-8b-8192-tool-use-preview</option> | |
| 3170 | - <option value="llama3-groq-70b-8192-tool-use-preview">llama3-groq-70b-8192-tool-use-preview</option> | |
| 3171 | - <option value="llama3-8b-8192">llama3-8b-8192</option> | |
| 3172 | 3211 | <option value="llama3-70b-8192">llama3-70b-8192 </option> |
| 3212 | + <option value="llama3-8b-8192">llama3-8b-8192 </option> | |
| 3173 | 3213 | </optgroup> |
| 3174 | 3214 | <optgroup label="GemmaMistral AI"> |
| 3175 | - <option value="gemma-7b-it">gemma-7b-it</option> | |
| 3176 | - <option value="gemma2-9b-it">gemma2-9b-it</option> | |
| 3177 | - </optgroup> | |
| 3178 | - <optgroup label="Other"> | |
| 3179 | 3215 | <option value="mixtral-8x7b-32768">mixtral-8x7b-32768</option> |
| 3180 | - <option value="llava-v1.5-7b-4096-preview">llava-v1.5-7b-4096-preview</option> | |
| 3181 | 3216 | </optgroup> |
| 3182 | 3217 | </select> |
| 3183 | 3218 | </div> |
| @@ -3227,32 +3262,23 @@ | ||
| 3227 | 3262 | <h4 data-i18n="Perplexity Model">Perplexity Model</h4> |
| 3228 | 3263 | <select id="model_perplexity_select"> |
| 3229 | 3264 | <optgroup label="Perplexity Sonar Models"> |
| 3265 | + <option value="sonar">sonar</option> | |
| 3266 | + <option value="sonar-pro">sonar-pro</option> | |
| 3267 | + <option value="sonar-reasoning">sonar-reasoning</option> | |
| 3268 | + <option value="sonar-reasoning-pro">sonar-reasoning-pro</option> | |
| 3269 | + </optgroup> | |
| 3270 | + <optgroup label="Offline Models"> | |
| 3271 | + <option value="r1-1776">r1-1776</option> | |
| 3272 | + </optgroup> | |
| 3273 | + <optgroup label="Deprecated Models"> | |
| 3274 | + <!-- These are scheduled for deprecation after 2/22/2025 --> | |
| 3230 | 3275 | <option value="llama-3.1-sonar-small-128k-online">llama-3.1-sonar-small-128k-online</option> |
| 3231 | 3276 | <option value="llama-3.1-sonar-large-128k-online">llama-3.1-sonar-large-128k-online</option> |
| 3232 | 3277 | <option value="llama-3.1-sonar-huge-128k-online">llama-3.1-sonar-huge-128k-online</option> |
| 3233 | - </optgroup> | |
| 3278 | + <!-- These are not listed on the site anymore --> | |
| 3234 | - <optgroup label="Perplexity Chat Models"> | |
| 3235 | 3279 | <option value="llama-3.1-sonar-small-128k-chat">llama-3.1-sonar-small-128k-chat</option> |
| 3236 | 3280 | <option value="llama-3.1-sonar-large-128k-chat">llama-3.1-sonar-large-128k-chat</option> |
| 3237 | 3281 | </optgroup> |
| 3238 | - <optgroup label="Open-Source Models"> | |
| 3239 | - <option value="llama-3.1-8b-instruct">llama-3.1-8b-instruct</option> | |
| 3240 | - <option value="llama-3.1-70b-instruct">llama-3.1-70b-instruct</option> | |
| 3241 | - </optgroup> | |
| 3242 | - <optgroup label="Deprecated Models"> | |
| 3243 | - <option value="llama-3-sonar-small-32k-chat">llama-3-sonar-small-32k-chat</option> | |
| 3244 | - <option value="llama-3-sonar-small-32k-online">llama-3-sonar-small-32k-online</option> | |
| 3245 | - <option value="llama-3-sonar-large-32k-chat">llama-3-sonar-large-32k-chat</option> | |
| 3246 | - <option value="llama-3-sonar-large-32k-online">llama-3-sonar-large-32k-online</option> | |
| 3247 | - <option value="sonar-small-chat">sonar-small-chat</option> | |
| 3248 | - <option value="sonar-small-online">sonar-small-online</option> | |
| 3249 | - <option value="sonar-medium-chat">sonar-medium-chat</option> | |
| 3250 | - <option value="sonar-medium-online">sonar-medium-online</option> | |
| 3251 | - <option value="llama-3-8b-instruct">llama-3-8b-instruct</option> | |
| 3252 | - <option value="llama-3-70b-instruct">llama-3-70b-instruct</option> | |
| 3253 | - <option value="mistral-7b-instruct">mistral-7b-instruct (v0.2)</option> | |
| 3254 | - <option value="mixtral-8x7b-instruct">mixtral-8x7b-instruct</option> | |
| 3255 | - </optgroup> | |
| 3256 | 3282 | </select> |
| 3257 | 3283 | </div> |
| 3258 | 3284 | <form id="cohere_form" data-source="cohere" action="javascript:void(null);" method="post" enctype="multipart/form-data"> |
| @@ -3524,7 +3550,7 @@ | ||
| 3524 | 3550 | </label> |
| 3525 | 3551 | <label id="instruct_enabled_label"for="instruct_enabled" class="checkbox_label flex1" title="Enable Instruct Mode" data-i18n="[title]instruct_enabled"> |
| 3526 | 3552 | <input id="instruct_enabled" type="checkbox" style="display:none;" /> |
| 3527 | 3553 | <small><i class="fa-solid fa-power-off menu_button togglable margin0"></i></small> |
| 3528 | 3554 | </label> |
| 3529 | 3555 | </div> |
| 3530 | 3556 | </h4> |
| @@ -3702,7 +3728,7 @@ | ||
| 3702 | 3728 | <div class="flex-container"> |
| 3703 | 3729 | <label id="sysprompt_enabled_label" for="sysprompt_enabled" class="checkbox_label flex1" title="Enable System Prompt" data-i18n="[title]sysprompt_enabled"> |
| 3704 | 3730 | <input id="sysprompt_enabled" type="checkbox" style="display:none;" /> |
| 3705 | 3731 | <small><i class="fa-solid fa-power-off menu_button togglable margin0"></i></small> |
| 3706 | 3732 | </label> |
| 3707 | 3733 | </div> |
| 3708 | 3734 | </h4> |
| @@ -3756,8 +3782,8 @@ | ||
| 3756 | 3782 | </div> |
| 3757 | 3783 | <label class="checkbox_label" for="custom_stopping_strings_macro"> |
| 3758 | 3784 | <input id="custom_stopping_strings_macro" type="checkbox" checked> |
| 3759 | 3785 | <small data-i18n="Replace Macro in Custom StoppingStop Strings"> |
| 3760 | 3786 | Replace Macro in Custom StoppingStop Strings |
| 3761 | 3787 | </small> |
| 3762 | 3788 | </label> |
| 3763 | 3789 | </div> |
| @@ -3804,12 +3830,42 @@ | ||
| 3804 | 3830 | <span data-i18n="Reasoning">Reasoning</span> |
| 3805 | 3831 | </h4> |
| 3806 | 3832 | <div> |
| 3807 | - <label class="checkbox_label" for="reasoning_add_to_prompts" title="Add existing reasoning blocks to prompts. To add a new reasoning block, use the message edit menu." data-i18n="[title]reasoning_add_to_prompts"> | |
| 3833 | + <div class="flex-container alignItemsBaseline"> | |
| 3834 | + <label class="checkbox_label flex1" for="reasoning_auto_parse" title="Automatically parse reasoning blocks from main content between the reasoning prefix/suffix. Both fields must be defined and non-empty." data-i18n="[title]reasoning_auto_parse"> | |
| 3835 | + <input id="reasoning_auto_parse" type="checkbox" /> | |
| 3836 | + <small data-i18n="Auto-Parse"> | |
| 3837 | + Auto-Parse | |
| 3838 | + </small> | |
| 3839 | + </label> | |
| 3840 | + <label class="checkbox_label flex1" for="reasoning_auto_expand" title="Automatically expand reasoning blocks." data-i18n="[title]reasoning_auto_expand"> | |
| 3841 | + <input id="reasoning_auto_expand" type="checkbox" /> | |
| 3842 | + <small data-i18n="Auto-Expand"> | |
| 3843 | + Auto-Expand | |
| 3844 | + </small> | |
| 3845 | + </label> | |
| 3846 | + <label class="checkbox_label flex1" for="reasoning_show_hidden" title="Show reasoning time for models with hidden reasoning." data-i18n="[title]reasoning_show_hidden"> | |
| 3847 | + <input id="reasoning_show_hidden" type="checkbox" /> | |
| 3848 | + <small data-i18n="Show Hidden"> | |
| 3849 | + Show Hidden | |
| 3850 | + </small> | |
| 3851 | + </label> | |
| 3852 | + </div> | |
| 3853 | + <div class="flex-container alignItemsBaseline"> | |
| 3854 | + <label class="checkbox_label flex1" for="reasoning_add_to_prompts" title="Add existing reasoning blocks to prompts. To add a new reasoning block, use the message edit menu." data-i18n="[title]reasoning_add_to_prompts"> | |
| 3808 | 3855 | <input id="reasoning_add_to_prompts" type="checkbox" /> |
| 3809 | 3856 | <small data-i18n="Add Reasoning to Prompts"> |
| 3810 | 3857 | Add Reasoning to Prompts |
| 3811 | 3858 | </small> |
| 3812 | 3859 | </label> |
| 3860 | + <div class="flex1 flex-container alignItemsBaseline" title="Maximum number of reasoning blocks to be added per prompt, counting from the last message." data-i18n="[title]reasoning_max_additions"> | |
| 3861 | + <input id="reasoning_max_additions" class="text_pole textarea_compact widthUnset" type="number" min="0" max="999"></textarea> | |
| 3862 | + <small data-i18n="Max">Max</small> | |
| 3863 | + </div> | |
| 3864 | + </div> | |
| 3865 | + <details> | |
| 3866 | + <summary data-i18n="Reasoning Formatting"> | |
| 3867 | + Reasoning Formatting | |
| 3868 | + </summary> | |
| 3813 | 3869 | <div class="flex-container"> |
| 3814 | 3870 | <div class="flex1" title="Inserted before the reasoning content." data-i18n="[title]reasoning_prefix"> |
| 3815 | 3871 | <small data-i18n="Prefix">Prefix</small> |
| @@ -3825,11 +3881,8 @@ | ||
| 3825 | 3881 | <small data-i18n="Separator">Separator</small> |
| 3826 | 3882 | <textarea id="reasoning_separator" class="text_pole textarea_compact autoSetHeight"></textarea> |
| 3827 | 3883 | </div> |
| 3828 | - <div class="flex1" title="Maximum number of reasoning blocks to be added per prompt, counting from the last message." data-i18n="[title]reasoning_max_additions"> | |
| 3829 | - <small data-i18n="Max Additions">Max Additions</small> | |
| 3830 | - <input id="reasoning_max_additions" class="text_pole textarea_compact" type="number" min="0" max="999"></textarea> | |
| 3831 | - </div> | |
| 3832 | 3884 | </div> |
| 3885 | + </details> | |
| 3833 | 3886 | </div> |
| 3834 | 3887 | </div> |
| 3835 | 3888 | <div> |
| @@ -3964,7 +4017,7 @@ | ||
| 3964 | 4017 | <div class="alignitemscenter flex-container flexFlowColumn flexGrow flexShrink gap0 flexBasis48p" title="Cap the number of entry activation recursions" data-i18n="[title]Cap the number of entry activation recursions"> |
| 3965 | 4018 | <small> |
| 3966 | 4019 | <span data-i18n="Max Recursion Steps">Max Recursion Steps</span> |
| 3967 | 4020 | <div class="fa-solid fa-triangle-exclamation opacity50p" data-i18n="[title]0 = unlimited, 1 = scans once and doesn't recurse, 2 = scans once and recurses once, etc\n(disabled when min activations are used)" title="0 = unlimited, 1 = scans once and doesn't recurse, 2 = scans once and recurses once, etc (disabled when min activations are used)"></div> |
| 3968 | 4021 | </small> |
| 3969 | 4022 | <input class="neo-range-slider" type="range" id="world_info_max_recursion_steps" name="world_info_max_recursion_steps" min="0" max="10" step="1"> |
| 3970 | 4023 | <input class="neo-range-input" type="number" min="0" max="10" step="1" data-for="world_info_max_recursion_steps" id="world_info_max_recursion_steps_counter"> |
| @@ -4629,7 +4682,7 @@ | ||
| 4629 | 4682 | <small data-i18n="Enabled">Enabled</small> |
| 4630 | 4683 | </label> |
| 4631 | 4684 | <small data-i18n="Minimum generated message length">Minimum generated message length</small> |
| 4632 | 4685 | <input id="auto_swipe_minimum_length" name="auto_swipe_minimum_length" type="number" min="0" step="1" value="0" class="text_pole" title="If the generated message is shorter than thisthese many characters, trigger an auto-swipe." data-i18n="[title]If the generated message is shorter than thisthese many characters, trigger an auto-swipe"> |
| 4633 | 4686 | <small data-i18n="Blacklisted words">Blacklisted words</small> |
| 4634 | 4687 | <div class="auto_swipe"> |
| 4635 | 4688 | <textarea id="auto_swipe_blacklist" name="auto_swipe_blacklist" data-i18n="[placeholder]words you dont want generated separated by comma ','" placeholder="words you don't want generated separated by comma ','" class="text_pole textarea_compact" value="" autocomplete="off" rows="3"></textarea> |
| @@ -4845,6 +4898,7 @@ | ||
| 4845 | 4898 | </div> |
| 4846 | 4899 | <div id="extensions_settings" class="flex1 wide50p"> |
| 4847 | 4900 | <div id="assets_container" class="extension_container"></div> |
| 4901 | + <div id="typing_indicator_container" class="extension_container"></div> | |
| 4848 | 4902 | <div id="expressions_container" class="extension_container"></div> |
| 4849 | 4903 | <div id="sd_container" class="extension_container"></div> |
| 4850 | 4904 | <div id="tts_container" class="extension_container"></div> |
| @@ -5880,7 +5934,7 @@ | ||
| 5880 | 5934 | <div class="inline-drawer-content flex-container paddingBottom5px wide100p"> |
| 5881 | 5935 | <div class="flex-container wide100p alignitemscenter"> |
| 5882 | 5936 | <div name="keywordsAndLogicBlock" class="flex-container wide100p alignitemscenter"> |
| 5883 | 5937 | <div class="world_entry_form_control keyprimary flex1"> |
| 5884 | 5938 | <small class="displayNone"> |
| 5885 | 5939 | <span data-i18n="Comma separated (required)"> |
| 5886 | 5940 | Comma separated (required) |
| @@ -6302,14 +6356,19 @@ | ||
| 6302 | 6356 | </div> |
| 6303 | 6357 | </div> |
| 6304 | 6358 | <details class="mes_reasoning_details"> |
| 6305 | 6359 | <summary class="mes_reasoning_summary flex-container"> |
| 6306 | - <span data-i18n="Reasoning">Reasoning</span> | |
| 6360 | + <div class="mes_reasoning_header_block flex-container"> | |
| 6307 | 6361 | <div class="mes_reasoning_actionsmes_reasoning_header flex-container"> |
| 6308 | - <div class="mes_reasoning_edit_done mes_button fa-solid fa-check" title="Confirm" data-i18n="[title]Confirmedit"></div> | |
| 6362 | + <span class="mes_reasoning_header_title" data-i18n="Thought for some time">Thought for some time</span> | |
| 6309 | 6363 | <div class="mes_reasoning_edit_cancel mes_buttonmes_reasoning_arrow fa-solid fa-xmark" title="Cancel edit" datachevron-i18n="[title]Cancel editup"></div> |
| 6310 | - <div class="mes_reasoning_edit mes_button fa-solid fa-pencil" title="Edit reasoning" data-i18n="[title]Edit reasoning"></div> | |
| 6364 | + </div> | |
| 6365 | + </div> | |
| 6366 | + <div class="mes_reasoning_actions flex-container"> | |
| 6367 | + <div class="mes_reasoning_edit_done menu_button edit_button fa-solid fa-check" title="Confirm" data-i18n="[title]Confirmedit"></div> | |
| 6368 | + <div class="mes_reasoning_delete menu_button edit_button fa-solid fa-trash-can" title="Remove reasoning" data-i18n="[title]Remove reasoning"></div> | |
| 6369 | + <div class="mes_reasoning_edit_cancel menu_button edit_button fa-solid fa-xmark" title="Cancel edit" data-i18n="[title]Cancel edit"></div> | |
| 6311 | 6370 | <div class="mes_reasoning_copy mes_button fa-solid fa-copy" title="Copy reasoning" data-i18n="[title]Copy reasoning"></div> |
| 6312 | 6371 | <div class="mes_reasoning_deletemes_reasoning_edit mes_button fa-solid fa-trash-canpencil" title="RemoveEdit reasoning" data-i18n="[title]RemoveEdit reasoning"></div> |
| 6313 | 6372 | </div> |
| 6314 | 6373 | </summary> |
| 6315 | 6374 | <div class="mes_reasoning"></div> |
| @@ -6528,9 +6587,6 @@ | ||
| 6528 | 6587 | </div> |
| 6529 | 6588 | |
| 6530 | 6589 | <!-- chat and input bar --> |
| 6531 | - <div id="typing_indicator_template" class="template_element"> | |
| 6532 | - <div class="typing_indicator"><span class="typing_indicator_name">CHAR</span> is typing</div> | |
| 6533 | - </div> | |
| 6534 | 6590 | <div id="message_file_template" class="template_element"> |
| 6535 | 6591 | <div class="mes_file_container"> |
| 6536 | 6592 | <div class="fa-lg fa-solid fa-file-alt mes_file_icon"></div> |
| @@ -6890,8 +6946,8 @@ | ||
| 6890 | 6946 | </div> |
| 6891 | 6947 | <div id="form_sheld"> |
| 6892 | 6948 | <div id="dialogue_del_mes"> |
| 6893 | 6949 | <div id="dialogue_del_mes_ok" data-i18n="Delete" class="menu_button">Delete</div> |
| 6894 | 6950 | <div id="dialogue_del_mes_cancel" data-i18n="Cancel" class="menu_button">Cancel</div> |
| 6895 | 6951 | </div> |
| 6896 | 6952 | <div id="send_form" class="no-connection"> |
| 6897 | 6953 | <form id="file_form" class="wide100p displayNone"> |
| @@ -24,10 +24,22 @@ if (typeof Array.prototype.indexOf === 'function') { | ||
| 24 | 24 | |
| 25 | 25 | |
| 26 | 26 | /* Polyfill EventEmitter. */ |
| 27 | -var EventEmitter = function () { | |
| 27 | +/** | |
| 28 | + * Creates an event emitter. | |
| 29 | + * @param {string[]} autoFireAfterEmit Auto-fire event names | |
| 30 | + */ | |
| 31 | +var EventEmitter = function (autoFireAfterEmit = []) { | |
| 28 | 32 | this.events = {}; |
| 33 | + this.autoFireLastArgs = new Map(); | |
| 34 | + this.autoFireAfterEmit = new Set(autoFireAfterEmit); | |
| 29 | 35 | }; |
| 30 | 36 | |
| 37 | +/** | |
| 38 | + * Adds a listener to an event. | |
| 39 | + * @param {string} event Event name | |
| 40 | + * @param {function} listener Event listener | |
| 41 | + * @returns | |
| 42 | + */ | |
| 31 | 43 | EventEmitter.prototype.on = function (event, listener) { |
| 32 | 44 | // Unknown event used by external libraries? |
| 33 | 45 | if (event === undefined) { |
| @@ -40,6 +52,10 @@ EventEmitter.prototype.on = function (event, listener) { | ||
| 40 | 52 | } |
| 41 | 53 | |
| 42 | 54 | this.events[event].push(listener); |
| 55 | + | |
| 56 | + if (this.autoFireAfterEmit.has(event) && this.autoFireLastArgs.has(event)) { | |
| 57 | + listener.apply(this, this.autoFireLastArgs.get(event)); | |
| 58 | + } | |
| 43 | 59 | }; |
| 44 | 60 | |
| 45 | 61 | /** |
| @@ -60,6 +76,10 @@ EventEmitter.prototype.makeLast = function (event, listener) { | ||
| 60 | 76 | } |
| 61 | 77 | |
| 62 | 78 | events.push(listener); |
| 79 | + | |
| 80 | + if (this.autoFireAfterEmit.has(event) && this.autoFireLastArgs.has(event)) { | |
| 81 | + listener.apply(this, this.autoFireLastArgs.get(event)); | |
| 82 | + } | |
| 63 | 83 | } |
| 64 | 84 | |
| 65 | 85 | /** |
| @@ -80,8 +100,17 @@ EventEmitter.prototype.makeFirst = function (event, listener) { | ||
| 80 | 100 | } |
| 81 | 101 | |
| 82 | 102 | events.unshift(listener); |
| 103 | + | |
| 104 | + if (this.autoFireAfterEmit.has(event) && this.autoFireLastArgs.has(event)) { | |
| 105 | + listener.apply(this, this.autoFireLastArgs.get(event)); | |
| 106 | + } | |
| 83 | 107 | } |
| 84 | 108 | |
| 109 | +/** | |
| 110 | + * Removes a listener from an event. | |
| 111 | + * @param {string} event Event name | |
| 112 | + * @param {function} listener Event listener | |
| 113 | + */ | |
| 85 | 114 | EventEmitter.prototype.removeListener = function (event, listener) { |
| 86 | 115 | var idx; |
| 87 | 116 | |
| @@ -94,6 +123,10 @@ EventEmitter.prototype.removeListener = function (event, listener) { | ||
| 94 | 123 | } |
| 95 | 124 | }; |
| 96 | 125 | |
| 126 | +/** | |
| 127 | + * Emits an event with optional arguments. | |
| 128 | + * @param {string} event Event name | |
| 129 | + */ | |
| 97 | 130 | EventEmitter.prototype.emit = async function (event) { |
| 98 | 131 | let args = [].slice.call(arguments, 1); |
| 99 | 132 | if (localStorage.getItem('eventTracing') === 'true') { |
| @@ -118,6 +151,10 @@ EventEmitter.prototype.emit = async function (event) { | ||
| 118 | 151 | } |
| 119 | 152 | } |
| 120 | 153 | } |
| 154 | + | |
| 155 | + if (this.autoFireAfterEmit.has(event)) { | |
| 156 | + this.autoFireLastArgs.set(event, args); | |
| 157 | + } | |
| 121 | 158 | }; |
| 122 | 159 | |
| 123 | 160 | EventEmitter.prototype.emitAndWait = function (event) { |
| @@ -144,6 +181,10 @@ EventEmitter.prototype.emitAndWait = function (event) { | ||
| 144 | 181 | } |
| 145 | 182 | } |
| 146 | 183 | } |
| 184 | + | |
| 185 | + if (this.autoFireAfterEmit.has(event)) { | |
| 186 | + this.autoFireLastArgs.set(event, args); | |
| 187 | + } | |
| 147 | 188 | }; |
| 148 | 189 | |
| 149 | 190 | EventEmitter.prototype.once = function (event, listener) { |
| @@ -482,7 +482,7 @@ | ||
| 482 | 482 | "separate with commas w/o space between": "فصل بفواصل دون مسافة بينها", |
| 483 | 483 | "Custom Stopping Strings": "سلاسل توقف مخصصة", |
| 484 | 484 | "JSON serialized array of strings": "مصفوفة سلسلة JSON متسلسلة", |
| 485 | 485 | "Replace Macro in Custom StoppingStop Strings": "استبدال الماكرو في سلاسل التوقف المخصصة", |
| 486 | 486 | "Auto-Continue": "المتابعة التلقائية", |
| 487 | 487 | "Allow for Chat Completion APIs": "السماح بواجهات برمجة التطبيقات لإكمال الدردشة", |
| 488 | 488 | "Target length (tokens)": "الطول المستهدف (رموز)", |
| @@ -709,7 +709,7 @@ | ||
| 709 | 709 | "Auto-swipe": "السحب التلقائي", |
| 710 | 710 | "Enable the auto-swipe function. Settings in this section only have an effect when auto-swipe is enabled": "تمكين وظيفة السحب التلقائي. الإعدادات في هذا القسم تؤثر فقط عند تمكين السحب التلقائي", |
| 711 | 711 | "Minimum generated message length": "الحد الأدنى لطول الرسالة المولدة", |
| 712 | 712 | "If the generated message is shorter than thisthese many characters, trigger an auto-swipe": "إذا كانت الرسالة المولدة أقصر من هذا، فتحريض السحب التلقائي", |
| 713 | 713 | "Blacklisted words": "الكلمات الممنوعة", |
| 714 | 714 | "words you dont want generated separated by comma ','": "الكلمات التي لا تريد توليدها مفصولة بفاصلة ','", |
| 715 | 715 | "Blacklisted word count to swipe": "عدد الكلمات الممنوعة للسحب", |
| @@ -482,7 +482,7 @@ | ||
| 482 | 482 | "separate with commas w/o space between": "getrennt durch Kommas ohne Leerzeichen dazwischen", |
| 483 | 483 | "Custom Stopping Strings": "Benutzerdefinierte Stoppzeichenfolgen", |
| 484 | 484 | "JSON serialized array of strings": "JSON serialisierte Reihe von Zeichenfolgen", |
| 485 | 485 | "Replace Macro in Custom StoppingStop Strings": "Makro in benutzerdefinierten Stoppzeichenfolgen ersetzen", |
| 486 | 486 | "Auto-Continue": "Automatisch fortsetzen", |
| 487 | 487 | "Allow for Chat Completion APIs": "Erlaube Chat-Vervollständigungs-APIs", |
| 488 | 488 | "Target length (tokens)": "Ziel-Länge (Tokens)", |
| @@ -709,7 +709,7 @@ | ||
| 709 | 709 | "Auto-swipe": "Automatisches Wischen", |
| 710 | 710 | "Enable the auto-swipe function. Settings in this section only have an effect when auto-swipe is enabled": "Aktiviere die Auto-Wisch-Funktion. Einstellungen in diesem Abschnitt haben nur dann Auswirkungen, wenn das automatische Wischen aktiviert ist", |
| 711 | 711 | "Minimum generated message length": "Minimale generierte Nachrichtenlänge", |
| 712 | 712 | "If the generated message is shorter than thisthese many characters, trigger an auto-swipe": "Wenn die generierte Nachricht kürzer ist als diese, löse automatisches Wischen aus", |
| 713 | 713 | "Blacklisted words": "Verbotene Wörter", |
| 714 | 714 | "words you dont want generated separated by comma ','": "Wörter, die du nicht generiert haben möchtest, durch Komma ',' getrennt", |
| 715 | 715 | "Blacklisted word count to swipe": "Anzahl der verbotenen Wörter, um zu wischen", |
| @@ -482,7 +482,7 @@ | ||
| 482 | 482 | "separate with commas w/o space between": "separe con comas sin espacio entre ellas", |
| 483 | 483 | "Custom Stopping Strings": "Cadenas de Detención Personalizadas", |
| 484 | 484 | "JSON serialized array of strings": "Arreglo de cadenas serializado en JSON", |
| 485 | 485 | "Replace Macro in Custom StoppingStop Strings": "Reemplazar macro en Cadenas de Detención Personalizadas", |
| 486 | 486 | "Auto-Continue": "Autocontinuar", |
| 487 | 487 | "Allow for Chat Completion APIs": "Permitir para APIs de Completado de Chat", |
| 488 | 488 | "Target length (tokens)": "Longitud objetivo (tokens)", |
| @@ -709,7 +709,7 @@ | ||
| 709 | 709 | "Auto-swipe": "Deslizamiento automático", |
| 710 | 710 | "Enable the auto-swipe function. Settings in this section only have an effect when auto-swipe is enabled": "Habilitar la función de deslizamiento automático. La configuración en esta sección solo tiene efecto cuando el deslizamiento automático está habilitado", |
| 711 | 711 | "Minimum generated message length": "Longitud mínima del mensaje generado", |
| 712 | 712 | "If the generated message is shorter than thisthese many characters, trigger an auto-swipe": "Si el mensaje generado es más corto que esto, activar un deslizamiento automático", |
| 713 | 713 | "Blacklisted words": "Palabras prohibidas", |
| 714 | 714 | "words you dont want generated separated by comma ','": "palabras que no desea generar separadas por coma ','", |
| 715 | 715 | "Blacklisted word count to swipe": "Número de palabras prohibidas para deslizar", |
| @@ -434,7 +434,7 @@ | ||
| 434 | 434 | "Non-markdown strings": "Chaînes non Markdown", |
| 435 | 435 | "Custom Stopping Strings": "Chaînes d'arrêt personnalisées", |
| 436 | 436 | "JSON serialized array of strings": "Tableau de chaînes sérialisé JSON", |
| 437 | 437 | "Replace Macro in Custom StoppingStop Strings": "Remplacer les macro dans les chaînes d'arrêt personnalisées", |
| 438 | 438 | "Auto-Continue": "Auto-Continue", |
| 439 | 439 | "Allow for Chat Completion APIs": "Autoriser les APIs de complétion de chat", |
| 440 | 440 | "Target length (tokens)": "Longueur cible (tokens)", |
| @@ -656,7 +656,7 @@ | ||
| 656 | 656 | "Auto-swipe": "Balayage automatique", |
| 657 | 657 | "Enable the auto-swipe function. Settings in this section only have an effect when auto-swipe is enabled": "Activer la fonction de balayage automatique. Les paramètres de cette section n'ont d'effet que lorsque le balayage automatique est activé", |
| 658 | 658 | "Minimum generated message length": "Longueur minimale du message généré", |
| 659 | 659 | "If the generated message is shorter than thisthese many characters, trigger an auto-swipe": "Si le message généré est plus court que cela, déclenchez un balayage automatique", |
| 660 | 660 | "Blacklisted words": "Mots en liste noire", |
| 661 | 661 | "words you dont want generated separated by comma ','": "mots que vous ne voulez pas générer séparés par des virgules ','", |
| 662 | 662 | "Blacklisted word count to swipe": "Nombre de mots en liste noire pour balayer", |
| @@ -1385,8 +1385,8 @@ | ||
| 1385 | 1385 | "enable_functions_desc_1": "Autorise l'utilisation", |
| 1386 | 1386 | "enable_functions_desc_2": "outils de fonction", |
| 1387 | 1387 | "enable_functions_desc_3": "Peut être utilisé par diverses extensions pour fournir des fonctionnalités supplémentaires.", |
| 1388 | 1388 | "ShowRequest model reasoning": "AfficherDemander les pensées du modèle", |
| 1389 | 1389 | "DisplayAllows the model's internalto thoughtsreturn inits thethinking responseprocess.": "AfficherPermet lesau penséesmodèle internesde duretourner modèleson dansprocessus lade réponseréflexion.", |
| 1390 | 1390 | "Confirm token parsing with": "Confirmer l'analyse des tokens avec", |
| 1391 | 1391 | "openai_logit_bias_no_items": "Aucun élément", |
| 1392 | 1392 | "api_no_connection": "Pas de connection...", |
| @@ -1485,7 +1485,7 @@ | ||
| 1485 | 1485 | "(disabled when max recursion steps are used)": "(désactivé lorsque le nombre maximum de pas de récursivité est utilisé)", |
| 1486 | 1486 | "Cap the number of entry activation recursions": "Plafonner le nombre de récursions d'activation d'entrée", |
| 1487 | 1487 | "Max Recursion Steps": "Nombre maximal d'étapes de récursivité", |
| 1488 | 1488 | "0 = unlimited, 1 = scans once and doesn't recurse, 2 = scans once and recurses once, etc\\n(disabled when min activations are used)": "0 = illimité, 1 = scanne une fois et ne récure pas, 2 = scanne une fois et récure une fois, etc.\n(désactivé lorsque des activations minimales sont utilisées)", |
| 1489 | 1489 | "Include names with each message into the context for scanning": "Inclure les noms dans chaque message dans le contexte pour l'analyse.", |
| 1490 | 1490 | "Apply current sorting as Order": "Appliquer le tri actuel comme ordre", |
| 1491 | 1491 | "Display swipe numbers for all messages, not just the last.": "Afficher le nombre de balayage sur tous les messages, et pas seulement le dernier.", |
| @@ -1602,7 +1602,6 @@ | ||
| 1602 | 1602 | "Character Expressions": "Expressions de personnages", |
| 1603 | 1603 | "Translate text to English before classification": "Traduire le texte en anglais avant de le classer", |
| 1604 | 1604 | "Show default images (emojis) if sprite missing": "Afficher les images par défaut (emojis) si le sprite est manquant", |
| 1605 | - "Image Type - talkinghead (extras)": "Type d'image - talkinghead (extras)", | |
| 1606 | 1605 | "Classifier API": "API de classification", |
| 1607 | 1606 | "Select the API for classifying expressions.": "Sélectionnez l'API pour classer les expressions.", |
| 1608 | 1607 | "Main API": "API principale", |
| @@ -482,7 +482,7 @@ | ||
| 482 | 482 | "separate with commas w/o space between": "aðskilið með kommum án bila milli", |
| 483 | 483 | "Custom Stopping Strings": "Eigin stopp-strengir", |
| 484 | 484 | "JSON serialized array of strings": "JSON raðað fylki af strengjum", |
| 485 | 485 | "Replace Macro in Custom StoppingStop Strings": "Skiptu út í macro í sérsniðnum stoppa strengjum", |
| 486 | 486 | "Auto-Continue": "Sjálfvirk Forná", |
| 487 | 487 | "Allow for Chat Completion APIs": "Leyfa fyrir spjall Loka APIs", |
| 488 | 488 | "Target length (tokens)": "Markaðarlengd (texti)", |
| @@ -709,7 +709,7 @@ | ||
| 709 | 709 | "Auto-swipe": "Sjálfvirkur sveip", |
| 710 | 710 | "Enable the auto-swipe function. Settings in this section only have an effect when auto-swipe is enabled": "Virkjaðu sjálfvirka sveiflugerð. Stillingar í þessum hluta hafa aðeins áhrif þegar sjálfvirkur sveiflugerð er virk", |
| 711 | 711 | "Minimum generated message length": "Lágmarks lengd á mynduðum skilaboðum", |
| 712 | 712 | "If the generated message is shorter than thisthese many characters, trigger an auto-swipe": "Ef mynduðu skilaboðin eru styttri en þessi, kallaðu fram sjálfvirkar sveiflugerðar", |
| 713 | 713 | "Blacklisted words": "Svört orð", |
| 714 | 714 | "words you dont want generated separated by comma ','": "orð sem þú vilt ekki að framleiða aðskilin með kommu ','", |
| 715 | 715 | "Blacklisted word count to swipe": "Fjöldi svörtra orða til að sveipa", |
| @@ -482,7 +482,7 @@ | ||
| 482 | 482 | "separate with commas w/o space between": "separati con virgole senza spazio tra loro", |
| 483 | 483 | "Custom Stopping Strings": "Stringhe di Stop Personalizzate", |
| 484 | 484 | "JSON serialized array of strings": "Matrice serializzata JSON di stringhe", |
| 485 | 485 | "Replace Macro in Custom StoppingStop Strings": "Sostituisci Macro in Stringhe di Arresto Personalizzate", |
| 486 | 486 | "Auto-Continue": "Auto-continua", |
| 487 | 487 | "Allow for Chat Completion APIs": "Consenti per API di completamento chat", |
| 488 | 488 | "Target length (tokens)": "Lunghezza obiettivo (token)", |
| @@ -709,7 +709,7 @@ | ||
| 709 | 709 | "Auto-swipe": "Auto-swipe", |
| 710 | 710 | "Enable the auto-swipe function. Settings in this section only have an effect when auto-swipe is enabled": "Abilita la funzione di auto-swipe. Le impostazioni in questa sezione hanno effetto solo quando l'auto-swipe è abilitato", |
| 711 | 711 | "Minimum generated message length": "Lunghezza minima del messaggio generato", |
| 712 | 712 | "If the generated message is shorter than thisthese many characters, trigger an auto-swipe": "Se il messaggio generato è più breve di questo, attiva un'automatica rimozione", |
| 713 | 713 | "Blacklisted words": "Parole in blacklist", |
| 714 | 714 | "words you dont want generated separated by comma ','": "parole che non vuoi generate separate da virgola ','", |
| 715 | 715 | "Blacklisted word count to swipe": "Numero di parole in blacklist per attivare un'automatica rimozione", |
| @@ -482,7 +482,7 @@ | ||
| 482 | 482 | "separate with commas w/o space between": "間にスペースのないカンマで区切ります", |
| 483 | 483 | "Custom Stopping Strings": "カスタム停止文字列", |
| 484 | 484 | "JSON serialized array of strings": "文字列のJSONシリアル化配列", |
| 485 | 485 | "Replace Macro in Custom StoppingStop Strings": "カスタム停止文字列内のマクロを置換する", |
| 486 | 486 | "Auto-Continue": "自動継続", |
| 487 | 487 | "Allow for Chat Completion APIs": "チャット補完APIを許可", |
| 488 | 488 | "Target length (tokens)": "ターゲット長さ(トークン)", |
| @@ -709,7 +709,7 @@ | ||
| 709 | 709 | "Auto-swipe": "オートスワイプ", |
| 710 | 710 | "Enable the auto-swipe function. Settings in this section only have an effect when auto-swipe is enabled": "自動スワイプ機能を有効にします。このセクションの設定は、自動スワイプが有効になっている場合にのみ効果があります", |
| 711 | 711 | "Minimum generated message length": "生成されたメッセージの最小長", |
| 712 | 712 | "If the generated message is shorter than thisthese many characters, trigger an auto-swipe": "生成されたメッセージがこれよりも短い場合、自動スワイプをトリガーします", |
| 713 | 713 | "Blacklisted words": "ブラックリストされた単語", |
| 714 | 714 | "words you dont want generated separated by comma ','": "コンマ ',' で区切られた生成したくない単語", |
| 715 | 715 | "Blacklisted word count to swipe": "スワイプするブラックリストされた単語の数", |
| @@ -211,7 +211,7 @@ | ||
| 211 | 211 | "Sampler Priority": "샘플러 우선 순위", |
| 212 | 212 | "Ooba only. Determines the order of samplers.": "Ooba 전용. 샘플러의 순서를 결정합니다.", |
| 213 | 213 | "Character Names Behavior": "캐릭터 이름 동작", |
| 214 | 214 | "[title]character_names_none": "캐릭터 이름 접두사를 추가하지 않습니다. 그룹 채팅에서는 좋지 않을 수 있으므로, 이 설정을 선택할 때는 주의해야 합니다.", |
| 215 | 215 | "Helps the model to associate messages with characters.": "모델이 메시지를 캐릭터와 연관시키는 데 도움이 됩니다.", |
| 216 | 216 | "None": "없음", |
| 217 | 217 | "None (not injected)": "없음 (삽입되지 않음)", |
| @@ -404,7 +404,7 @@ | ||
| 404 | 404 | "Custom API Key": "커스텀 API 키", |
| 405 | 405 | "Available Models": "사용 가능한 모델", |
| 406 | 406 | "Prompt Post-Processing": "신속한 후처리", |
| 407 | 407 | "[title]API Connections;[no_connection_text]api_no_connection": "연결이 되지 않았습니다...", |
| 408 | 408 | "Applies additional processing to the prompt before sending it to the API.": "API로 보내기 전에 프롬프트에 추가 처리를 적용합니다.", |
| 409 | 409 | "Verifies your API connection by sending a short test message. Be aware that you'll be credited for it!": "짧은 테스트 메시지를 보내어 API 연결을 확인합니다. 이에 대해 유료 크레딧이 지불될 수 있음을 인식하세요!", |
| 410 | 410 | "Test Message": "테스트 메시지", |
| @@ -492,7 +492,7 @@ | ||
| 492 | 492 | "separate with commas w/o space between": "쉼표로 구분 (공백 없이)", |
| 493 | 493 | "Custom Stopping Strings": "사용자 정의 중지 문자열", |
| 494 | 494 | "JSON serialized array of strings": "문자열의 JSON 직렬화된 배열", |
| 495 | 495 | "Replace Macro in Custom StoppingStop Strings": "사용자 정의 중단 문자열에서 매크로 교체", |
| 496 | 496 | "Auto-Continue": "자동 계속하기", |
| 497 | 497 | "Allow for Chat Completion APIs": "채팅 완성 API 허용", |
| 498 | 498 | "Target length (tokens)": "대상 길이 (토큰)", |
| @@ -625,7 +625,7 @@ | ||
| 625 | 625 | "Single-row message input area. Mobile only, no effect on PC": "한 줄짜리 메시지 입력 영역. 모바일 전용, PC에는 영향 없음", |
| 626 | 626 | "Compact Input Area (Mobile)": "조그마한 입력 영역 (모바일)", |
| 627 | 627 | "Swipe # for All Messages": "모든 스와이프 메시지에 대해 번호 매기기", |
| 628 | 628 | "[title]Display swipe numbers for all messages, not just the last.": "마지막 메시지만이 아니라 모든 메시지에 대한 스와이프 번호를 표시합니다.", |
| 629 | 629 | "In the Character Management panel, show quick selection buttons for favorited characters": "캐릭터 관리 패널에서 즐겨찾는 캐릭터에 대한 빠른 선택 버튼을 표시합니다", |
| 630 | 630 | "Characters Hotswap": "캐릭터 핫스왑", |
| 631 | 631 | "Enable magnification for zoomed avatar display.": "마우스 포인터를 아바타 위에 올려두면 아바타가 확대 됩니다.", |
| @@ -724,7 +724,7 @@ | ||
| 724 | 724 | "Auto-swipe": "자동 스와이프", |
| 725 | 725 | "Enable the auto-swipe function. Settings in this section only have an effect when auto-swipe is enabled": "자동 스와이프 기능을 활성화합니다. 이 섹션의 설정은 자동 스와이프가 활성화되었을 때만 영향을 미칩니다", |
| 726 | 726 | "Minimum generated message length": "생성된 메시지 최소 길이", |
| 727 | 727 | "If the generated message is shorter than thisthese many characters, trigger an auto-swipe": "생성된 메시지가이보다 짧으면 자동 스와이프를 트리거합니다", |
| 728 | 728 | "Blacklisted words": "금지어", |
| 729 | 729 | "words you dont want generated separated by comma ','": "쉼표로 구분된 생성하지 않으려는 단어", |
| 730 | 730 | "Blacklisted word count to swipe": "스와이프할 금지어 개수", |
| @@ -1467,7 +1467,6 @@ | ||
| 1467 | 1467 | "menu within": "내의 메뉴", |
| 1468 | 1468 | "Translate text to English before classification": "분류 전에 텍스트를 영어로 번역합니다.", |
| 1469 | 1469 | "Show default images (emojis) if sprite missing": "해당하는 스프라이트가 없으면 기본 이미지 (이모지들)을 표시합니다.", |
| 1470 | - "Image Type - talkinghead (extras)": "이미지 유형 - 토킹 헤드 (부가 사항)", | |
| 1471 | 1470 | "Classifier API": "분류를 위한 API", |
| 1472 | 1471 | "Select the API for classifying expressions.": "감정 이미지들을 분류할 API를 선택하세요.", |
| 1473 | 1472 | "Local": "로컬", |
| @@ -1538,7 +1537,7 @@ | ||
| 1538 | 1537 | "Only apply color as accent": "색상은 오직 강조로써만 적용됩니다", |
| 1539 | 1538 | "qr--colorClear": "색상 지우기", |
| 1540 | 1539 | "Color": "색상", |
| 1541 | 1540 | "[title]world_button_title": "캐릭터 로어. 클릭하여 로드하세요. Shift를 클릭하면 '월드 인포 링크' 팝업이 열립니다.", |
| 1542 | 1541 | "Select TTS Provider": "TTS 공급자 선택", |
| 1543 | 1542 | "tts_enabled": "활성화", |
| 1544 | 1543 | "Narrate user messages": "사용자 메시지 나레이션", |
| @@ -1583,15 +1582,15 @@ | ||
| 1583 | 1582 | "Prompt Content": "프롬프트 내용", |
| 1584 | 1583 | "Instruct Sequences": "지시 시퀀스", |
| 1585 | 1584 | "Prefer Character Card Instructions": "캐릭터 카드의 지시사항을 선호", |
| 1586 | 1585 | "[title]If checked and the character card contains a Post-History Instructions override, use that instead": "활성화 된 경우, 캐릭터 카드에 Post-History 지시 무시 항목이 포함되어 있으면, 카드 지시사항의 내용으로 대신 사용합니다.", |
| 1587 | 1586 | "Auto-select Input Text": "입력 텍스트 자동 선택", |
| 1588 | 1587 | "[title]Enable auto-select of input text in some text fields when clicking/selecting them. Applies to popup input textboxes, and possible other custom input fields.": "일부 텍스트 필드를 클릭하거나 선택할 때 자동으로 입력된 텍스트가 선택되도록 설정합니다. 팝업 입력창과 기타 커스텀 입력 필드에 적용됩니다.", |
| 1589 | 1588 | "Markdown Hotkeys": "마크다운 입력 단축키", |
| 1590 | 1589 | "[title]markdown_hotkeys_desc": "특정 텍스트 입력창에서 마크다운 형식 문자를 입력하기 위한 단축키를 활성화합니다. '/help hotkeys'를 참고하세요.", |
| 1591 | 1590 | "Show group chat queue": "그룹 채팅 대기열 표시", |
| 1592 | 1591 | "[title]In group chat, highlight the character(s) that are currently queued to generate responses and the order in which they will respond.": "그룹 채팅에서 응답을 생성하기 위해 현재 대기 중인 캐릭터와 응답할 순서를 강조 표시합니다.", |
| 1593 | 1592 | "Quick 'Impersonate' button": "빠른 '사칭' 버튼", |
| 1594 | 1593 | "[title]Show a button in the input area to ask the AI to impersonate your character for a single message": "입력 영역에 AI에게 한 메시지 동안 당신의 캐릭터 연기를 사칭하도록 요청하는 버튼을 표시합니다.", |
| 1595 | 1594 | "Injection Template": "삽입 템플릿", |
| 1596 | 1595 | "Query messages": "쿼리 메시지 수", |
| 1597 | 1596 | "Score threshold": "점수 임계값", |
| @@ -482,7 +482,7 @@ | ||
| 482 | 482 | "separate with commas w/o space between": "gescheiden met komma's zonder spatie ertussen", |
| 483 | 483 | "Custom Stopping Strings": "Aangepaste Stopreeksen", |
| 484 | 484 | "JSON serialized array of strings": "JSON geserialiseerde reeks van strings", |
| 485 | 485 | "Replace Macro in Custom StoppingStop Strings": "Macro vervangen in aangepaste stopreeksen", |
| 486 | 486 | "Auto-Continue": "Automatisch doorgaan", |
| 487 | 487 | "Allow for Chat Completion APIs": "Chatvervolledigings-API's toestaan", |
| 488 | 488 | "Target length (tokens)": "Doellengte (tokens)", |
| @@ -709,7 +709,7 @@ | ||
| 709 | 709 | "Auto-swipe": "Automatisch vegen", |
| 710 | 710 | "Enable the auto-swipe function. Settings in this section only have an effect when auto-swipe is enabled": "Schakel de automatische-vegen functie in. Instellingen in dit gedeelte hebben alleen effect wanneer automatisch vegen is ingeschakeld", |
| 711 | 711 | "Minimum generated message length": "Minimale gegenereerde berichtlengte", |
| 712 | 712 | "If the generated message is shorter than thisthese many characters, trigger an auto-swipe": "Als het gegenereerde bericht korter is dan dit, activeer dan een automatische veeg", |
| 713 | 713 | "Blacklisted words": "Verboden woorden", |
| 714 | 714 | "words you dont want generated separated by comma ','": "woorden die je niet gegenereerd wilt hebben gescheiden door komma ','", |
| 715 | 715 | "Blacklisted word count to swipe": "Aantal verboden woorden om te vegen", |
| @@ -482,7 +482,7 @@ | ||
| 482 | 482 | "separate with commas w/o space between": "separe com vírgulas sem espaço entre", |
| 483 | 483 | "Custom Stopping Strings": "Cadeias de parada personalizadas", |
| 484 | 484 | "JSON serialized array of strings": "Matriz de strings serializada em JSON", |
| 485 | 485 | "Replace Macro in Custom StoppingStop Strings": "Substituir Macro em Strings de Parada Personalizadas", |
| 486 | 486 | "Auto-Continue": "Auto-Continuar", |
| 487 | 487 | "Allow for Chat Completion APIs": "Permitir APIs de Completar Chat", |
| 488 | 488 | "Target length (tokens)": "Comprimento alvo (tokens)", |
| @@ -709,7 +709,7 @@ | ||
| 709 | 709 | "Auto-swipe": "Auto-swipe", |
| 710 | 710 | "Enable the auto-swipe function. Settings in this section only have an effect when auto-swipe is enabled": "Ativar a função de auto-swipe. As configurações nesta seção só têm efeito quando o auto-swipe está ativado", |
| 711 | 711 | "Minimum generated message length": "Comprimento mínimo da mensagem gerada", |
| 712 | 712 | "If the generated message is shorter than thisthese many characters, trigger an auto-swipe": "Se a mensagem gerada for mais curta que isso, acione um auto-swipe", |
| 713 | 713 | "Blacklisted words": "Palavras proibidas", |
| 714 | 714 | "words you dont want generated separated by comma ','": "palavras que você não quer geradas separadas por vírgula ','", |
| 715 | 715 | "Blacklisted word count to swipe": "Contagem de palavras proibidas para swipe", |
| @@ -161,7 +161,7 @@ | ||
| 161 | 161 | "View hidden API keys": "Посмотреть скрытые API-ключи", |
| 162 | 162 | "Advanced Formatting": "Расширенное форматирование", |
| 163 | 163 | "Context Template": "Шаблон контекста", |
| 164 | 164 | "Replace Macro in Custom StoppingStop Strings": "Заменять макросы в пользовательских стоп-строках", |
| 165 | 165 | "Story String": "Строка истории", |
| 166 | 166 | "Example Separator": "Разделитель примеров сообщений", |
| 167 | 167 | "Chat Start": "Начало чата", |
| @@ -195,7 +195,7 @@ | ||
| 195 | 195 | "Yes": "Да", |
| 196 | 196 | "No": "Нет", |
| 197 | 197 | "Context %": "Процент контекста", |
| 198 | 198 | "Budget Cap": "БюджетныйЛимит лимитбюджета", |
| 199 | 199 | "(0 = disabled)": "(0 = отключено)", |
| 200 | 200 | "None": "Отсутствует", |
| 201 | 201 | "User Settings": "Настройки пользователя", |
| @@ -426,7 +426,7 @@ | ||
| 426 | 426 | "Requests logprobs from the API for the Token Probabilities feature": "Запросить логпробы из API для функции Token Probabilities.", |
| 427 | 427 | "Automatically reject and re-generate AI message based on configurable criteria": "Автоматическое отклонение и повторная генерация сообщений AI на основе настраиваемых критериев.", |
| 428 | 428 | "Enable the auto-swipe function. Settings in this section only have an effect when auto-swipe is enabled": "Включить авто-свайп. Настройки в этом разделе действуют только при включенном авто-свайпе.", |
| 429 | 429 | "If the generated message is shorter than thisthese many characters, trigger an auto-swipe": "Если сгенерированное сообщение короче этого значения, срабатывает авто-свайп.", |
| 430 | 430 | "Reload and redraw the currently open chat": "Перезагрузить и перерисовать открытый в данный момент чат.", |
| 431 | 431 | "Auto-Expand Message Actions": "Развернуть действия", |
| 432 | 432 | "Persona Management": "Управление персоной", |
| @@ -575,10 +575,10 @@ | ||
| 575 | 575 | "Characters sorting order": "Порядок сортировки персонажей", |
| 576 | 576 | "Remove": "Убрать", |
| 577 | 577 | "Select a World Info file for": "Выбрать файл с миром для", |
| 578 | 578 | "Primary Lorebook": "ОсновногоОсновной лорбукалорбук", |
| 579 | 579 | "A selected World Info will be bound to this character as its own Lorebook.": "Информация о мире будет привязана к персонажу как его собственный лорбук.", |
| 580 | 580 | "When generating an AI reply, it will be combined with the entries from a global World Info selector.": "Когда ИИ генерирует ответ, он будет совмещён с записями из глобально выбранного мира.", |
| 581 | 581 | "Exporting a character would also export the selected Lorebook file embedded in the JSON data.": "При экспорте персонажа вместе с ним также выгрузится выбранный лорбук в виде JSON.", |
| 582 | 582 | "Additional Lorebooks": "Вспомогательные лорбуки", |
| 583 | 583 | "Associate one or more auxillary Lorebooks with this character.": "Привязать к этому персонажу один или больше вспомогательных лорбуков", |
| 584 | 584 | "NOTE: These choices are optional and won't be preserved on character export!": "ВНИМАНИЕ: эти выборы необязательные и не будут сохранены при экспорте персонажа!", |
| @@ -593,7 +593,7 @@ | ||
| 593 | 593 | "Prompt": "Промпт", |
| 594 | 594 | "Copy": "Скопировать", |
| 595 | 595 | "Confirm": "Подтвердить", |
| 596 | 596 | "Copy this message": "СкопироватьПродублировать сообщение", |
| 597 | 597 | "Delete this message": "Удалить сообщение", |
| 598 | 598 | "Move message up": "Переместить сообщение вверх", |
| 599 | 599 | "Move message down": "Переместить сообщение вниз", |
| @@ -612,7 +612,7 @@ | ||
| 612 | 612 | "Ask AI to write your message for you": "Попросить ИИ написать сообщение за вас", |
| 613 | 613 | "Continue the last message": "Продолжить текущее сообщение", |
| 614 | 614 | "Bind user name to that avatar": "Закрепить имя за этим аватаром", |
| 615 | 615 | "Select this as default persona for the new chats.": "ВыбератьВыбирать эту Персону в качестве персоныперсону по умолчанию для всех новых чатов.", |
| 616 | 616 | "Change persona image": "Сменить аватар персоны", |
| 617 | 617 | "Delete persona": "Удалить персону", |
| 618 | 618 | "Reduced Motion": "Сокращение анимаций", |
| @@ -640,7 +640,7 @@ | ||
| 640 | 640 | "Token Probabilities": "Вероятности токенов", |
| 641 | 641 | "Close chat": "Закрыть чат", |
| 642 | 642 | "Manage chat files": "Все чаты", |
| 643 | 643 | "Import Extension From Git Repo": "Импортировать расширение из Git Repository-репозитория.", |
| 644 | 644 | "Install extension": "Установить расширение", |
| 645 | 645 | "Manage extensions": "Управление расширениями", |
| 646 | 646 | "Tokens persona description": "Токенов", |
| @@ -1122,7 +1122,7 @@ | ||
| 1122 | 1122 | "help_hotkeys_0": "Горячие клавиши", |
| 1123 | 1123 | "You can browse a list of bundled characters in the": "Комплектных персонажей можно найти в меню", |
| 1124 | 1124 | "Download Extensions & Assets": "Загрузить расширения и ресурсы", |
| 1125 | 1125 | "menu within": "внутри этихв кубиковменю", |
| 1126 | 1126 | "Assets URL": "URL с описанием ресурсов", |
| 1127 | 1127 | "Custom (OpenAI-compatible)": "Кастомный (совместимый с OpenAI)", |
| 1128 | 1128 | "Custom Endpoint (Base URL)": "Кастомный эндпоинт (базовый URL)", |
| @@ -1943,7 +1943,7 @@ | ||
| 1943 | 1943 | "and connect to an": "и подключитесь к", |
| 1944 | 1944 | "You can add more": "Можете добавить больше", |
| 1945 | 1945 | "from other websites": "с других сайтов.", |
| 1946 | 1946 | "Go to the": "ЗаглянитеЗаходите в", |
| 1947 | 1947 | "to install additional features.": ", чтобы установить разные дополнительные ресурсы.", |
| 1948 | 1948 | "or_welcome": "; также доступен", |
| 1949 | 1949 | "Claude API Key": "Ключ от API Claude", |
| @@ -1958,7 +1958,7 @@ | ||
| 1958 | 1958 | "Save": "Сохранить", |
| 1959 | 1959 | "Chat Lorebook": "Лорбук для чата", |
| 1960 | 1960 | "chat_world_template_txt": "Выбранный мир будет привязан к этому чату. Будет добавляться в промпт наряду с глобальным лорбуком и лором персонажа.", |
| 1961 | 1961 | "world_button_title": "Лор персонажа\n\nНажмите, чтобы загрузить\nShift + кликЛКМ, чтобы открыть диалог привязки мира", |
| 1962 | 1962 | "No auxillary Lorebooks set. Click here to select.": "Вспомогательный лорбук не выбран. Нажмите, чтобы выбрать.", |
| 1963 | 1963 | "ext_regex_user_input_desc": "Отправленные вами сообщения.", |
| 1964 | 1964 | "ext_regex_ai_input_desc": "Полученные от API ответы.", |
| @@ -2144,5 +2144,65 @@ | ||
| 2144 | 2144 | "Not connected to the API!": "Нет соединения с API!", |
| 2145 | 2145 | "ext_type_system": "Это комплектное расширение. Его нельзя удалить, а обновляется оно вместе со всей системой.", |
| 2146 | 2146 | "Update all": "Обновить все", |
| 2147 | 2147 | "Close": "Закрыть", |
| 2148 | + "Optional modules:": "Необязательные модули:", | |
| 2149 | + "Sort: Display Name": "Сортировать: по названию", | |
| 2150 | + "Sort: Loading Order": "Сортировать: в порядке загрузки", | |
| 2151 | + "Click to toggle": "Нажмите, чтобы включить или выключить", | |
| 2152 | + "Loading Asset List": "Загрузить список ресурсов", | |
| 2153 | + "Don't ask again for this URL": "Запомнить выбор для этого адреса", | |
| 2154 | + "Are you sure you want to connect to the following url?": "Вы точно хотите подключиться к этому адресу?", | |
| 2155 | + "All": "Всё", | |
| 2156 | + "Characters": "Персонажи", | |
| 2157 | + "Ambient sounds": "Звуковой эмбиент", | |
| 2158 | + "Blip sounds": "Звуки уведомлений", | |
| 2159 | + "Background music": "Фоновая музыка", | |
| 2160 | + "Search": "Поиск", | |
| 2161 | + "extension_install_1": "Чтобы загружать расширения из этого списка, у вас должен быть установлен ", | |
| 2162 | + "extension_install_2": ".", | |
| 2163 | + "extension_install_3": "Нажмите на иконку ", | |
| 2164 | + "extension_install_4": ", чтобы перейти в репозиторий расширения и получить более подробную информацию о нём.", | |
| 2165 | + "Extension repo/guide:": "Репозиторий расширения:", | |
| 2166 | + "Preview in browser": "Предпросмотр", | |
| 2167 | + "Adds a function tool": "Частично или полностью работает через вызов функций", | |
| 2168 | + "Tool": "Функции", | |
| 2169 | + "Move extension": "Переместить расширение", | |
| 2170 | + "ext_type_local": "Это локальное расширение, доступно только вам", | |
| 2171 | + "ext_type_global": "Это глобальное расширение, доступно всем пользователям", | |
| 2172 | + "Move": "Переместить", | |
| 2173 | + "Enter the Git URL of the extension to install": "Введите Git-адрес расширения", | |
| 2174 | + "Please be aware that using external extensions can have unintended side effects and may pose security risks. Always make sure you trust the source before importing an extension. We are not responsible for any damage caused by third-party extensions.": "помните, что используя расширения от сторонних авторов, вы можете подвергать систему опасности. Устанавливайте расширения только от проверенных разработчиков. Мы не несём ответственности за любой ущерб, причинённый сторонними расширениями.", | |
| 2175 | + "Disclaimer:": "Внимание:", | |
| 2176 | + "Example:": "Пример:", | |
| 2177 | + "context_derived": "Считывать из метаданных модели (по возможности)", | |
| 2178 | + "instruct_derived": "Считывать из метаданных модели (по возможности)", | |
| 2179 | + "Confirm token parsing with": "Чтобы убедиться в правильности выделения токенов, используйте", | |
| 2180 | + "Reasoning Effort": "Рассуждения", | |
| 2181 | + "Constrains effort on reasoning for reasoning models.": "Регулирует объём внутренних рассуждений модели (reasoning), для моделей которые поддерживают эту возможность.\nНа данный момент поддерживаются три значения: Подробные, Обычные, Поверхностные.\nПри менее подробном рассуждении ответ получается быстрее, а также экономятся токены, уходящие на рассуждения.", | |
| 2182 | + "openai_reasoning_effort_low": "Поверхностные", | |
| 2183 | + "openai_reasoning_effort_medium": "Обычные", | |
| 2184 | + "openai_reasoning_effort_high": "Подробные", | |
| 2185 | + "Persona Lore Alt+Click to open the lorebook": "Лорбук данной персоны\nAlt + ЛКМ чтобы открыть лорбук", | |
| 2186 | + "Persona Lorebook for": "Лорбук для персоны", | |
| 2187 | + "persona_world_template_txt": "Выбранная Информация о мире будет привязана к этой персоне. Информация будет добавляться в каждом промпте вместе с глобальным лорбуком и лорбуками персонажа и чата.", | |
| 2188 | + "Global list": "Глобальный список", | |
| 2189 | + "Preset-specific list": "Список для данного пресета", | |
| 2190 | + "Banned tokens/strings are being sent in the request.": "Запрещённые токены и строки отсылаются в запросе.", | |
| 2191 | + "Banned tokens/strings are NOT being sent in the request.": "Запрещённые токены и строки НЕ отсылаются в запросе.", | |
| 2192 | + "Add a reasoning block": "Добавить блок рассуждений", | |
| 2193 | + "Create a copy of this message?": "Продублировать это сообщение?", | |
| 2194 | + "Max Recursion Steps": "Макс. глубина рекурсии", | |
| 2195 | + "0 = unlimited, 1 = scans once and doesn't recurse, 2 = scans once and recurses once, etc": "0 = неограничено, 1 = сканировать единожды, 2 = сканировать единожды и сделать один повторный проход, и т.д.\n(неактивно при указанном мин. числе активаций)", | |
| 2196 | + "(disabled when max recursion steps are used)": "(неактивно при указанной макс. глубине рекурсии)", | |
| 2197 | + "Enter a valid API URL": "Введите корректный адрес API", | |
| 2198 | + "No Ollama model selected.": "Не выбрана модель Ollama", | |
| 2199 | + "Background Fitting": "Способ подгонки фона под разрешение", | |
| 2200 | + "Chat Lore Alt+Click to open the lorebook": "Лорбук данного чата\nAlt + ЛКМ чтобы открыть лорбук", | |
| 2201 | + "Token Counter": "Подсчитать токены", | |
| 2202 | + "Type / paste in the box below to see the number of tokens in the text.": "Введите или вставьте текст в окошко ниже, чтобы подсчитать количество токенов в нём.", | |
| 2203 | + "Selected tokenizer:": "Выбранный токенайзер:", | |
| 2204 | + "Input:": "Входные данные:", | |
| 2205 | + "Tokenized text:": "Токенизированный текст:", | |
| 2206 | + "Token IDs:": "Идентификаторы токенов:", | |
| 2207 | + "Tokens:": "Токенов:" | |
| 2148 | 2208 | } |
| @@ -482,7 +482,7 @@ | ||
| 482 | 482 | "separate with commas w/o space between": "розділяйте комами без пропусків між ними", |
| 483 | 483 | "Custom Stopping Strings": "Власні рядки зупинки", |
| 484 | 484 | "JSON serialized array of strings": "JSON-серіалізований масив рядків", |
| 485 | 485 | "Replace Macro in Custom StoppingStop Strings": "Замінювати макроси у власних рядках зупинки", |
| 486 | 486 | "Auto-Continue": "Автоматичне продовження", |
| 487 | 487 | "Allow for Chat Completion APIs": "Дозволити для Chat Completion API", |
| 488 | 488 | "Target length (tokens)": "Цільова довжина (токени)", |
| @@ -709,7 +709,7 @@ | ||
| 709 | 709 | "Auto-swipe": "Автоматичний змах", |
| 710 | 710 | "Enable the auto-swipe function. Settings in this section only have an effect when auto-swipe is enabled": "Вмикає функцію автоматичного змаху. Налаштування в цьому розділі діють лише тоді, коли увімкнено автоматичний змах", |
| 711 | 711 | "Minimum generated message length": "Мінімальна довжина згенерованого повідомлення", |
| 712 | 712 | "If the generated message is shorter than thisthese many characters, trigger an auto-swipe": "Якщо згенероване повідомлення коротше за це, викликайте автоматичний змаху", |
| 713 | 713 | "Blacklisted words": "Список заборонених слів", |
| 714 | 714 | "words you dont want generated separated by comma ','": "слова, які ви не хочете генерувати, розділені комою ','", |
| 715 | 715 | "Blacklisted word count to swipe": "Кількість заборонених слів для змаху", |
| @@ -482,7 +482,7 @@ | ||
| 482 | 482 | "separate with commas w/o space between": "phân tách bằng dấu phẩy không có khoảng trắng giữa", |
| 483 | 483 | "Custom Stopping Strings": "Chuỗi dừng tùy chỉnh", |
| 484 | 484 | "JSON serialized array of strings": "Mảng chuỗi được tuần tự hóa JSON", |
| 485 | 485 | "Replace Macro in Custom StoppingStop Strings": "Thay thế Macro trong Chuỗi Dừng Tùy chỉnh", |
| 486 | 486 | "Auto-Continue": "Tự động Tiếp tục", |
| 487 | 487 | "Allow for Chat Completion APIs": "Cho phép các API hoàn thành Trò chuyện", |
| 488 | 488 | "Target length (tokens)": "Độ dài mục tiêu (token)", |
| @@ -709,7 +709,7 @@ | ||
| 709 | 709 | "Auto-swipe": "Tự động vuốt", |
| 710 | 710 | "Enable the auto-swipe function. Settings in this section only have an effect when auto-swipe is enabled": "Bật chức năng tự động vuốt. Các cài đặt trong phần này chỉ có tác dụng khi tự động vuốt được bật", |
| 711 | 711 | "Minimum generated message length": "Độ dài tối thiểu của tin nhắn được tạo", |
| 712 | 712 | "If the generated message is shorter than thisthese many characters, trigger an auto-swipe": "Nếu tin nhắn được tạo ra ngắn hơn điều này, kích hoạt tự động vuốt", |
| 713 | 713 | "Blacklisted words": "Từ trong danh sách đen", |
| 714 | 714 | "words you dont want generated separated by comma ','": "các từ bạn không muốn được tạo ra được phân tách bằng dấu phẩy ','", |
| 715 | 715 | "Blacklisted word count to swipe": "Số từ trong danh sách đen để vuốt", |
| @@ -215,7 +215,7 @@ | ||
| 215 | 215 | "Classifier Free Guidance. More helpful tip coming soon": "无分类器指导(CFG)。更多有用的提示敬请期待。", |
| 216 | 216 | "Scale": "缩放比例", |
| 217 | 217 | "Negative Prompt": "负面提示词", |
| 218 | 218 | "Used if CFG Scale is unset globally, per chat or character": "如果无分类器指导(CFG)缩放比例未在全局设置如果CFG缩放比例未被全局设置,它将作用于每个聊天或每个角色它将作用于所有聊天或角色", |
| 219 | 219 | "Add text here that would make the AI generate things you don't want in your outputs.": "请在此处添加文本,以避免生成您不希望出现在输出中的内容。", |
| 220 | 220 | "Grammar String": "语法字符串", |
| 221 | 221 | "GBNF or EBNF, depends on the backend in use. If you're using this you should know which.": "GBNF 或 EBNF,取决于使用的后端。如果您使用这个,您应该知道该用哪一个。", |
| @@ -266,8 +266,8 @@ | ||
| 266 | 266 | "Use system prompt": "使用系统提示词", |
| 267 | 267 | "Merges_all_system_messages_desc_1": "合并所有系统消息,直到第一条具有非系统角色的消息,然后通过", |
| 268 | 268 | "Merges_all_system_messages_desc_2": "字段发送。", |
| 269 | 269 | "ShowRequest model reasoning": "展示思维链请求思维链", |
| 270 | 270 | "DisplayAllows the model's internalto thoughtsreturn inits thethinking responseprocess.": "展示模型在回复时的内部思维链允许模型返回其思维过程。", |
| 271 | 271 | "Assistant Prefill": "AI预填", |
| 272 | 272 | "Expand the editor": "展开编辑器", |
| 273 | 273 | "Start Claude's answer with...": "以如下内容开始Claude的回答...", |
| @@ -559,7 +559,7 @@ | ||
| 559 | 559 | "Prompt Content": "提示词内容", |
| 560 | 560 | "Custom Stopping Strings": "自定义停止字符串", |
| 561 | 561 | "JSON serialized array of strings": "JSON序列化的字符串数组", |
| 562 | 562 | "Replace Macro in Custom StoppingStop Strings": "替换自定义停止字符串中的宏", |
| 563 | 563 | "Token Padding": "词符填充", |
| 564 | 564 | "Miscellaneous": "杂项", |
| 565 | 565 | "Non-markdown strings": "非 Markdown 字符串", |
| @@ -584,7 +584,7 @@ | ||
| 584 | 584 | "(0 = unlimited, use budget)": "(“0”为无限制,使用预算)", |
| 585 | 585 | "Cap the number of entry activation recursions": "限制条目激活递归的次数", |
| 586 | 586 | "Max Recursion Steps": "最大递归深度", |
| 587 | 587 | "0 = unlimited, 1 = scans once and doesn't recurse, 2 = scans once and recurses once, etc\\n(disabled when min activations are used)": "“0”为无限制,“1”为扫描一次且不递归,“2”为扫描一次且递归一次,依此类推\n(当使用最小激活次数时,此功能被禁用)", |
| 588 | 588 | "Insertion Strategy": "插入策略", |
| 589 | 589 | "Sorted Evenly": "均匀排序", |
| 590 | 590 | "Character Lore First": "角色世界书优先", |
| @@ -804,7 +804,7 @@ | ||
| 804 | 804 | "Auto-swipe": "自动滑动", |
| 805 | 805 | "Enable the auto-swipe function. Settings in this section only have an effect when auto-swipe is enabled": "启用自动滑动功能。仅当启用自动滑动时,本节中的设置才会生效", |
| 806 | 806 | "Minimum generated message length": "生成的消息的最小长度", |
| 807 | 807 | "If the generated message is shorter than thisthese many characters, trigger an auto-swipe": "如果生成的消息短于此长度,则触发自动滑动", |
| 808 | 808 | "Blacklisted words": "屏蔽词", |
| 809 | 809 | "words you dont want generated separated by comma ','": "不想生成的词语,用半角逗号“,”分隔", |
| 810 | 810 | "Blacklisted word count to swipe": "触发滑动的黑名单词语数量", |
| @@ -1208,7 +1208,7 @@ | ||
| 1208 | 1208 | "View contents": "查看内容", |
| 1209 | 1209 | "Remove the file": "删除文件", |
| 1210 | 1210 | "Author's Note": "作者注释", |
| 1211 | 1211 | "Unique to this chat": "此聊天独有仅对此聊天生效", |
| 1212 | 1212 | "Checkpoints inherit the Note from their parent, and can be changed individually after that.": "检查点从其父级继承注释,之后可以单独更改。", |
| 1213 | 1213 | "Include in World Info Scanning": "纳入世界信息扫描", |
| 1214 | 1214 | "Before Main Prompt / Story String": "主提示词/故事线之前", |
| @@ -1224,13 +1224,13 @@ | ||
| 1224 | 1224 | "Replace Author's Note": "替换作者注", |
| 1225 | 1225 | "Default Author's Note": "默认作者注", |
| 1226 | 1226 | "Will be automatically added as the Author's Note for all new chats.": "将自动添加为所有新聊天的作者注释。", |
| 1227 | 1227 | "Chat CFG": "聊天CFG本聊天的CFG缩放", |
| 1228 | 1228 | "1 = disabled": "“1”为已禁用为禁用", |
| 1229 | 1229 | "write short replies, write replies using past tense": "写简短的回复,用过去时写回复", |
| 1230 | 1230 | "Positive Prompt": "正面提示词", |
| 1231 | 1231 | "Use character CFG scales": "单独为各个角色设置CFG缩放", |
| 1232 | 1232 | "Character CFG": "角色CFG配置", |
| 1233 | 1233 | "Will be automatically added as the CFG for this character.": "将自动添加为该角色的 CFG将自动添加到该角色的CFG设置中。", |
| 1234 | 1234 | "Global CFG": "全局CFG", |
| 1235 | 1235 | "Will be used as the default CFG options for every chat unless overridden.": "除非被覆盖,否则将用作每次聊天的默认 CFG 选项。", |
| 1236 | 1236 | "CFG Prompt Cascading": "CFG 提示词级联", |
| @@ -1349,7 +1349,6 @@ | ||
| 1349 | 1349 | "Character Expressions": "角色表情", |
| 1350 | 1350 | "Translate text to English before classification": "分类之前将文本翻译成英文", |
| 1351 | 1351 | "Show default images (emojis) if sprite missing": "如果表情包缺失,则显示默认图像(表情符号)", |
| 1352 | - "Image Type - talkinghead (extras)": "图像类型 - 说话头像(附加内容)", | |
| 1353 | 1352 | "Classifier API": "分类器 API", |
| 1354 | 1353 | "Select the API for classifying expressions.": "选择用于对表达式进行分类的API。", |
| 1355 | 1354 | "Main API": "主要 API", |
| @@ -1486,7 +1485,7 @@ | ||
| 1486 | 1485 | "ext_regex_replace_string_placeholder": "使用 {{match}} 包含来自“查找正则表达式”或“$1”、“$2”等的匹配文本作为捕获组。", |
| 1487 | 1486 | "Trim Out": "修剪掉", |
| 1488 | 1487 | "ext_regex_trim_placeholder": "在替换之前全局修剪正则表达式匹配中任何不需要的部分。用回车键分隔每个元素。", |
| 1489 | 1488 | "ext_regex_affects": "影响作用范围", |
| 1490 | 1489 | "ext_regex_user_input_desc": "用户发送的消息", |
| 1491 | 1490 | "ext_regex_user_input": "用户输入", |
| 1492 | 1491 | "ext_regex_ai_input_desc": "从生成式API中获取的信息。", |
| @@ -1720,9 +1719,9 @@ | ||
| 1720 | 1719 | "Chat Lorebook for": "聊天知识书", |
| 1721 | 1720 | "chat_world_template_txt": "选定的世界信息将绑定到此聊天。生成 AI 回复时,\n它将与全球和角色传说书中的条目相结合。", |
| 1722 | 1721 | "chat_rename_1": "输入聊天的新名称:", |
| 1723 | 1722 | "chat_rename_2": "注意!!使用已有文件名会导致错误与其他文件重名会导致错误!!", |
| 1724 | 1723 | "chat_rename_3": "此举会将次聊天与标记为此举会将此聊天与标记为“检查点”的聊天解绑。", |
| 1725 | 1724 | "chat_rename_4": "(不需要在结尾添加 '.JSONL' 后缀)", |
| 1726 | 1725 | "Enter Checkpoint Name:": "输入检查点名称:", |
| 1727 | 1726 | "(Leave empty to auto-generate)": "(留空以自动生成)", |
| 1728 | 1727 | "The currently existing checkpoint will be unlinked and replaced with the new checkpoint, but can still be found in the Chat Management.": "当前检查点将会被解绑并替换为新的检查点,但仍可在聊天管理中找到。", |
| @@ -1975,7 +1974,7 @@ | ||
| 1975 | 1974 | "Enter your password below to confirm:": "输入您的密码以确认:", |
| 1976 | 1975 | "Chat Scenario Override": "聊天场景覆盖", |
| 1977 | 1976 | "Remove": "移除", |
| 1978 | 1977 | "Unique to this chat.": "Unique to this chat.仅对此聊天生效。", |
| 1979 | 1978 | "All group members will use the following scenario text instead of what is specified in their character cards.": "All group members will use the following scenario text instead of what is specified in their character cards.", |
| 1980 | 1979 | "The following scenario text will be used instead of the value set in the character card.": "The following scenario text will be used instead of the value set in the character card.", |
| 1981 | 1980 | "Checkpoints inherit the scenario override from their parent, and can be changed individually after that.": "Checkpoints inherit the scenario override from their parent, and can be changed individually after that.", |
| @@ -483,7 +483,7 @@ | ||
| 483 | 483 | "separate with commas w/o space between": "用逗號分隔,之間無空格", |
| 484 | 484 | "Custom Stopping Strings": "自訂停止字串", |
| 485 | 485 | "JSON serialized array of strings": "JSON 序列化字串數組", |
| 486 | 486 | "Replace Macro in Custom StoppingStop Strings": "取代自訂停止字串中的巨集", |
| 487 | 487 | "Auto-Continue": "自動繼續", |
| 488 | 488 | "Allow for Chat Completion APIs": "允許聊天補全 API", |
| 489 | 489 | "Target length (tokens)": "目標長度(符元)", |
| @@ -710,7 +710,7 @@ | ||
| 710 | 710 | "Auto-swipe": "自動滑動", |
| 711 | 711 | "Enable the auto-swipe function. Settings in this section only have an effect when auto-swipe is enabled": "啟用自動滑動功能。此部分的設定僅在啟用自動滑動時有效。", |
| 712 | 712 | "Minimum generated message length": "生成訊息的最小長度", |
| 713 | 713 | "If the generated message is shorter than thisthese many characters, trigger an auto-swipe": "如果生成的訊息比這個短,將觸發自動滑動。", |
| 714 | 714 | "Blacklisted words": "黑名單詞語", |
| 715 | 715 | "words you dont want generated separated by comma ','": "您不想生成的文字,使用逗號分隔", |
| 716 | 716 | "Blacklisted word count to swipe": "滑動的黑名單詞語數量", |
| @@ -1458,7 +1458,7 @@ | ||
| 1458 | 1458 | "Example: http://localhost:1234/v1": "例如:http://localhost:1234/v1", |
| 1459 | 1459 | "popup-button-crop": "裁剪", |
| 1460 | 1460 | "(disabled when max recursion steps are used)": "(當最大遞歸步驟數使用時將停用)", |
| 1461 | 1461 | "0 = unlimited, 1 = scans once and doesn't recurse, 2 = scans once and recurses once, etc\n(disabled when min activations are used)": "0 = 無限制,1 = 掃描一次且不遞歸,2 = 掃描一次並遞歸一次,以此類推\n(使用最小啟動設定時將停用)", |
| 1462 | 1462 | "A greedy, brute-force algorithm used in LLM sampling to find the most likely sequence of words or tokens. It expands multiple candidate sequences at once, maintaining a fixed number (beam width) of top sequences at each step.": "一種用於 LLM 抽樣的貪婪演算法,用於尋找最可能的單詞或標記序列。該方法會同時展開多個候選序列,並在每一步中保持固定數量的頂級序列(beam width)。", |
| 1463 | 1463 | "A multiplicative factor to expand the overall area that the nodes take up.": "節點佔用該擴充功能區域的倍數。", |
| 1464 | 1464 | "Abort current image generation task": "終止目前的圖片生成任務", |
| @@ -1653,7 +1653,6 @@ | ||
| 1653 | 1653 | "HuggingFace Token": "HuggingFace 符元", |
| 1654 | 1654 | "Image Captioning": "圖片註解", |
| 1655 | 1655 | "Generate Caption": "產生圖片註解", |
| 1656 | - "Image Type - talkinghead (extras)": "圖片類型 - talkinghead(額外選項)", | |
| 1657 | 1656 | "Injection Position": "插入位置", |
| 1658 | 1657 | "Injection position. Relative (to other prompts in prompt manager) or In-chat @ Depth.": "插入位置(與提示詞管理器中的其他提示相比)或聊天中的深度位置。", |
| 1659 | 1658 | "Injection Template": "插入範本", |
| @@ -1806,7 +1805,7 @@ | ||
| 1806 | 1805 | "context_derived": "若可能,根據模型元數據推導。", |
| 1807 | 1806 | "instruct_derived": "若可能,根據模型元數據推導。", |
| 1808 | 1807 | "Inserted before the first User's message.": "插入於第一則使用者訊息之前。", |
| 1809 | 1808 | "0 = unlimited, 1 = scans once and doesn't recurse, 2 = scans once and recurses once, etc\\n(disabled when min activations are used)": "0 = 無限制,1 = 掃描一次不遞歸,2 = 掃描一次後遞歸一次 ⋯以此類推\n(啟用最小啟動次數時無效)", |
| 1810 | 1809 | "Quick 'Impersonate' button": "快速「AI 扮演使用者」按鈕", |
| 1811 | 1810 | "Manual": "手動", |
| 1812 | 1811 | "Any contents here will replace the default Post-History Instructions used for this character. (v2 spec: post_history_instructions)": "此處填入的內容將取代該角色的默認聊天歷史後指示(Post-History Instructions)。\n(v2 格式:specpost_history_instructions)", |
| @@ -2357,8 +2356,8 @@ | ||
| 2357 | 2356 | "Forbid": "禁止", |
| 2358 | 2357 | "Aphrodite only. Determines the order of samplers. Skew is always applied post-softmax, so it's not included here.": "僅限 Aphrodite 使用。決定採樣器的順序。偏移總是在 softmax 後應用,因此不包括在此。", |
| 2359 | 2358 | "Aphrodite only. Determines the order of samplers.": "僅限 Aphrodite 使用。決定採樣器的順序。", |
| 2360 | 2359 | "ShowRequest model reasoning": "顯示模型思維鏈請求模型思維鏈", |
| 2361 | 2360 | "DisplayAllows the model's internalto thoughtsreturn inits thethinking responseprocess.": "在回應中顯示模型的思維鏈(內部思考過程)讓模型回傳其思考過程。", |
| 2362 | 2361 | "Generic (OpenAI-compatible) [LM Studio, LiteLLM, etc.]": "通用(兼容 OpenAI)[LM Studio, LiteLLM 等]", |
| 2363 | 2362 | "Model ID (optional)": "模型 ID(可選)", |
| 2364 | 2363 | "DeepSeek API Key": "DeepSeek API 金鑰", |
| @@ -95,6 +95,7 @@ import { | ||
| 95 | 95 | resetMovableStyles, |
| 96 | 96 | forceCharacterEditorTokenize, |
| 97 | 97 | applyPowerUserSettings, |
| 98 | + generatedTextFiltered, | |
| 98 | 99 | } from './scripts/power-user.js'; |
| 99 | 100 | |
| 100 | 101 | import { |
| @@ -169,6 +170,7 @@ import { | ||
| 169 | 170 | toggleDrawer, |
| 170 | 171 | isElementInViewport, |
| 171 | 172 | copyText, |
| 173 | + escapeHtml, | |
| 172 | 174 | } from './scripts/utils.js'; |
| 173 | 175 | import { debounce_timeout } from './scripts/constants.js'; |
| 174 | 176 | |
| @@ -272,7 +274,8 @@ import { initSettingsSearch } from './scripts/setting-search.js'; | ||
| 272 | 274 | import { initBulkEdit } from './scripts/bulk-edit.js'; |
| 273 | 275 | import { deriveTemplatesFromChatTemplate } from './scripts/chat-templates.js'; |
| 274 | 276 | import { getContext } from './scripts/st-context.js'; |
| 275 | 277 | import { extractReasoningFromData, initReasoning, PromptReasoning, ReasoningHandler, removeReasoningFromString, updateReasoningUI } from './scripts/reasoning.js'; |
| 278 | +import { accountStorage } from './scripts/util/AccountStorage.js'; | |
| 276 | 279 | |
| 277 | 280 | // API OBJECT FOR EXTERNAL WIRING |
| 278 | 281 | globalThis.SillyTavern = { |
| @@ -368,6 +371,10 @@ DOMPurify.addHook('uponSanitizeElement', (node, _, config) => { | ||
| 368 | 371 | return; |
| 369 | 372 | } |
| 370 | 373 | |
| 374 | + if (!(node instanceof Element)) { | |
| 375 | + return; | |
| 376 | + } | |
| 377 | + | |
| 371 | 378 | let mediaBlocked = false; |
| 372 | 379 | |
| 373 | 380 | switch (node.tagName) { |
| @@ -422,7 +429,7 @@ DOMPurify.addHook('uponSanitizeElement', (node, _, config) => { | ||
| 422 | 429 | const entityId = getCurrentEntityId(); |
| 423 | 430 | const warningShownKey = `mediaWarningShown:${entityId}`; |
| 424 | 431 | |
| 425 | 432 | if (localStorageaccountStorage.getItem(warningShownKey) === null) { |
| 426 | 433 | const warningToast = toastr.warning( |
| 427 | 434 | t`Use the 'Ext. Media' button to allow it. Click on this message to dismiss.`, |
| 428 | 435 | t`External media has been blocked`, |
| @@ -433,7 +440,7 @@ DOMPurify.addHook('uponSanitizeElement', (node, _, config) => { | ||
| 433 | 440 | }, |
| 434 | 441 | ); |
| 435 | 442 | |
| 436 | 443 | localStorageaccountStorage.setItem(warningShownKey, 'true'); |
| 437 | 444 | } |
| 438 | 445 | } |
| 439 | 446 | }); |
| @@ -495,9 +502,11 @@ export const event_types = { | ||
| 495 | 502 | // TODO: Naming convention is inconsistent with other events |
| 496 | 503 | CHARACTER_DELETED: 'characterDeleted', |
| 497 | 504 | CHARACTER_DUPLICATED: 'character_duplicated', |
| 505 | + CHARACTER_RENAMED: 'character_renamed', | |
| 498 | 506 | /** @deprecated The event is aliased to STREAM_TOKEN_RECEIVED. */ |
| 499 | 507 | SMOOTH_STREAM_TOKEN_RECEIVED: 'stream_token_received', |
| 500 | 508 | STREAM_TOKEN_RECEIVED: 'stream_token_received', |
| 509 | + STREAM_REASONING_DONE: 'stream_reasoning_done', | |
| 501 | 510 | FILE_ATTACHMENT_DELETED: 'file_attachment_deleted', |
| 502 | 511 | WORLDINFO_FORCE_ACTIVATE: 'worldinfo_force_activate', |
| 503 | 512 | OPEN_CHARACTER_LIBRARY: 'open_character_library', |
| @@ -508,7 +517,7 @@ export const event_types = { | ||
| 508 | 517 | TOOL_CALLS_RENDERED: 'tool_calls_rendered', |
| 509 | 518 | }; |
| 510 | 519 | |
| 511 | 520 | export const eventSource = new EventEmitter([event_types.APP_READY]); |
| 512 | 521 | |
| 513 | 522 | eventSource.on(event_types.CHAT_CHANGED, processChatSlashCommands); |
| 514 | 523 | |
| @@ -1028,12 +1037,22 @@ export function setAnimationDuration(ms = null) { | ||
| 1028 | 1037 | document.documentElement.style.setProperty('--animation-duration', `${animation_duration}ms`); |
| 1029 | 1038 | } |
| 1030 | 1039 | |
| 1040 | +/** | |
| 1041 | + * Sets the currently active character | |
| 1042 | + * @param {object|number|string} [entityOrKey] - An entity with id property (character, group, tag), or directly an id or tag key. If not provided, the active character is reset to `null`. | |
| 1043 | + */ | |
| 1031 | 1044 | export function setActiveCharacter(entityOrKey) { |
| 1032 | 1045 | active_character = entityOrKey ? getTagKeyForEntity(entityOrKey) : null; |
| 1046 | + if (active_character) active_group = null; | |
| 1033 | 1047 | } |
| 1034 | 1048 | |
| 1049 | +/** | |
| 1050 | + * Sets the currently active group. | |
| 1051 | + * @param {object|number|string} [entityOrKey] - An entity with id property (character, group, tag), or directly an id or tag key. If not provided, the active group is reset to `null`. | |
| 1052 | + */ | |
| 1035 | 1053 | export function setActiveGroup(entityOrKey) { |
| 1036 | 1054 | active_group = entityOrKey ? getTagKeyForEntity(entityOrKey) : null; |
| 1055 | + if (active_group) active_character = null; | |
| 1037 | 1056 | } |
| 1038 | 1057 | |
| 1039 | 1058 | /** |
| @@ -1500,7 +1519,7 @@ export async function printCharacters(fullRefresh = false) { | ||
| 1500 | 1519 | |
| 1501 | 1520 | $('#rm_print_characters_pagination').pagination({ |
| 1502 | 1521 | dataSource: entities, |
| 1503 | 1522 | pageSize: Number(localStorageaccountStorage.getItem(storageKey)) || per_page_default, |
| 1504 | 1523 | sizeChangerOptions: [10, 25, 50, 100, 250, 500, 1000], |
| 1505 | 1524 | pageRange: 1, |
| 1506 | 1525 | pageNumber: saveCharactersPage || 1, |
| @@ -1544,7 +1563,7 @@ export async function printCharacters(fullRefresh = false) { | ||
| 1544 | 1563 | eventSource.emit(event_types.CHARACTER_PAGE_LOADED); |
| 1545 | 1564 | }, |
| 1546 | 1565 | afterSizeSelectorChange: function (e) { |
| 1547 | 1566 | localStorageaccountStorage.setItem(storageKey, e.target.value); |
| 1548 | 1567 | }, |
| 1549 | 1568 | afterPaging: function (e) { |
| 1550 | 1569 | saveCharactersPage = e; |
| @@ -2007,14 +2026,15 @@ export async function sendTextareaMessage() { | ||
| 2007 | 2026 | * @param {boolean} isUser If the message was sent by the user |
| 2008 | 2027 | * @param {number} messageId Message index in chat array |
| 2009 | 2028 | * @param {object} [sanitizerOverrides] DOMPurify sanitizer option overrides |
| 2029 | + * @param {boolean} [isReasoning] If the message is reasoning output | |
| 2010 | 2030 | * @returns {string} HTML string |
| 2011 | 2031 | */ |
| 2012 | 2032 | export function messageFormatting(mes, ch_name, isSystem, isUser, messageId, sanitizerOverrides = {}, isReasoning = false) { |
| 2013 | 2033 | if (!mes) { |
| 2014 | 2034 | return ''; |
| 2015 | 2035 | } |
| 2016 | 2036 | |
| 2017 | 2037 | if (Number(messageId) === 0 && !isSystem && !isUser && !isReasoning) { |
| 2018 | 2038 | const mesBeforeReplace = mes; |
| 2019 | 2039 | const chatMessage = chat[messageId]; |
| 2020 | 2040 | mes = substituteParams(mes, undefined, ch_name); |
| @@ -2043,6 +2063,9 @@ export function messageFormatting(mes, ch_name, isSystem, isUser, messageId, san | ||
| 2043 | 2063 | if (!isSystem) { |
| 2044 | 2064 | function getRegexPlacement() { |
| 2045 | 2065 | try { |
| 2066 | + if (isReasoning) { | |
| 2067 | + return regex_placement.REASONING; | |
| 2068 | + } | |
| 2046 | 2069 | if (isUser) { |
| 2047 | 2070 | return regex_placement.USER_INPUT; |
| 2048 | 2071 | } else if (chat[messageId]?.extra?.type === 'narrator') { |
| @@ -2076,6 +2099,17 @@ export function messageFormatting(mes, ch_name, isSystem, isUser, messageId, san | ||
| 2076 | 2099 | mes = mes.replaceAll('<', '<').replaceAll('>', '>'); |
| 2077 | 2100 | } |
| 2078 | 2101 | |
| 2102 | + // Make sure reasoning strings are always shown, even if they include "<" or ">" | |
| 2103 | + [power_user.reasoning.prefix, power_user.reasoning.suffix].forEach((reasoningString) => { | |
| 2104 | + if (!reasoningString || !reasoningString.trim().length) { | |
| 2105 | + return; | |
| 2106 | + } | |
| 2107 | + // Only replace the first occurrence of the reasoning string | |
| 2108 | + if (mes.includes(reasoningString)) { | |
| 2109 | + mes = mes.replace(reasoningString, escapeHtml(reasoningString)); | |
| 2110 | + } | |
| 2111 | + }); | |
| 2112 | + | |
| 2079 | 2113 | if (!isSystem) { |
| 2080 | 2114 | // Save double quotes in tags as a special character to prevent them from being encoded |
| 2081 | 2115 | if (!power_user.encode_tags) { |
| @@ -2186,26 +2220,29 @@ function insertSVGIcon(mes, extra) { | ||
| 2186 | 2220 | modelName = extra.api; |
| 2187 | 2221 | } |
| 2188 | 2222 | |
| 2189 | - const image = new Image(); | |
| 2223 | + const insertOrReplaceSVG = (image, className, targetSelector, insertBefore) => { | |
| 2190 | - // Add classes for styling and identification | |
| 2191 | - image.classList.add('icon-svg', 'timestamp-icon'); | |
| 2192 | - image.src = `/img/${modelName}.svg`; | |
| 2193 | - image.title = `${extra?.api ? extra.api + ' - ' : ''}${extra?.model ?? ''}`; | |
| 2194 | - | |
| 2195 | 2224 | image.onload = async function () { |
| 2196 | - // Check if an SVG already exists adjacent to the timestamp | |
| 2225 | + let existingSVG = insertBefore ? mes.find(targetSelector).prev(`.${className}`) : mes.find(targetSelector).next(`.${className}`); | |
| 2197 | - let existingSVG = mes.find('.timestamp').next('.timestamp-icon'); | |
| 2198 | - | |
| 2199 | 2226 | if (existingSVG.length) { |
| 2200 | - // Replace existing SVG | |
| 2201 | 2227 | existingSVG.replaceWith(image); |
| 2202 | 2228 | } else { |
| 2203 | - // Append the new SVG if none exists | |
| 2229 | + if (insertBefore) mes.find(targetSelector).before(image); | |
| 2204 | 2230 | else mes.find('.timestamp'targetSelector).after(image); |
| 2205 | 2231 | } |
| 2206 | - | |
| 2207 | 2232 | await SVGInject(image); |
| 2208 | 2233 | }; |
| 2234 | + }; | |
| 2235 | + | |
| 2236 | + const createModelImage = (className, targetSelector, insertBefore) => { | |
| 2237 | + const image = new Image(); | |
| 2238 | + image.classList.add('icon-svg', className); | |
| 2239 | + image.src = `/img/${modelName}.svg`; | |
| 2240 | + image.title = `${extra?.api ? extra.api + ' - ' : ''}${extra?.model ?? ''}`; | |
| 2241 | + insertOrReplaceSVG(image, className, targetSelector, insertBefore); | |
| 2242 | + }; | |
| 2243 | + | |
| 2244 | + createModelImage('timestamp-icon', '.timestamp'); | |
| 2245 | + createModelImage('thinking-icon', '.mes_reasoning_header_title', true); | |
| 2209 | 2246 | } |
| 2210 | 2247 | |
| 2211 | 2248 | |
| @@ -2216,7 +2253,6 @@ function getMessageFromTemplate({ | ||
| 2216 | 2253 | isUser, |
| 2217 | 2254 | avatarImg, |
| 2218 | 2255 | bias, |
| 2219 | - reasoning, | |
| 2220 | 2256 | isSystem, |
| 2221 | 2257 | title, |
| 2222 | 2258 | timerValue, |
| @@ -2241,7 +2277,6 @@ function getMessageFromTemplate({ | ||
| 2241 | 2277 | mes.find('.avatar img').attr('src', avatarImg); |
| 2242 | 2278 | mes.find('.ch_name .name_text').text(characterName); |
| 2243 | 2279 | mes.find('.mes_bias').html(bias); |
| 2244 | - mes.find('.mes_reasoning').html(reasoning); | |
| 2245 | 2280 | mes.find('.timestamp').text(timestamp).attr('title', `${extra?.api ? extra.api + ' - ' : ''}${extra?.model ?? ''}`); |
| 2246 | 2281 | mes.find('.mesIDDisplay').text(`#${mesId}`); |
| 2247 | 2282 | tokenCount && mes.find('.tokenCounterDisplay').text(`${tokenCount}t`); |
| @@ -2249,6 +2284,8 @@ function getMessageFromTemplate({ | ||
| 2249 | 2284 | timerValue && mes.find('.mes_timer').attr('title', timerTitle).text(timerValue); |
| 2250 | 2285 | bookmarkLink && updateBookmarkDisplay(mes); |
| 2251 | 2286 | |
| 2287 | + updateReasoningUI(mes); | |
| 2288 | + | |
| 2252 | 2289 | if (power_user.timestamp_model_icon && extra?.api) { |
| 2253 | 2290 | insertSVGIcon(mes, extra); |
| 2254 | 2291 | } |
| @@ -2260,12 +2297,18 @@ function getMessageFromTemplate({ | ||
| 2260 | 2297 | * Re-renders a message block with updated content. |
| 2261 | 2298 | * @param {number} messageId Message ID |
| 2262 | 2299 | * @param {object} message Message object |
| 2300 | + * @param {object} [options={}] Optional arguments | |
| 2301 | + * @param {boolean} [options.rerenderMessage=true] Whether to re-render the message content (inside <c>.mes_text</c>) | |
| 2263 | 2302 | */ |
| 2264 | 2303 | export function updateMessageBlock(messageId, message, { rerenderMessage = true } = {}) { |
| 2265 | 2304 | const messageElement = $(`#chat [mesid="${messageId}"]`); |
| 2305 | + if (rerenderMessage) { | |
| 2266 | 2306 | const text = message?.extra?.display_text ?? message.mes; |
| 2267 | 2307 | messageElement.find('.mes_text').html(messageFormatting(text, message.name, message.is_system, message.is_user, messageId, {}, false)); |
| 2268 | - messageElement.find('.mes_reasoning').html(messageFormatting(message.extra?.reasoning ?? '', '', false, false, -1)); | |
| 2308 | + } | |
| 2309 | + | |
| 2310 | + updateReasoningUI(messageElement); | |
| 2311 | + | |
| 2269 | 2312 | addCopyToCodeBlocks(messageElement); |
| 2270 | 2313 | appendMediaToMessage(message, messageElement); |
| 2271 | 2314 | } |
| @@ -2422,9 +2465,9 @@ export function addOneMessage(mes, { type = 'normal', insertAfter = null, scroll | ||
| 2422 | 2465 | mes.is_user, |
| 2423 | 2466 | chat.indexOf(mes), |
| 2424 | 2467 | sanitizerOverrides, |
| 2468 | + false, | |
| 2425 | 2469 | ); |
| 2426 | 2470 | const bias = messageFormatting(mes.extra?.bias ?? '', '', false, false, -1, {}, false); |
| 2427 | - const reasoning = messageFormatting(mes.extra?.reasoning ?? '', '', false, false, -1); | |
| 2428 | 2471 | let bookmarkLink = mes?.extra?.bookmark_link ?? ''; |
| 2429 | 2472 | |
| 2430 | 2473 | let params = { |
| @@ -2434,7 +2477,6 @@ export function addOneMessage(mes, { type = 'normal', insertAfter = null, scroll | ||
| 2434 | 2477 | isUser: mes.is_user, |
| 2435 | 2478 | avatarImg: avatarImg, |
| 2436 | 2479 | bias: bias, |
| 2437 | - reasoning: reasoning, | |
| 2438 | 2480 | isSystem: isSystem, |
| 2439 | 2481 | title: title, |
| 2440 | 2482 | bookmarkLink: bookmarkLink, |
| @@ -2442,7 +2484,7 @@ export function addOneMessage(mes, { type = 'normal', insertAfter = null, scroll | ||
| 2442 | 2484 | timestamp: timestamp, |
| 2443 | 2485 | extra: mes.extra, |
| 2444 | 2486 | tokenCount: mes.extra?.token_count ?? 0, |
| 2445 | 2487 | ...formatGenerationTimer(mes.gen_started, mes.gen_finished, mes.extra?.token_count, mes.extra?.reasoning_duration), |
| 2446 | 2488 | }; |
| 2447 | 2489 | |
| 2448 | 2490 | const renderedMessage = getMessageFromTemplate(params); |
| @@ -2494,8 +2536,8 @@ export function addOneMessage(mes, { type = 'normal', insertAfter = null, scroll | ||
| 2494 | 2536 | const swipeMessage = chatElement.find(`[mesid="${chat.length - 1}"]`); |
| 2495 | 2537 | swipeMessage.attr('swipeid', params.swipeId); |
| 2496 | 2538 | swipeMessage.find('.mes_text').html(messageText).attr('title', title); |
| 2497 | - swipeMessage.find('.mes_reasoning').html(reasoning); | |
| 2498 | 2539 | swipeMessage.find('.timestamp').text(timestamp).attr('title', `${params.extra.api} - ${params.extra.model}`); |
| 2540 | + updateReasoningUI(swipeMessage); | |
| 2499 | 2541 | appendMediaToMessage(mes, swipeMessage); |
| 2500 | 2542 | if (power_user.timestamp_model_icon && params.extra?.api) { |
| 2501 | 2543 | insertSVGIcon(swipeMessage, params.extra); |
| @@ -2562,13 +2604,14 @@ export function formatCharacterAvatar(characterAvatar) { | ||
| 2562 | 2604 | * @param {Date} gen_started Date when generation was started |
| 2563 | 2605 | * @param {Date} gen_finished Date when generation was finished |
| 2564 | 2606 | * @param {number} tokenCount Number of tokens generated (0 if not available) |
| 2607 | + * @param {number?} [reasoningDuration=null] Reasoning duration (null if no reasoning was done) | |
| 2565 | 2608 | * @returns {Object} Object containing the formatted timer value and title |
| 2566 | 2609 | * @example |
| 2567 | 2610 | * const { timerValue, timerTitle } = formatGenerationTimer(gen_started, gen_finished, tokenCount); |
| 2568 | 2611 | * console.log(timerValue); // 1.2s |
| 2569 | 2612 | * console.log(timerTitle); // Generation queued: 12:34:56 7 Jan 2021\nReply received: 12:34:57 7 Jan 2021\nTime to generate: 1.2 seconds\nToken rate: 5 t/s |
| 2570 | 2613 | */ |
| 2571 | 2614 | function formatGenerationTimer(gen_started, gen_finished, tokenCount, reasoningDuration = null) { |
| 2572 | 2615 | if (!gen_started || !gen_finished) { |
| 2573 | 2616 | return {}; |
| 2574 | 2617 | } |
| @@ -2582,8 +2625,9 @@ function formatGenerationTimer(gen_started, gen_finished, tokenCount) { | ||
| 2582 | 2625 | `Generation queued: ${start.format(dateFormat)}`, |
| 2583 | 2626 | `Reply received: ${finish.format(dateFormat)}`, |
| 2584 | 2627 | `Time to generate: ${seconds} seconds`, |
| 2628 | + reasoningDuration > 0 ? `Time to think: ${reasoningDuration / 1000} seconds` : '', | |
| 2585 | 2629 | tokenCount > 0 ? `Token rate: ${Number(tokenCount / seconds).toFixed(1)} t/s` : '', |
| 2586 | 2630 | ].filter(x => x).join('\n').trim(); |
| 2587 | 2631 | |
| 2588 | 2632 | if (isNaN(seconds) || seconds < 0) { |
| 2589 | 2633 | return { timerValue: '', timerTitle }; |
| @@ -2771,7 +2815,8 @@ export async function generateQuietPrompt(quiet_prompt, quietToLoud, skipWIAN, q | ||
| 2771 | 2815 | TempResponseLength.save(main_api, responseLength); |
| 2772 | 2816 | eventHook = TempResponseLength.setupEventHook(main_api); |
| 2773 | 2817 | } |
| 2774 | 2818 | returnconst result = await Generate('quiet', options); |
| 2819 | + return removeReasoningFromString(result); | |
| 2775 | 2820 | } finally { |
| 2776 | 2821 | if (responseLengthCustomized && TempResponseLength.isCustomized()) { |
| 2777 | 2822 | TempResponseLength.restore(main_api); |
| @@ -3071,8 +3116,8 @@ export function isStreamingEnabled() { | ||
| 3071 | 3116 | (main_api == 'openai' && |
| 3072 | 3117 | oai_settings.stream_openai && |
| 3073 | 3118 | !noStreamSources.includes(oai_settings.chat_completion_source) && |
| 3074 | 3119 | !(oai_settings.chat_completion_source == chat_completion_sources.OPENAI && oai_settings.openai_model.startsWith(['o1-2024-12-17', 'o1'].includes(oai_settings.openai_model)) && |
| 3075 | - !(oai_settings.chat_completion_source == chat_completion_sources.MAKERSUITE && oai_settings.google_model.includes('bison'))) | |
| 3120 | + ) | |
| 3076 | 3121 | || (main_api == 'kobold' && kai_settings.streaming_kobold && kai_flags.can_use_streaming) |
| 3077 | 3122 | || (main_api == 'novel' && nai_settings.streaming_novel) |
| 3078 | 3123 | || (main_api == 'textgenerationwebui' && textgen_settings.streaming)); |
| @@ -3101,11 +3146,14 @@ class StreamingProcessor { | ||
| 3101 | 3146 | constructor(type, forceName2, timeStarted, continueMessage) { |
| 3102 | 3147 | this.result = ''; |
| 3103 | 3148 | this.messageId = -1; |
| 3149 | + /** @type {HTMLElement} */ | |
| 3104 | 3150 | this.messageDom = null; |
| 3151 | + /** @type {HTMLElement} */ | |
| 3105 | 3152 | this.messageTextDom = null; |
| 3153 | + /** @type {HTMLElement} */ | |
| 3106 | 3154 | this.messageTimerDom = null; |
| 3155 | + /** @type {HTMLElement} */ | |
| 3107 | 3156 | this.messageTokenCounterDom = null; |
| 3108 | - this.messageReasoningDom = null; | |
| 3109 | 3157 | /** @type {HTMLTextAreaElement} */ |
| 3110 | 3158 | this.sendTextarea = document.querySelector('#send_textarea'); |
| 3111 | 3159 | this.type = type; |
| @@ -3121,7 +3169,8 @@ class StreamingProcessor { | ||
| 3121 | 3169 | /** @type {import('./scripts/logprobs.js').TokenLogprobs[]} */ |
| 3122 | 3170 | this.messageLogprobs = []; |
| 3123 | 3171 | this.toolCalls = []; |
| 3124 | - this.reasoning = ''; | |
| 3172 | + // Initialize reasoning in its own handler | |
| 3173 | + this.reasoningHandler = new ReasoningHandler(timeStarted); | |
| 3125 | 3174 | } |
| 3126 | 3175 | |
| 3127 | 3176 | #checkDomElements(messageId) { |
| @@ -3130,8 +3179,8 @@ class StreamingProcessor { | ||
| 3130 | 3179 | this.messageTextDom = this.messageDom?.querySelector('.mes_text'); |
| 3131 | 3180 | this.messageTimerDom = this.messageDom?.querySelector('.mes_timer'); |
| 3132 | 3181 | this.messageTokenCounterDom = this.messageDom?.querySelector('.tokenCounterDisplay'); |
| 3133 | - this.messageReasoningDom = this.messageDom?.querySelector('.mes_reasoning'); | |
| 3134 | 3182 | } |
| 3183 | + this.reasoningHandler.updateDom(messageId); | |
| 3135 | 3184 | } |
| 3136 | 3185 | |
| 3137 | 3186 | #updateMessageBlockVisibility() { |
| @@ -3141,22 +3190,12 @@ class StreamingProcessor { | ||
| 3141 | 3190 | } |
| 3142 | 3191 | } |
| 3143 | 3192 | |
| 3144 | 3193 | showMessageButtonsmarkUIGenStarted(messageId) { |
| 3145 | - if (messageId == -1) { | |
| 3194 | + deactivateSendButtons(); | |
| 3146 | - return; | |
| 3147 | - } | |
| 3148 | - | |
| 3149 | - showStopButton(); | |
| 3150 | - $(`#chat .mes[mesid="${messageId}"] .mes_buttons`).css({ 'display': 'none' }); | |
| 3151 | - } | |
| 3152 | - | |
| 3153 | - hideMessageButtons(messageId) { | |
| 3154 | - if (messageId == -1) { | |
| 3155 | - return; | |
| 3156 | 3195 | } |
| 3157 | 3196 | |
| 3158 | - hideStopButton(); | |
| 3197 | + markUIGenStopped() { | |
| 3159 | - $(`#chat .mes[mesid="${messageId}"] .mes_buttons`).css({ 'display': 'flex' }); | |
| 3198 | + activateSendButtons(); | |
| 3160 | 3199 | } |
| 3161 | 3200 | |
| 3162 | 3201 | async onStartStreaming(text) { |
| @@ -3165,20 +3204,18 @@ class StreamingProcessor { | ||
| 3165 | 3204 | if (this.type == 'impersonate') { |
| 3166 | 3205 | this.sendTextarea.value = ''; |
| 3167 | 3206 | this.sendTextarea.dispatchEvent(new Event('input', { bubbles: true })); |
| 3168 | 3207 | } else { |
| 3169 | - else { | |
| 3208 | + await saveReply(this.type, text, true, '', [], ''); | |
| 3170 | - await saveReply(this.type, text, true); | |
| 3171 | 3209 | messageId = chat.length - 1; |
| 3172 | 3210 | this.#checkDomElements(messageId); |
| 3173 | 3211 | this.showMessageButtonsmarkUIGenStarted(messageId); |
| 3174 | 3212 | } |
| 3175 | - | |
| 3176 | 3213 | hideSwipeButtons(); |
| 3177 | 3214 | scrollChatToBottom(); |
| 3178 | 3215 | return messageId; |
| 3179 | 3216 | } |
| 3180 | 3217 | |
| 3181 | 3218 | async onProgressStreaming(messageId, text, isFinal) { |
| 3182 | 3219 | const isImpersonate = this.type == 'impersonate'; |
| 3183 | 3220 | const isContinue = this.type == 'continue'; |
| 3184 | 3221 | |
| @@ -3190,11 +3227,9 @@ class StreamingProcessor { | ||
| 3190 | 3227 | |
| 3191 | 3228 | let processedText = cleanUpMessage(text, isImpersonate, isContinue, !isFinal, this.stoppingStrings); |
| 3192 | 3229 | |
| 3193 | - // Predict unbalanced asterisks / quotes during streaming | |
| 3194 | 3230 | const charsToBalance = ['*', '"', '```']; |
| 3195 | 3231 | for (const char of charsToBalance) { |
| 3196 | 3232 | if (!isFinal && isOdd(countOccurrences(processedText, char))) { |
| 3197 | - // Add character at the end to balance it | |
| 3198 | 3233 | const separator = char.length > 1 ? '\n' : ''; |
| 3199 | 3234 | processedText = processedText.trimEnd() + separator + char; |
| 3200 | 3235 | } |
| @@ -3203,31 +3238,25 @@ class StreamingProcessor { | ||
| 3203 | 3238 | if (isImpersonate) { |
| 3204 | 3239 | this.sendTextarea.value = processedText; |
| 3205 | 3240 | this.sendTextarea.dispatchEvent(new Event('input', { bubbles: true })); |
| 3206 | 3241 | } else { |
| 3207 | - else { | |
| 3242 | + const mesChanged = chat[messageId]['mes'] !== processedText; | |
| 3208 | 3243 | this.#checkDomElements(messageId); |
| 3209 | 3244 | this.#updateMessageBlockVisibility(); |
| 3210 | 3245 | const currentTime = new Date(); |
| 3211 | 3246 | chat[messageId]['mes'] = processedText; |
| 3212 | 3247 | chat[messageId]['gen_started'] = this.timeStarted; |
| 3213 | 3248 | chat[messageId]['gen_finished'] = currentTime; |
| 3214 | - | |
| 3215 | 3249 | if (!chat[messageId]['extra']) { |
| 3216 | 3250 | chat[messageId]['extra'] = {}; |
| 3217 | 3251 | } |
| 3218 | 3252 | |
| 3219 | - if (this.reasoning) { | |
| 3253 | + // Update reasoning | |
| 3220 | - chat[messageId]['extra']['reasoning'] = this.reasoning; | |
| 3254 | + await this.reasoningHandler.process(messageId, mesChanged); | |
| 3221 | - if (this.messageReasoningDom instanceof HTMLElement) { | |
| 3255 | + processedText = chat[messageId]['mes']; | |
| 3222 | - const formattedReasoning = messageFormatting(this.reasoning, '', false, false, -1); | |
| 3223 | - this.messageReasoningDom.innerHTML = formattedReasoning; | |
| 3224 | - } | |
| 3225 | - } | |
| 3226 | 3256 | |
| 3227 | - // Don't waste time calculating token count for streaming | |
| 3257 | + // Token count update. | |
| 3228 | 3258 | const tokenCountText = (this.reasoningHandler.reasoning || '') + processedText; |
| 3229 | 3259 | const currentTokenCount = isFinal && power_user.message_token_count_enabled ? getTokenCount(tokenCountText, 0) : 0; |
| 3230 | - | |
| 3231 | 3260 | if (currentTokenCount) { |
| 3232 | 3261 | chat[messageId]['extra']['token_count'] = currentTokenCount; |
| 3233 | 3262 | if (this.messageTokenCounterDom instanceof HTMLElement) { |
| @@ -3246,12 +3275,14 @@ class StreamingProcessor { | ||
| 3246 | 3275 | chat[messageId].is_system, |
| 3247 | 3276 | chat[messageId].is_user, |
| 3248 | 3277 | messageId, |
| 3278 | + {}, | |
| 3279 | + false, | |
| 3249 | 3280 | ); |
| 3250 | 3281 | if (this.messageTextDom instanceof HTMLElement) { |
| 3251 | 3282 | this.messageTextDom.innerHTML = formattedText; |
| 3252 | 3283 | } |
| 3253 | 3284 | |
| 3254 | 3285 | const timePassed = formatGenerationTimer(this.timeStarted, currentTime, currentTokenCount, this.reasoningHandler.getDuration()); |
| 3255 | 3286 | if (this.messageTimerDom instanceof HTMLElement) { |
| 3256 | 3287 | this.messageTimerDom.textContent = timePassed.timerValue; |
| 3257 | 3288 | this.messageTimerDom.title = timePassed.timerTitle; |
| @@ -3266,10 +3297,12 @@ class StreamingProcessor { | ||
| 3266 | 3297 | } |
| 3267 | 3298 | |
| 3268 | 3299 | async onFinishStreaming(messageId, text) { |
| 3269 | 3300 | this.hideMessageButtonsmarkUIGenStopped(this.messageId); |
| 3270 | 3301 | await this.onProgressStreaming(messageId, text, true); |
| 3271 | 3302 | addCopyToCodeBlocks($(`#chat .mes[mesid="${messageId}"]`)); |
| 3272 | 3303 | |
| 3304 | + await this.reasoningHandler.finish(messageId); | |
| 3305 | + | |
| 3273 | 3306 | if (Array.isArray(this.swipes) && this.swipes.length > 0) { |
| 3274 | 3307 | const message = chat[messageId]; |
| 3275 | 3308 | const swipeInfo = { |
| @@ -3297,39 +3330,11 @@ class StreamingProcessor { | ||
| 3297 | 3330 | unblockGeneration(); |
| 3298 | 3331 | generatedPromptCache = ''; |
| 3299 | 3332 | |
| 3300 | - //console.log("Generated text size:", text.length, text) | |
| 3301 | - | |
| 3302 | 3333 | const isAborted = this.abortController.signal.aborted; |
| 3303 | 3334 | if (!isAborted && power_user.auto_swipe && !isAbortedgeneratedTextFiltered(text)) { |
| 3304 | - function containsBlacklistedWords(str, blacklist, threshold) { | |
| 3335 | + return swipe_right(); | |
| 3305 | - const regex = new RegExp(`\\b(${blacklist.join('|')})\\b`, 'gi'); | |
| 3306 | - const matches = str.match(regex) || []; | |
| 3307 | - return matches.length >= threshold; | |
| 3308 | - } | |
| 3309 | - | |
| 3310 | - const generatedTextFiltered = (text) => { | |
| 3311 | - if (text) { | |
| 3312 | - if (power_user.auto_swipe_minimum_length) { | |
| 3313 | - if (text.length < power_user.auto_swipe_minimum_length && text.length !== 0) { | |
| 3314 | - console.log('Generated text size too small'); | |
| 3315 | - return true; | |
| 3316 | - } | |
| 3317 | - } | |
| 3318 | - if (power_user.auto_swipe_blacklist_threshold) { | |
| 3319 | - if (containsBlacklistedWords(text, power_user.auto_swipe_blacklist, power_user.auto_swipe_blacklist_threshold)) { | |
| 3320 | - console.log('Generated text has blacklisted words'); | |
| 3321 | - return true; | |
| 3322 | 3336 | } |
| 3323 | - } | |
| 3324 | - } | |
| 3325 | - return false; | |
| 3326 | - }; | |
| 3327 | 3337 | |
| 3328 | - if (generatedTextFiltered(text)) { | |
| 3329 | - swipe_right(); | |
| 3330 | - return; | |
| 3331 | - } | |
| 3332 | - } | |
| 3333 | 3338 | playMessageSound(); |
| 3334 | 3339 | } |
| 3335 | 3340 | |
| @@ -3337,7 +3342,7 @@ class StreamingProcessor { | ||
| 3337 | 3342 | this.abortController.abort(); |
| 3338 | 3343 | this.isStopped = true; |
| 3339 | 3344 | |
| 3340 | 3345 | this.hideMessageButtonsmarkUIGenStopped(this.messageId); |
| 3341 | 3346 | generatedPromptCache = ''; |
| 3342 | 3347 | unblockGeneration(); |
| 3343 | 3348 | |
| @@ -3387,8 +3392,8 @@ class StreamingProcessor { | ||
| 3387 | 3392 | const timestamps = []; |
| 3388 | 3393 | for await (const { text, swipes, logprobs, toolCalls, state } of this.generator()) { |
| 3389 | 3394 | timestamps.push(Date.now()); |
| 3390 | 3395 | if (this.isStopped || this.abortController.signal.aborted) { |
| 3391 | 3396 | return this.result; |
| 3392 | 3397 | } |
| 3393 | 3398 | |
| 3394 | 3399 | this.toolCalls = toolCalls; |
| @@ -3397,9 +3402,10 @@ class StreamingProcessor { | ||
| 3397 | 3402 | if (logprobs) { |
| 3398 | 3403 | this.messageLogprobs.push(...(Array.isArray(logprobs) ? logprobs : [logprobs])); |
| 3399 | 3404 | } |
| 3400 | - this.reasoning = state?.reasoning ?? ''; | |
| 3405 | + // Get the updated reasoning string into the handler | |
| 3406 | + this.reasoningHandler.updateReasoning(this.messageId, state?.reasoning); | |
| 3401 | 3407 | await eventSource.emit(event_types.STREAM_TOKEN_RECEIVED, text); |
| 3402 | 3408 | await sw.tick(async () => await this.onProgressStreaming(this.messageId, this.continueMessage + text)); |
| 3403 | 3409 | } |
| 3404 | 3410 | const seconds = (timestamps[timestamps.length - 1] - timestamps[0]) / 1000; |
| 3405 | 3411 | console.warn(`Stream stats: ${timestamps.length} tokens, ${seconds.toFixed(2)} seconds, rate: ${Number(timestamps.length / seconds).toFixed(2)} TPS`); |
| @@ -3475,7 +3481,7 @@ export async function generateRaw(prompt, api, instructOverride, quietToLoud, sy | ||
| 3475 | 3481 | break; |
| 3476 | 3482 | } |
| 3477 | 3483 | case 'textgenerationwebui': |
| 3478 | 3484 | generateData = await getTextGenGenerationData(prompt, amount_gen, false, false, null, 'quiet'); |
| 3479 | 3485 | TempResponseLength.restore(api); |
| 3480 | 3486 | break; |
| 3481 | 3487 | case 'openai': { |
| @@ -3864,14 +3870,6 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro | ||
| 3864 | 3870 | coreChat.pop(); |
| 3865 | 3871 | } |
| 3866 | 3872 | |
| 3867 | - const reasoning = new PromptReasoning(); | |
| 3868 | - for (let i = coreChat.length - 1; i >= 0; i--) { | |
| 3869 | - if (reasoning.isLimitReached()) { | |
| 3870 | - break; | |
| 3871 | - } | |
| 3872 | - coreChat[i] = { ...coreChat[i], mes: reasoning.addToMessage(coreChat[i].mes, coreChat[i].extra?.reasoning) }; | |
| 3873 | - } | |
| 3874 | - | |
| 3875 | 3873 | coreChat = await Promise.all(coreChat.map(async (chatItem, index) => { |
| 3876 | 3874 | let message = chatItem.mes; |
| 3877 | 3875 | let regexType = chatItem.is_user ? regex_placement.USER_INPUT : regex_placement.AI_OUTPUT; |
| @@ -3891,6 +3889,27 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro | ||
| 3891 | 3889 | }; |
| 3892 | 3890 | })); |
| 3893 | 3891 | |
| 3892 | + const reasoning = new PromptReasoning(); | |
| 3893 | + for (let i = coreChat.length - 1; i >= 0; i--) { | |
| 3894 | + const depth = coreChat.length - i - 1; | |
| 3895 | + const isPrefix = isContinue && i === coreChat.length - 1; | |
| 3896 | + coreChat[i] = { | |
| 3897 | + ...coreChat[i], | |
| 3898 | + mes: reasoning.addToMessage( | |
| 3899 | + coreChat[i].mes, | |
| 3900 | + getRegexedString( | |
| 3901 | + String(coreChat[i].extra?.reasoning ?? ''), | |
| 3902 | + regex_placement.REASONING, | |
| 3903 | + { isPrompt: true, depth: depth }, | |
| 3904 | + ), | |
| 3905 | + isPrefix, | |
| 3906 | + ), | |
| 3907 | + }; | |
| 3908 | + if (reasoning.isLimitReached()) { | |
| 3909 | + break; | |
| 3910 | + } | |
| 3911 | + } | |
| 3912 | + | |
| 3894 | 3913 | // Determine token limit |
| 3895 | 3914 | let this_max_context = getMaxContextSize(); |
| 3896 | 3915 | |
| @@ -4449,7 +4468,7 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro | ||
| 4449 | 4468 | // For prompt bit itemization |
| 4450 | 4469 | let mesSendString = ''; |
| 4451 | 4470 | |
| 4452 | 4471 | async function getCombinedPrompt(isNegative) { |
| 4453 | 4472 | // Only return if the guidance scale doesn't exist or the value is 1 |
| 4454 | 4473 | // Also don't return if constructing the neutral prompt |
| 4455 | 4474 | if (isNegative && !useCfgPrompt) { |
| @@ -4476,10 +4495,16 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro | ||
| 4476 | 4495 | // TODO: Make all extension prompts use an array/splice method |
| 4477 | 4496 | const lengthDiff = mesSend.length - cfgPrompt.depth; |
| 4478 | 4497 | const cfgDepth = lengthDiff >= 0 ? lengthDiff : 0; |
| 4498 | + const cfgMessage = finalMesSend[cfgDepth]; | |
| 4499 | + if (cfgMessage) { | |
| 4500 | + if (!Array.isArray(finalMesSend[cfgDepth].extensionPrompts)) { | |
| 4501 | + finalMesSend[cfgDepth].extensionPrompts = []; | |
| 4502 | + } | |
| 4479 | 4503 | finalMesSend[cfgDepth].extensionPrompts.push(`${cfgPrompt.value}\n`); |
| 4480 | 4504 | } |
| 4481 | 4505 | } |
| 4482 | 4506 | } |
| 4507 | + } | |
| 4483 | 4508 | |
| 4484 | 4509 | // Add prompt bias after everything else |
| 4485 | 4510 | // Always run with continue |
| @@ -4552,13 +4577,13 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro | ||
| 4552 | 4577 | }; |
| 4553 | 4578 | |
| 4554 | 4579 | // Before returning the combined prompt, give available context related information to all subscribers. |
| 4555 | 4580 | await eventSource.emitAndWaitemit(event_types.GENERATE_BEFORE_COMBINE_PROMPTS, data); |
| 4556 | 4581 | |
| 4557 | 4582 | // If one or multiple subscribers return a value, forfeit the responsibillity of flattening the context. |
| 4558 | 4583 | return !data.combinedPrompt ? combine() : data.combinedPrompt; |
| 4559 | 4584 | } |
| 4560 | 4585 | |
| 4561 | 4586 | let finalPrompt = await getCombinedPrompt(false); |
| 4562 | 4587 | |
| 4563 | 4588 | const eventData = { prompt: finalPrompt, dryRun: dryRun }; |
| 4564 | 4589 | await eventSource.emit(event_types.GENERATE_AFTER_COMBINE_PROMPTS, eventData); |
| @@ -4592,8 +4617,8 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro | ||
| 4592 | 4617 | } |
| 4593 | 4618 | break; |
| 4594 | 4619 | case 'textgenerationwebui': { |
| 4595 | 4620 | const cfgValues = useCfgPrompt ? { guidanceScale: cfgGuidanceScale, negativePrompt: await getCombinedPrompt(true) } : null; |
| 4596 | 4621 | generate_data = await getTextGenGenerationData(finalPrompt, maxLength, isImpersonate, isContinue, cfgValues, type); |
| 4597 | 4622 | break; |
| 4598 | 4623 | } |
| 4599 | 4624 | case 'novel': { |
| @@ -4799,6 +4824,11 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro | ||
| 4799 | 4824 | const swipes = extractMultiSwipes(data, type); |
| 4800 | 4825 | |
| 4801 | 4826 | messageChunk = cleanUpMessage(getMessage, isImpersonate, isContinue, false); |
| 4827 | + reasoning = getRegexedString(reasoning, regex_placement.REASONING); | |
| 4828 | + | |
| 4829 | + if (power_user.trim_spaces) { | |
| 4830 | + reasoning = reasoning.trim(); | |
| 4831 | + } | |
| 4802 | 4832 | |
| 4803 | 4833 | if (isContinue) { |
| 4804 | 4834 | getMessage = continue_mag + getMessage; |
| @@ -4857,32 +4887,9 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro | ||
| 4857 | 4887 | } |
| 4858 | 4888 | |
| 4859 | 4889 | const isAborted = abortController && abortController.signal.aborted; |
| 4860 | 4890 | if (!isAborted && power_user.auto_swipe && !isAbortedgeneratedTextFiltered(getMessage)) { |
| 4861 | - console.debug('checking for autoswipeblacklist on non-streaming message'); | |
| 4862 | - function containsBlacklistedWords(getMessage, blacklist, threshold) { | |
| 4863 | - console.debug('checking blacklisted words'); | |
| 4864 | - const regex = new RegExp(`\\b(${blacklist.join('|')})\\b`, 'gi'); | |
| 4865 | - const matches = getMessage.match(regex) || []; | |
| 4866 | - return matches.length >= threshold; | |
| 4867 | - } | |
| 4868 | - | |
| 4869 | - const generatedTextFiltered = (getMessage) => { | |
| 4870 | - if (power_user.auto_swipe_blacklist_threshold) { | |
| 4871 | - if (containsBlacklistedWords(getMessage, power_user.auto_swipe_blacklist, power_user.auto_swipe_blacklist_threshold)) { | |
| 4872 | - console.debug('Generated text has blacklisted words'); | |
| 4873 | - return true; | |
| 4874 | - } | |
| 4875 | - } | |
| 4876 | - | |
| 4877 | - return false; | |
| 4878 | - }; | |
| 4879 | - if (generatedTextFiltered(getMessage)) { | |
| 4880 | - console.debug('swiping right automatically'); | |
| 4881 | 4891 | is_send_press = false; |
| 4882 | 4892 | return swipe_right(); |
| 4883 | - // TODO: do we want to resolve after an auto-swipe? | |
| 4884 | - return; | |
| 4885 | - } | |
| 4886 | 4893 | } |
| 4887 | 4894 | |
| 4888 | 4895 | console.debug('/api/chats/save called by /Generate'); |
| @@ -5537,7 +5544,7 @@ async function promptItemize(itemizedPrompts, requestedMesId) { | ||
| 5537 | 5544 | toastr.info(t`Copied!`); |
| 5538 | 5545 | }); |
| 5539 | 5546 | |
| 5540 | 5547 | popup.dlg.querySelector('#showRawPrompt').addEventListener('click', async function () { |
| 5541 | 5548 | //console.log(itemizedPrompts[PromptArrayItemForRawPromptDisplay].rawPrompt); |
| 5542 | 5549 | console.log(PromptArrayItemForRawPromptDisplay); |
| 5543 | 5550 | console.log(itemizedPrompts); |
| @@ -5545,6 +5552,17 @@ async function promptItemize(itemizedPrompts, requestedMesId) { | ||
| 5545 | 5552 | |
| 5546 | 5553 | const rawPrompt = flatten(itemizedPrompts[PromptArrayItemForRawPromptDisplay].rawPrompt); |
| 5547 | 5554 | |
| 5555 | + // Mobile needs special handholding. The side-view on the popup wouldn't work, | |
| 5556 | + // so we just show an additional popup for this. | |
| 5557 | + if (isMobile()) { | |
| 5558 | + const content = document.createElement('div'); | |
| 5559 | + content.classList.add('tokenItemizingMaintext'); | |
| 5560 | + content.innerText = rawPrompt; | |
| 5561 | + const popup = new Popup(content, POPUP_TYPE.TEXT, null, { allowVerticalScrolling: true, leftAlign: true }); | |
| 5562 | + await popup.show(); | |
| 5563 | + return; | |
| 5564 | + } | |
| 5565 | + | |
| 5548 | 5566 | //let DisplayStringifiedPrompt = JSON.stringify(itemizedPrompts[PromptArrayItemForRawPromptDisplay].rawPrompt).replace(/\n+/g, '<br>'); |
| 5549 | 5567 | const rawPromptWrapper = document.getElementById('rawPromptWrapper'); |
| 5550 | 5568 | rawPromptWrapper.innerText = rawPrompt; |
| @@ -5728,26 +5746,6 @@ function extractMessageFromData(data) { | ||
| 5728 | 5746 | } |
| 5729 | 5747 | |
| 5730 | 5748 | /** |
| 5731 | - * Extracts the reasoning from the response data. | |
| 5732 | - * @param {object} data Response data | |
| 5733 | - * @returns {string} Extracted reasoning | |
| 5734 | - */ | |
| 5735 | -function extractReasoningFromData(data) { | |
| 5736 | - if (main_api === 'openai' && oai_settings.show_thoughts) { | |
| 5737 | - switch (oai_settings.chat_completion_source) { | |
| 5738 | - case chat_completion_sources.DEEPSEEK: | |
| 5739 | - return data?.choices?.[0]?.message?.reasoning_content ?? ''; | |
| 5740 | - case chat_completion_sources.OPENROUTER: | |
| 5741 | - return data?.choices?.[0]?.message?.reasoning ?? ''; | |
| 5742 | - case chat_completion_sources.MAKERSUITE: | |
| 5743 | - return data?.responseContent?.parts?.filter(part => part.thought)?.map(part => part.text)?.join('\n\n') ?? ''; | |
| 5744 | - } | |
| 5745 | - } | |
| 5746 | - | |
| 5747 | - return ''; | |
| 5748 | -} | |
| 5749 | - | |
| 5750 | -/** | |
| 5751 | 5749 | * Extracts multiswipe swipes from the response data. |
| 5752 | 5750 | * @param {Object} data Response data |
| 5753 | 5751 | * @param {string} type Type of generation |
| @@ -5937,6 +5935,15 @@ export async function saveReply(type, getMessage, fromStreaming, title, swipes, | ||
| 5937 | 5935 | chat[chat.length - 1]['extra'] = {}; |
| 5938 | 5936 | } |
| 5939 | 5937 | |
| 5938 | + // Coerce null/undefined to empty string | |
| 5939 | + if (chat.length && !chat[chat.length - 1]['extra']['reasoning']) { | |
| 5940 | + chat[chat.length - 1]['extra']['reasoning'] = ''; | |
| 5941 | + } | |
| 5942 | + | |
| 5943 | + if (!reasoning) { | |
| 5944 | + reasoning = ''; | |
| 5945 | + } | |
| 5946 | + | |
| 5940 | 5947 | let oldMessage = ''; |
| 5941 | 5948 | const generationFinished = new Date(); |
| 5942 | 5949 | const img = extractImageFromMessage(getMessage); |
| @@ -5953,6 +5960,7 @@ export async function saveReply(type, getMessage, fromStreaming, title, swipes, | ||
| 5953 | 5960 | chat[chat.length - 1]['extra']['api'] = getGeneratingApi(); |
| 5954 | 5961 | chat[chat.length - 1]['extra']['model'] = getGeneratingModel(); |
| 5955 | 5962 | chat[chat.length - 1]['extra']['reasoning'] = reasoning; |
| 5963 | + chat[chat.length - 1]['extra']['reasoning_duration'] = null; | |
| 5956 | 5964 | if (power_user.message_token_count_enabled) { |
| 5957 | 5965 | const tokenCountText = (reasoning || '') + chat[chat.length - 1]['mes']; |
| 5958 | 5966 | chat[chat.length - 1]['extra']['token_count'] = await getTokenCountAsync(tokenCountText, 0); |
| @@ -5974,7 +5982,8 @@ export async function saveReply(type, getMessage, fromStreaming, title, swipes, | ||
| 5974 | 5982 | chat[chat.length - 1]['send_date'] = getMessageTimeStamp(); |
| 5975 | 5983 | chat[chat.length - 1]['extra']['api'] = getGeneratingApi(); |
| 5976 | 5984 | chat[chat.length - 1]['extra']['model'] = getGeneratingModel(); |
| 5977 | 5985 | chat[chat.length - 1]['extra']['reasoning'] += reasoning; |
| 5986 | + chat[chat.length - 1]['extra']['reasoning_duration'] = null; | |
| 5978 | 5987 | if (power_user.message_token_count_enabled) { |
| 5979 | 5988 | const tokenCountText = (reasoning || '') + chat[chat.length - 1]['mes']; |
| 5980 | 5989 | chat[chat.length - 1]['extra']['token_count'] = await getTokenCountAsync(tokenCountText, 0); |
| @@ -5994,6 +6003,7 @@ export async function saveReply(type, getMessage, fromStreaming, title, swipes, | ||
| 5994 | 6003 | chat[chat.length - 1]['extra']['api'] = getGeneratingApi(); |
| 5995 | 6004 | chat[chat.length - 1]['extra']['model'] = getGeneratingModel(); |
| 5996 | 6005 | chat[chat.length - 1]['extra']['reasoning'] += reasoning; |
| 6006 | + // We don't know if the reasoning duration extended, so we don't update it here on purpose. | |
| 5997 | 6007 | if (power_user.message_token_count_enabled) { |
| 5998 | 6008 | const tokenCountText = (reasoning || '') + chat[chat.length - 1]['mes']; |
| 5999 | 6009 | chat[chat.length - 1]['extra']['token_count'] = await getTokenCountAsync(tokenCountText, 0); |
| @@ -6013,6 +6023,7 @@ export async function saveReply(type, getMessage, fromStreaming, title, swipes, | ||
| 6013 | 6023 | chat[chat.length - 1]['extra']['api'] = getGeneratingApi(); |
| 6014 | 6024 | chat[chat.length - 1]['extra']['model'] = getGeneratingModel(); |
| 6015 | 6025 | chat[chat.length - 1]['extra']['reasoning'] = reasoning; |
| 6026 | + chat[chat.length - 1]['extra']['reasoning_duration'] = null; | |
| 6016 | 6027 | if (power_user.trim_spaces) { |
| 6017 | 6028 | getMessage = getMessage.trim(); |
| 6018 | 6029 | } |
| @@ -6153,20 +6164,21 @@ function extractImageFromMessage(getMessage) { | ||
| 6153 | 6164 | return { getMessage, image, title }; |
| 6154 | 6165 | } |
| 6155 | 6166 | |
| 6167 | +/** | |
| 6168 | + * A function mainly used to switch 'generating' state - setting it to false and activating the buttons again | |
| 6169 | + */ | |
| 6156 | 6170 | export function activateSendButtons() { |
| 6157 | 6171 | is_send_press = false; |
| 6158 | - $('#send_but').removeClass('displayNone'); | |
| 6159 | - $('#mes_continue').removeClass('displayNone'); | |
| 6160 | - $('#mes_impersonate').removeClass('displayNone'); | |
| 6161 | - $('.mes_buttons:last').show(); | |
| 6162 | 6172 | hideStopButton(); |
| 6173 | + delete document.body.dataset.generating; | |
| 6163 | 6174 | } |
| 6164 | 6175 | |
| 6176 | +/** | |
| 6177 | + * A function mainly used to switch 'generating' state - setting it to true and deactivating the buttons | |
| 6178 | + */ | |
| 6165 | 6179 | export function deactivateSendButtons() { |
| 6166 | - $('#send_but').addClass('displayNone'); | |
| 6167 | - $('#mes_continue').addClass('displayNone'); | |
| 6168 | - $('#mes_impersonate').addClass('displayNone'); | |
| 6169 | 6180 | showStopButton(); |
| 6181 | + document.body.dataset.generating = 'true'; | |
| 6170 | 6182 | } |
| 6171 | 6183 | |
| 6172 | 6184 | export function resetChatState() { |
| @@ -6259,9 +6271,35 @@ export async function renameCharacter(name = null, { silent = false, renameChats | ||
| 6259 | 6271 | const data = await response.json(); |
| 6260 | 6272 | const newAvatar = data.avatar; |
| 6261 | 6273 | |
| 6262 | - // Replace tags list | |
| 6274 | + const oldName = getCharaFilename(null, { manualAvatarKey: oldAvatar }); | |
| 6275 | + const newName = getCharaFilename(null, { manualAvatarKey: newAvatar }); | |
| 6276 | + | |
| 6277 | + // Replace other auxillery fields where was referenced by avatar key | |
| 6278 | + // Tag List | |
| 6263 | 6279 | renameTagKey(oldAvatar, newAvatar); |
| 6264 | 6280 | |
| 6281 | + // Addtional lore books | |
| 6282 | + const charLore = world_info.charLore?.find(x => x.name == oldName); | |
| 6283 | + if (charLore) { | |
| 6284 | + charLore.name = newName; | |
| 6285 | + saveSettingsDebounced(); | |
| 6286 | + } | |
| 6287 | + | |
| 6288 | + // Char-bound Author's Notes | |
| 6289 | + const charNote = extension_settings.note.chara?.find(x => x.name == oldName); | |
| 6290 | + if (charNote) { | |
| 6291 | + charNote.name = newName; | |
| 6292 | + saveSettingsDebounced(); | |
| 6293 | + } | |
| 6294 | + | |
| 6295 | + // Update active character, if the current one was the currently active one | |
| 6296 | + if (active_character === oldAvatar) { | |
| 6297 | + active_character = newAvatar; | |
| 6298 | + saveSettingsDebounced(); | |
| 6299 | + } | |
| 6300 | + | |
| 6301 | + await eventSource.emit(event_types.CHARACTER_RENAMED, oldAvatar, newAvatar); | |
| 6302 | + | |
| 6265 | 6303 | // Reload characters list |
| 6266 | 6304 | await getCharacters(); |
| 6267 | 6305 | |
| @@ -6868,10 +6906,11 @@ export async function getSettings() { | ||
| 6868 | 6906 | $('#your_name').text(name1); |
| 6869 | 6907 | } |
| 6870 | 6908 | |
| 6909 | + accountStorage.init(settings?.accountStorage); | |
| 6871 | 6910 | await setUserControls(data.enable_accounts); |
| 6872 | 6911 | |
| 6873 | 6912 | // Allow subscribers to mutate settings |
| 6874 | 6913 | await eventSource.emit(event_types.SETTINGS_LOADED_BEFORE, settings); |
| 6875 | 6914 | |
| 6876 | 6915 | //Load KoboldAI settings |
| 6877 | 6916 | koboldai_setting_names = data.koboldai_setting_names; |
| @@ -6968,7 +7007,7 @@ export async function getSettings() { | ||
| 6968 | 7007 | loadProxyPresets(settings); |
| 6969 | 7008 | |
| 6970 | 7009 | // Allow subscribers to mutate settings |
| 6971 | 7010 | await eventSource.emit(event_types.SETTINGS_LOADED_AFTER, settings); |
| 6972 | 7011 | |
| 6973 | 7012 | // Set context size after loading power user (may override the max value) |
| 6974 | 7013 | $('#max_context').val(max_context); |
| @@ -7028,7 +7067,7 @@ export async function getSettings() { | ||
| 7028 | 7067 | } |
| 7029 | 7068 | await validateDisabledSamplers(); |
| 7030 | 7069 | settingsReady = true; |
| 7031 | 7070 | await eventSource.emit(event_types.SETTINGS_LOADED); |
| 7032 | 7071 | } |
| 7033 | 7072 | |
| 7034 | 7073 | function selectKoboldGuiPreset() { |
| @@ -7039,7 +7078,8 @@ function selectKoboldGuiPreset() { | ||
| 7039 | 7078 | |
| 7040 | 7079 | export async function saveSettings(loopCounter = 0) { |
| 7041 | 7080 | if (!settingsReady) { |
| 7042 | 7081 | console.warn('Settings not ready, abortingscheduling another save'); |
| 7082 | + saveSettingsDebounced(); | |
| 7043 | 7083 | return; |
| 7044 | 7084 | } |
| 7045 | 7085 | |
| @@ -7060,6 +7100,7 @@ export async function saveSettings(loopCounter = 0) { | ||
| 7060 | 7100 | url: '/api/settings/save', |
| 7061 | 7101 | data: JSON.stringify({ |
| 7062 | 7102 | firstRun: firstRun, |
| 7103 | + accountStorage: accountStorage.getState(), | |
| 7063 | 7104 | currentVersion: currentVersion, |
| 7064 | 7105 | username: name1, |
| 7065 | 7106 | active_character: active_character, |
| @@ -7125,8 +7166,10 @@ export function setGenerationParamsFromPreset(preset) { | ||
| 7125 | 7166 | // Common code for message editor done and auto-save |
| 7126 | 7167 | function updateMessage(div) { |
| 7127 | 7168 | const mesBlock = div.closest('.mes_block'); |
| 7128 | 7169 | let text = mesBlock.find('.edit_textarea').val(); |
| 7129 | - const mes = chat[this_edit_mes_id]; | |
| 7170 | + ?? mesBlock.find('.mes_text').text(); | |
| 7171 | + const mesElement = div.closest('.mes'); | |
| 7172 | + const mes = chat[mesElement.attr('mesid')]; | |
| 7130 | 7173 | |
| 7131 | 7174 | let regexPlacement; |
| 7132 | 7175 | if (mes.is_user) { |
| @@ -7210,9 +7253,11 @@ function messageEditAuto(div) { | ||
| 7210 | 7253 | mes.is_system, |
| 7211 | 7254 | mes.is_user, |
| 7212 | 7255 | this_edit_mes_id, |
| 7256 | + {}, | |
| 7257 | + false, | |
| 7213 | 7258 | )); |
| 7214 | 7259 | mesBlock.find('.mes_bias').empty(); |
| 7215 | 7260 | mesBlock.find('.mes_bias').append(messageFormatting(bias, '', false, false, -1, {}, false)); |
| 7216 | 7261 | saveChatDebounced(); |
| 7217 | 7262 | } |
| 7218 | 7263 | |
| @@ -7234,13 +7279,20 @@ async function messageEditDone(div) { | ||
| 7234 | 7279 | mes.is_system, |
| 7235 | 7280 | mes.is_user, |
| 7236 | 7281 | this_edit_mes_id, |
| 7282 | + {}, | |
| 7283 | + false, | |
| 7237 | 7284 | ), |
| 7238 | 7285 | ); |
| 7239 | 7286 | mesBlock.find('.mes_bias').empty(); |
| 7240 | 7287 | mesBlock.find('.mes_bias').append(messageFormatting(bias, '', false, false, -1, {}, false)); |
| 7241 | 7288 | appendMediaToMessage(mes, div.closest('.mes')); |
| 7242 | 7289 | addCopyToCodeBlocks(div.closest('.mes')); |
| 7243 | 7290 | |
| 7291 | + const reasoningEditDone = mesBlock.find('.mes_reasoning_edit_done:visible'); | |
| 7292 | + if (reasoningEditDone.length > 0) { | |
| 7293 | + reasoningEditDone.trigger('click'); | |
| 7294 | + } | |
| 7295 | + | |
| 7244 | 7296 | await eventSource.emit(event_types.MESSAGE_UPDATED, this_edit_mes_id); |
| 7245 | 7297 | this_edit_mes_id = undefined; |
| 7246 | 7298 | await saveChatConditional(); |
| @@ -7504,7 +7556,7 @@ export function select_rm_info(type, charId, previousCharId = null) { | ||
| 7504 | 7556 | } |
| 7505 | 7557 | |
| 7506 | 7558 | try { |
| 7507 | 7559 | const perPage = Number(localStorageaccountStorage.getItem('Characters_PerPage')) || per_page_default; |
| 7508 | 7560 | const page = Math.floor(charIndex / perPage) + 1; |
| 7509 | 7561 | const selector = `#rm_print_characters_block [title*="${avatarFileName}"]`; |
| 7510 | 7562 | $('#rm_print_characters_pagination').pagination('go', page); |
| @@ -7536,7 +7588,7 @@ export function select_rm_info(type, charId, previousCharId = null) { | ||
| 7536 | 7588 | return; |
| 7537 | 7589 | } |
| 7538 | 7590 | |
| 7539 | 7591 | const perPage = Number(localStorageaccountStorage.getItem('Characters_PerPage')) || per_page_default; |
| 7540 | 7592 | const page = Math.floor(charIndex / perPage) + 1; |
| 7541 | 7593 | $('#rm_print_characters_pagination').pagination('go', page); |
| 7542 | 7594 | const selector = `#rm_print_characters_block [grid="${charId}"]`; |
| @@ -8741,11 +8793,6 @@ const swipe_right = () => { | ||
| 8741 | 8793 | easing: animation_easing, |
| 8742 | 8794 | queue: false, |
| 8743 | 8795 | complete: async function () { |
| 8744 | - /*if (!selected_group) { | |
| 8745 | - var typingIndicator = $("#typing_indicator_template .typing_indicator").clone(); | |
| 8746 | - typingIndicator.find(".typing_indicator_name").text(characters[this_chid].name); | |
| 8747 | - } */ | |
| 8748 | - /* $("#chat").append(typingIndicator); */ | |
| 8749 | 8796 | const is_animation_scroll = ($('#chat').scrollTop() >= ($('#chat').prop('scrollHeight') - $('#chat').outerHeight()) - 10); |
| 8750 | 8797 | //console.log(parseInt(chat[chat.length-1]['swipe_id'])); |
| 8751 | 8798 | //console.log(chat[chat.length-1]['swipes'].length); |
| @@ -8756,7 +8803,7 @@ const swipe_right = () => { | ||
| 8756 | 8803 | // resets the timer |
| 8757 | 8804 | swipeMessage.find('.mes_timer').html(''); |
| 8758 | 8805 | swipeMessage.find('.tokenCounterDisplay').text(''); |
| 8759 | - swipeMessage.find('.mes_reasoning').html(''); | |
| 8806 | + updateReasoningUI(swipeMessage, { reset: true }); | |
| 8760 | 8807 | } else { |
| 8761 | 8808 | //console.log('showing previously generated swipe candidate, or "..."'); |
| 8762 | 8809 | //console.log('onclick right swipe calling addOneMessage'); |
| @@ -8806,7 +8853,6 @@ const swipe_right = () => { | ||
| 8806 | 8853 | if (run_generate && !is_send_press && parseInt(chat[chat.length - 1]['swipe_id']) === chat[chat.length - 1]['swipes'].length) { |
| 8807 | 8854 | console.debug('caught here 2'); |
| 8808 | 8855 | is_send_press = true; |
| 8809 | - $('.mes_buttons:last').hide(); | |
| 8810 | 8856 | await Generate('swipe'); |
| 8811 | 8857 | } else { |
| 8812 | 8858 | if (parseInt(chat[chat.length - 1]['swipe_id']) !== chat[chat.length - 1]['swipes'].length) { |
| @@ -9408,6 +9454,9 @@ export async function deleteCharacter(characterKey, { deleteChats = true } = {}) | ||
| 9408 | 9454 | continue; |
| 9409 | 9455 | } |
| 9410 | 9456 | |
| 9457 | + accountStorage.removeItem(`AlertWI_${character.avatar}`); | |
| 9458 | + accountStorage.removeItem(`AlertRegex_${character.avatar}`); | |
| 9459 | + accountStorage.removeItem(`mediaWarningShown:${character.avatar}`); | |
| 9411 | 9460 | delete tag_map[character.avatar]; |
| 9412 | 9461 | select_rm_info('char_delete', character.name); |
| 9413 | 9462 | |
| @@ -9610,8 +9659,8 @@ function addDebugFunctions() { | ||
| 9610 | 9659 | }); |
| 9611 | 9660 | |
| 9612 | 9661 | registerDebugFunction('toggleRegenerateWarning', 'Toggle Ctrl+Enter regeneration confirmation', 'Toggle the warning when regenerating a message with a Ctrl+Enter hotkey.', () => { |
| 9613 | 9662 | localStorageaccountStorage.setItem('RegenerateWithCtrlEnter', localStorageaccountStorage.getItem('RegenerateWithCtrlEnter') === 'true' ? 'false' : 'true'); |
| 9614 | 9663 | toastr.info('Regenerate warning is now ' + (localStorageaccountStorage.getItem('RegenerateWithCtrlEnter') === 'true' ? 'disabled' : 'enabled')); |
| 9615 | 9664 | }); |
| 9616 | 9665 | |
| 9617 | 9666 | registerDebugFunction('copySetup', 'Copy ST setup to clipboard [WIP]', 'Useful data when reporting bugs', async () => { |
| @@ -10796,6 +10845,12 @@ jQuery(async function () { | ||
| 10796 | 10845 | var edit_mes_id = $(this).closest('.mes').attr('mesid'); |
| 10797 | 10846 | this_edit_mes_id = edit_mes_id; |
| 10798 | 10847 | |
| 10848 | + // Also edit reasoning, if it exists | |
| 10849 | + const reasoningEdit = $(this).closest('.mes_block').find('.mes_reasoning_edit:visible'); | |
| 10850 | + if (reasoningEdit.length > 0) { | |
| 10851 | + reasoningEdit.trigger('click'); | |
| 10852 | + } | |
| 10853 | + | |
| 10799 | 10854 | var text = chat[edit_mes_id]['mes']; |
| 10800 | 10855 | if (chat[edit_mes_id]['is_user']) { |
| 10801 | 10856 | this_edit_mes_chname = name1; |
| @@ -10923,10 +10978,17 @@ jQuery(async function () { | ||
| 10923 | 10978 | chat[this_edit_mes_id].is_system, |
| 10924 | 10979 | chat[this_edit_mes_id].is_user, |
| 10925 | 10980 | this_edit_mes_id, |
| 10981 | + {}, | |
| 10982 | + false, | |
| 10926 | 10983 | )); |
| 10927 | 10984 | appendMediaToMessage(chat[this_edit_mes_id], $(this).closest('.mes')); |
| 10928 | 10985 | addCopyToCodeBlocks($(this).closest('.mes')); |
| 10929 | 10986 | |
| 10987 | + const reasoningEditDone = $(this).closest('.mes_block').find('.mes_reasoning_edit_cancel:visible'); | |
| 10988 | + if (reasoningEditDone.length > 0) { | |
| 10989 | + reasoningEditDone.trigger('click'); | |
| 10990 | + } | |
| 10991 | + | |
| 10930 | 10992 | await eventSource.emit(event_types.MESSAGE_UPDATED, this_edit_mes_id); |
| 10931 | 10993 | this_edit_mes_id = undefined; |
| 10932 | 10994 | }); |
| @@ -10990,7 +11052,7 @@ jQuery(async function () { | ||
| 10990 | 11052 | }); |
| 10991 | 11053 | |
| 10992 | 11054 | $(document).on('click', '.mes_edit_copy', async function () { |
| 10993 | 11055 | const confirmation = await callGenericPopup('t`Create a copy of this message?'`, POPUP_TYPE.CONFIRM); |
| 10994 | 11056 | if (!confirmation) { |
| 10995 | 11057 | return; |
| 10996 | 11058 | } |
| @@ -11469,7 +11531,7 @@ jQuery(async function () { | ||
| 11469 | 11531 | ); |
| 11470 | 11532 | break;*/ |
| 11471 | 11533 | default: |
| 11472 | 11534 | await eventSource.emit('charManagementDropdown', target); |
| 11473 | 11535 | } |
| 11474 | 11536 | $('#char-management-dropdown').prop('selectedIndex', 0); |
| 11475 | 11537 | }); |
| @@ -11631,7 +11693,7 @@ jQuery(async function () { | ||
| 11631 | 11693 | |
| 11632 | 11694 | $(document).on('click', '.open_characters_library', async function () { |
| 11633 | 11695 | await getCharacters(); |
| 11634 | 11696 | await eventSource.emit(event_types.OPEN_CHARACTER_LIBRARY); |
| 11635 | 11697 | }); |
| 11636 | 11698 | |
| 11637 | 11699 | // Added here to prevent execution before script.js is loaded and get rid of quirky timeouts |
| @@ -27,7 +27,6 @@ import { | ||
| 27 | 27 | send_on_enter_options, |
| 28 | 28 | } from './power-user.js'; |
| 29 | 29 | |
| 30 | -import { LoadLocal, SaveLocal, LoadLocalBool } from './f-localStorage.js'; | |
| 31 | 30 | import { selected_group, is_group_generating, openGroupById } from './group-chats.js'; |
| 32 | 31 | import { getTagKeyForEntity, applyTagsOnCharacterSelect } from './tags.js'; |
| 33 | 32 | import { |
| @@ -41,6 +40,8 @@ import { textgen_types, textgenerationwebui_settings as textgen_settings, getTex | ||
| 41 | 40 | import { debounce_timeout } from './constants.js'; |
| 42 | 41 | |
| 43 | 42 | import { Popup } from './popup.js'; |
| 43 | +import { accountStorage } from './util/AccountStorage.js'; | |
| 44 | +import { getCurrentUserHandle } from './user.js'; | |
| 44 | 45 | |
| 45 | 46 | var RPanelPin = document.getElementById('rm_button_panel_pin'); |
| 46 | 47 | var LPanelPin = document.getElementById('lm_button_panel_pin'); |
| @@ -279,17 +280,32 @@ async function RA_autoloadchat() { | ||
| 279 | 280 | // active character is the name, we should look it up in the character list and get the id |
| 280 | 281 | if (active_character !== null && active_character !== undefined) { |
| 281 | 282 | const active_character_id = characters.findIndex(x => getTagKeyForEntity(x) === active_character); |
| 282 | 283 | if (active_character_id !== null-1) { |
| 283 | 284 | await selectCharacterById(active_character_id); |
| 284 | 285 | |
| 285 | 286 | // Do a little tomfoolery to spoof the tag selector |
| 286 | 287 | const selectedCharElement = $(`#rm_print_characters_block .character_select[chid="${active_character_id}"]`); |
| 287 | 288 | applyTagsOnCharacterSelect.call(selectedCharElement); |
| 289 | + } else { | |
| 290 | + setActiveCharacter(null); | |
| 291 | + saveSettingsDebounced(); | |
| 292 | + console.warn(`Currently active character with ID ${active_character} not found. Resetting to no active character.`); | |
| 288 | 293 | } |
| 289 | 294 | } |
| 290 | 295 | |
| 291 | 296 | if (active_group !== null && active_group !== undefined) { |
| 292 | - await openGroupById(String(active_group)); | |
| 297 | + if (active_character) { | |
| 298 | + console.warn('Active character and active group are both set. Only active character will be loaded. Resetting active group.'); | |
| 299 | + setActiveGroup(null); | |
| 300 | + saveSettingsDebounced(); | |
| 301 | + } else { | |
| 302 | + const result = await openGroupById(String(active_group)); | |
| 303 | + if (!result) { | |
| 304 | + setActiveGroup(null); | |
| 305 | + saveSettingsDebounced(); | |
| 306 | + console.warn(`Currently active group with ID ${active_group} not found. Resetting to no active group.`); | |
| 307 | + } | |
| 308 | + } | |
| 293 | 309 | } |
| 294 | 310 | |
| 295 | 311 | // if the character list hadn't been loaded yet, try again. |
| @@ -409,32 +425,34 @@ function RA_autoconnect(PrevApi) { | ||
| 409 | 425 | function OpenNavPanels() { |
| 410 | 426 | if (!isMobile()) { |
| 411 | 427 | //auto-open R nav if locked and previously open |
| 412 | 428 | if (LoadLocalBoolaccountStorage.getItem('NavLockOn') == 'true' && LoadLocalBoolaccountStorage.getItem('NavOpened') == 'true') { |
| 413 | 429 | //console.log("RA -- clicking right nav to open"); |
| 414 | 430 | $('#rightNavDrawerIcon').click(); |
| 415 | 431 | } |
| 416 | 432 | |
| 417 | 433 | //auto-open L nav if locked and previously open |
| 418 | 434 | if (LoadLocalBoolaccountStorage.getItem('LNavLockOn') == 'true' && LoadLocalBoolaccountStorage.getItem('LNavOpened') == 'true') { |
| 419 | 435 | console.debug('RA -- clicking left nav to open'); |
| 420 | 436 | $('#leftNavDrawerIcon').click(); |
| 421 | 437 | } |
| 422 | 438 | |
| 423 | 439 | //auto-open WI if locked and previously open |
| 424 | 440 | if (LoadLocalBoolaccountStorage.getItem('WINavLockOn') == 'true' && LoadLocalBoolaccountStorage.getItem('WINavOpened') == 'true') { |
| 425 | 441 | console.debug('RA -- clicking WI to open'); |
| 426 | 442 | $('#WIDrawerIcon').click(); |
| 427 | 443 | } |
| 428 | 444 | } |
| 429 | 445 | } |
| 430 | 446 | |
| 447 | +const getUserInputKey = () => getCurrentUserHandle() + '_userInput'; | |
| 448 | + | |
| 431 | 449 | function restoreUserInput() { |
| 432 | 450 | if (!power_user.restore_user_input) { |
| 433 | 451 | console.debug('restoreUserInput disabled'); |
| 434 | 452 | return; |
| 435 | 453 | } |
| 436 | 454 | |
| 437 | 455 | const userInput = LoadLocallocalStorage.getItem('userInput'getUserInputKey()); |
| 438 | 456 | if (userInput) { |
| 439 | 457 | $('#send_textarea').val(userInput)[0].dispatchEvent(new Event('input', { bubbles: true })); |
| 440 | 458 | } |
| @@ -442,7 +460,8 @@ function restoreUserInput() { | ||
| 442 | 460 | |
| 443 | 461 | function saveUserInput() { |
| 444 | 462 | const userInput = String($('#send_textarea').val()); |
| 445 | 463 | SaveLocallocalStorage.setItem('userInput'getUserInputKey(), userInput); |
| 464 | + console.debug('User Input -- ', userInput); | |
| 446 | 465 | } |
| 447 | 466 | const saveUserInputDebounced = debounce(saveUserInput); |
| 448 | 467 | |
| @@ -739,7 +758,7 @@ export function initRossMods() { | ||
| 739 | 758 | |
| 740 | 759 | //toggle pin class when lock toggle clicked |
| 741 | 760 | $(RPanelPin).on('click', function () { |
| 742 | 761 | SaveLocalaccountStorage.setItem('NavLockOn', $(RPanelPin).prop('checked')); |
| 743 | 762 | if ($(RPanelPin).prop('checked') == true) { |
| 744 | 763 | //console.log('adding pin class to right nav'); |
| 745 | 764 | $(RightNavPanel).addClass('pinnedOpen'); |
| @@ -757,7 +776,7 @@ export function initRossMods() { | ||
| 757 | 776 | } |
| 758 | 777 | }); |
| 759 | 778 | $(LPanelPin).on('click', function () { |
| 760 | 779 | SaveLocalaccountStorage.setItem('LNavLockOn', $(LPanelPin).prop('checked')); |
| 761 | 780 | if ($(LPanelPin).prop('checked') == true) { |
| 762 | 781 | //console.log('adding pin class to Left nav'); |
| 763 | 782 | $(LeftNavPanel).addClass('pinnedOpen'); |
| @@ -776,7 +795,7 @@ export function initRossMods() { | ||
| 776 | 795 | }); |
| 777 | 796 | |
| 778 | 797 | $(WIPanelPin).on('click', function () { |
| 779 | 798 | SaveLocalaccountStorage.setItem('WINavLockOn', $(WIPanelPin).prop('checked')); |
| 780 | 799 | if ($(WIPanelPin).prop('checked') == true) { |
| 781 | 800 | console.debug('adding pin class to WI'); |
| 782 | 801 | $(WorldInfo).addClass('pinnedOpen'); |
| @@ -796,8 +815,8 @@ export function initRossMods() { | ||
| 796 | 815 | }); |
| 797 | 816 | |
| 798 | 817 | // read the state of right Nav Lock and apply to rightnav classlist |
| 799 | 818 | $(RPanelPin).prop('checked', LoadLocalBoolaccountStorage.getItem('NavLockOn') == 'true'); |
| 800 | 819 | if (LoadLocalBoolaccountStorage.getItem('NavLockOn') == 'true') { |
| 801 | 820 | //console.log('setting pin class via local var'); |
| 802 | 821 | $(RightNavPanel).addClass('pinnedOpen'); |
| 803 | 822 | $(RightNavDrawerIcon).addClass('drawerPinnedOpen'); |
| @@ -808,8 +827,8 @@ export function initRossMods() { | ||
| 808 | 827 | $(RightNavDrawerIcon).addClass('drawerPinnedOpen'); |
| 809 | 828 | } |
| 810 | 829 | // read the state of left Nav Lock and apply to leftnav classlist |
| 811 | 830 | $(LPanelPin).prop('checked', LoadLocalBoolaccountStorage.getItem('LNavLockOn') === 'true'); |
| 812 | 831 | if (LoadLocalBoolaccountStorage.getItem('LNavLockOn') == 'true') { |
| 813 | 832 | //console.log('setting pin class via local var'); |
| 814 | 833 | $(LeftNavPanel).addClass('pinnedOpen'); |
| 815 | 834 | $(LeftNavDrawerIcon).addClass('drawerPinnedOpen'); |
| @@ -821,8 +840,8 @@ export function initRossMods() { | ||
| 821 | 840 | } |
| 822 | 841 | |
| 823 | 842 | // read the state of left Nav Lock and apply to leftnav classlist |
| 824 | 843 | $(WIPanelPin).prop('checked', LoadLocalBoolaccountStorage.getItem('WINavLockOn') === 'true'); |
| 825 | 844 | if (LoadLocalBoolaccountStorage.getItem('WINavLockOn') == 'true') { |
| 826 | 845 | //console.log('setting pin class via local var'); |
| 827 | 846 | $(WorldInfo).addClass('pinnedOpen'); |
| 828 | 847 | $(WIDrawerIcon).addClass('drawerPinnedOpen'); |
| @@ -837,22 +856,22 @@ export function initRossMods() { | ||
| 837 | 856 | //save state of Right nav being open or closed |
| 838 | 857 | $('#rightNavDrawerIcon').on('click', function () { |
| 839 | 858 | if (!$('#rightNavDrawerIcon').hasClass('openIcon')) { |
| 840 | 859 | SaveLocalaccountStorage.setItem('NavOpened', 'true'); |
| 841 | 860 | } else { SaveLocalaccountStorage.setItem('NavOpened', 'false'); } |
| 842 | 861 | }); |
| 843 | 862 | |
| 844 | 863 | //save state of Left nav being open or closed |
| 845 | 864 | $('#leftNavDrawerIcon').on('click', function () { |
| 846 | 865 | if (!$('#leftNavDrawerIcon').hasClass('openIcon')) { |
| 847 | 866 | SaveLocalaccountStorage.setItem('LNavOpened', 'true'); |
| 848 | 867 | } else { SaveLocalaccountStorage.setItem('LNavOpened', 'false'); } |
| 849 | 868 | }); |
| 850 | 869 | |
| 851 | 870 | //save state of Left nav being open or closed |
| 852 | 871 | $('#WorldInfo').on('click', function () { |
| 853 | 872 | if (!$('#WorldInfo').hasClass('openIcon')) { |
| 854 | 873 | SaveLocalaccountStorage.setItem('WINavOpened', 'true'); |
| 855 | 874 | } else { SaveLocalaccountStorage.setItem('WINavOpened', 'false'); } |
| 856 | 875 | }); |
| 857 | 876 | |
| 858 | 877 | var chatbarInFocus = false; |
| @@ -868,8 +887,8 @@ export function initRossMods() { | ||
| 868 | 887 | OpenNavPanels(); |
| 869 | 888 | }, 300); |
| 870 | 889 | |
| 871 | 890 | $(SelectedCharacterTab).click(function () { SaveLocalaccountStorage.setItem('SelectedNavTab', 'rm_button_selected_ch'); }); |
| 872 | 891 | $('#rm_button_characters').click(function () { SaveLocalaccountStorage.setItem('SelectedNavTab', 'rm_button_characters'); }); |
| 873 | 892 | |
| 874 | 893 | // when a char is selected from the list, save them as the auto-load character for next page load |
| 875 | 894 | |
| @@ -1063,14 +1082,21 @@ export function initRossMods() { | ||
| 1063 | 1082 | // Ctrl+Enter for Regeneration Last Response. If editing, accept the edits instead |
| 1064 | 1083 | if (event.ctrlKey && event.key == 'Enter') { |
| 1065 | 1084 | const editMesDone = $('.mes_edit_done:visible'); |
| 1085 | + const reasoningMesDone = $('.mes_reasoning_edit_done:visible'); | |
| 1066 | 1086 | if (editMesDone.length > 0) { |
| 1067 | 1087 | console.debug('Accepting edits with Ctrl+Enter'); |
| 1068 | 1088 | $('#send_textarea').focustrigger('focus'); |
| 1069 | 1089 | editMesDone.trigger('click'); |
| 1070 | 1090 | return; |
| 1071 | 1091 | } else if (is_send_pressreasoningMesDone.length ==> false0) { |
| 1092 | + console.debug('Accepting edits with Ctrl+Enter'); | |
| 1093 | + $('#send_textarea').trigger('focus'); | |
| 1094 | + reasoningMesDone.trigger('click'); | |
| 1095 | + return; | |
| 1096 | + } | |
| 1097 | + else if (is_send_press == false) { | |
| 1072 | 1098 | const skipConfirmKey = 'RegenerateWithCtrlEnter'; |
| 1073 | 1099 | const skipConfirm = LoadLocalBoolaccountStorage.getItem(skipConfirmKey) === 'true'; |
| 1074 | 1100 | function doRegenerate() { |
| 1075 | 1101 | console.debug('Regenerating with Ctrl+Enter'); |
| 1076 | 1102 | $('#option_regenerate').trigger('click'); |
| @@ -1082,13 +1108,15 @@ export function initRossMods() { | ||
| 1082 | 1108 | let regenerateWithCtrlEnter = false; |
| 1083 | 1109 | const result = await Popup.show.confirm('Regenerate Message', 'Are you sure you want to regenerate the latest message?', { |
| 1084 | 1110 | customInputs: [{ id: 'regenerateWithCtrlEnter', label: 'Don\'t ask again' }], |
| 1085 | 1111 | onClose: (popup) => regenerateWithCtrlEnter = popup.inputResults.get('regenerateWithCtrlEnter') ?? false,{ |
| 1112 | + regenerateWithCtrlEnter = popup.inputResults.get('regenerateWithCtrlEnter') ?? false; | |
| 1113 | + }, | |
| 1086 | 1114 | }); |
| 1087 | 1115 | if (!result) { |
| 1088 | 1116 | return; |
| 1089 | 1117 | } |
| 1090 | 1118 | |
| 1091 | 1119 | SaveLocalaccountStorage.setItem(skipConfirmKey, String(regenerateWithCtrlEnter)); |
| 1092 | 1120 | doRegenerate(); |
| 1093 | 1121 | } |
| 1094 | 1122 | return; |
| @@ -566,7 +566,7 @@ export function initAuthorsNote() { | ||
| 566 | 566 | namedArgumentList: [], |
| 567 | 567 | unnamedArgumentList: [ |
| 568 | 568 | new SlashCommandArgument( |
| 569 | 569 | 'positionrole', [ARGUMENT_TYPE.STRING], false, false, null, ['system', 'user', 'assistant'], |
| 570 | 570 | ), |
| 571 | 571 | ], |
| 572 | 572 | helpString: ` |
| @@ -96,8 +96,13 @@ function highlightLockedBackground() { | ||
| 96 | 96 | }); |
| 97 | 97 | } |
| 98 | 98 | |
| 99 | +/** | |
| 100 | + * Locks the background for the current chat | |
| 101 | + * @param {Event} e Click event | |
| 102 | + * @returns {string} Empty string | |
| 103 | + */ | |
| 99 | 104 | function onLockBackgroundClick(e) { |
| 100 | 105 | e?.stopPropagation(); |
| 101 | 106 | |
| 102 | 107 | const chatName = getCurrentChatId(); |
| 103 | 108 | |
| @@ -106,7 +111,7 @@ function onLockBackgroundClick(e) { | ||
| 106 | 111 | return ''; |
| 107 | 112 | } |
| 108 | 113 | |
| 109 | 114 | const relativeBgImage = getUrlParameter(this) ?? background_settings.url; |
| 110 | 115 | |
| 111 | 116 | saveBackgroundMetadata(relativeBgImage); |
| 112 | 117 | setCustomBackground(); |
| @@ -114,8 +119,13 @@ function onLockBackgroundClick(e) { | ||
| 114 | 119 | return ''; |
| 115 | 120 | } |
| 116 | 121 | |
| 122 | +/** | |
| 123 | + * Locks the background for the current chat | |
| 124 | + * @param {Event} e Click event | |
| 125 | + * @returns {string} Empty string | |
| 126 | + */ | |
| 117 | 127 | function onUnlockBackgroundClick(e) { |
| 118 | 128 | e?.stopPropagation(); |
| 119 | 129 | removeBackgroundMetadata(); |
| 120 | 130 | unsetCustomBackground(); |
| 121 | 131 | highlightLockedBackground(); |
| @@ -513,12 +523,12 @@ export function initBackgrounds() { | ||
| 513 | 523 | $('#add_bg_button').on('change', onBackgroundUploadSelected); |
| 514 | 524 | $('#bg-filter').on('input', onBackgroundFilterInput); |
| 515 | 525 | SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'lockbg', |
| 516 | - callback: onLockBackgroundClick, | |
| 526 | + callback: () => onLockBackgroundClick(new CustomEvent('click')), | |
| 517 | 527 | aliases: ['bglock'], |
| 518 | 528 | helpString: 'Locks a background for the currently selected chat', |
| 519 | 529 | })); |
| 520 | 530 | SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'unlockbg', |
| 521 | - callback: onUnlockBackgroundClick, | |
| 531 | + callback: () => onUnlockBackgroundClick(new CustomEvent('click')), | |
| 522 | 532 | aliases: ['bgunlock'], |
| 523 | 533 | helpString: 'Unlocks a background for the currently selected chat', |
| 524 | 534 | })); |
| @@ -69,6 +69,7 @@ const hash_derivations = { | ||
| 69 | 69 | // DeepSeek R1 |
| 70 | 70 | 'b6835114b7303ddd78919a82e4d9f7d8c26ed0d7dfc36beeb12d524f6144eab1': |
| 71 | 71 | 'DeepSeek-V2.5' |
| 72 | + , | |
| 72 | 73 | }; |
| 73 | 74 | |
| 74 | 75 | const substr_derivations = { |
| @@ -97,6 +98,6 @@ export async function deriveTemplatesFromChatTemplate(chat_template, hash) { | ||
| 97 | 98 | } |
| 98 | 99 | } |
| 99 | 100 | |
| 100 | 101 | console.logwarn(`Unknown chat template hash: ${hash} for [${chat_template}]`); |
| 101 | 102 | return null; |
| 102 | 103 | } |
| @@ -45,6 +45,7 @@ import { DragAndDropHandler } from './dragdrop.js'; | ||
| 45 | 45 | import { renderTemplateAsync } from './templates.js'; |
| 46 | 46 | import { t } from './i18n.js'; |
| 47 | 47 | import { humanizedDateTime } from './RossAscends-mods.js'; |
| 48 | +import { accountStorage } from './util/AccountStorage.js'; | |
| 48 | 49 | |
| 49 | 50 | /** |
| 50 | 51 | * @typedef {Object} FileAttachment |
| @@ -621,21 +622,56 @@ async function enlargeMessageImage() { | ||
| 621 | 622 | } |
| 622 | 623 | |
| 623 | 624 | async function deleteMessageImage() { |
| 624 | 625 | const value = await callGenericPopup('<h3>Delete image from message?<br>This action can\'t be undone.</h3>', POPUP_TYPE.CONFIRM);TEXT, '', { |
| 626 | + okButton: t`Delete one`, | |
| 627 | + customButtons: [ | |
| 628 | + { | |
| 629 | + text: t`Delete all`, | |
| 630 | + appendAtEnd: true, | |
| 631 | + result: POPUP_RESULT.CUSTOM1, | |
| 632 | + }, | |
| 633 | + { | |
| 634 | + text: t`Cancel`, | |
| 635 | + appendAtEnd: true, | |
| 636 | + result: POPUP_RESULT.CANCELLED, | |
| 637 | + }, | |
| 638 | + ], | |
| 639 | + }); | |
| 625 | 640 | |
| 626 | 641 | if (value !== POPUP_RESULT.AFFIRMATIVEvalue) { |
| 627 | 642 | return; |
| 628 | 643 | } |
| 629 | 644 | |
| 630 | 645 | const mesBlock = $(this).closest('.mes'); |
| 631 | 646 | const mesId = mesBlock.attr('mesid'); |
| 632 | 647 | const message = chat[mesId]; |
| 648 | + | |
| 649 | + let isLastImage = true; | |
| 650 | + | |
| 651 | + if (Array.isArray(message.extra.image_swipes)) { | |
| 652 | + const indexOf = message.extra.image_swipes.indexOf(message.extra.image); | |
| 653 | + if (indexOf > -1) { | |
| 654 | + message.extra.image_swipes.splice(indexOf, 1); | |
| 655 | + isLastImage = message.extra.image_swipes.length === 0; | |
| 656 | + if (!isLastImage) { | |
| 657 | + const newIndex = Math.min(indexOf, message.extra.image_swipes.length - 1); | |
| 658 | + message.extra.image = message.extra.image_swipes[newIndex]; | |
| 659 | + } | |
| 660 | + } | |
| 661 | + } | |
| 662 | + | |
| 663 | + if (isLastImage || value === POPUP_RESULT.CUSTOM1) { | |
| 633 | 664 | delete message.extra.image; |
| 634 | 665 | delete message.extra.inline_image; |
| 635 | 666 | delete message.extra.title; |
| 636 | 667 | delete message.extra.append_title; |
| 668 | + delete message.extra.image_swipes; | |
| 637 | 669 | mesBlock.find('.mes_img_container').removeClass('img_extra'); |
| 638 | 670 | mesBlock.find('.mes_img').attr('src', ''); |
| 671 | + } else { | |
| 672 | + appendMediaToMessage(message, mesBlock); | |
| 673 | + } | |
| 674 | + | |
| 639 | 675 | await saveChatConditional(); |
| 640 | 676 | } |
| 641 | 677 | |
| @@ -1043,8 +1079,8 @@ async function openAttachmentManager() { | ||
| 1043 | 1079 | renderAttachments(); |
| 1044 | 1080 | }); |
| 1045 | 1081 | |
| 1046 | 1082 | let sortField = localStorageaccountStorage.getItem('DataBank_sortField') || 'created'; |
| 1047 | 1083 | let sortOrder = localStorageaccountStorage.getItem('DataBank_sortOrder') || 'desc'; |
| 1048 | 1084 | let filterString = ''; |
| 1049 | 1085 | |
| 1050 | 1086 | const template = $(await renderExtensionTemplateAsync('attachments', 'manager', {})); |
| @@ -1060,8 +1096,8 @@ async function openAttachmentManager() { | ||
| 1060 | 1096 | |
| 1061 | 1097 | sortField = this.selectedOptions[0].dataset.sortField; |
| 1062 | 1098 | sortOrder = this.selectedOptions[0].dataset.sortOrder; |
| 1063 | 1099 | localStorageaccountStorage.setItem('DataBank_sortField', sortField); |
| 1064 | 1100 | localStorageaccountStorage.setItem('DataBank_sortOrder', sortOrder); |
| 1065 | 1101 | renderAttachments(); |
| 1066 | 1102 | }); |
| 1067 | 1103 | function handleBulkAction(action) { |
| @@ -1451,7 +1487,7 @@ jQuery(function () { | ||
| 1451 | 1487 | ...chat.filter(x => x?.extra?.type !== system_message_types.ASSISTANT_NOTE), |
| 1452 | 1488 | ]; |
| 1453 | 1489 | |
| 1454 | 1490 | download(JSONchatToSave.stringifymap(chatToSave,(m) null,=> 4JSON.stringify(m)).join('\n'), `Assistant - ${humanizedDateTime()}.jsonjsonl`, 'application/json'); |
| 1455 | 1491 | }); |
| 1456 | 1492 | |
| 1457 | 1493 | // Do not change. #attachFile is added by extension. |
| @@ -9,6 +9,7 @@ import { getContext } from './st-context.js'; | ||
| 9 | 9 | import { isAdmin } from './user.js'; |
| 10 | 10 | import { t } from './i18n.js'; |
| 11 | 11 | import { debounce_timeout } from './constants.js'; |
| 12 | +import { accountStorage } from './util/AccountStorage.js'; | |
| 12 | 13 | |
| 13 | 14 | export { |
| 14 | 15 | getContext, |
| @@ -153,8 +154,18 @@ export const extension_settings = { | ||
| 153 | 154 | refine_mode: false, |
| 154 | 155 | }, |
| 155 | 156 | expressions: { |
| 157 | + /** @type {number} see `EXPRESSION_API` */ | |
| 158 | + api: undefined, | |
| 156 | 159 | /** @type {string[]} */ |
| 157 | 160 | custom: [], |
| 161 | + showDefault: false, | |
| 162 | + translate: false, | |
| 163 | + /** @type {string} */ | |
| 164 | + fallback_expression: undefined, | |
| 165 | + /** @type {string} */ | |
| 166 | + llmPrompt: undefined, | |
| 167 | + allowMultiple: true, | |
| 168 | + rerollIfSame: false, | |
| 158 | 169 | }, |
| 159 | 170 | connectionManager: { |
| 160 | 171 | selectedProfile: '', |
| @@ -602,12 +613,12 @@ function generateExtensionHtml(name, manifest, isActive, isDisabled, isExternal, | ||
| 602 | 613 | } |
| 603 | 614 | |
| 604 | 615 | let toggleElement = isActive || isDisabled ? |
| 605 | 616 | `'<input type="checkbox" title="' + t`Click to toggle` + `" data-name="${name}" class="${isActive ? 'toggle_disable' : 'toggle_enable'} ${checkboxClass}" ${isActive ? 'checked' : ''}>` : |
| 606 | 617 | `<input type="checkbox" title="Cannot enable extension" data-name="${name}" class="extension_missing ${checkboxClass}" disabled>`; |
| 607 | 618 | |
| 608 | 619 | let deleteButton = isExternal ? `<button class="btn_delete menu_button" data-name="${externalId}" data-i18n="[title]Delete" title="Delete"><i class="fa-fw fa-solid fa-trash-can"></i></button>` : ''; |
| 609 | 620 | let updateButton = isExternal ? `<button class="btn_update menu_button displayNone" data-name="${externalId}" title="Update available"><i class="fa-solid fa-download fa-fw"></i></button>` : ''; |
| 610 | 621 | let moveButton = isExternal && isUserAdmin ? `<button class="btn_move menu_button" data-name="${externalId}" data-i18n="[title]Move" title="Move"><i class="fa-solid fa-folder-tree fa-fw"></i></button>` : ''; |
| 611 | 622 | let modulesInfo = ''; |
| 612 | 623 | |
| 613 | 624 | if (isActive && Array.isArray(manifest.optional)) { |
| @@ -615,7 +626,7 @@ function generateExtensionHtml(name, manifest, isActive, isDisabled, isExternal, | ||
| 615 | 626 | modules.forEach(x => optional.delete(x)); |
| 616 | 627 | if (optional.size > 0) { |
| 617 | 628 | const optionalString = DOMPurify.sanitize([...optional].join(', ')); |
| 618 | 629 | modulesInfo = `'<div class="extension_modules">' + t`Optional modules:` + ` <span class="optional">${optionalString}</span></div>`; |
| 619 | 630 | } |
| 620 | 631 | } else if (!isDisabled) { // Neither active nor disabled |
| 621 | 632 | const requirements = new Set(manifest.requires); |
| @@ -714,7 +725,7 @@ async function showExtensionsDetails() { | ||
| 714 | 725 | htmlExternal.append(htmlLoading); |
| 715 | 726 | |
| 716 | 727 | const sortOrderKey = 'extensions_sortByName'; |
| 717 | 728 | const sortByName = localStorageaccountStorage.getItem(sortOrderKey) === 'true'; |
| 718 | 729 | const sortFn = sortByName ? sortManifestsByName : sortManifestsByOrder; |
| 719 | 730 | const extensions = Object.entries(manifests).sort((a, b) => sortFn(a[1], b[1])).map(getExtensionData); |
| 720 | 731 | |
| @@ -745,7 +756,7 @@ async function showExtensionsDetails() { | ||
| 745 | 756 | text: sortByName ? t`Sort: Display Name` : t`Sort: Loading Order`, |
| 746 | 757 | action: async () => { |
| 747 | 758 | abortController.abort(); |
| 748 | 759 | localStorageaccountStorage.setItem(sortOrderKey, sortByName ? 'false' : 'true'); |
| 749 | 760 | await showExtensionsDetails(); |
| 750 | 761 | }, |
| 751 | 762 | }; |
| @@ -1153,11 +1164,11 @@ async function checkForExtensionUpdates(force) { | ||
| 1153 | 1164 | const currentDate = new Date().toDateString(); |
| 1154 | 1165 | |
| 1155 | 1166 | // Don't nag more than once a day |
| 1156 | 1167 | if (localStorageaccountStorage.getItem(STORAGE_NAG_KEY) === currentDate) { |
| 1157 | 1168 | return; |
| 1158 | 1169 | } |
| 1159 | 1170 | |
| 1160 | 1171 | localStorageaccountStorage.setItem(STORAGE_NAG_KEY, currentDate); |
| 1161 | 1172 | } |
| 1162 | 1173 | |
| 1163 | 1174 | const isCurrentUserAdmin = isAdmin(); |
| @@ -8,7 +8,9 @@ import { getRequestHeaders, processDroppedFiles, eventSource, event_types } from | ||
| 8 | 8 | import { deleteExtension, extensionNames, getContext, installExtension, renderExtensionTemplateAsync } from '../../extensions.js'; |
| 9 | 9 | import { POPUP_TYPE, Popup, callGenericPopup } from '../../popup.js'; |
| 10 | 10 | import { executeSlashCommands } from '../../slash-commands.js'; |
| 11 | +import { accountStorage } from '../../util/AccountStorage.js'; | |
| 11 | 12 | import { flashHighlight, getStringHash, isValidUrl } from '../../utils.js'; |
| 13 | +import { t } from '../../i18n.js'; | |
| 12 | 14 | export { MODULE_NAME }; |
| 13 | 15 | |
| 14 | 16 | const MODULE_NAME = 'assets'; |
| @@ -58,11 +60,11 @@ const KNOWN_TYPES = { | ||
| 58 | 60 | 'blip': 'Blip sounds', |
| 59 | 61 | }; |
| 60 | 62 | |
| 61 | 63 | async function downloadAssetsList(url) { |
| 62 | 64 | updateCurrentAssets().then(async function () { |
| 63 | 65 | fetch(url, { cache: 'no-cache' }) |
| 64 | 66 | .then(response => response.json()) |
| 65 | 67 | .then(jsonasync =>function(json) { |
| 66 | 68 | |
| 67 | 69 | availableAssets = {}; |
| 68 | 70 | $('#assets_menu').empty(); |
| @@ -83,10 +85,10 @@ function downloadAssetsList(url) { | ||
| 83 | 85 | |
| 84 | 86 | $('#assets_type_select').empty(); |
| 85 | 87 | $('#assets_search').val(''); |
| 86 | 88 | $('#assets_type_select').append($('<option />', { value: '', text: 't`All'` })); |
| 87 | 89 | |
| 88 | 90 | for (const type of assetTypes) { |
| 89 | 91 | const option = $('<option />', { value: type, text: t([KNOWN_TYPES[type] || type]) }); |
| 90 | 92 | $('#assets_type_select').append(option); |
| 91 | 93 | } |
| 92 | 94 | |
| @@ -103,11 +105,7 @@ function downloadAssetsList(url) { | ||
| 103 | 105 | assetTypeMenu.append(`<h3>${KNOWN_TYPES[assetType] || assetType}</h3>`).hide(); |
| 104 | 106 | |
| 105 | 107 | if (assetType == 'extension') { |
| 106 | - assetTypeMenu.append(` | |
| 108 | + assetTypeMenu.append(await renderExtensionTemplateAsync('assets', 'installation')); | |
| 107 | - <div class="assets-list-git"> | |
| 108 | - To download extensions from this page, you need to have <a href="https://git-scm.com/downloads" target="_blank">Git</a> installed.<br> | |
| 109 | - Click the <i class="fa-solid fa-sm fa-arrow-up-right-from-square"></i> icon to visit the Extension's repo for tips on how to use it. | |
| 110 | - </div>`); | |
| 111 | 109 | } |
| 112 | 110 | |
| 113 | 111 | for (const i in availableAssets[assetType].sort((a, b) => a?.name && b?.name && a['name'].localeCompare(b['name']))) { |
| @@ -183,7 +181,7 @@ function downloadAssetsList(url) { | ||
| 183 | 181 | const displayName = DOMPurify.sanitize(asset['name'] || asset['id']); |
| 184 | 182 | const description = DOMPurify.sanitize(asset['description'] || ''); |
| 185 | 183 | const url = isValidUrl(asset['url']) ? asset['url'] : ''; |
| 186 | 184 | const title = assetType === 'extension' ? t`Extension repo/guide:` + ` ${url}` : 't`Preview in browser'`; |
| 187 | 185 | const previewIcon = (assetType === 'extension' || assetType === 'character') ? 'fa-arrow-up-right-from-square' : 'fa-headphones-simple'; |
| 188 | 186 | const toolTag = assetType === 'extension' && asset['tool']; |
| 189 | 187 | |
| @@ -194,9 +192,10 @@ function downloadAssetsList(url) { | ||
| 194 | 192 | <b>${displayName}</b> |
| 195 | 193 | <a class="asset_preview" href="${url}" target="_blank" title="${title}"> |
| 196 | 194 | <i class="fa-solid fa-sm ${previewIcon}"></i> |
| 197 | 195 | </a>` + |
| 198 | 196 | ${(toolTag ? '<span class="tag" title="' + t`Adds a function tool` + '"><i class="fa-solid fa-sm fa-wrench"></i> Tool</span>' : ''}+ |
| 199 | - </span> | |
| 197 | + t`Tool` + '</span>' : '') + | |
| 198 | + `</span> | |
| 200 | 199 | <small class="asset-description"> |
| 201 | 200 | ${description} |
| 202 | 201 | </small> |
| @@ -432,14 +431,14 @@ jQuery(async () => { | ||
| 432 | 431 | connectButton.on('click', async function () { |
| 433 | 432 | const url = DOMPurify.sanitize(String(assetsJsonUrl.val())); |
| 434 | 433 | const rememberKey = `Assets_SkipConfirm_${getStringHash(url)}`; |
| 435 | 434 | const skipConfirm = localStorageaccountStorage.getItem(rememberKey) === 'true'; |
| 436 | 435 | |
| 437 | 436 | const confirmation = skipConfirm || await Popup.show.confirm('t`Loading Asset List'`, `'<span>' + t`Are you sure you want to connect to the following url?` + `</span><var>${url}</var>`, { |
| 438 | 437 | customInputs: [{ id: 'assets-remember', label: 'Don\'t ask again for this URL' }], |
| 439 | 438 | onClose: popup => { |
| 440 | 439 | if (popup.result) { |
| 441 | 440 | const rememberValue = popup.inputResults.get('assets-remember'); |
| 442 | 441 | localStorageaccountStorage.setItem(rememberKey, String(rememberValue)); |
| 443 | 442 | } |
| 444 | 443 | }, |
| 445 | 444 | }); |
| @@ -0,0 +1,4 @@ | ||
| 1 | +<div class="assets-list-git"> | |
| 2 | + <span data-i18n="extension_install_1">To download extensions from this page, you need to have </span><a href="https://git-scm.com/downloads" target="_blank">Git</a><span data-i18n="extension_install_2"> installed.</span><br> | |
| 3 | + <span data-i18n="extension_install_3">Click the </span><i class="fa-solid fa-sm fa-arrow-up-right-from-square"></i><span data-i18n="extension_install_4"> icon to visit the Extension's repo for tips on how to use it.</span> | |
| 4 | +</div> | |
| 4 | \ No newline at end of file | |
| @@ -33,7 +33,7 @@ To install a single 3rd party extension, use the "Install Extensions" | ||
| 33 | 33 | <div id="assets_filters" class="flex-container"> |
| 34 | 34 | <select id="assets_type_select" class="text_pole flex1"> |
| 35 | 35 | </select> |
| 36 | 36 | <input id="assets_search" class="text_pole flex1" data-i18n="[placeholder]Search" placeholder="Search" type="search"> |
| 37 | 37 | <div id="assets-characters-button" class="menu_button menu_button_icon"> |
| 38 | 38 | <i class="fa-solid fa-image-portrait"></i> |
| 39 | 39 | <span data-i18n="Characters">Characters</span> |
| @@ -10,7 +10,7 @@ | ||
| 10 | 10 | <select id="caption_source" class="text_pole"> |
| 11 | 11 | <option value="local" data-i18n="Local">Local</option> |
| 12 | 12 | <option value="multimodal" data-i18n="Multimodal (OpenAI / Anthropic / llama / Google)">Multimodal (OpenAI / Anthropic / llama / Google)</option> |
| 13 | 13 | <option value="extras" data-i18n="Extras">Extras (deprecated)</option> |
| 14 | 14 | <option value="horde" data-i18n="Horde">Horde</option> |
| 15 | 15 | </select> |
| 16 | 16 | <div id="caption_multimodal_block" class="flex-container wide100p"> |
| @@ -53,6 +53,12 @@ | ||
| 53 | 53 | <option data-type="anthropic" value="claude-3-opus-20240229">claude-3-opus-20240229</option> |
| 54 | 54 | <option data-type="anthropic" value="claude-3-sonnet-20240229">claude-3-sonnet-20240229</option> |
| 55 | 55 | <option data-type="anthropic" value="claude-3-haiku-20240307">claude-3-haiku-20240307</option> |
| 56 | + <option data-type="google" value="gemini-2.0-pro-exp">gemini-2.0-pro-exp</option> | |
| 57 | + <option data-type="google" value="gemini-2.0-pro-exp-02-05">gemini-2.0-pro-exp-02-05</option> | |
| 58 | + <option data-type="google" value="gemini-2.0-flash-lite-preview">gemini-2.0-flash-lite-preview</option> | |
| 59 | + <option data-type="google" value="gemini-2.0-flash-lite-preview-02-05">gemini-2.0-flash-lite-preview-02-05</option> | |
| 60 | + <option data-type="google" value="gemini-2.0-flash">gemini-2.0-flash</option> | |
| 61 | + <option data-type="google" value="gemini-2.0-flash-001">gemini-2.0-flash-001</option> | |
| 56 | 62 | <option data-type="google" value="gemini-2.0-flash-exp">gemini-2.0-flash-exp</option> |
| 57 | 63 | <option data-type="google" value="gemini-2.0-flash-thinking-exp">gemini-2.0-flash-thinking-exp</option> |
| 58 | 64 | <option data-type="google" value="gemini-2.0-flash-thinking-exp-01-21">gemini-2.0-flash-thinking-exp-01-21</option> |
| @@ -30,6 +30,7 @@ const CC_COMMANDS = [ | ||
| 30 | 30 | 'api-url', |
| 31 | 31 | 'model', |
| 32 | 32 | 'proxy', |
| 33 | + 'stop-strings', | |
| 33 | 34 | ]; |
| 34 | 35 | |
| 35 | 36 | const TC_COMMANDS = [ |
| @@ -43,6 +44,7 @@ const TC_COMMANDS = [ | ||
| 43 | 44 | 'context', |
| 44 | 45 | 'instruct-state', |
| 45 | 46 | 'tokenizer', |
| 47 | + 'stop-strings', | |
| 46 | 48 | ]; |
| 47 | 49 | |
| 48 | 50 | const FANCY_NAMES = { |
| @@ -57,6 +59,7 @@ const FANCY_NAMES = { | ||
| 57 | 59 | 'instruct': 'Instruct Template', |
| 58 | 60 | 'context': 'Context Template', |
| 59 | 61 | 'tokenizer': 'Tokenizer', |
| 62 | + 'stop-strings': 'Custom Stopping Strings', | |
| 60 | 63 | }; |
| 61 | 64 | |
| 62 | 65 | /** |
| @@ -138,6 +141,7 @@ const profilesProvider = () => [ | ||
| 138 | 141 | * @property {string} [context] Context Template |
| 139 | 142 | * @property {string} [instruct-state] Instruct Mode |
| 140 | 143 | * @property {string} [tokenizer] Tokenizer |
| 144 | + * @property {string} [stop-strings] Custom Stopping Strings | |
| 141 | 145 | * @property {string[]} [exclude] Commands to exclude |
| 142 | 146 | */ |
| 143 | 147 | |
| @@ -1,11 +1,11 @@ | ||
| 1 | 1 | import { Fuse } from '../../../lib.js'; |
| 2 | 2 | |
| 3 | 3 | import { callPopupcharacters, eventSource, event_types, generateRaw, getRequestHeaders, main_api, online_status, saveSettingsDebounced, substituteParams, substituteParamsExtended, system_message_types, this_chid } from '../../../script.js'; |
| 4 | 4 | import { dragElement, isMobile } from '../../RossAscends-mods.js'; |
| 5 | 5 | import { getContext, getApiUrl, modules, extension_settings, ModuleWorkerWrapper, doExtrasFetch, renderExtensionTemplateAsync } from '../../extensions.js'; |
| 6 | 6 | import { loadMovingUIState, performFuzzySearch, power_user } from '../../power-user.js'; |
| 7 | 7 | import { onlyUnique, debounce, getCharaFilename, trimToEndSentence, trimToStartSentence, waitUntilCondition, findChar } from '../../utils.js'; |
| 8 | 8 | import { hideMutedSprites, selected_group } from '../../group-chats.js'; |
| 9 | 9 | import { isJsonSchemaSupported } from '../../textgen-settings.js'; |
| 10 | 10 | import { debounce_timeout } from '../../constants.js'; |
| 11 | 11 | import { SlashCommandParser } from '../../slash-commands/SlashCommandParser.js'; |
| @@ -15,16 +15,32 @@ import { SlashCommandEnumValue, enumTypes } from '../../slash-commands/SlashComm | ||
| 15 | 15 | import { commonEnumProviders } from '../../slash-commands/SlashCommandCommonEnumsProvider.js'; |
| 16 | 16 | import { slashCommandReturnHelper } from '../../slash-commands/SlashCommandReturnHelper.js'; |
| 17 | 17 | import { generateWebLlmChatPrompt, isWebLlmSupported } from '../shared.js'; |
| 18 | +import { Popup, POPUP_RESULT } from '../../popup.js'; | |
| 19 | +import { t } from '../../i18n.js'; | |
| 18 | 20 | export { MODULE_NAME }; |
| 19 | 21 | |
| 22 | +/** | |
| 23 | +* @typedef {object} Expression Expression definition with label and file path | |
| 24 | +* @property {string} label The label of the expression | |
| 25 | +* @property {ExpressionImage[]} files One or more images to represent this expression | |
| 26 | +*/ | |
| 27 | + | |
| 28 | +/** | |
| 29 | + * @typedef {object} ExpressionImage An expression image | |
| 30 | + * @property {string} expression - The expression | |
| 31 | + * @property {boolean} [isCustom=false] - If the expression is added by user | |
| 32 | + * @property {string} fileName - The filename with extension | |
| 33 | + * @property {string} title - The title for the image | |
| 34 | + * @property {string} imageSrc - The image source / full path | |
| 35 | + * @property {'success' | 'additional' | 'failure'} type - The type of the image | |
| 36 | + */ | |
| 37 | + | |
| 20 | 38 | const MODULE_NAME = 'expressions'; |
| 21 | 39 | const UPDATE_INTERVAL = 2000; |
| 22 | 40 | const STREAMING_UPDATE_INTERVAL = 10000; |
| 23 | -const TALKINGCHECK_UPDATE_INTERVAL = 500; | |
| 24 | 41 | const DEFAULT_FALLBACK_EXPRESSION = 'joy'; |
| 25 | 42 | const DEFAULT_LLM_PROMPT = 'Ignore previous instructions. Classify the emotion of the last message. Output just one word, e.g. "joy" or "anger". Choose only one of the following labels: {{labels}}'; |
| 26 | 43 | const DEFAULT_EXPRESSIONS = [ |
| 27 | - 'talkinghead', | |
| 28 | 44 | 'admiration', |
| 29 | 45 | 'amusement', |
| 30 | 46 | 'anger', |
| @@ -54,6 +70,12 @@ const DEFAULT_EXPRESSIONS = [ | ||
| 54 | 70 | 'surprise', |
| 55 | 71 | 'neutral', |
| 56 | 72 | ]; |
| 73 | + | |
| 74 | +const OPTION_NO_FALLBACK = '#none'; | |
| 75 | +const OPTION_EMOJI_FALLBACK = '#emoji'; | |
| 76 | +const RESET_SPRITE_LABEL = '#reset'; | |
| 77 | + | |
| 78 | + | |
| 57 | 79 | /** @enum {number} */ |
| 58 | 80 | const EXPRESSION_API = { |
| 59 | 81 | local: 0, |
| @@ -65,35 +87,29 @@ const EXPRESSION_API = { | ||
| 65 | 87 | let expressionsList = null; |
| 66 | 88 | let lastCharacter = undefined; |
| 67 | 89 | let lastMessage = null; |
| 68 | -let lastTalkingState = false; | |
| 90 | +/** @type {{[characterKey: string]: Expression[]}} */ | |
| 69 | -let lastTalkingStateMessage = null; // last message as seen by `updateTalkingState` (tracked separately, different timer) | |
| 70 | 91 | let spriteCache = {}; |
| 71 | 92 | let inApiCall = false; |
| 72 | 93 | let lastServerResponseTime = 0; |
| 73 | -export let lastExpression = {}; | |
| 74 | - | |
| 75 | -function isTalkingHeadEnabled() { | |
| 76 | - return extension_settings.expressions.talkinghead && extension_settings.expressions.api == EXPRESSION_API.extras; | |
| 77 | -} | |
| 78 | 94 | |
| 79 | -/** | |
| 95 | +/** @type {{[characterName: string]: string}} */ | |
| 80 | - * Returns the fallback expression if explicitly chosen, otherwise the default one | |
| 96 | +export let lastExpression = {}; | |
| 81 | - * @returns {string} expression name | |
| 82 | - */ | |
| 83 | -function getFallbackExpression() { | |
| 84 | - return extension_settings.expressions.fallback_expression ?? DEFAULT_FALLBACK_EXPRESSION; | |
| 85 | -} | |
| 86 | 97 | |
| 87 | 98 | /** |
| 88 | - * Toggles Talkinghead mode on/off. | |
| 99 | + * Returns a placeholder image object for a given expression | |
| 89 | - * | |
| 100 | + * @param {string} expression - The expression label | |
| 90 | - * Implements the `/th` slash command, which is meant to be bound to a Quick Reply button | |
| 101 | + * @param {boolean} [isCustom=false] - Whether the expression is custom | |
| 91 | - * as a quick way to switch Talkinghead on or off (e.g. to conserve GPU resources when AFK | |
| 102 | + * @returns {ExpressionImage} The placeholder image object | |
| 92 | - * for a long time). | |
| 93 | 103 | */ |
| 94 | 104 | function toggleTalkingHeadCommandgetPlaceholderImage(_expression, isCustom = false) { |
| 95 | - setTalkingHeadState(!extension_settings.expressions.talkinghead); | |
| 105 | + return { | |
| 96 | - return String(extension_settings.expressions.talkinghead); | |
| 106 | + expression: expression, | |
| 107 | + isCustom: isCustom, | |
| 108 | + title: 'No Image', | |
| 109 | + type: 'failure', | |
| 110 | + fileName: 'No-Image-Placeholder.svg', | |
| 111 | + imageSrc: '/img/No-Image-Placeholder.svg', | |
| 112 | + }; | |
| 97 | 113 | } |
| 98 | 114 | |
| 99 | 115 | function isVisualNovelMode() { |
| @@ -108,21 +124,21 @@ async function forceUpdateVisualNovelMode() { | ||
| 108 | 124 | |
| 109 | 125 | const updateVisualNovelModeDebounced = debounce(forceUpdateVisualNovelMode, debounce_timeout.quick); |
| 110 | 126 | |
| 111 | 127 | async function updateVisualNovelMode(namespriteFolderName, expression) { |
| 112 | 128 | const containervnContainer = $('#visual-novel-wrapper'); |
| 113 | 129 | |
| 114 | 130 | await visualNovelRemoveInactive(containervnContainer); |
| 115 | 131 | |
| 116 | 132 | const setSpritePromises = await visualNovelSetCharacterSprites(containervnContainer, namespriteFolderName, expression); |
| 117 | 133 | |
| 118 | 134 | // calculate layer indices based on recent messages |
| 119 | 135 | await visualNovelUpdateLayers(containervnContainer); |
| 120 | 136 | |
| 121 | 137 | await Promise.allSettled(setSpritePromises); |
| 122 | 138 | |
| 123 | 139 | // update again based on new sprites |
| 124 | 140 | if (setSpritePromises.length > 0) { |
| 125 | 141 | await visualNovelUpdateLayers(containervnContainer); |
| 126 | 142 | } |
| 127 | 143 | } |
| 128 | 144 | |
| @@ -153,52 +169,60 @@ async function visualNovelRemoveInactive(container) { | ||
| 153 | 169 | await Promise.allSettled(removeInactiveCharactersPromises); |
| 154 | 170 | } |
| 155 | 171 | |
| 156 | -async function visualNovelSetCharacterSprites(container, name, expression) { | |
| 172 | +/** | |
| 173 | + * Sets the character sprites for visual novel mode based on the provided container, name, and expression. | |
| 174 | + * | |
| 175 | + * @param {JQuery<HTMLElement>} vnContainer - The container element where the sprites will be set | |
| 176 | + * @param {string} spriteFolderName - The name of the sprite folder | |
| 177 | + * @param {string} expression - The expression to set for the characters | |
| 178 | + * @returns {Promise<Array>} - An array of promises that resolve when the sprites are set | |
| 179 | + */ | |
| 180 | +async function visualNovelSetCharacterSprites(vnContainer, spriteFolderName, expression) { | |
| 181 | + const originalExpression = expression; | |
| 157 | 182 | const context = getContext(); |
| 158 | 183 | const group = context.groups.find(x => x.id == context.groupId); |
| 159 | - const labels = await getExpressionsList(); | |
| 160 | 184 | |
| 161 | - const createCharacterPromises = []; | |
| 162 | 185 | const setSpritePromises = []; |
| 163 | 186 | |
| 164 | 187 | for (const avatar of group.members) { |
| 165 | - const isDisabled = group.disabled_members.includes(avatar); | |
| 166 | - | |
| 167 | 188 | // skip disabled characters |
| 189 | + const isDisabled = group.disabled_members.includes(avatar); | |
| 168 | 190 | if (isDisabled && hideMutedSprites) { |
| 169 | 191 | continue; |
| 170 | 192 | } |
| 171 | 193 | |
| 172 | 194 | const character = context.characters.find(x => x.avatar == avatar); |
| 173 | - | |
| 174 | 195 | if (!character) { |
| 175 | 196 | continue; |
| 176 | 197 | } |
| 177 | 198 | |
| 178 | - const spriteFolderName = getSpriteFolderName({ original_avatar: character.avatar }, character.name); | |
| 199 | + const expressionImage = vnContainer.find(`.expression-holder[data-avatar="${avatar}"]`); | |
| 200 | + /** @type {JQuery<HTMLElement>} */ | |
| 201 | + let img; | |
| 202 | + | |
| 203 | + const memberSpriteFolderName = getSpriteFolderName({ original_avatar: character.avatar }, character.name); | |
| 179 | 204 | |
| 180 | 205 | // download images if not downloaded yet |
| 181 | 206 | if (spriteCache[spriteFolderNamememberSpriteFolderName] === undefined) { |
| 182 | 207 | spriteCache[spriteFolderNamememberSpriteFolderName] = await getSpritesList(spriteFolderNamememberSpriteFolderName); |
| 183 | 208 | } |
| 184 | 209 | |
| 185 | - const sprites = spriteCache[spriteFolderName]; | |
| 210 | + const prevExpressionSrc = expressionImage.find('img').attr('src') || null; | |
| 186 | - const expressionImage = container.find(`.expression-holder[data-avatar="${avatar}"]`); | |
| 187 | - const defaultExpression = getFallbackExpression(); | |
| 188 | - const defaultSpritePath = sprites.find(x => x.label === defaultExpression)?.path; | |
| 189 | - const noSprites = sprites.length === 0; | |
| 190 | 211 | |
| 191 | 212 | if (expressionImage!originalExpression && Array.isArray(spriteCache[memberSpriteFolderName]) && spriteCache[memberSpriteFolderName].length > 0) { |
| 192 | - if (name == spriteFolderName) { | |
| 213 | + expression = await getLastMessageSprite(avatar); | |
| 193 | - await validateImages(spriteFolderName, true); | |
| 214 | + } | |
| 194 | - setExpressionOverrideHtml(true); // <= force clear expression override input | |
| 195 | - const currentSpritePath = labels.includes(expression) ? sprites.find(x => x.label === expression)?.path : ''; | |
| 196 | 215 | |
| 197 | - const path = currentSpritePath || defaultSpritePath || ''; | |
| 216 | + const spriteFile = chooseSpriteForExpression(memberSpriteFolderName, expression, { prevExpressionSrc: prevExpressionSrc }); | |
| 198 | - const img = expressionImage.find('img'); | |
| 217 | + if (expressionImage.length) { | |
| 218 | + if (!spriteFolderName || spriteFolderName == memberSpriteFolderName) { | |
| 219 | + await validateImages(memberSpriteFolderName, true); | |
| 220 | + setExpressionOverrideHtml(true); // <= force clear expression override input | |
| 221 | + const path = spriteFile?.imageSrc || ''; | |
| 222 | + img = expressionImage.find('img'); | |
| 199 | 223 | await setImage(img, path); |
| 200 | 224 | } |
| 201 | 225 | expressionImage.toggleClass('hidden', noSprites!spriteFile); |
| 202 | 226 | } else { |
| 203 | 227 | const template = $('#expression-holder').clone(); |
| 204 | 228 | template.attr('id', `expression-${avatar}`); |
| @@ -206,21 +230,49 @@ async function visualNovelSetCharacterSprites(container, name, expression) { | ||
| 206 | 230 | template.find('.drag-grabber').attr('id', `expression-${avatar}header`); |
| 207 | 231 | $('#visual-novel-wrapper').append(template); |
| 208 | 232 | dragElement($(template[0])); |
| 209 | 233 | template.toggleClass('hidden', noSprites!spriteFile); |
| 210 | 234 | awaitimg setImage(= template.find('img'), defaultSpritePath || ''); |
| 235 | + await setImage(img, spriteFile?.imageSrc || ''); | |
| 211 | 236 | const fadeInPromise = new Promise(resolve => { |
| 212 | 237 | template.fadeIn(250, () => resolve()); |
| 213 | 238 | }); |
| 214 | 239 | createCharacterPromisessetSpritePromises.push(fadeInPromise); |
| 215 | - const setSpritePromise = setLastMessageSprite(template.find('img'), avatar, labels); | |
| 216 | - setSpritePromises.push(setSpritePromise); | |
| 217 | 240 | } |
| 241 | + | |
| 242 | + if (!img) { | |
| 243 | + continue; | |
| 244 | + } | |
| 245 | + | |
| 246 | + img.attr('data-sprite-folder-name', spriteFolderName); | |
| 247 | + img.attr('data-expression', expression); | |
| 248 | + img.attr('data-sprite-filename', spriteFile?.fileName || null); | |
| 249 | + img.attr('title', expression); | |
| 250 | + | |
| 251 | + if (spriteFile) console.info(`Expression set for group member ${character.name}`, { expression: spriteFile.expression, file: spriteFile.fileName }); | |
| 252 | + else if (expressionImage.length) console.info(`Expression unset for group member ${character.name} - No sprite found`, { expression: expression }); | |
| 253 | + else console.info(`Expression not available for group member ${character.name}`, { expression: expression }); | |
| 218 | 254 | } |
| 219 | 255 | |
| 220 | - await Promise.allSettled(createCharacterPromises); | |
| 221 | 256 | return setSpritePromises; |
| 222 | 257 | } |
| 223 | 258 | |
| 259 | +/** | |
| 260 | + * Classifies the text of the latest message and returns the expression label. | |
| 261 | + * @param {string} avatar - The avatar of the character to get the last message for | |
| 262 | + * @returns {Promise<string>} - The expression label | |
| 263 | + */ | |
| 264 | +async function getLastMessageSprite(avatar) { | |
| 265 | + const context = getContext(); | |
| 266 | + const lastMessage = context.chat.slice().reverse().find(x => x.original_avatar == avatar || (x.force_avatar && x.force_avatar.includes(encodeURIComponent(avatar)))); | |
| 267 | + | |
| 268 | + if (lastMessage) { | |
| 269 | + const text = lastMessage.mes || ''; | |
| 270 | + return await getExpressionLabel(text); | |
| 271 | + } | |
| 272 | + | |
| 273 | + return null; | |
| 274 | +} | |
| 275 | + | |
| 224 | 276 | async function visualNovelUpdateLayers(container) { |
| 225 | 277 | const context = getContext(); |
| 226 | 278 | const group = context.groups.find(x => x.id == context.groupId); |
| @@ -256,11 +308,17 @@ async function visualNovelUpdateLayers(container) { | ||
| 256 | 308 | const containerWidth = container.width(); |
| 257 | 309 | const pivotalPoint = containerWidth * 0.5; |
| 258 | 310 | |
| 259 | 311 | let images = Array.from($('#visual-novel-wrapper .expression-holder')).sort(sortFunction); |
| 260 | 312 | let imagesWidth = []; |
| 261 | 313 | |
| 262 | - images.sort(sortFunction).each(function () { | |
| 314 | + for (const image of images) { | |
| 263 | - imagesWidth.push($(this).width()); | |
| 315 | + if (image instanceof HTMLImageElement && !image.complete) { | |
| 316 | + await new Promise(resolve => image.addEventListener('load', resolve, { once: true })); | |
| 317 | + } | |
| 318 | + } | |
| 319 | + | |
| 320 | + images.forEach(image => { | |
| 321 | + imagesWidth.push($(image).width()); | |
| 264 | 322 | }); |
| 265 | 323 | |
| 266 | 324 | let totalWidth = imagesWidth.reduce((a, b) => a + b, 0); |
| @@ -274,7 +332,7 @@ async function visualNovelUpdateLayers(container) { | ||
| 274 | 332 | currentPosition = 0; // Reset the initial position to 0 |
| 275 | 333 | } |
| 276 | 334 | |
| 277 | 335 | images.sort(sortFunction).eachforEach((indexcurrent, currentindex) => { |
| 278 | 336 | const element = $(current); |
| 279 | 337 | const elementID = element.attr('id'); |
| 280 | 338 | |
| @@ -294,9 +352,15 @@ async function visualNovelUpdateLayers(container) { | ||
| 294 | 352 | element.show(); |
| 295 | 353 | |
| 296 | 354 | const promise = new Promise(resolve => { |
| 355 | + if (power_user.reduced_motion) { | |
| 356 | + element.css('left', currentPosition + 'px'); | |
| 357 | + requestAnimationFrame(() => resolve()); | |
| 358 | + } | |
| 359 | + else { | |
| 297 | 360 | element.animate({ left: currentPosition + 'px' }, 500, () => { |
| 298 | 361 | resolve(); |
| 299 | 362 | }); |
| 363 | + } | |
| 300 | 364 | }); |
| 301 | 365 | |
| 302 | 366 | currentPosition += imagesWidth[index]; |
| @@ -307,23 +371,12 @@ async function visualNovelUpdateLayers(container) { | ||
| 307 | 371 | await Promise.allSettled(setLayerIndicesPromises); |
| 308 | 372 | } |
| 309 | 373 | |
| 310 | -async function setLastMessageSprite(img, avatar, labels) { | |
| 374 | +/** | |
| 311 | - const context = getContext(); | |
| 375 | + * Sets the expression for the given character image. | |
| 312 | - const lastMessage = context.chat.slice().reverse().find(x => x.original_avatar == avatar || (x.force_avatar && x.force_avatar.includes(encodeURIComponent(avatar)))); | |
| 376 | + * @param {JQuery<HTMLElement>} img - The image element to set the image on | |
| 313 | - | |
| 377 | + * @param {string} path - The path to the image | |
| 314 | - if (lastMessage) { | |
| 378 | + * @returns {Promise<void>} - A promise that resolves when the image is set | |
| 315 | - const text = lastMessage.mes || ''; | |
| 379 | + */ | |
| 316 | - const spriteFolderName = getSpriteFolderName(lastMessage, lastMessage.name); | |
| 317 | - const sprites = spriteCache[spriteFolderName] || []; | |
| 318 | - const label = await getExpressionLabel(text); | |
| 319 | - const path = labels.includes(label) ? sprites.find(x => x.label === label)?.path : ''; | |
| 320 | - | |
| 321 | - if (path) { | |
| 322 | - setImage(img, path); | |
| 323 | - } | |
| 324 | - } | |
| 325 | -} | |
| 326 | - | |
| 327 | 380 | async function setImage(img, path) { |
| 328 | 381 | // Cohee: If something goes wrong, uncomment this to return to the old behavior |
| 329 | 382 | /* |
| @@ -340,7 +393,7 @@ async function setImage(img, path) { | ||
| 340 | 393 | return new Promise(resolve => { |
| 341 | 394 | const prevExpressionSrc = img.attr('src'); |
| 342 | 395 | const expressionClone = img.clone(); |
| 343 | 396 | const originalId = img.attrdata('idfilename'); |
| 344 | 397 | |
| 345 | 398 | //only swap expressions when necessary |
| 346 | 399 | if (prevExpressionSrc !== path && !img.hasClass('expression-animating')) { |
| @@ -348,7 +401,7 @@ async function setImage(img, path) { | ||
| 348 | 401 | expressionClone.addClass('expression-clone'); |
| 349 | 402 | //make invisible and remove id to prevent double ids |
| 350 | 403 | //must be made invisible to start because they share the same Z-index |
| 351 | 404 | expressionClone.attrdata('idfilename', '').css({ opacity: 0 }); |
| 352 | 405 | //add new sprite path to clone src |
| 353 | 406 | expressionClone.attr('src', path); |
| 354 | 407 | //add invisible clone to html |
| @@ -384,14 +437,18 @@ async function setImage(img, path) { | ||
| 384 | 437 | //remove old expression |
| 385 | 438 | img.remove(); |
| 386 | 439 | //replace ID so it becomes the new 'original' expression for next change |
| 387 | 440 | expressionClone.attrdata('idfilename', originalId); |
| 388 | 441 | expressionClone.removeClass('expression-animating'); |
| 389 | 442 | |
| 390 | 443 | // Reset the expression holder min height and width |
| 391 | 444 | expressionHolder.css('min-width', 100); |
| 392 | 445 | expressionHolder.css('min-height', 100); |
| 393 | 446 | |
| 447 | + if (expressionClone.prop('complete')) { | |
| 394 | 448 | resolve(); |
| 449 | + } else { | |
| 450 | + expressionClone.one('load', () => resolve()); | |
| 451 | + } | |
| 395 | 452 | }); |
| 396 | 453 | |
| 397 | 454 | expressionClone.removeClass('expression-clone'); |
| @@ -410,216 +467,9 @@ async function setImage(img, path) { | ||
| 410 | 467 | }); |
| 411 | 468 | } |
| 412 | 469 | |
| 413 | -function onExpressionsShowDefaultInput() { | |
| 470 | +async function moduleWorker({ newChat = false } = {}) { | |
| 414 | - const value = $(this).prop('checked'); | |
| 415 | - extension_settings.expressions.showDefault = value; | |
| 416 | - saveSettingsDebounced(); | |
| 417 | - | |
| 418 | - const existingImageSrc = $('img.expression').prop('src'); | |
| 419 | - if (existingImageSrc !== undefined) { //if we have an image in src | |
| 420 | - if (!value && existingImageSrc.includes('/img/default-expressions/')) { //and that image is from /img/ (default) | |
| 421 | - $('img.expression').prop('src', ''); //remove it | |
| 422 | - lastMessage = null; | |
| 423 | - } | |
| 424 | - if (value) { | |
| 425 | - lastMessage = null; | |
| 426 | - } | |
| 427 | - } | |
| 428 | -} | |
| 429 | - | |
| 430 | -/** | |
| 431 | - * Stops animating Talkinghead. | |
| 432 | - */ | |
| 433 | -async function unloadTalkingHead() { | |
| 434 | - if (!modules.includes('talkinghead')) { | |
| 435 | - console.debug('talkinghead module is disabled'); | |
| 436 | - return; | |
| 437 | - } | |
| 438 | - console.debug('expressions: Stopping Talkinghead'); | |
| 439 | - | |
| 440 | - try { | |
| 441 | - const url = new URL(getApiUrl()); | |
| 442 | - url.pathname = '/api/talkinghead/unload'; | |
| 443 | - const loadResponse = await doExtrasFetch(url); | |
| 444 | - if (!loadResponse.ok) { | |
| 445 | - throw new Error(loadResponse.statusText); | |
| 446 | - } | |
| 447 | - //console.log(`Response: ${loadResponseText}`); | |
| 448 | - } catch (error) { | |
| 449 | - //console.error(`Error unloading - ${error}`); | |
| 450 | - } | |
| 451 | -} | |
| 452 | - | |
| 453 | -/** | |
| 454 | - * Posts `talkinghead.png` of the current character to the talkinghead module in SillyTavern-extras, to start animating it. | |
| 455 | - */ | |
| 456 | -async function loadTalkingHead() { | |
| 457 | - if (!modules.includes('talkinghead')) { | |
| 458 | - console.debug('talkinghead module is disabled'); | |
| 459 | - return; | |
| 460 | - } | |
| 461 | - console.debug('expressions: Starting Talkinghead'); | |
| 462 | - | |
| 463 | - const spriteFolderName = getSpriteFolderName(); | |
| 464 | - | |
| 465 | - const talkingheadPath = `/characters/${encodeURIComponent(spriteFolderName)}/talkinghead.png`; | |
| 466 | - const emotionsSettingsPath = `/characters/${encodeURIComponent(spriteFolderName)}/_emotions.json`; | |
| 467 | - const animatorSettingsPath = `/characters/${encodeURIComponent(spriteFolderName)}/_animator.json`; | |
| 468 | - | |
| 469 | - try { | |
| 470 | - const spriteResponse = await fetch(talkingheadPath); | |
| 471 | - | |
| 472 | - if (!spriteResponse.ok) { | |
| 473 | - throw new Error(spriteResponse.statusText); | |
| 474 | - } | |
| 475 | - | |
| 476 | - const spriteBlob = await spriteResponse.blob(); | |
| 477 | - const spriteFile = new File([spriteBlob], 'talkinghead.png', { type: 'image/png' }); | |
| 478 | - const formData = new FormData(); | |
| 479 | - formData.append('file', spriteFile); | |
| 480 | - | |
| 481 | - const url = new URL(getApiUrl()); | |
| 482 | - url.pathname = '/api/talkinghead/load'; | |
| 483 | - | |
| 484 | - const loadResponse = await doExtrasFetch(url, { | |
| 485 | - method: 'POST', | |
| 486 | - body: formData, | |
| 487 | - }); | |
| 488 | - | |
| 489 | - if (!loadResponse.ok) { | |
| 490 | - throw new Error(loadResponse.statusText); | |
| 491 | - } | |
| 492 | - | |
| 493 | - const loadResponseText = await loadResponse.text(); | |
| 494 | - console.log(`Load talkinghead response: ${loadResponseText}`); | |
| 495 | - | |
| 496 | - // Optional: per-character emotion templates | |
| 497 | - let emotionsSettings; | |
| 498 | - try { | |
| 499 | - const emotionsResponse = await fetch(emotionsSettingsPath); | |
| 500 | - if (emotionsResponse.ok) { | |
| 501 | - emotionsSettings = await emotionsResponse.json(); | |
| 502 | - console.log(`Loaded ${emotionsSettingsPath}`); | |
| 503 | - } else { | |
| 504 | - throw new Error(); | |
| 505 | - } | |
| 506 | - } | |
| 507 | - catch (error) { | |
| 508 | - emotionsSettings = {}; // blank -> use server defaults (to unload the previous character's customizations) | |
| 509 | - console.log(`No valid config at ${emotionsSettingsPath}, using server defaults`); | |
| 510 | - } | |
| 511 | - try { | |
| 512 | - const url = new URL(getApiUrl()); | |
| 513 | - url.pathname = '/api/talkinghead/load_emotion_templates'; | |
| 514 | - const apiResult = await doExtrasFetch(url, { | |
| 515 | - method: 'POST', | |
| 516 | - headers: { | |
| 517 | - 'Content-Type': 'application/json', | |
| 518 | - 'Bypass-Tunnel-Reminder': 'bypass', | |
| 519 | - }, | |
| 520 | - body: JSON.stringify(emotionsSettings), | |
| 521 | - }); | |
| 522 | - | |
| 523 | - if (!apiResult.ok) { | |
| 524 | - throw new Error(apiResult.statusText); | |
| 525 | - } | |
| 526 | - } | |
| 527 | - catch (error) { | |
| 528 | - // it's ok if not supported | |
| 529 | - console.log('Failed to send _emotions.json (backend too old?), ignoring'); | |
| 530 | - } | |
| 531 | - | |
| 532 | - // Optional: per-character animator and postprocessor config | |
| 533 | - let animatorSettings; | |
| 534 | - try { | |
| 535 | - const animatorResponse = await fetch(animatorSettingsPath); | |
| 536 | - if (animatorResponse.ok) { | |
| 537 | - animatorSettings = await animatorResponse.json(); | |
| 538 | - console.log(`Loaded ${animatorSettingsPath}`); | |
| 539 | - } else { | |
| 540 | - throw new Error(); | |
| 541 | - } | |
| 542 | - } | |
| 543 | - catch (error) { | |
| 544 | - animatorSettings = {}; // blank -> use server defaults (to unload the previous character's customizations) | |
| 545 | - console.log(`No valid config at ${animatorSettingsPath}, using server defaults`); | |
| 546 | - } | |
| 547 | - try { | |
| 548 | - const url = new URL(getApiUrl()); | |
| 549 | - url.pathname = '/api/talkinghead/load_animator_settings'; | |
| 550 | - const apiResult = await doExtrasFetch(url, { | |
| 551 | - method: 'POST', | |
| 552 | - headers: { | |
| 553 | - 'Content-Type': 'application/json', | |
| 554 | - 'Bypass-Tunnel-Reminder': 'bypass', | |
| 555 | - }, | |
| 556 | - body: JSON.stringify(animatorSettings), | |
| 557 | - }); | |
| 558 | - | |
| 559 | - if (!apiResult.ok) { | |
| 560 | - throw new Error(apiResult.statusText); | |
| 561 | - } | |
| 562 | - } | |
| 563 | - catch (error) { | |
| 564 | - // it's ok if not supported | |
| 565 | - console.log('Failed to send _animator.json (backend too old?), ignoring'); | |
| 566 | - } | |
| 567 | - } catch (error) { | |
| 568 | - console.error(`Error loading talkinghead image: ${talkingheadPath} - ${error}`); | |
| 569 | - } | |
| 570 | -} | |
| 571 | - | |
| 572 | -function handleImageChange() { | |
| 573 | - const imgElement = document.querySelector('img#expression-image.expression'); | |
| 574 | - | |
| 575 | - if (!imgElement || !(imgElement instanceof HTMLImageElement)) { | |
| 576 | - console.log('Cannot find addExpressionImage()'); | |
| 577 | - return; | |
| 578 | - } | |
| 579 | - | |
| 580 | - if (isTalkingHeadEnabled() && modules.includes('talkinghead')) { | |
| 581 | - const talkingheadResultFeedSrc = `${getApiUrl()}/api/talkinghead/result_feed`; | |
| 582 | - $('#expression-holder').css({ display: '' }); | |
| 583 | - if (imgElement.src !== talkingheadResultFeedSrc) { | |
| 584 | - const expressionImageElement = document.querySelector('.expression_list_image'); | |
| 585 | - | |
| 586 | - if (expressionImageElement && expressionImageElement instanceof HTMLImageElement) { | |
| 587 | - doExtrasFetch(expressionImageElement.src, { | |
| 588 | - method: 'HEAD', | |
| 589 | - }) | |
| 590 | - .then(response => { | |
| 591 | - if (response.ok) { | |
| 592 | - imgElement.src = talkingheadResultFeedSrc; | |
| 593 | - } | |
| 594 | - }) | |
| 595 | - .catch(error => { | |
| 596 | - console.error(error); | |
| 597 | - }); | |
| 598 | - } | |
| 599 | - } | |
| 600 | - } else { | |
| 601 | - imgElement.src = ''; // remove in case char doesn't have expressions | |
| 602 | - | |
| 603 | - // When switching Talkinghead off, force-set the character to the last known expression, if any. | |
| 604 | - // This preserves the same expression Talkinghead had at the moment it was switched off. | |
| 605 | - const charName = getContext().name2; | |
| 606 | - const last = lastExpression[charName]; | |
| 607 | - const targetExpression = last ? last : getFallbackExpression(); | |
| 608 | - setExpression(charName, targetExpression, true); | |
| 609 | - } | |
| 610 | -} | |
| 611 | - | |
| 612 | -async function moduleWorker() { | |
| 613 | 471 | const context = getContext(); |
| 614 | 472 | |
| 615 | - // Hide and disable Talkinghead while not in extras | |
| 616 | - $('#image_type_block').toggle(extension_settings.expressions.api == EXPRESSION_API.extras); | |
| 617 | - | |
| 618 | - if (extension_settings.expressions.api != EXPRESSION_API.extras && extension_settings.expressions.talkinghead) { | |
| 619 | - $('#image_type_toggle').prop('checked', false); | |
| 620 | - setTalkingHeadState(false); | |
| 621 | - } | |
| 622 | - | |
| 623 | 473 | // non-characters not supported |
| 624 | 474 | if (!context.groupId && context.characterId === undefined) { |
| 625 | 475 | removeExpression(); |
| @@ -646,7 +496,7 @@ async function moduleWorker() { | ||
| 646 | 496 | } |
| 647 | 497 | |
| 648 | 498 | const currentLastMessage = getLastCharacterMessage(); |
| 649 | 499 | let spriteFolderName = context.groupId ? getSpriteFolderName(currentLastMessage, currentLastMessage.name) : getSpriteFolderName(); |
| 650 | 500 | |
| 651 | 501 | // character has no expressions or it is not loaded |
| 652 | 502 | if (Object.keys(spriteCache).length === 0) { |
| @@ -686,6 +536,10 @@ async function moduleWorker() { | ||
| 686 | 536 | offlineMode.css('display', 'none'); |
| 687 | 537 | } |
| 688 | 538 | |
| 539 | + if (context.groupId && vnMode && newChat) { | |
| 540 | + await forceUpdateVisualNovelMode(); | |
| 541 | + } | |
| 542 | + | |
| 689 | 543 | // Don't bother classifying if current char has no sprites and no default expressions are enabled |
| 690 | 544 | if ((!Array.isArray(spriteCache[spriteFolderName]) || spriteCache[spriteFolderName].length === 0) && !extension_settings.expressions.showDefault) { |
| 691 | 545 | return; |
| @@ -732,11 +586,11 @@ async function moduleWorker() { | ||
| 732 | 586 | const force = !!context.groupId; |
| 733 | 587 | |
| 734 | 588 | // Character won't be angry on you for swiping |
| 735 | 589 | if (currentLastMessage.mes == '...' && expressionsList.includes(getFallbackExpression()extension_settings.expressions.fallback_expression)) { |
| 736 | 590 | expression = getFallbackExpression()extension_settings.expressions.fallback_expression; |
| 737 | 591 | } |
| 738 | 592 | |
| 739 | 593 | await sendExpressionCall(spriteFolderName, expression, { force: force, vnMode: vnMode }); |
| 740 | 594 | } |
| 741 | 595 | catch (error) { |
| 742 | 596 | console.log(error); |
| @@ -749,91 +603,6 @@ async function moduleWorker() { | ||
| 749 | 603 | } |
| 750 | 604 | } |
| 751 | 605 | |
| 752 | -/** | |
| 753 | - * Starts/stops Talkinghead talking animation. | |
| 754 | - * | |
| 755 | - * Talking starts only when all the following conditions are met: | |
| 756 | - * - The LLM is currently streaming its output. | |
| 757 | - * - The AI's current last message is non-empty, and also not just '...' (as produced by a swipe). | |
| 758 | - * - The AI's current last message has changed from what we saw during the previous call. | |
| 759 | - * | |
| 760 | - * In all other cases, talking stops. | |
| 761 | - * | |
| 762 | - * A Talkinghead API call is made only when the talking state changes. | |
| 763 | - * | |
| 764 | - * Note that also the TTS system, if enabled, starts/stops the Talkinghead talking animation. | |
| 765 | - * See `talkingAnimation` in `SillyTavern/public/scripts/extensions/tts/index.js`. | |
| 766 | - */ | |
| 767 | -async function updateTalkingState() { | |
| 768 | - // Don't bother if Talkinghead is disabled or not loaded. | |
| 769 | - if (!isTalkingHeadEnabled() || !modules.includes('talkinghead')) { | |
| 770 | - return; | |
| 771 | - } | |
| 772 | - | |
| 773 | - const context = getContext(); | |
| 774 | - const currentLastMessage = getLastCharacterMessage(); | |
| 775 | - | |
| 776 | - try { | |
| 777 | - // TODO: Not sure if we need also "&& !context.groupId" here - the classify check in `moduleWorker` | |
| 778 | - // (that similarly checks the streaming processor state) does that for some reason. | |
| 779 | - // Talkinghead isn't currently designed to work with groups. | |
| 780 | - const lastMessageChanged = !((lastCharacter === context.characterId || lastCharacter === context.groupId) && lastTalkingStateMessage === currentLastMessage.mes); | |
| 781 | - const url = new URL(getApiUrl()); | |
| 782 | - let newTalkingState; | |
| 783 | - if (context.streamingProcessor && !context.streamingProcessor.isFinished && | |
| 784 | - currentLastMessage.mes.length !== 0 && currentLastMessage.mes !== '...' && lastMessageChanged) { | |
| 785 | - url.pathname = '/api/talkinghead/start_talking'; | |
| 786 | - newTalkingState = true; | |
| 787 | - } else { | |
| 788 | - url.pathname = '/api/talkinghead/stop_talking'; | |
| 789 | - newTalkingState = false; | |
| 790 | - } | |
| 791 | - try { | |
| 792 | - // Call the Talkinghead API only if the talking state changed. | |
| 793 | - if (newTalkingState !== lastTalkingState) { | |
| 794 | - console.debug(`updateTalkingState: calling ${url.pathname}`); | |
| 795 | - await doExtrasFetch(url); | |
| 796 | - } | |
| 797 | - } | |
| 798 | - catch (error) { | |
| 799 | - // it's ok if not supported | |
| 800 | - } | |
| 801 | - finally { | |
| 802 | - lastTalkingState = newTalkingState; | |
| 803 | - } | |
| 804 | - } | |
| 805 | - catch (error) { | |
| 806 | - // console.log(error); | |
| 807 | - } | |
| 808 | - finally { | |
| 809 | - lastTalkingStateMessage = currentLastMessage.mes; | |
| 810 | - } | |
| 811 | -} | |
| 812 | - | |
| 813 | -/** | |
| 814 | - * Checks whether the current character has a talkinghead image available. | |
| 815 | - * @returns {Promise<boolean>} True if the character has a talkinghead image available, false otherwise. | |
| 816 | - */ | |
| 817 | -async function isTalkingHeadAvailable() { | |
| 818 | - let spriteFolderName = getSpriteFolderName(); | |
| 819 | - | |
| 820 | - try { | |
| 821 | - await validateImages(spriteFolderName); | |
| 822 | - | |
| 823 | - let talkingheadObj = spriteCache[spriteFolderName].find(obj => obj.label === 'talkinghead'); | |
| 824 | - let talkingheadPath = talkingheadObj ? talkingheadObj.path : null; | |
| 825 | - | |
| 826 | - if (talkingheadPath != null) { | |
| 827 | - return true; | |
| 828 | - } else { | |
| 829 | - await unloadTalkingHead(); | |
| 830 | - return false; | |
| 831 | - } | |
| 832 | - } catch (err) { | |
| 833 | - return err; | |
| 834 | - } | |
| 835 | -} | |
| 836 | - | |
| 837 | 606 | function getSpriteFolderName(characterMessage = null, characterName = null) { |
| 838 | 607 | const context = getContext(); |
| 839 | 608 | let spriteFolderName = characterName ?? context.name2; |
| @@ -848,33 +617,6 @@ function getSpriteFolderName(characterMessage = null, characterName = null) { | ||
| 848 | 617 | return spriteFolderName; |
| 849 | 618 | } |
| 850 | 619 | |
| 851 | -function setTalkingHeadState(newState) { | |
| 852 | - console.debug(`expressions: New talkinghead state: ${newState}`); | |
| 853 | - extension_settings.expressions.talkinghead = newState; // Store setting | |
| 854 | - saveSettingsDebounced(); | |
| 855 | - | |
| 856 | - if ([EXPRESSION_API.local, EXPRESSION_API.llm, EXPRESSION_API.webllm].includes(extension_settings.expressions.api)) { | |
| 857 | - return; | |
| 858 | - } | |
| 859 | - | |
| 860 | - isTalkingHeadAvailable().then(result => { | |
| 861 | - if (result) { | |
| 862 | - //console.log("talkinghead exists!"); | |
| 863 | - | |
| 864 | - if (extension_settings.expressions.talkinghead) { | |
| 865 | - loadTalkingHead(); | |
| 866 | - } else { | |
| 867 | - unloadTalkingHead(); | |
| 868 | - } | |
| 869 | - handleImageChange(); // Change image as needed | |
| 870 | - | |
| 871 | - | |
| 872 | - } else { | |
| 873 | - //console.log("talkinghead does not exist."); | |
| 874 | - } | |
| 875 | - }); | |
| 876 | -} | |
| 877 | - | |
| 878 | 620 | function getFolderNameByMessage(message) { |
| 879 | 621 | const context = getContext(); |
| 880 | 622 | let avatarPath = ''; |
| @@ -894,48 +636,55 @@ function getFolderNameByMessage(message) { | ||
| 894 | 636 | return folderName; |
| 895 | 637 | } |
| 896 | 638 | |
| 897 | -async function sendExpressionCall(name, expression, force, vnMode) { | |
| 639 | +/** | |
| 898 | - lastExpression[name.split('/')[0]] = expression; | |
| 640 | + * Update the expression for the given character. | |
| 899 | - if (!vnMode) { | |
| 641 | + * | |
| 642 | + * @param {string} spriteFolderName The character name, optionally with a sprite folder override, e.g. "folder/expression". | |
| 643 | + * @param {string} expression The expression label, e.g. "amusement", "joy", etc. | |
| 644 | + * @param {Object} [options] Additional options | |
| 645 | + * @param {boolean} [options.force=false] If true, the expression will be sent even if it is the same as the current expression. | |
| 646 | + * @param {boolean} [options.vnMode=null] If true, the expression will be sent in Visual Novel mode. If null, it will be determined by the current chat mode. | |
| 647 | + * @param {string?} [options.overrideSpriteFile=null] - Set if a specific sprite file should be used. Must be sprite file name. | |
| 648 | + */ | |
| 649 | +export async function sendExpressionCall(spriteFolderName, expression, { force = false, vnMode = null, overrideSpriteFile = null } = {}) { | |
| 650 | + lastExpression[spriteFolderName.split('/')[0]] = expression; | |
| 651 | + if (vnMode === null) { | |
| 900 | 652 | vnMode = isVisualNovelMode(); |
| 901 | 653 | } |
| 902 | 654 | |
| 903 | 655 | if (vnMode) { |
| 904 | 656 | await updateVisualNovelMode(namespriteFolderName, expression); |
| 905 | 657 | } else { |
| 906 | 658 | setExpression(namespriteFolderName, expression, { force: force, overrideSpriteFile: overrideSpriteFile }); |
| 907 | 659 | } |
| 908 | 660 | } |
| 909 | 661 | |
| 910 | 662 | async function setSpriteSetCommandsetSpriteFolderCommand(_, folder) { |
| 911 | 663 | if (!folder) { |
| 912 | 664 | console.log('Clearing sprite set'); |
| 913 | 665 | folder = ''; |
| 914 | 666 | } |
| 915 | 667 | |
| 916 | 668 | if (folder.startsWith('/') || folder.startsWith('\\')) { |
| 917 | - folder = folder.slice(1); | |
| 918 | - | |
| 919 | 669 | const currentLastMessage = getLastCharacterMessage(); |
| 670 | + folder = folder.slice(1); | |
| 920 | 671 | folder = `${currentLastMessage.name}/${folder}`; |
| 921 | 672 | } |
| 922 | 673 | |
| 923 | 674 | $('#expression_override').val(folder.trim()); |
| 924 | 675 | onClickExpressionOverrideButton(); |
| 925 | - // removeExpression(); | |
| 676 | + | |
| 926 | - // moduleWorker(); | |
| 677 | + // No need to resend the expression, the folder override will automatically update the currently displayed one. | |
| 927 | - const vnMode = isVisualNovelMode(); | |
| 928 | - await sendExpressionCall(folder, lastExpression, true, vnMode); | |
| 929 | 678 | return ''; |
| 930 | 679 | } |
| 931 | 680 | |
| 932 | 681 | async function classifyCallback(/** @type {{api: string?, prompt: string?}} */ { api = null, prompt = null }, text) { |
| 933 | 682 | if (!text) { |
| 934 | 683 | toastr.warningerror('No text provided'); |
| 935 | 684 | return ''; |
| 936 | 685 | } |
| 937 | 686 | if (api && !Object.keys(EXPRESSION_API).includes(api)) { |
| 938 | 687 | toastr.warningerror('Invalid API provided'); |
| 939 | 688 | return ''; |
| 940 | 689 | } |
| 941 | 690 | |
| @@ -951,37 +700,69 @@ async function classifyCallback(/** @type {{api: string?, prompt: string?}} */ { | ||
| 951 | 700 | return label; |
| 952 | 701 | } |
| 953 | 702 | |
| 954 | -async function setSpriteSlashCommand(_, spriteId) { | |
| 703 | +/** @type {(args: {type: 'expression' | 'sprite'}, searchTerm: string) => Promise<string>} */ | |
| 955 | - if (!spriteId) { | |
| 704 | +async function setSpriteSlashCommand({ type }, searchTerm) { | |
| 956 | - console.log('No sprite id provided'); | |
| 705 | + type ??= 'expression'; | |
| 706 | + searchTerm = searchTerm.trim().toLowerCase(); | |
| 707 | + if (!searchTerm) { | |
| 708 | + toastr.error(t`No expression or sprite name provided`, t`Set Sprite`); | |
| 957 | 709 | return ''; |
| 958 | 710 | } |
| 959 | 711 | |
| 960 | - spriteId = spriteId.trim().toLowerCase(); | |
| 712 | + const currentLastMessage = selected_group ? getLastCharacterMessage() : null; | |
| 713 | + const spriteFolderName = getSpriteFolderName(currentLastMessage, currentLastMessage?.name); | |
| 714 | + | |
| 715 | + let label = searchTerm; | |
| 716 | + | |
| 717 | + /** @type {string?} */ | |
| 718 | + let spriteFile = null; | |
| 961 | 719 | |
| 962 | - // In Talkinghead mode, don't check for the existence of the sprite | |
| 963 | - // (emotion names are the same as for sprites, but it only needs "talkinghead.png"). | |
| 964 | - const currentLastMessage = getLastCharacterMessage(); | |
| 965 | - const spriteFolderName = getSpriteFolderName(currentLastMessage, currentLastMessage.name); | |
| 966 | - let label = spriteId; | |
| 967 | - if (!isTalkingHeadEnabled() || !modules.includes('talkinghead')) { | |
| 968 | 720 | await validateImages(spriteFolderName); |
| 969 | 721 | |
| 970 | - // Fuzzy search for sprite | |
| 722 | + // Handle reset as a special term and just reset the sprite via expression call | |
| 971 | - const fuse = new Fuse(spriteCache[spriteFolderName], { keys: ['label'] }); | |
| 723 | + if (searchTerm === RESET_SPRITE_LABEL) { | |
| 972 | - const results = fuse.search(spriteId); | |
| 724 | + await sendExpressionCall(spriteFolderName, label, { force: true }); | |
| 973 | - const spriteItem = results[0]?.item; | |
| 725 | + return lastExpression[spriteFolderName] ?? ''; | |
| 726 | + } | |
| 727 | + | |
| 728 | + switch (type) { | |
| 729 | + case 'expression': { | |
| 730 | + // Fuzzy search for expression | |
| 731 | + const existingExpressions = getCachedExpressions().map(x => ({ label: x })); | |
| 732 | + const results = performFuzzySearch('expression-expressions', existingExpressions, [ | |
| 733 | + { name: 'label', weight: 1 }, | |
| 734 | + ], searchTerm); | |
| 735 | + const matchedExpression = results[0]?.item; | |
| 736 | + if (!matchedExpression) { | |
| 737 | + toastr.warning(t`No expression found for search term ${searchTerm}`, t`Set Sprite`); | |
| 738 | + return ''; | |
| 739 | + } | |
| 974 | 740 | |
| 975 | - if (!spriteItem) { | |
| 741 | + label = matchedExpression.label; | |
| 976 | - console.log('No sprite found for search term ' + spriteId); | |
| 742 | + break; | |
| 743 | + } | |
| 744 | + case 'sprite': { | |
| 745 | + // Fuzzy search for sprite file | |
| 746 | + const sprites = spriteCache[spriteFolderName].map(x => x.files).flat(); | |
| 747 | + const results = performFuzzySearch('expression-expressions', sprites, [ | |
| 748 | + { name: 'title', weight: 1 }, | |
| 749 | + { name: 'fileName', weight: 1 }, | |
| 750 | + ], searchTerm); | |
| 751 | + const matchedSprite = results[0]?.item; | |
| 752 | + if (!matchedSprite) { | |
| 753 | + toastr.warning(t`No sprite file found for search term ${searchTerm}`, t`Set Sprite`); | |
| 977 | 754 | return ''; |
| 978 | 755 | } |
| 979 | 756 | |
| 980 | 757 | label = spriteItemmatchedSprite.labelexpression; |
| 758 | + spriteFile = matchedSprite.fileName; | |
| 759 | + break; | |
| 760 | + } | |
| 761 | + default: throw Error('Invalid sprite set type: ' + type); | |
| 981 | 762 | } |
| 982 | 763 | |
| 983 | - const vnMode = isVisualNovelMode(); | |
| 764 | + await sendExpressionCall(spriteFolderName, label, { force: true, overrideSpriteFile: spriteFile }); | |
| 984 | - await sendExpressionCall(spriteFolderName, label, true, vnMode); | |
| 765 | + | |
| 985 | 766 | return label; |
| 986 | 767 | } |
| 987 | 768 | |
| @@ -999,6 +780,21 @@ function spriteFolderNameFromCharacter(char) { | ||
| 999 | 780 | } |
| 1000 | 781 | |
| 1001 | 782 | /** |
| 783 | + * Generates a unique sprite name by appending an index to the given expression. * | |
| 784 | + * @param {string} expression - The base expression to be used as the prefix for the sprite name. | |
| 785 | + * @param {ExpressionImage[]} existingFiles - An array of existing file objects, each containing a fileName property. | |
| 786 | + * @returns {string} - A unique sprite name with the format "expression-index". | |
| 787 | + */ | |
| 788 | +function generateUniqueSpriteName(expression, existingFiles) { | |
| 789 | + let index = existingFiles.length; | |
| 790 | + let newSpriteName; | |
| 791 | + do { | |
| 792 | + newSpriteName = `${expression}-${index++}`; | |
| 793 | + } while (existingFiles.some(file => withoutExtension(file.fileName) === newSpriteName)); | |
| 794 | + return newSpriteName; | |
| 795 | +} | |
| 796 | + | |
| 797 | +/** | |
| 1002 | 798 | * Slash command callback for /uploadsprite |
| 1003 | 799 | * |
| 1004 | 800 | * label= is required |
| @@ -1011,16 +807,29 @@ function spriteFolderNameFromCharacter(char) { | ||
| 1011 | 807 | * @param {object} args |
| 1012 | 808 | * @param {string} args.name Character name or avatar key, passed through findChar |
| 1013 | 809 | * @param {string} args.label Expression label |
| 1014 | 810 | * @param {string} [args.folder=null] SpriteOptional sprite folder path, processed using backslash rules |
| 811 | + * @param {string?} [args.spriteName=null] Optional sprite name | |
| 1015 | 812 | * @param {string} imageUrl Image URI to fetch and upload |
| 1016 | 813 | * @returns {Promise<voidstring>} the sprite name |
| 1017 | 814 | */ |
| 1018 | 815 | async function uploadSpriteCommand({ name, label, folder = null, spriteName = null }, imageUrl) { |
| 1019 | 816 | if (!imageUrl) throw new Error('Image URL is required'); |
| 1020 | 817 | if (!label || typeof label !== 'string') throw new Error('Expression label is required');{ |
| 818 | + toastr.error(t`Expression label is required`, t`Error Uploading Sprite`); | |
| 819 | + return ''; | |
| 820 | + } | |
| 1021 | 821 | |
| 1022 | 822 | label = label.replace(/[^a-z]/gi, '').toLowerCase().trim(); |
| 1023 | - if (!label) throw new Error('Expression label must contain at least one letter'); | |
| 823 | + if (!label) { | |
| 824 | + toastr.error(t`Expression label must contain at least one letter`, t`Error Uploading Sprite`); | |
| 825 | + return ''; | |
| 826 | + } | |
| 827 | + | |
| 828 | + spriteName = spriteName || label; | |
| 829 | + if (!validateExpressionSpriteName(label, spriteName)) { | |
| 830 | + toastr.error(t`Invalid sprite name. Must follow the naming pattern for expression sprites.`, t`Error Uploading Sprite`); | |
| 831 | + return ''; | |
| 832 | + } | |
| 1024 | 833 | |
| 1025 | 834 | name = name || getLastCharacterMessage().original_avatar || getLastCharacterMessage().name; |
| 1026 | 835 | const char = findChar({ name }); |
| @@ -1041,6 +850,7 @@ async function uploadSpriteCommand({ name, label, folder }, imageUrl) { | ||
| 1041 | 850 | formData.append('name', folder); // this is the folder or character name |
| 1042 | 851 | formData.append('label', label); // this is the expression label |
| 1043 | 852 | formData.append('avatar', file); // this is the image file |
| 853 | + formData.append('spriteName', spriteName); // this is a redundant comment | |
| 1044 | 854 | |
| 1045 | 855 | await handleFileUpload('/api/sprites/upload', formData); |
| 1046 | 856 | console.debug(`[${MODULE_NAME}] Upload of ${imageUrl} completed for ${name} with label ${label}`); |
| @@ -1048,6 +858,8 @@ async function uploadSpriteCommand({ name, label, folder }, imageUrl) { | ||
| 1048 | 858 | console.error(`[${MODULE_NAME}] Error uploading file:`, error); |
| 1049 | 859 | throw error; |
| 1050 | 860 | } |
| 861 | + | |
| 862 | + return spriteName; | |
| 1051 | 863 | } |
| 1052 | 864 | |
| 1053 | 865 | /** |
| @@ -1159,7 +971,7 @@ function getJsonSchema(emotions) { | ||
| 1159 | 971 | function onTextGenSettingsReady(args) { |
| 1160 | 972 | // Only call if inside an API call |
| 1161 | 973 | if (inApiCall && extension_settings.expressions.api === EXPRESSION_API.llm && isJsonSchemaSupported()) { |
| 1162 | - const emotions = DEFAULT_EXPRESSIONS.filter((e) => e != 'talkinghead'); | |
| 974 | + const emotions = DEFAULT_EXPRESSIONS; | |
| 1163 | 975 | Object.assign(args, { |
| 1164 | 976 | top_k: 1, |
| 1165 | 977 | stop: [], |
| @@ -1177,16 +989,16 @@ function onTextGenSettingsReady(args) { | ||
| 1177 | 989 | * @param {EXPRESSION_API} [expressionsApi=extension_settings.expressions.api] - The expressions API to use for classification. |
| 1178 | 990 | * @param {object} [options={}] - Optional arguments. |
| 1179 | 991 | * @param {string?} [options.customPrompt=null] - The custom prompt to use for classification. |
| 1180 | 992 | * @returns {Promise<string?>} - The label of the expression. |
| 1181 | 993 | */ |
| 1182 | 994 | export async function getExpressionLabel(text, expressionsApi = extension_settings.expressions.api, { customPrompt = null } = {}) { |
| 1183 | 995 | // Return if text is undefined, saving a costly fetch request |
| 1184 | 996 | if ((!modules.includes('classify') && expressionsApi == EXPRESSION_API.extras) || !text) { |
| 1185 | 997 | return getFallbackExpression()extension_settings.expressions.fallback_expression; |
| 1186 | 998 | } |
| 1187 | 999 | |
| 1188 | 1000 | if (extension_settings.expressions.translate && typeof window['globalThis.translate'] === 'function') { |
| 1189 | 1001 | text = await window['globalThis.translate'](text, 'en'); |
| 1190 | 1002 | } |
| 1191 | 1003 | |
| 1192 | 1004 | text = sampleClassifyText(text); |
| @@ -1212,7 +1024,7 @@ export async function getExpressionLabel(text, expressionsApi = extension_settin | ||
| 1212 | 1024 | await waitUntilCondition(() => online_status !== 'no_connection', 3000, 250); |
| 1213 | 1025 | } catch (error) { |
| 1214 | 1026 | console.warn('No LLM connection. Using fallback expression', error); |
| 1215 | 1027 | return getFallbackExpression()extension_settings.expressions.fallback_expression; |
| 1216 | 1028 | } |
| 1217 | 1029 | |
| 1218 | 1030 | const expressionsList = await getExpressionsList(); |
| @@ -1225,7 +1037,7 @@ export async function getExpressionLabel(text, expressionsApi = extension_settin | ||
| 1225 | 1037 | case EXPRESSION_API.webllm: { |
| 1226 | 1038 | if (!isWebLlmSupported()) { |
| 1227 | 1039 | console.warn('WebLLM is not supported. Using fallback expression'); |
| 1228 | 1040 | return getFallbackExpression()extension_settings.expressions.fallback_expression; |
| 1229 | 1041 | } |
| 1230 | 1042 | |
| 1231 | 1043 | const expressionsList = await getExpressionsList(); |
| @@ -1258,9 +1070,9 @@ export async function getExpressionLabel(text, expressionsApi = extension_settin | ||
| 1258 | 1070 | } break; |
| 1259 | 1071 | } |
| 1260 | 1072 | } catch (error) { |
| 1261 | 1073 | toastr.infoerror('Could not classify expression. Check the console or your backend for more information.'); |
| 1262 | 1074 | console.error(error); |
| 1263 | 1075 | return getFallbackExpression()extension_settings.expressions.fallback_expression; |
| 1264 | 1076 | } |
| 1265 | 1077 | } |
| 1266 | 1078 | |
| @@ -1288,75 +1100,155 @@ function removeExpression() { | ||
| 1288 | 1100 | $('#no_chat_expressions').show(); |
| 1289 | 1101 | } |
| 1290 | 1102 | |
| 1291 | -async function validateImages(character, forceRedrawCached) { | |
| 1103 | +/** | |
| 1292 | - if (!character) { | |
| 1104 | + * Validate a character's sprites, and redraw the sprites list if not done before or forced to redraw. | |
| 1105 | + * @param {string} spriteFolderName - The character sprite folder to validate | |
| 1106 | + * @param {boolean} [forceRedrawCached=false] - Whether to force redrawing the sprites list even if it's already been drawn before | |
| 1107 | + */ | |
| 1108 | +async function validateImages(spriteFolderName, forceRedrawCached = false) { | |
| 1109 | + if (!spriteFolderName) { | |
| 1293 | 1110 | return; |
| 1294 | 1111 | } |
| 1295 | 1112 | |
| 1296 | 1113 | const labels = await getExpressionsList(); |
| 1297 | 1114 | |
| 1298 | 1115 | if (spriteCache[characterspriteFolderName]) { |
| 1299 | 1116 | if (forceRedrawCached && $('#image_list').data('name') !== characterspriteFolderName) { |
| 1300 | 1117 | console.debug('force redrawing character sprites list'); |
| 1301 | 1118 | await drawSpritesList(characterspriteFolderName, labels, spriteCache[characterspriteFolderName]); |
| 1302 | 1119 | } |
| 1303 | 1120 | |
| 1304 | 1121 | return; |
| 1305 | 1122 | } |
| 1306 | 1123 | |
| 1307 | 1124 | const sprites = await getSpritesList(characterspriteFolderName); |
| 1308 | 1125 | let validExpressions = await drawSpritesList(characterspriteFolderName, labels, sprites); |
| 1309 | 1126 | spriteCache[characterspriteFolderName] = validExpressions; |
| 1127 | +} | |
| 1128 | + | |
| 1129 | +/** | |
| 1130 | + * Takes a given sprite as returned from the server, and enriches it with additional data for display/sorting | |
| 1131 | + * @param {{ path: string, label: string }} sprite | |
| 1132 | + * @returns {ExpressionImage} | |
| 1133 | + */ | |
| 1134 | +function getExpressionImageData(sprite) { | |
| 1135 | + const fileName = sprite.path.split('/').pop().split('?')[0]; | |
| 1136 | + const fileNameWithoutExtension = fileName.replace(/\.[^/.]+$/, ''); | |
| 1137 | + return { | |
| 1138 | + expression: sprite.label, | |
| 1139 | + fileName: fileName, | |
| 1140 | + title: fileNameWithoutExtension, | |
| 1141 | + imageSrc: sprite.path, | |
| 1142 | + type: 'success', | |
| 1143 | + isCustom: extension_settings.expressions.custom?.includes(sprite.label), | |
| 1144 | + }; | |
| 1310 | 1145 | } |
| 1311 | 1146 | |
| 1312 | -async function drawSpritesList(character, labels, sprites) { | |
| 1147 | +/** | |
| 1148 | + * Populate the character expression list with sprites for the given character. | |
| 1149 | + * @param {string} spriteFolderName - The name of the character to populate the list for | |
| 1150 | + * @param {string[]} labels - An array of expression labels that are valid | |
| 1151 | + * @param {Expression[]} sprites - An array of sprites | |
| 1152 | + * @returns {Promise<Expression[]>} An array of valid expression labels | |
| 1153 | + */ | |
| 1154 | +async function drawSpritesList(spriteFolderName, labels, sprites) { | |
| 1155 | + /** @type {Expression[]} */ | |
| 1313 | 1156 | let validExpressions = []; |
| 1157 | + | |
| 1314 | 1158 | $('#no_chat_expressions').hide(); |
| 1315 | 1159 | $('#open_chat_expressions').show(); |
| 1316 | 1160 | $('#image_list').empty(); |
| 1317 | 1161 | $('#image_list').data('name', characterspriteFolderName); |
| 1318 | 1162 | $('#image_list_header_name').text(characterspriteFolderName); |
| 1319 | 1163 | |
| 1320 | 1164 | if (!Array.isArray(labels)) { |
| 1321 | 1165 | return []; |
| 1322 | 1166 | } |
| 1323 | 1167 | |
| 1324 | 1168 | for (const itemexpression of labels.sort()) { |
| 1325 | - const sprite = sprites.find(x => x.label == item); | |
| 1169 | + const isCustom = extension_settings.expressions.custom?.includes(expression); | |
| 1326 | - const isCustom = extension_settings.expressions.custom.includes(item); | |
| 1170 | + const images = sprites | |
| 1327 | - | |
| 1171 | + .filter(s => s.label === expression) | |
| 1328 | - if (sprite) { | |
| 1172 | + .map(s => s.files) | |
| 1329 | 1173 | validExpressions.pushflat(sprite); |
| 1330 | - const listItem = await getListItem(item, sprite.path, 'success', isCustom); | |
| 1174 | + | |
| 1175 | + if (images.length === 0) { | |
| 1176 | + const listItem = await getListItem(expression, { | |
| 1177 | + isCustom, | |
| 1178 | + images: [getPlaceholderImage(expression, isCustom)], | |
| 1179 | + }); | |
| 1331 | 1180 | $('#image_list').append(listItem); |
| 1181 | + continue; | |
| 1332 | 1182 | } |
| 1333 | - else { | |
| 1183 | + | |
| 1334 | - const listItem = await getListItem(item, '/img/No-Image-Placeholder.svg', 'failure', isCustom); | |
| 1184 | + validExpressions.push({ label: expression, files: images }); | |
| 1185 | + | |
| 1186 | + // Render main = first file, additional = rest | |
| 1187 | + let listItem = await getListItem(expression, { | |
| 1188 | + isCustom, | |
| 1189 | + images, | |
| 1190 | + }); | |
| 1335 | 1191 | $('#image_list').append(listItem); |
| 1336 | 1192 | } |
| 1337 | - } | |
| 1338 | 1193 | return validExpressions; |
| 1339 | 1194 | } |
| 1340 | 1195 | |
| 1341 | 1196 | /** |
| 1342 | 1197 | * Renders a list item template for the expressions list. |
| 1343 | 1198 | * @param {string} itemexpression Expression name |
| 1344 | 1199 | * @param {stringobject} imageSrc Pathargs toArguments imageobject |
| 1345 | - * @param {'success' | 'failure'} textClass 'success' or 'failure' | |
| 1200 | + * @param {ExpressionImage[]} [args.images] Array of image objects | |
| 1346 | 1201 | * @param {boolean} [args.isCustom=false] If expression is added by user |
| 1347 | 1202 | * @returns {Promise<string>} Rendered list item template |
| 1348 | 1203 | */ |
| 1349 | 1204 | async function getListItem(itemexpression, imageSrc,{ textClassimages, isCustom = false } = {}) { |
| 1350 | 1205 | return renderExtensionTemplateAsync(MODULE_NAME, 'list-item', { itemexpression, imageSrcimages, textClass,isCustom: isCustom ?? false }); |
| 1351 | 1206 | } |
| 1352 | 1207 | |
| 1208 | +/** | |
| 1209 | + * Fetches and processes the list of sprites for a given character name. | |
| 1210 | + * Retrieves sprite data from the server and organizes it into labeled groups. | |
| 1211 | + * | |
| 1212 | + * @param {string} name - The character name to fetch sprites for | |
| 1213 | + * @returns {Promise<Expression[]>} A promise that resolves to an array of grouped expression objects, each containing a label and associated image data | |
| 1214 | + */ | |
| 1215 | + | |
| 1353 | 1216 | async function getSpritesList(name) { |
| 1354 | 1217 | console.debug('getting sprites list'); |
| 1355 | 1218 | |
| 1356 | 1219 | try { |
| 1357 | 1220 | const result = await fetch(`/api/sprites/get?name=${encodeURIComponent(name)}`); |
| 1221 | + /** @type {{ label: string, path: string }[]} */ | |
| 1358 | 1222 | let sprites = result.ok ? (await result.json()) : []; |
| 1359 | - return sprites; | |
| 1223 | + | |
| 1224 | + /** @type {Expression[]} */ | |
| 1225 | + const grouped = sprites.reduce((acc, sprite) => { | |
| 1226 | + const imageData = getExpressionImageData(sprite); | |
| 1227 | + let existingExpression = acc.find(exp => exp.label === sprite.label); | |
| 1228 | + if (existingExpression) { | |
| 1229 | + existingExpression.files.push(imageData); | |
| 1230 | + } else { | |
| 1231 | + acc.push({ label: sprite.label, files: [imageData] }); | |
| 1232 | + } | |
| 1233 | + | |
| 1234 | + return acc; | |
| 1235 | + }, []); | |
| 1236 | + | |
| 1237 | + // Sort the sprites for each expression alphabetically, but keep the main expression file at the front | |
| 1238 | + for (const expression of grouped) { | |
| 1239 | + expression.files.sort((a, b) => { | |
| 1240 | + if (a.title === expression.label) return -1; | |
| 1241 | + if (b.title === expression.label) return 1; | |
| 1242 | + return a.title.localeCompare(b.title); | |
| 1243 | + }); | |
| 1244 | + | |
| 1245 | + // Mark all besides the first sprite as 'additional' | |
| 1246 | + for (let i = 1; i < expression.files.length; i++) { | |
| 1247 | + expression.files[i].type = 'additional'; | |
| 1248 | + } | |
| 1249 | + } | |
| 1250 | + | |
| 1251 | + return grouped; | |
| 1360 | 1252 | } |
| 1361 | 1253 | catch (err) { |
| 1362 | 1254 | console.log(err); |
| @@ -1395,17 +1287,31 @@ async function renderFallbackExpressionPicker() { | ||
| 1395 | 1287 | const defaultPicker = $('#expression_fallback'); |
| 1396 | 1288 | defaultPicker.empty(); |
| 1397 | 1289 | |
| 1398 | - const fallbackExpression = getFallbackExpression(); | |
| 1290 | + | |
| 1291 | + addOption(OPTION_NO_FALLBACK, '[ No fallback ]', !extension_settings.expressions.fallback_expression); | |
| 1292 | + addOption(OPTION_EMOJI_FALLBACK, '[ Default emojis ]', !!extension_settings.expressions.showDefault); | |
| 1399 | 1293 | |
| 1400 | 1294 | for (const expression of expressions) { |
| 1295 | + addOption(expression, expression, expression == extension_settings.expressions.fallback_expression); | |
| 1296 | + } | |
| 1297 | + | |
| 1298 | + /** @type {(value: string, label: string, isSelected: boolean) => void} */ | |
| 1299 | + function addOption(value, label, isSelected) { | |
| 1401 | 1300 | const option = document.createElement('option'); |
| 1402 | 1301 | option.value = expressionvalue; |
| 1403 | 1302 | option.text = expressionlabel; |
| 1404 | 1303 | option.selected = expression == fallbackExpressionisSelected; |
| 1405 | 1304 | defaultPicker.append(option); |
| 1406 | 1305 | } |
| 1407 | 1306 | } |
| 1408 | 1307 | |
| 1308 | +/** | |
| 1309 | + * Retrieves a unique list of cached expressions. | |
| 1310 | + * Combines the default expressions list with custom user-defined expressions. | |
| 1311 | + * | |
| 1312 | + * @returns {string[]} An array of unique expression labels | |
| 1313 | + */ | |
| 1314 | + | |
| 1409 | 1315 | function getCachedExpressions() { |
| 1410 | 1316 | if (!Array.isArray(expressionsList)) { |
| 1411 | 1317 | return []; |
| @@ -1463,7 +1369,7 @@ export async function getExpressionsList() { | ||
| 1463 | 1369 | } |
| 1464 | 1370 | |
| 1465 | 1371 | // If there was no specific list, or an error, just return the default expressions |
| 1466 | 1372 | expressionsList = DEFAULT_EXPRESSIONS.filter(e => e !== 'talkinghead').slice(); |
| 1467 | 1373 | return expressionsList; |
| 1468 | 1374 | } |
| 1469 | 1375 | |
| @@ -1471,38 +1377,88 @@ export async function getExpressionsList() { | ||
| 1471 | 1377 | return [...result, ...extension_settings.expressions.custom].filter(onlyUnique); |
| 1472 | 1378 | } |
| 1473 | 1379 | |
| 1474 | -async function setExpression(character, expression, force) { | |
| 1380 | +/** | |
| 1475 | - if (!isTalkingHeadEnabled() || !modules.includes('talkinghead')) { | |
| 1381 | + * Selects a sprite from the given sprite folder for the given expression. | |
| 1476 | - console.debug('entered setExpressions'); | |
| 1382 | + * | |
| 1477 | - await validateImages(character); | |
| 1383 | + * If multiple sprites are allowed for the expression, it will randomly select one. | |
| 1384 | + * If the rerollIfSame option is enabled, it will only select a different sprite if the previous sprite was the same. | |
| 1385 | + * If the overrideSpriteFile option is set, it will look for the sprite with the given file name instead of randomly selecting one. | |
| 1386 | + * | |
| 1387 | + * @param {string} spriteFolderName - The name of the sprite folder | |
| 1388 | + * @param {string} expression - The expression to find the sprite for | |
| 1389 | + * @param {object} [options] - Options to select the sprite | |
| 1390 | + * @param {string} [options.prevExpressionSrc=null] - The source of the previous expression | |
| 1391 | + * @param {string} [options.overrideSpriteFile=null] - The file name of the sprite to select | |
| 1392 | + * @returns {ExpressionImage?} - The selected sprite | |
| 1393 | + */ | |
| 1394 | +function chooseSpriteForExpression(spriteFolderName, expression, { prevExpressionSrc = null, overrideSpriteFile = null } = {}) { | |
| 1395 | + if (!spriteCache[spriteFolderName]) return null; | |
| 1396 | + if (expression === RESET_SPRITE_LABEL) return null; | |
| 1397 | + | |
| 1398 | + // Search for sprites of that expression - or fallback expression sprites if enabled | |
| 1399 | + let sprite = spriteCache[spriteFolderName].find(x => x.label === expression); | |
| 1400 | + if (!(sprite?.files.length > 0) && extension_settings.expressions.fallback_expression) { | |
| 1401 | + sprite = spriteCache[spriteFolderName].find(x => x.label === extension_settings.expressions.fallback_expression); | |
| 1402 | + console.debug('Expression', expression, 'not found. Using fallback expression', extension_settings.expressions.fallback_expression); | |
| 1403 | + } | |
| 1404 | + if (!(sprite?.files.length > 0)) return null; | |
| 1405 | + | |
| 1406 | + let spriteFile = sprite.files[0]; | |
| 1407 | + | |
| 1408 | + // If a specific sprite file should be set, we are looking it up here | |
| 1409 | + if (overrideSpriteFile) { | |
| 1410 | + const searched = sprite.files.find(x => x.fileName === overrideSpriteFile); | |
| 1411 | + if (searched) spriteFile = searched; | |
| 1412 | + else toastr.warning(t`Couldn't find sprite file ${overrideSpriteFile} for expression ${expression}.`, t`Sprite Not Found`); | |
| 1413 | + } | |
| 1414 | + // Else calculate next expression, if multiple are allowed | |
| 1415 | + else if (extension_settings.expressions.allowMultiple && sprite.files.length > 1) { | |
| 1416 | + let possibleFiles = sprite.files; | |
| 1417 | + if (extension_settings.expressions.rerollIfSame) { | |
| 1418 | + possibleFiles = possibleFiles.filter(x => !prevExpressionSrc || x.imageSrc !== prevExpressionSrc); | |
| 1419 | + } | |
| 1420 | + spriteFile = possibleFiles[Math.floor(Math.random() * possibleFiles.length)]; | |
| 1421 | + } | |
| 1422 | + | |
| 1423 | + return spriteFile; | |
| 1424 | + | |
| 1425 | +} | |
| 1426 | + | |
| 1427 | +/** | |
| 1428 | + * Set the expression of a character. | |
| 1429 | + * @param {string} spriteFolderName - The name of the character (folder name - can also be a costume override) | |
| 1430 | + * @param {string} expression - The expression or sprite name to set | |
| 1431 | + * @param {Object} options - Optional parameters | |
| 1432 | + * @param {boolean} [options.force=false] - Whether to force the expression change even if Visual Novel mode is on | |
| 1433 | + * @param {string?} [options.overrideSpriteFile=null] - Set if a specific sprite file should be used. Must be sprite file name. | |
| 1434 | + * @returns {Promise<void>} A promise that resolves when the expression has been set. | |
| 1435 | + */ | |
| 1436 | +async function setExpression(spriteFolderName, expression, { force = false, overrideSpriteFile = null } = {}) { | |
| 1437 | + await validateImages(spriteFolderName); | |
| 1478 | 1438 | const img = $('img.expression'); |
| 1479 | 1439 | const prevExpressionSrc = img.attr('src'); |
| 1480 | 1440 | const expressionClone = img.clone(); |
| 1481 | 1441 | |
| 1482 | - const sprite = (spriteCache[character] && spriteCache[character].find(x => x.label === expression)); | |
| 1442 | + const spriteFile = chooseSpriteForExpression(spriteFolderName, expression, { prevExpressionSrc: prevExpressionSrc, overrideSpriteFile: overrideSpriteFile }); | |
| 1483 | - console.debug('checking for expression images to show..'); | |
| 1443 | + if (spriteFile) { | |
| 1484 | - if (sprite) { | |
| 1485 | - console.debug('setting expression from character images folder'); | |
| 1486 | - | |
| 1487 | 1444 | if (force && isVisualNovelMode()) { |
| 1488 | 1445 | const context = getContext(); |
| 1489 | 1446 | const group = context.groups.find(x => x.id === context.groupId); |
| 1490 | 1447 | |
| 1491 | - for (const member of group.members) { | |
| 1448 | + // If it's a folder, make sure we find the group member based on the actual name | |
| 1492 | - const groupMember = context.characters.find(x => x.avatar === member); | |
| 1449 | + const memberName = spriteFolderName.split('/')[0] ?? spriteFolderName; | |
| 1493 | - | |
| 1494 | - if (!groupMember) { | |
| 1495 | - continue; | |
| 1496 | - } | |
| 1497 | 1450 | |
| 1498 | - if (groupMember.name == character) { | |
| 1451 | + const groupMember = group.members | |
| 1499 | - await setImage($(`.expression-holder[data-avatar="${member}"] img`), sprite.path); | |
| 1452 | + .map(member => context.characters.find(x => x.avatar === member)) | |
| 1453 | + .find(groupMember => groupMember && groupMember.name === memberName); | |
| 1454 | + if (groupMember) { | |
| 1455 | + await setImage($(`.expression-holder[data-avatar="${groupMember.avatar}"] img`), spriteFile.imageSrc); | |
| 1500 | 1456 | return; |
| 1501 | 1457 | } |
| 1502 | 1458 | } |
| 1503 | - } | |
| 1459 | + | |
| 1504 | 1460 | //only swap expressions when necessary |
| 1505 | 1461 | if (prevExpressionSrc !== spritespriteFile.pathimageSrc |
| 1506 | 1462 | && !img.hasClass('expression-animating')) { |
| 1507 | 1463 | //clone expression |
| 1508 | 1464 | expressionClone.addClass('expression-clone'); |
| @@ -1510,7 +1466,12 @@ async function setExpression(character, expression, force) { | ||
| 1510 | 1466 | //must be made invisible to start because they share the same Z-index |
| 1511 | 1467 | expressionClone.attr('id', '').css({ opacity: 0 }); |
| 1512 | 1468 | //add new sprite path to clone src |
| 1513 | 1469 | expressionClone.attr('src', spritespriteFile.pathimageSrc); |
| 1470 | + //set relevant data tags | |
| 1471 | + expressionClone.attr('data-sprite-folder-name', spriteFolderName); | |
| 1472 | + expressionClone.attr('data-expression', expression); | |
| 1473 | + expressionClone.attr('data-sprite-filename', spriteFile.fileName); | |
| 1474 | + expressionClone.attr('title', expression); | |
| 1514 | 1475 | //add invisible clone to html |
| 1515 | 1476 | expressionClone.appendTo($('#expression-holder')); |
| 1516 | 1477 | |
| @@ -1552,80 +1513,85 @@ async function setExpression(character, expression, force) { | ||
| 1552 | 1513 | expressionHolder.css('min-height', 100); |
| 1553 | 1514 | }); |
| 1554 | 1515 | |
| 1555 | - | |
| 1556 | 1516 | expressionClone.removeClass('expression-clone'); |
| 1557 | 1517 | |
| 1558 | 1518 | expressionClone.removeClass('default'); |
| 1559 | 1519 | expressionClone.off('error'); |
| 1560 | 1520 | expressionClone.on('error', function (error) { |
| 1561 | 1521 | console.debug('Expression image error', spritespriteFile.pathimageSrc, error); |
| 1562 | 1522 | $(this).attr('src', ''); |
| 1563 | 1523 | $(this).off('error'); |
| 1564 | 1524 | if (force && extension_settings.expressions.showDefault) { |
| 1565 | - setDefault(); | |
| 1525 | + setDefaultEmojiForImage(img, expression); | |
| 1566 | 1526 | } |
| 1567 | 1527 | }); |
| 1568 | 1528 | } |
| 1529 | + | |
| 1530 | + console.info('Expression set', { expression: spriteFile.expression, file: spriteFile.fileName }); | |
| 1569 | 1531 | } |
| 1570 | 1532 | else { |
| 1571 | - if (extension_settings.expressions.showDefault) { | |
| 1533 | + img.attr('data-sprite-folder-name', spriteFolderName); | |
| 1572 | - setDefault(); | |
| 1573 | - } | |
| 1574 | - } | |
| 1575 | 1534 | |
| 1576 | - function setDefault() { | |
| 1535 | + img.off('error'); | |
| 1577 | - console.debug('setting default'); | |
| 1578 | - const defImgUrl = `/img/default-expressions/${expression}.png`; | |
| 1579 | - //console.log(defImgUrl); | |
| 1580 | - img.attr('src', defImgUrl); | |
| 1581 | - img.addClass('default'); | |
| 1582 | - } | |
| 1583 | - document.getElementById('expression-holder').style.display = ''; | |
| 1584 | 1536 | |
| 1537 | + if (extension_settings.expressions.showDefault && expression !== RESET_SPRITE_LABEL) { | |
| 1538 | + setDefaultEmojiForImage(img, expression); | |
| 1585 | 1539 | } else { |
| 1586 | - // Set the Talkinghead emotion to the specified expression | |
| 1540 | + setNoneForImage(img, expression); | |
| 1587 | - // TODO: For now, Talkinghead emote only supported when VN mode is off; see also updateVisualNovelMode. | |
| 1588 | - try { | |
| 1589 | - let result = await isTalkingHeadAvailable(); | |
| 1590 | - if (result) { | |
| 1591 | - const url = new URL(getApiUrl()); | |
| 1592 | - url.pathname = '/api/talkinghead/set_emotion'; | |
| 1593 | - await doExtrasFetch(url, { | |
| 1594 | - method: 'POST', | |
| 1595 | - headers: { | |
| 1596 | - 'Content-Type': 'application/json', | |
| 1597 | - }, | |
| 1598 | - body: JSON.stringify({ emotion_name: expression }), | |
| 1599 | - }); | |
| 1600 | 1541 | } |
| 1601 | - } | |
| 1542 | + console.debug('Expression unset - No sprite found', { expression: expression }); | |
| 1602 | - catch (error) { | |
| 1603 | - // `set_emotion` is not present in old versions, so let it 404. | |
| 1604 | 1543 | } |
| 1605 | 1544 | |
| 1606 | - try { | |
| 1545 | + document.getElementById('expression-holder').style.display = ''; | |
| 1607 | - // Find the <img> element with id="expression-image" and class="expression" | |
| 1608 | - const imgElement = document.querySelector('img#expression-image.expression'); | |
| 1609 | - //console.log("searching"); | |
| 1610 | - if (imgElement && imgElement instanceof HTMLImageElement) { | |
| 1611 | - //console.log("setting value"); | |
| 1612 | - imgElement.src = getApiUrl() + '/api/talkinghead/result_feed'; | |
| 1613 | - } | |
| 1614 | 1546 | } |
| 1615 | - catch (error) { | |
| 1547 | + | |
| 1616 | - //console.log("The fetch failed!"); | |
| 1548 | +/** | |
| 1549 | + * Sets the default expression image for the given image element and expression | |
| 1550 | + * @param {JQuery<HTMLElement>} img - The image element to set the default expression for | |
| 1551 | + * @param {string} expression - The expression label to use for the default image | |
| 1552 | + */ | |
| 1553 | +function setDefaultEmojiForImage(img, expression) { | |
| 1554 | + if (extension_settings.expressions.custom?.includes(expression)) { | |
| 1555 | + console.debug(`Can't set default emoji for a custom expression (${expression}). setting to ${DEFAULT_FALLBACK_EXPRESSION} instead.`); | |
| 1556 | + expression = DEFAULT_FALLBACK_EXPRESSION; | |
| 1617 | 1557 | } |
| 1558 | + | |
| 1559 | + const defImgUrl = `/img/default-expressions/${expression}.png`; | |
| 1560 | + img.attr('src', defImgUrl); | |
| 1561 | + img.attr('data-expression', expression); | |
| 1562 | + img.attr('data-sprite-filename', null); | |
| 1563 | + img.attr('title', expression); | |
| 1564 | + img.addClass('default'); | |
| 1618 | 1565 | } |
| 1566 | + | |
| 1567 | +/** | |
| 1568 | + * Sets the image element to display no expression by clearing its source attribute. | |
| 1569 | + * @param {JQuery<HTMLElement>} img - The image element to clear the expression for | |
| 1570 | + * @param {string} expression - The expression label to use | |
| 1571 | + */ | |
| 1572 | +function setNoneForImage(img, expression) { | |
| 1573 | + img.attr('src', ''); | |
| 1574 | + img.attr('data-expression', expression); | |
| 1575 | + img.attr('data-sprite-filename', null); | |
| 1576 | + img.attr('title', expression); | |
| 1577 | + img.removeClass('default'); | |
| 1619 | 1578 | } |
| 1620 | 1579 | |
| 1621 | 1580 | function onClickExpressionImage() { |
| 1622 | - const expression = $(this).attr('id'); | |
| 1581 | + // If there is no expression image and we clicked on the placeholder, we remove the sprite by calling via the expression label | |
| 1623 | - setSpriteSlashCommand({}, expression); | |
| 1582 | + if ($(this).attr('data-expression-type') === 'failure') { | |
| 1583 | + const label = $(this).attr('data-expression'); | |
| 1584 | + setSpriteSlashCommand({ type: 'expression' }, label); | |
| 1585 | + return; | |
| 1586 | + } | |
| 1587 | + | |
| 1588 | + const spriteFile = $(this).attr('data-filename'); | |
| 1589 | + setSpriteSlashCommand({ type: 'sprite' }, spriteFile); | |
| 1624 | 1590 | } |
| 1625 | 1591 | |
| 1626 | 1592 | async function onClickExpressionAddCustom() { |
| 1627 | 1593 | const template = await renderExtensionTemplateAsync(MODULE_NAME, 'add-custom-expression'); |
| 1628 | 1594 | let expressionName = await callPopupPopup.show.input(templatenull, 'input'template); |
| 1629 | 1595 | |
| 1630 | 1596 | if (!expressionName) { |
| 1631 | 1597 | console.debug('No custom expression name provided'); |
| @@ -1636,19 +1602,15 @@ async function onClickExpressionAddCustom() { | ||
| 1636 | 1602 | |
| 1637 | 1603 | // a-z, 0-9, dashes and underscores only |
| 1638 | 1604 | if (!/^[a-z0-9-_]+$/.test(expressionName)) { |
| 1639 | 1605 | toastr.infowarning('Invalid custom expression name provided', 'Add Custom Expression'); |
| 1640 | 1606 | return; |
| 1641 | 1607 | } |
| 1642 | - | |
| 1608 | + if (DEFAULT_EXPRESSIONS.includes(expressionName) || DEFAULT_EXPRESSIONS.some(x => expressionName.startsWith(x))) { | |
| 1643 | - // Check if expression name already exists in default expressions | |
| 1609 | + toastr.warning('Expression name already exists', 'Add Custom Expression'); | |
| 1644 | - if (DEFAULT_EXPRESSIONS.includes(expressionName)) { | |
| 1645 | - toastr.info('Expression name already exists'); | |
| 1646 | 1610 | return; |
| 1647 | 1611 | } |
| 1648 | - | |
| 1649 | - // Check if expression name already exists in custom expressions | |
| 1650 | 1612 | if (extension_settings.expressions.custom.includes(expressionName)) { |
| 1651 | 1613 | toastr.infowarning('Custom expression already exists', 'Add Custom Expression'); |
| 1652 | 1614 | return; |
| 1653 | 1615 | } |
| 1654 | 1616 | |
| @@ -1665,14 +1627,15 @@ async function onClickExpressionAddCustom() { | ||
| 1665 | 1627 | |
| 1666 | 1628 | async function onClickExpressionRemoveCustom() { |
| 1667 | 1629 | const selectedExpression = String($('#expression_custom').val()); |
| 1630 | + const noCustomExpressions = extension_settings.expressions.custom.length === 0; | |
| 1668 | 1631 | |
| 1669 | 1632 | if (!selectedExpression || noCustomExpressions) { |
| 1670 | 1633 | console.debug('No custom expression selected'); |
| 1671 | 1634 | return; |
| 1672 | 1635 | } |
| 1673 | 1636 | |
| 1674 | 1637 | const template = await renderExtensionTemplateAsync(MODULE_NAME, 'remove-custom-expression', { expression: selectedExpression }); |
| 1675 | 1638 | const confirmation = await callPopupPopup.show.confirm(templatenull, 'confirm'template); |
| 1676 | 1639 | |
| 1677 | 1640 | if (!confirmation) { |
| 1678 | 1641 | console.debug('Custom expression removal cancelled'); |
| @@ -1682,8 +1645,8 @@ async function onClickExpressionRemoveCustom() { | ||
| 1682 | 1645 | // Remove custom expression from settings |
| 1683 | 1646 | const index = extension_settings.expressions.custom.indexOf(selectedExpression); |
| 1684 | 1647 | extension_settings.expressions.custom.splice(index, 1); |
| 1685 | 1648 | if (selectedExpression == getFallbackExpression()extension_settings.expressions.fallback_expression) { |
| 1686 | 1649 | toastr.warning(`Deleted custom expression '${selectedExpression}' that was also selected as the fallback expression.\nFallback expression has been reset to '${DEFAULT_FALLBACK_EXPRESSION}'.`, 'Remove Custom Expression'); |
| 1687 | 1650 | extension_settings.expressions.fallback_expression = DEFAULT_FALLBACK_EXPRESSION; |
| 1688 | 1651 | } |
| 1689 | 1652 | await renderAdditionalExpressionSettings(); |
| @@ -1707,12 +1670,35 @@ function onExpressionApiChanged() { | ||
| 1707 | 1670 | } |
| 1708 | 1671 | } |
| 1709 | 1672 | |
| 1710 | 1673 | async function onExpressionFallbackChanged() { |
| 1711 | - const expression = this.value; | |
| 1674 | + /** @type {HTMLSelectElement} */ | |
| 1712 | - if (expression) { | |
| 1675 | + const select = this; | |
| 1713 | - extension_settings.expressions.fallback_expression = expression; | |
| 1676 | + const selectedValue = select.value; | |
| 1714 | - saveSettingsDebounced(); | |
| 1677 | + | |
| 1678 | + switch (selectedValue) { | |
| 1679 | + case OPTION_NO_FALLBACK: | |
| 1680 | + extension_settings.expressions.fallback_expression = null; | |
| 1681 | + extension_settings.expressions.showDefault = false; | |
| 1682 | + break; | |
| 1683 | + case OPTION_EMOJI_FALLBACK: | |
| 1684 | + extension_settings.expressions.fallback_expression = null; | |
| 1685 | + extension_settings.expressions.showDefault = true; | |
| 1686 | + break; | |
| 1687 | + default: | |
| 1688 | + extension_settings.expressions.fallback_expression = selectedValue; | |
| 1689 | + extension_settings.expressions.showDefault = false; | |
| 1690 | + break; | |
| 1691 | + } | |
| 1692 | + | |
| 1693 | + const img = $('img.expression'); | |
| 1694 | + const spriteFolderName = img.attr('data-sprite-folder-name'); | |
| 1695 | + const expression = img.attr('data-expression'); | |
| 1696 | + | |
| 1697 | + if (spriteFolderName && expression) { | |
| 1698 | + await sendExpressionCall(spriteFolderName, expression, { force: true }); | |
| 1715 | 1699 | } |
| 1700 | + | |
| 1701 | + saveSettingsDebounced(); | |
| 1716 | 1702 | } |
| 1717 | 1703 | |
| 1718 | 1704 | async function handleFileUpload(url, formData) { |
| @@ -1739,34 +1725,111 @@ async function handleFileUpload(url, formData) { | ||
| 1739 | 1725 | } |
| 1740 | 1726 | } |
| 1741 | 1727 | |
| 1728 | +/** | |
| 1729 | + * Removes the file extension from a file name | |
| 1730 | + * @param {string} fileName The file name to remove the extension from | |
| 1731 | + * @returns {string} The file name without the extension | |
| 1732 | + */ | |
| 1733 | +function withoutExtension(fileName) { | |
| 1734 | + return fileName.replace(/\.[^/.]+$/, ''); | |
| 1735 | +} | |
| 1736 | + | |
| 1737 | +function validateExpressionSpriteName(expression, spriteName) { | |
| 1738 | + const filenameValidationRegex = new RegExp(`^${expression}(?:[-\\.].*?)?$`); | |
| 1739 | + const validFileName = filenameValidationRegex.test(spriteName); | |
| 1740 | + return validFileName; | |
| 1741 | +} | |
| 1742 | + | |
| 1742 | 1743 | async function onClickExpressionUpload(event) { |
| 1743 | 1744 | // Prevents the expression from being set |
| 1744 | 1745 | event.stopPropagation(); |
| 1745 | 1746 | |
| 1746 | 1747 | const idexpressionListItem = $(this).closest('.expression_list_item').attr('id'); |
| 1748 | + | |
| 1749 | + const clickedFileName = expressionListItem.attr('data-expression-type') !== 'failure' ? expressionListItem.attr('data-filename') : null; | |
| 1750 | + const expression = expressionListItem.data('expression'); | |
| 1747 | 1751 | const name = $('#image_list').data('name'); |
| 1748 | 1752 | |
| 1749 | 1753 | const handleExpressionUploadChange = async (e) => { |
| 1750 | 1754 | const file = e.target.files[0]; |
| 1751 | 1755 | |
| 1752 | 1756 | if (!file || !file.name) { |
| 1757 | + console.debug('No valid file selected'); | |
| 1758 | + return; | |
| 1759 | + } | |
| 1760 | + | |
| 1761 | + const existingFiles = spriteCache[name]?.find(x => x.label === expression)?.files || []; | |
| 1762 | + | |
| 1763 | + let spriteName = expression; | |
| 1764 | + | |
| 1765 | + if (extension_settings.expressions.allowMultiple) { | |
| 1766 | + const matchesExisting = existingFiles.some(x => x.fileName === file.name); | |
| 1767 | + const fileNameWithoutExtension = withoutExtension(file.name); | |
| 1768 | + const validFileName = validateExpressionSpriteName(expression, fileNameWithoutExtension); | |
| 1769 | + | |
| 1770 | + // If there is no expression yet and it's a valid expression, we just take it | |
| 1771 | + if (!clickedFileName && validFileName) { | |
| 1772 | + spriteName = fileNameWithoutExtension; | |
| 1773 | + } | |
| 1774 | + // If the filename matches the one that was clicked, we just take it and replace it | |
| 1775 | + else if (clickedFileName === file.name) { | |
| 1776 | + spriteName = fileNameWithoutExtension; | |
| 1777 | + } | |
| 1778 | + // If it's a valid filename and there's no existing file with the same name, we just take it | |
| 1779 | + else if (!matchesExisting && validFileName) { | |
| 1780 | + spriteName = fileNameWithoutExtension; | |
| 1781 | + } | |
| 1782 | + else { | |
| 1783 | + /** @type {import('../../popup.js').CustomPopupButton[]} */ | |
| 1784 | + const customButtons = []; | |
| 1785 | + if (clickedFileName) { | |
| 1786 | + customButtons.push({ | |
| 1787 | + text: t`Replace Existing`, | |
| 1788 | + result: POPUP_RESULT.NEGATIVE, | |
| 1789 | + action: () => { | |
| 1790 | + console.debug('Replacing existing sprite'); | |
| 1791 | + spriteName = withoutExtension(clickedFileName); | |
| 1792 | + }, | |
| 1793 | + }); | |
| 1794 | + } | |
| 1795 | + | |
| 1796 | + spriteName = null; | |
| 1797 | + const suggestedSpriteName = generateUniqueSpriteName(expression, existingFiles); | |
| 1798 | + | |
| 1799 | + const message = await renderExtensionTemplateAsync(MODULE_NAME, 'templates/upload-expression', { expression, clickedFileName }); | |
| 1800 | + | |
| 1801 | + const input = await Popup.show.input(t`Upload Expression Sprite`, message, | |
| 1802 | + suggestedSpriteName, { customButtons: customButtons }); | |
| 1803 | + | |
| 1804 | + if (input) { | |
| 1805 | + if (!validateExpressionSpriteName(expression, input)) { | |
| 1806 | + toastr.warning(t`The name you entered does not follow the naming schema for the selected expression '${expression}'.`, t`Invalid Expression Sprite Name`); | |
| 1807 | + return; | |
| 1808 | + } | |
| 1809 | + spriteName = input; | |
| 1810 | + } | |
| 1811 | + } | |
| 1812 | + } else { | |
| 1813 | + spriteName = withoutExtension(clickedFileName); | |
| 1814 | + } | |
| 1815 | + | |
| 1816 | + if (!spriteName) { | |
| 1817 | + toastr.warning(t`Cancelled uploading sprite.`, t`Upload Cancelled`); | |
| 1818 | + // Reset the input | |
| 1819 | + e.target.form.reset(); | |
| 1753 | 1820 | return; |
| 1754 | 1821 | } |
| 1755 | 1822 | |
| 1756 | 1823 | const formData = new FormData(); |
| 1757 | 1824 | formData.append('name', name); |
| 1758 | 1825 | formData.append('label', idexpression); |
| 1759 | 1826 | formData.append('avatar', file); |
| 1827 | + formData.append('spriteName', spriteName); | |
| 1760 | 1828 | |
| 1761 | 1829 | await handleFileUpload('/api/sprites/upload', formData); |
| 1762 | 1830 | |
| 1763 | 1831 | // Reset the input |
| 1764 | 1832 | e.target.form.reset(); |
| 1765 | - | |
| 1766 | - // In Talkinghead mode, when a new talkinghead image is uploaded, refresh the live char. | |
| 1767 | - if (id === 'talkinghead' && isTalkingHeadEnabled() && modules.includes('talkinghead')) { | |
| 1768 | - await loadTalkingHead(); | |
| 1769 | - } | |
| 1770 | 1833 | }; |
| 1771 | 1834 | |
| 1772 | 1835 | $('#expression_upload') |
| @@ -1822,8 +1885,9 @@ async function onClickExpressionOverrideButton() { | ||
| 1822 | 1885 | inApiCall = true; |
| 1823 | 1886 | $('#visual-novel-wrapper').empty(); |
| 1824 | 1887 | await validateImages(overridePath.length === 0 ? currentLastMessage.name : overridePath, true); |
| 1888 | + const name = overridePath.length === 0 ? currentLastMessage.name : overridePath; | |
| 1825 | 1889 | const expression = await getExpressionLabel(currentLastMessage.mes); |
| 1826 | 1890 | await sendExpressionCall(overridePath.length === 0 ? currentLastMessage.name : overridePath, expression, { force: true }); |
| 1827 | 1891 | forceUpdateVisualNovelMode(); |
| 1828 | 1892 | } catch (error) { |
| 1829 | 1893 | console.debug(`Setting expression override for ${avatarFileName} failed with error: ${error}`); |
| @@ -1849,7 +1913,7 @@ async function onClickExpressionOverrideRemoveAllButton() { | ||
| 1849 | 1913 | const currentLastMessage = getLastCharacterMessage(); |
| 1850 | 1914 | await validateImages(currentLastMessage.name, true); |
| 1851 | 1915 | const expression = await getExpressionLabel(currentLastMessage.mes); |
| 1852 | 1916 | await sendExpressionCall(currentLastMessage.name, expression, { force: true }); |
| 1853 | 1917 | forceUpdateVisualNovelMode(); |
| 1854 | 1918 | |
| 1855 | 1919 | console.debug(extension_settings.expressionOverrides); |
| @@ -1872,16 +1936,13 @@ async function onClickExpressionUploadPackButton() { | ||
| 1872 | 1936 | formData.append('name', name); |
| 1873 | 1937 | formData.append('avatar', file); |
| 1874 | 1938 | |
| 1939 | + const uploadToast = toastr.info('Please wait...', 'Upload is processing', { timeOut: 0, extendedTimeOut: 0 }); | |
| 1875 | 1940 | const { count } = await handleFileUpload('/api/sprites/upload-zip', formData); |
| 1941 | + toastr.clear(uploadToast); | |
| 1876 | 1942 | toastr.success(`Uploaded ${count} image(s) for ${name}`); |
| 1877 | 1943 | |
| 1878 | 1944 | // Reset the input |
| 1879 | 1945 | e.target.form.reset(); |
| 1880 | - | |
| 1881 | - // In Talkinghead mode, refresh the live char. | |
| 1882 | - if (isTalkingHeadEnabled() && modules.includes('talkinghead')) { | |
| 1883 | - await loadTalkingHead(); | |
| 1884 | - } | |
| 1885 | 1946 | }; |
| 1886 | 1947 | |
| 1887 | 1948 | $('#expression_upload_pack') |
| @@ -1894,20 +1955,28 @@ async function onClickExpressionDelete(event) { | ||
| 1894 | 1955 | // Prevents the expression from being set |
| 1895 | 1956 | event.stopPropagation(); |
| 1896 | 1957 | |
| 1897 | - const confirmation = await callPopup('<h3>Are you sure?</h3>Once deleted, it\'s gone forever!', 'confirm'); | |
| 1958 | + const expressionListItem = $(this).closest('.expression_list_item'); | |
| 1959 | + const expression = expressionListItem.data('expression'); | |
| 1960 | + | |
| 1961 | + if (expressionListItem.attr('data-expression-type') === 'failure') { | |
| 1962 | + return; | |
| 1963 | + } | |
| 1898 | 1964 | |
| 1965 | + const confirmation = await Popup.show.confirm(t`Delete Expression`, t`Are you sure you want to delete this expression? Once deleted, it\'s gone forever!` | |
| 1966 | + + '<br /><br />' | |
| 1967 | + + t`Expression:` + ' <tt>' + expressionListItem.attr('data-filename') + '</tt>'); | |
| 1899 | 1968 | if (!confirmation) { |
| 1900 | 1969 | return; |
| 1901 | 1970 | } |
| 1902 | 1971 | |
| 1903 | 1972 | const idfileName = $(this).closestwithoutExtension('.expression_list_item')expressionListItem.attr('iddata-filename')); |
| 1904 | 1973 | const name = $('#image_list').data('name'); |
| 1905 | 1974 | |
| 1906 | 1975 | try { |
| 1907 | 1976 | await fetch('/api/sprites/delete', { |
| 1908 | 1977 | method: 'POST', |
| 1909 | 1978 | headers: getRequestHeaders(), |
| 1910 | 1979 | body: JSON.stringify({ name, label: idexpression, spriteName: fileName }), |
| 1911 | 1980 | }); |
| 1912 | 1981 | } catch (error) { |
| 1913 | 1982 | toastr.error('Failed to delete image. Try again later.'); |
| @@ -1984,6 +2053,16 @@ function migrateSettings() { | ||
| 1984 | 2053 | extension_settings.expressions.llmPrompt = DEFAULT_LLM_PROMPT; |
| 1985 | 2054 | saveSettingsDebounced(); |
| 1986 | 2055 | } |
| 2056 | + | |
| 2057 | + if (extension_settings.expressions.allowMultiple === undefined) { | |
| 2058 | + extension_settings.expressions.allowMultiple = true; | |
| 2059 | + saveSettingsDebounced(); | |
| 2060 | + } | |
| 2061 | + | |
| 2062 | + if (extension_settings.expressions.showDefault && extension_settings.expressions.fallback_expression !== undefined) { | |
| 2063 | + extension_settings.expressions.showDefault = false; | |
| 2064 | + saveSettingsDebounced(); | |
| 2065 | + } | |
| 1987 | 2066 | } |
| 1988 | 2067 | |
| 1989 | 2068 | (async function () { |
| @@ -2010,13 +2089,19 @@ function migrateSettings() { | ||
| 2010 | 2089 | const template = await renderExtensionTemplateAsync(MODULE_NAME, 'settings'); |
| 2011 | 2090 | $('#expressions_container').append(template); |
| 2012 | 2091 | $('#expression_override_button').on('click', onClickExpressionOverrideButton); |
| 2013 | - $('#expressions_show_default').on('input', onExpressionsShowDefaultInput); | |
| 2014 | 2092 | $('#expression_upload_pack_button').on('click', onClickExpressionUploadPackButton); |
| 2015 | - $('#expressions_show_default').prop('checked', extension_settings.expressions.showDefault).trigger('input'); | |
| 2016 | 2093 | $('#expression_translate').prop('checked', extension_settings.expressions.translate).on('input', function () { |
| 2017 | 2094 | extension_settings.expressions.translate = !!$(this).prop('checked'); |
| 2018 | 2095 | saveSettingsDebounced(); |
| 2019 | 2096 | }); |
| 2097 | + $('#expressions_allow_multiple').prop('checked', extension_settings.expressions.allowMultiple).on('input', function () { | |
| 2098 | + extension_settings.expressions.allowMultiple = !!$(this).prop('checked'); | |
| 2099 | + saveSettingsDebounced(); | |
| 2100 | + }); | |
| 2101 | + $('#expressions_reroll_if_same').prop('checked', extension_settings.expressions.rerollIfSame).on('input', function () { | |
| 2102 | + extension_settings.expressions.rerollIfSame = !!$(this).prop('checked'); | |
| 2103 | + saveSettingsDebounced(); | |
| 2104 | + }); | |
| 2020 | 2105 | $('#expression_override_cleanup_button').on('click', onClickExpressionOverrideRemoveAllButton); |
| 2021 | 2106 | $(document).on('dragstart', '.expression', (e) => { |
| 2022 | 2107 | e.preventDefault(); |
| @@ -2025,21 +2110,15 @@ function migrateSettings() { | ||
| 2025 | 2110 | $(document).on('click', '.expression_list_item', onClickExpressionImage); |
| 2026 | 2111 | $(document).on('click', '.expression_list_upload', onClickExpressionUpload); |
| 2027 | 2112 | $(document).on('click', '.expression_list_delete', onClickExpressionDelete); |
| 2028 | 2113 | $(window).on('resize', () => updateVisualNovelModeDebounced()); |
| 2029 | 2114 | $('#open_chat_expressions').hide(); |
| 2030 | 2115 | |
| 2031 | - $('#image_type_toggle').on('click', function () { | |
| 2032 | - if (this instanceof HTMLInputElement) { | |
| 2033 | - setTalkingHeadState(this.checked); | |
| 2034 | - } | |
| 2035 | - }); | |
| 2036 | - | |
| 2037 | 2116 | await renderAdditionalExpressionSettings(); |
| 2038 | 2117 | $('#expression_api').val(extension_settings.expressions.api ?? EXPRESSION_API.extras); |
| 2039 | 2118 | $('.expression_llm_prompt_block').toggle([EXPRESSION_API.llm, EXPRESSION_API.webllm].includes(extension_settings.expressions.api)); |
| 2040 | 2119 | $('#expression_llm_prompt').val(extension_settings.expressions.llmPrompt ?? ''); |
| 2041 | 2120 | $('#expression_llm_prompt').on('input', function () { |
| 2042 | 2121 | extension_settings.expressions.llmPrompt = String($(this).val()); |
| 2043 | 2122 | saveSettingsDebounced(); |
| 2044 | 2123 | }); |
| 2045 | 2124 | $('#expression_llm_prompt_restore').on('click', function () { |
| @@ -2054,34 +2133,6 @@ function migrateSettings() { | ||
| 2054 | 2133 | $('#expression_api').on('change', onExpressionApiChanged); |
| 2055 | 2134 | } |
| 2056 | 2135 | |
| 2057 | - // Pause Talkinghead to save resources when the ST tab is not visible or the window is minimized. | |
| 2058 | - // We currently do this via loading/unloading. Could be improved by adding new pause/unpause endpoints to Extras. | |
| 2059 | - document.addEventListener('visibilitychange', function (event) { | |
| 2060 | - let pageIsVisible; | |
| 2061 | - if (document.hidden) { | |
| 2062 | - console.debug('expressions: SillyTavern is now hidden'); | |
| 2063 | - pageIsVisible = false; | |
| 2064 | - } else { | |
| 2065 | - console.debug('expressions: SillyTavern is now visible'); | |
| 2066 | - pageIsVisible = true; | |
| 2067 | - } | |
| 2068 | - | |
| 2069 | - if (isTalkingHeadEnabled() && modules.includes('talkinghead')) { | |
| 2070 | - isTalkingHeadAvailable().then(result => { | |
| 2071 | - if (result) { | |
| 2072 | - if (pageIsVisible) { | |
| 2073 | - loadTalkingHead(); | |
| 2074 | - } else { | |
| 2075 | - unloadTalkingHead(); | |
| 2076 | - } | |
| 2077 | - handleImageChange(); // Change image as needed | |
| 2078 | - } else { | |
| 2079 | - //console.log("talkinghead does not exist."); | |
| 2080 | - } | |
| 2081 | - }); | |
| 2082 | - } | |
| 2083 | - }); | |
| 2084 | - | |
| 2085 | 2136 | addExpressionImage(); |
| 2086 | 2137 | addVisualNovelMode(); |
| 2087 | 2138 | migrateSettings(); |
| @@ -2090,11 +2141,6 @@ function migrateSettings() { | ||
| 2090 | 2141 | const updateFunction = wrapper.update.bind(wrapper); |
| 2091 | 2142 | setInterval(updateFunction, UPDATE_INTERVAL); |
| 2092 | 2143 | moduleWorker(); |
| 2093 | - // For setting the Talkinghead talking animation on/off quickly enough for realtime use, we need another timer on a shorter schedule. | |
| 2094 | - const wrapperTalkingState = new ModuleWorkerWrapper(updateTalkingState); | |
| 2095 | - const updateTalkingStateFunction = wrapperTalkingState.update.bind(wrapperTalkingState); | |
| 2096 | - setInterval(updateTalkingStateFunction, TALKINGCHECK_UPDATE_INTERVAL); | |
| 2097 | - updateTalkingState(); | |
| 2098 | 2144 | dragElement($('#expression-holder')); |
| 2099 | 2145 | eventSource.on(event_types.CHAT_CHANGED, () => { |
| 2100 | 2146 | // character changed |
| @@ -2108,111 +2154,137 @@ function migrateSettings() { | ||
| 2108 | 2154 | imgElement.src = ''; |
| 2109 | 2155 | } |
| 2110 | 2156 | |
| 2111 | - //set checkbox to global var | |
| 2112 | - $('#image_type_toggle').prop('checked', extension_settings.expressions.talkinghead); | |
| 2113 | - if (extension_settings.expressions.talkinghead) { | |
| 2114 | - setTalkingHeadState(extension_settings.expressions.talkinghead); | |
| 2115 | - } | |
| 2116 | - | |
| 2117 | 2157 | setExpressionOverrideHtml(); |
| 2118 | 2158 | |
| 2119 | 2159 | if (isVisualNovelMode()) { |
| 2120 | 2160 | $('#visual-novel-wrapper').empty(); |
| 2121 | 2161 | } |
| 2122 | 2162 | |
| 2123 | 2163 | updateFunction({ newChat: true }); |
| 2124 | 2164 | }); |
| 2125 | 2165 | eventSource.on(event_types.MOVABLE_PANELS_RESET, updateVisualNovelModeDebounced); |
| 2126 | 2166 | eventSource.on(event_types.GROUP_UPDATED, updateVisualNovelModeDebounced); |
| 2127 | - eventSource.on(event_types.EXTRAS_CONNECTED, () => { | |
| 2128 | - if (extension_settings.expressions.talkinghead) { | |
| 2129 | - setTalkingHeadState(extension_settings.expressions.talkinghead); | |
| 2130 | - } | |
| 2131 | - }); | |
| 2132 | 2167 | |
| 2133 | 2168 | const localEnumProviders = { |
| 2134 | 2169 | expressions: () => getCachedExpressions().map(expression => { |
| 2170 | + const currentLastMessage = selected_group ? getLastCharacterMessage() : null; | |
| 2171 | + const spriteFolderName = getSpriteFolderName(currentLastMessage, currentLastMessage?.name); | |
| 2172 | + const expressions = getCachedExpressions(); | |
| 2173 | + return expressions.map(expression => { | |
| 2174 | + const spriteCount = spriteCache[spriteFolderName]?.find(x => x.label === expression)?.files.length ?? 0; | |
| 2135 | 2175 | const isCustom = extension_settings.expressions.custom?.includes(expression); |
| 2136 | - return new SlashCommandEnumValue(expression, null, isCustom ? enumTypes.name : enumTypes.enum, isCustom ? 'C' : 'D'); | |
| 2176 | + const subtitle = spriteCount == 0 ? '❌ No sprites available for this expression' : | |
| 2137 | - }), | |
| 2177 | + spriteCount > 1 ? `${spriteCount} sprites` : null; | |
| 2178 | + return new SlashCommandEnumValue(expression, | |
| 2179 | + subtitle, | |
| 2180 | + isCustom ? enumTypes.name : enumTypes.enum, | |
| 2181 | + isCustom ? 'C' : 'D'); | |
| 2182 | + }); | |
| 2183 | + }, | |
| 2184 | + sprites: () => { | |
| 2185 | + const currentLastMessage = selected_group ? getLastCharacterMessage() : null; | |
| 2186 | + const spriteFolderName = getSpriteFolderName(currentLastMessage, currentLastMessage?.name); | |
| 2187 | + const sprites = spriteCache[spriteFolderName]?.map(x => x.files)?.flat() ?? []; | |
| 2188 | + return sprites.map(x => { | |
| 2189 | + return new SlashCommandEnumValue(x.title, | |
| 2190 | + x.title !== x.expression ? x.expression : null, | |
| 2191 | + x.isCustom ? enumTypes.name : enumTypes.enum, | |
| 2192 | + x.isCustom ? 'C' : 'D'); | |
| 2193 | + }); | |
| 2194 | + }, | |
| 2138 | 2195 | }; |
| 2139 | 2196 | |
| 2140 | 2197 | SlashCommandParser.addCommandObject(SlashCommand.fromProps({ |
| 2141 | 2198 | name: 'spriteexpression-set', |
| 2142 | 2199 | aliases: ['sprite', 'emote'], |
| 2143 | 2200 | callback: setSpriteSlashCommand, |
| 2201 | + namedArgumentList: [ | |
| 2202 | + SlashCommandNamedArgument.fromProps({ | |
| 2203 | + name: 'type', | |
| 2204 | + description: 'Whether to set an expression or a specific sprite.', | |
| 2205 | + typeList: [ARGUMENT_TYPE.STRING], | |
| 2206 | + isRequired: false, | |
| 2207 | + defaultValue: 'expression', | |
| 2208 | + enumList: ['expression', 'sprite'], | |
| 2209 | + }), | |
| 2210 | + ], | |
| 2144 | 2211 | unnamedArgumentList: [ |
| 2145 | 2212 | SlashCommandArgument.fromProps({ |
| 2146 | 2213 | description: 'spriteIdexpression label to set', |
| 2147 | 2214 | typeList: [ARGUMENT_TYPE.STRING], |
| 2148 | 2215 | isRequired: true, |
| 2149 | - enumProvider: localEnumProviders.expressions, | |
| 2216 | + enumProvider: (executor, _) => { | |
| 2217 | + // Check if command is used to set a sprite, then use those enums | |
| 2218 | + const type = executor.namedArgumentList.find(it => it.name == 'type')?.value || 'expression'; | |
| 2219 | + if (type == 'sprite') return localEnumProviders.sprites(); | |
| 2220 | + else return [ | |
| 2221 | + ...localEnumProviders.expressions(), | |
| 2222 | + new SlashCommandEnumValue(RESET_SPRITE_LABEL, 'Resets the expression (to either default or no sprite)', enumTypes.enum, '❌'), | |
| 2223 | + ]; | |
| 2224 | + }, | |
| 2150 | 2225 | }), |
| 2151 | 2226 | ], |
| 2152 | 2227 | helpString: 'Force sets the spriteexpression for the current character.', |
| 2153 | 2228 | returns: 'theThe currently set spriteexpression label after setting it.', |
| 2154 | 2229 | })); |
| 2155 | 2230 | SlashCommandParser.addCommandObject(SlashCommand.fromProps({ |
| 2156 | 2231 | name: 'spriteoverrideexpression-folder-override', |
| 2157 | 2232 | aliases: ['spriteoverride', 'costume'], |
| 2158 | 2233 | callback: setSpriteSetCommandsetSpriteFolderCommand, |
| 2159 | 2234 | unnamedArgumentList: [ |
| 2160 | 2235 | new SlashCommandArgument( |
| 2161 | 2236 | 'optional folder', [ARGUMENT_TYPE.STRING], false, |
| 2162 | 2237 | ), |
| 2163 | 2238 | ], |
| 2164 | - helpString: 'Sets an override sprite folder for the current character. If the name starts with a slash or a backslash, selects a sub-folder in the character-named folder. Empty value to reset to default.', | |
| 2239 | + helpString: ` | |
| 2240 | + <div> | |
| 2241 | + Sets an override sprite folder for the current character.<br /> | |
| 2242 | + In groups, this will apply to the character who last sent a message. | |
| 2243 | + </div> | |
| 2244 | + <div> | |
| 2245 | + If the name starts with a slash or a backslash, selects a sub-folder in the character-named folder. Empty value to reset to default. | |
| 2246 | + </div> | |
| 2247 | + `, | |
| 2165 | 2248 | })); |
| 2166 | 2249 | SlashCommandParser.addCommandObject(SlashCommand.fromProps({ |
| 2167 | 2250 | name: 'lastspriteexpression-last', |
| 2168 | - callback: (_, name) => { | |
| 2251 | + aliases: ['lastsprite'], | |
| 2252 | + /** @type {(args: object, name: string) => Promise<string>} */ | |
| 2253 | + callback: async (_, name) => { | |
| 2169 | 2254 | if (typeof name !== 'string') throw new Error('name must be a string'); |
| 2255 | + if (!name) { | |
| 2256 | + if (selected_group) { | |
| 2257 | + toastr.error(t`In group chats, you must specify a character name.`, t`No character name specified`); | |
| 2258 | + return ''; | |
| 2259 | + } | |
| 2260 | + name = characters[this_chid]?.avatar; | |
| 2261 | + } | |
| 2262 | + | |
| 2170 | 2263 | const char = findChar({ name: name }); |
| 2264 | + if (!char) toastr.warning(t`Couldn't find character ${name}.`, t`Character not found`); | |
| 2265 | + | |
| 2171 | 2266 | const sprite = lastExpression[char?.name ?? name] ?? ''; |
| 2172 | 2267 | return sprite; |
| 2173 | 2268 | }, |
| 2174 | 2269 | returns: 'the last set sprite / expression for the named character.', |
| 2175 | 2270 | unnamedArgumentList: [ |
| 2176 | 2271 | SlashCommandArgument.fromProps({ |
| 2177 | 2272 | description: 'Character name - or unique character identifier (avatar key). If not provided, the current character for this chat will be used (does not work in group chats)', |
| 2178 | 2273 | typeList: [ARGUMENT_TYPE.STRING], |
| 2179 | - isRequired: true, | |
| 2180 | 2274 | enumProvider: commonEnumProviders.characters('character'), |
| 2181 | - forceEnum: true, | |
| 2182 | 2275 | }), |
| 2183 | 2276 | ], |
| 2184 | 2277 | helpString: 'Returns the last set sprite / expression for the named character.', |
| 2185 | - })); | |
| 2186 | - SlashCommandParser.addCommandObject(SlashCommand.fromProps({ | |
| 2187 | - name: 'th', | |
| 2188 | - callback: toggleTalkingHeadCommand, | |
| 2189 | - aliases: ['talkinghead'], | |
| 2190 | - helpString: 'Character Expressions: toggles <i>Image Type - talkinghead (extras)</i> on/off.', | |
| 2191 | - returns: 'the current state of the <i>Image Type - talkinghead (extras)</i> on/off.', | |
| 2192 | 2278 | })); |
| 2193 | 2279 | SlashCommandParser.addCommandObject(SlashCommand.fromProps({ |
| 2194 | 2280 | name: 'classifyexpression-expressionslist', |
| 2195 | 2281 | aliases: ['expressions'], |
| 2282 | + /** @type {(args: {return: string}) => Promise<string>} */ | |
| 2196 | 2283 | callback: async (args) => { |
| 2284 | + let returnType = | |
| 2197 | 2285 | /** @type {import('../../slash-commands/SlashCommandReturnHelper.js').SlashCommandReturnType} */ |
| 2198 | - // @ts-ignore | |
| 2286 | + (args.return); | |
| 2199 | - let returnType = args.return; | |
| 2200 | - | |
| 2201 | - // Old legacy return type handling | |
| 2202 | - if (args.format) { | |
| 2203 | - toastr.warning(`Legacy argument 'format' with value '${args.format}' is deprecated. Please use 'return' instead. Routing to the correct return type...`, 'Deprecation warning'); | |
| 2204 | - const type = String(args?.format).toLowerCase().trim(); | |
| 2205 | - switch (type) { | |
| 2206 | - case 'json': | |
| 2207 | - returnType = 'object'; | |
| 2208 | - break; | |
| 2209 | - default: | |
| 2210 | - returnType = 'pipe'; | |
| 2211 | - break; | |
| 2212 | - } | |
| 2213 | - } | |
| 2214 | 2287 | |
| 2215 | - // Now the actual new return type handling | |
| 2216 | 2288 | const list = await getExpressionsList(); |
| 2217 | 2289 | |
| 2218 | 2290 | return await slashCommandReturnHelper.doReturn(returnType ?? 'pipe', list, { objectToStringFunc: list => list.join(', ') }); |
| @@ -2226,22 +2298,13 @@ function migrateSettings() { | ||
| 2226 | 2298 | enumList: slashCommandReturnHelper.enumList({ allowObject: true }), |
| 2227 | 2299 | forceEnum: true, |
| 2228 | 2300 | }), |
| 2229 | - // TODO remove some day | |
| 2230 | - SlashCommandNamedArgument.fromProps({ | |
| 2231 | - name: 'format', | |
| 2232 | - description: '!!! DEPRECATED - use "return" instead !!! The format to return the list in: comma-separated plain text or JSON array. Default is plain text.', | |
| 2233 | - typeList: [ARGUMENT_TYPE.STRING], | |
| 2234 | - enumList: [ | |
| 2235 | - new SlashCommandEnumValue('plain', null, enumTypes.enum, ', '), | |
| 2236 | - new SlashCommandEnumValue('json', null, enumTypes.enum, '[]'), | |
| 2237 | - ], | |
| 2238 | - }), | |
| 2239 | 2301 | ], |
| 2240 | 2302 | returns: 'The comma-separated list of available expressions, including custom expressions.', |
| 2241 | 2303 | helpString: 'Returns a list of available expressions, including custom expressions.', |
| 2242 | 2304 | })); |
| 2243 | 2305 | SlashCommandParser.addCommandObject(SlashCommand.fromProps({ |
| 2244 | 2306 | name: 'expression-classify', |
| 2307 | + aliases: ['classify'], | |
| 2245 | 2308 | callback: classifyCallback, |
| 2246 | 2309 | namedArgumentList: [ |
| 2247 | 2310 | SlashCommandNamedArgument.fromProps({ |
| @@ -2280,11 +2343,13 @@ function migrateSettings() { | ||
| 2280 | 2343 | `, |
| 2281 | 2344 | })); |
| 2282 | 2345 | SlashCommandParser.addCommandObject(SlashCommand.fromProps({ |
| 2283 | 2346 | name: 'uploadspriteexpression-upload', |
| 2347 | + aliases: ['uploadsprite'], | |
| 2348 | + /** @type {(args: {name: string, label: string, folder: string?, spriteName: string?}, url: string) => Promise<string>} */ | |
| 2284 | 2349 | callback: async (args, url) => { |
| 2285 | 2350 | return await uploadSpriteCommand(args, url); |
| 2286 | - return ''; | |
| 2287 | 2351 | }, |
| 2352 | + returns: 'the resulting sprite name', | |
| 2288 | 2353 | unnamedArgumentList: [ |
| 2289 | 2354 | SlashCommandArgument.fromProps({ |
| 2290 | 2355 | description: 'URL of the image to upload', |
| @@ -2298,7 +2363,6 @@ function migrateSettings() { | ||
| 2298 | 2363 | description: 'Character name or avatar key (default is current character)', |
| 2299 | 2364 | typeList: [ARGUMENT_TYPE.STRING], |
| 2300 | 2365 | isRequired: false, |
| 2301 | - acceptsMultiple: false, | |
| 2302 | 2366 | }), |
| 2303 | 2367 | SlashCommandNamedArgument.fromProps({ |
| 2304 | 2368 | name: 'label', |
| @@ -2306,16 +2370,32 @@ function migrateSettings() { | ||
| 2306 | 2370 | typeList: [ARGUMENT_TYPE.STRING], |
| 2307 | 2371 | enumProvider: localEnumProviders.expressions, |
| 2308 | 2372 | isRequired: true, |
| 2309 | - acceptsMultiple: false, | |
| 2310 | 2373 | }), |
| 2311 | 2374 | SlashCommandNamedArgument.fromProps({ |
| 2312 | 2375 | name: 'folder', |
| 2313 | 2376 | description: 'Override folder to upload into', |
| 2314 | 2377 | typeList: [ARGUMENT_TYPE.STRING], |
| 2315 | 2378 | isRequired: false, |
| 2316 | - acceptsMultiple: false, | |
| 2379 | + }), | |
| 2380 | + SlashCommandNamedArgument.fromProps({ | |
| 2381 | + name: 'spriteName', | |
| 2382 | + description: 'Override sprite name to allow multiple sprites per expressions. Has to follow the naming pattern. If unspecified, the label will be used as sprite name.', | |
| 2383 | + typeList: [ARGUMENT_TYPE.STRING], | |
| 2384 | + isRequired: false, | |
| 2317 | 2385 | }), |
| 2318 | 2386 | ], |
| 2319 | - helpString: '<div>Upload a sprite from a URL.</div><div>Example:</div><pre><code>/uploadsprite name=Seraphina label=joy /user/images/Seraphina/Seraphina_2024-12-22@12h37m57s.png</code></pre>', | |
| 2387 | + helpString: ` | |
| 2388 | + <div> | |
| 2389 | + Upload a sprite from a URL. | |
| 2390 | + </div> | |
| 2391 | + <div> | |
| 2392 | + <strong>Example:</strong> | |
| 2393 | + <ul> | |
| 2394 | + <li> | |
| 2395 | + <pre><code>/uploadsprite name=Seraphina label=joy /user/images/Seraphina/Seraphina_2024-12-22@12h37m57s.png</code></pre> | |
| 2396 | + </li> | |
| 2397 | + </ul> | |
| 2398 | + </div> | |
| 2399 | + `, | |
| 2320 | 2400 | })); |
| 2321 | 2401 | })(); |
| @@ -1,4 +1,5 @@ | ||
| 1 | -<div id="{{item}}" class="expression_list_item"> | |
| 1 | +{{#each images}} | |
| 2 | +<div class="expression_list_item interactable" data-expression="{{../expression}}" data-expression-type="{{this.type}}" data-filename="{{this.fileName}}"> | |
| 2 | 3 | <div class="expression_list_buttons"> |
| 3 | 4 | <div class="menu_button expression_list_upload" title="Upload image"> |
| 4 | 5 | <i class="fa-solid fa-upload"></i> |
| @@ -7,11 +8,14 @@ | ||
| 7 | 8 | <i class="fa-solid fa-trash"></i> |
| 8 | 9 | </div> |
| 9 | 10 | </div> |
| 10 | 11 | <div class="expression_list_title {{textClass}}"> |
| 11 | 12 | <span>{{item../expression}}</span> |
| 12 | 13 | {{#if ../isCustom}} |
| 13 | 14 | <small class="expression_list_custom">(custom)</small> |
| 14 | 15 | {{/if}} |
| 15 | 16 | </div> |
| 16 | 17 | <imgdiv class="expression_list_imageexpression_list_image_container" srctitle="{{imageSrcthis.title}}" /> |
| 18 | + <img class="expression_list_image" src="{{this.imageSrc}}" alt="{{this.title}}" data-epression="{{../expression}}" /> | |
| 17 | 19 | </div> |
| 20 | +</div> | |
| 21 | +{{/each}} | |
| @@ -6,24 +6,24 @@ | ||
| 6 | 6 | </div> |
| 7 | 7 | |
| 8 | 8 | <div class="inline-drawer-content"> |
| 9 | 9 | <label class="checkbox_label" for="expression_translate" title="Use the selected API from Chat Translation extension settings." data-i18n="[title]Use the selected API from Chat Translation extension settings."> |
| 10 | 10 | <input id="expression_translate" type="checkbox"> |
| 11 | 11 | <span data-i18n="Translate text to English before classification">Translate text to English before classification</span> |
| 12 | 12 | </label> |
| 13 | - <label class="checkbox_label" for="expressions_show_default"> | |
| 13 | + <label class="checkbox_label" for="expressions_allow_multiple" title="A single expression can have multiple sprites. Whenever the expression is chosen, a random sprite for this expression will be selected." data-i18n="[title]A single expression can have multiple sprites. Whenever the expression is chosen, a random sprite for this expression will be selected."> | |
| 14 | 14 | <input id="expressions_show_defaultexpressions_allow_multiple" type="checkbox"> |
| 15 | 15 | <span data-i18n="Show default imagesAllow (emojis)multiple ifsprites spriteper missingexpression">Show default imagesAllow (emojis)multiple ifsprites spriteper missingexpression</span> |
| 16 | 16 | </label> |
| 17 | - <label id="image_type_block" class="checkbox_label" for="image_type_toggle"> | |
| 17 | + <label class="checkbox_label" for="expressions_reroll_if_same" title="If the same expression is used again, re-roll the sprite. This only applies to expressions that have multiple available sprites assigned." data-i18n="[title]If the same expression is used again, re-roll the sprite. This only applies to expressions that have multiple available sprites assigned."> | |
| 18 | 18 | <input id="image_type_toggleexpressions_reroll_if_same" type="checkbox"> |
| 19 | 19 | <span data-i18n="ImageRe-roll Typeif -same talkingheadexpression (extras)is used again">ImageRe-roll Typeif -same talkingheadsprite (extras)is used again</span> |
| 20 | 20 | </label> |
| 21 | 21 | <div class="expression_api_block m-b-1 m-t-1"> |
| 22 | 22 | <label for="expression_api" data-i18n="Classifier API">Classifier API</label> |
| 23 | 23 | <small data-i18n="Select the API for classifying expressions.">Select the API for classifying expressions.</small> |
| 24 | 24 | <select id="expression_api" class="flex1 margin0"> |
| 25 | 25 | <option value="0" data-i18n="Local">Local</option> |
| 26 | 26 | <option value="1" data-i18n="Extras">Extras (deprecated)</option> |
| 27 | 27 | <option value="2" data-i18n="Main API">Main API</option> |
| 28 | 28 | <option value="3" data-i18n="WebLLM Extension">WebLLM Extension</option> |
| 29 | 29 | </select> |
| @@ -75,8 +75,20 @@ | ||
| 75 | 75 | <span data-i18n="Remove all image overrides">Remove all image overrides</span> |
| 76 | 76 | </div> |
| 77 | 77 | </div> |
| 78 | - <p class="hint"><b data-i18n="Hint:">Hint:</b> <i><span data-i18n="Create new folder in the _space">Create new folder in the </span><b>/characters/</b> <span data-i18n="folder of your user data directory and name it as the name of the character.">folder of your user data directory and name it as the name of the character.</span> | |
| 78 | + <p class="hint"> | |
| 79 | - <span data-i18n="Put images with expressions there. File names should follow the pattern:">Put images with expressions there. File names should follow the pattern: </span><tt data-i18n="expression_label_pattern">[expression_label].[image_format]</tt></i></p> | |
| 79 | + <b data-i18n="Hint:">Hint:</b> | |
| 80 | + <i> | |
| 81 | + <span data-i18n="Create new folder in the _space">Create new folder in the </span><b>/characters/</b> <span data-i18n="folder of your user data directory and name it as the name of the character.">folder of your user data directory and name it as the name of the character.</span> | |
| 82 | + <span data-i18n="Put images with expressions there. File names should follow the pattern:">Put images with expressions there. File names should follow the pattern: </span><tt data-i18n="expression_label_pattern">[expression_label].[image_format]</tt> | |
| 83 | + </i> | |
| 84 | + </p> | |
| 85 | + <p> | |
| 86 | + <i> | |
| 87 | + <span>In case of multiple files per expression, file names can contain a suffix, either separated by a dot or a | |
| 88 | + dash. | |
| 89 | + Examples: </span><tt>joy.png</tt>, <tt>joy-1.png</tt>, <tt>joy.expressive.png</tt> | |
| 90 | + </i> | |
| 91 | + </p> | |
| 80 | 92 | <h3 id="image_list_header"> |
| 81 | 93 | <strong data-i18n="Sprite set:">Sprite set:</strong> <span id="image_list_header_name"></span> |
| 82 | 94 | </h3> |
| @@ -111,6 +111,10 @@ img.expression.default { | ||
| 111 | 111 | justify-content: center; |
| 112 | 112 | } |
| 113 | 113 | |
| 114 | +.expression_list_image_container { | |
| 115 | + overflow: hidden; | |
| 116 | +} | |
| 117 | + | |
| 114 | 118 | .expression_list_title { |
| 115 | 119 | position: absolute; |
| 116 | 120 | bottom: 0; |
| @@ -126,6 +130,9 @@ img.expression.default { | ||
| 126 | 130 | flex-direction: column; |
| 127 | 131 | line-height: 1; |
| 128 | 132 | } |
| 133 | +.expression_list_custom { | |
| 134 | + font-size: 0.66rem; | |
| 135 | +} | |
| 129 | 136 | |
| 130 | 137 | .expression_list_buttons { |
| 131 | 138 | position: absolute; |
| @@ -162,11 +169,24 @@ img.expression.default { | ||
| 162 | 169 | row-gap: 1rem; |
| 163 | 170 | } |
| 164 | 171 | |
| 165 | 172 | #image_list .expression_list_item[data-expression-type="success"] .expression_list_title { |
| 166 | 173 | color: green; |
| 167 | 174 | } |
| 168 | 175 | |
| 169 | -#image_list .failure { | |
| 176 | +#image_list .expression_list_item[data-expression-type="additional"] .expression_list_title { | |
| 177 | + color: darkolivegreen; | |
| 178 | +} | |
| 179 | +#image_list .expression_list_item[data-expression-type="additional"] .expression_list_title::before { | |
| 180 | + content: '➕'; | |
| 181 | + position: absolute; | |
| 182 | + top: -7px; | |
| 183 | + left: -9px; | |
| 184 | + font-size: 14px; | |
| 185 | + color: transparent; | |
| 186 | + text-shadow: 0 0 0 darkolivegreen; | |
| 187 | +} | |
| 188 | + | |
| 189 | +#image_list .expression_list_item[data-expression-type="failure"] .expression_list_title { | |
| 170 | 190 | color: red; |
| 171 | 191 | } |
| 172 | 192 | |
| @@ -189,3 +209,12 @@ img.expression.default { | ||
| 189 | 209 | flex-direction: row; |
| 190 | 210 | } |
| 191 | 211 | |
| 212 | +#expressions_container:has(#expressions_allow_multiple:not(:checked)) #image_list .expression_list_item[data-expression-type="additional"], | |
| 213 | +#expressions_container:has(#expressions_allow_multiple:not(:checked)) label[for="expressions_reroll_if_same"] { | |
| 214 | + opacity: 0.3; | |
| 215 | + transition: opacity var(--animation-duration) ease; | |
| 216 | +} | |
| 217 | +#expressions_container:has(#expressions_allow_multiple:not(:checked)) #image_list .expression_list_item[data-expression-type="additional"]:hover, | |
| 218 | +#expressions_container:has(#expressions_allow_multiple:not(:checked)) #image_list .expression_list_item[data-expression-type="additional"]:focus { | |
| 219 | + opacity: unset; | |
| 220 | +} | |
| @@ -0,0 +1,12 @@ | ||
| 1 | +<div class="m-b-1" data-i18n="upload_expression_request">Please enter a name for the sprite (without extension).</div> | |
| 2 | +<div class="m-b-1" data-i18n="upload_expression_naming_1"> | |
| 3 | + Sprite names must follow the naming schema for the selected expression: {{expression}} | |
| 4 | +</div> | |
| 5 | +<div data-i18n="upload_expression_naming_2"> | |
| 6 | + For multiple expressions, the name must follow the expression name and a valid suffix. Allowed separators are '-' or dot '.'. | |
| 7 | +</div> | |
| 8 | +<span class="m-b-1" data-i18n="Examples:">Examples:</span> <tt>{{expression}}.png</tt>, <tt>{{expression}}-1.png</tt>, <tt>{{expression}}.expressive.png</tt> | |
| 9 | +{{#if clickedFileName}} | |
| 10 | +<div class="m-t-1" data-i18n="upload_expression_replace">Click 'Replace' to replace the existing expression:</div> | |
| 11 | +<tt>{{clickedFileName}}</tt> | |
| 12 | +{{/if}} | |
| @@ -441,7 +441,6 @@ SlashCommandParser.addCommandObject(SlashCommand.fromProps({ | ||
| 441 | 441 | description: 'character name', |
| 442 | 442 | typeList: [ARGUMENT_TYPE.STRING], |
| 443 | 443 | enumProvider: commonEnumProviders.characters('character'), |
| 444 | - forceEnum: true, | |
| 445 | 444 | }), |
| 446 | 445 | SlashCommandNamedArgument.fromProps({ |
| 447 | 446 | name: 'group', |
| @@ -12,7 +12,7 @@ | ||
| 12 | 12 | <label for="summary_source" data-i18n="ext_sum_with">Summarize with:</label> |
| 13 | 13 | <select id="summary_source"> |
| 14 | 14 | <option value="main" data-i18n="ext_sum_main_api">Main API</option> |
| 15 | 15 | <option value="extras">Extras API (deprecated)</option> |
| 16 | 16 | <option value="webllm" data-i18n="ext_sum_webllm">WebLLM Extension</option> |
| 17 | 17 | </select><br> |
| 18 | 18 | |
| @@ -10,6 +10,7 @@ import { SlashCommandExecutor } from '../../../slash-commands/SlashCommandExecut | ||
| 10 | 10 | import { SlashCommandParser } from '../../../slash-commands/SlashCommandParser.js'; |
| 11 | 11 | import { SlashCommandParserError } from '../../../slash-commands/SlashCommandParserError.js'; |
| 12 | 12 | import { SlashCommandScope } from '../../../slash-commands/SlashCommandScope.js'; |
| 13 | +import { accountStorage } from '../../../util/AccountStorage.js'; | |
| 13 | 14 | import { debounce, delay, getSortableDelay, showFontAwesomePicker } from '../../../utils.js'; |
| 14 | 15 | import { log, quickReplyApi, warn } from '../index.js'; |
| 15 | 16 | import { QuickReplyContextLink } from './QuickReplyContextLink.js'; |
| @@ -544,9 +545,9 @@ export class QuickReply { | ||
| 544 | 545 | this.editorSyntax = messageSyntaxInner; |
| 545 | 546 | /**@type {HTMLInputElement}*/ |
| 546 | 547 | const wrap = dom.querySelector('#qr--modal-wrap'); |
| 547 | 548 | wrap.checked = JSON.parse(localStorageaccountStorage.getItem('qr--wrap') ?? 'false'); |
| 548 | 549 | wrap.addEventListener('click', () => { |
| 549 | 550 | localStorageaccountStorage.setItem('qr--wrap', JSON.stringify(wrap.checked)); |
| 550 | 551 | updateWrap(); |
| 551 | 552 | }); |
| 552 | 553 | const updateWrap = () => { |
| @@ -594,27 +595,27 @@ export class QuickReply { | ||
| 594 | 595 | }; |
| 595 | 596 | /**@type {HTMLInputElement}*/ |
| 596 | 597 | const tabSize = dom.querySelector('#qr--modal-tabSize'); |
| 597 | 598 | tabSize.value = JSON.parse(localStorageaccountStorage.getItem('qr--tabSize') ?? '4'); |
| 598 | 599 | const updateTabSize = () => { |
| 599 | 600 | message.style.tabSize = tabSize.value; |
| 600 | 601 | messageSyntaxInner.style.tabSize = tabSize.value; |
| 601 | 602 | updateScrollDebounced(); |
| 602 | 603 | }; |
| 603 | 604 | tabSize.addEventListener('change', () => { |
| 604 | 605 | localStorageaccountStorage.setItem('qr--tabSize', JSON.stringify(Number(tabSize.value))); |
| 605 | 606 | updateTabSize(); |
| 606 | 607 | }); |
| 607 | 608 | /**@type {HTMLInputElement}*/ |
| 608 | 609 | const executeShortcut = dom.querySelector('#qr--modal-executeShortcut'); |
| 609 | 610 | executeShortcut.checked = JSON.parse(localStorageaccountStorage.getItem('qr--executeShortcut') ?? 'true'); |
| 610 | 611 | executeShortcut.addEventListener('click', () => { |
| 611 | 612 | localStorageaccountStorage.setItem('qr--executeShortcut', JSON.stringify(executeShortcut.checked)); |
| 612 | 613 | }); |
| 613 | 614 | /**@type {HTMLInputElement}*/ |
| 614 | 615 | const syntax = dom.querySelector('#qr--modal-syntax'); |
| 615 | 616 | syntax.checked = JSON.parse(localStorageaccountStorage.getItem('qr--syntax') ?? 'true'); |
| 616 | 617 | syntax.addEventListener('click', () => { |
| 617 | 618 | localStorageaccountStorage.setItem('qr--syntax', JSON.stringify(syntax.checked)); |
| 618 | 619 | updateSyntaxEnabled(); |
| 619 | 620 | }); |
| 620 | 621 | if (navigator.keyboard) { |
| @@ -1,15 +1,14 @@ | ||
| 1 | 1 | import { getRequestHeaders, substituteParams } from '../../../../script.js'; |
| 2 | 2 | import { Popup, POPUP_RESULT, POPUP_TYPE } from '../../../popup.js'; |
| 3 | 3 | import { executeSlashCommands, executeSlashCommandsOnChatInput, executeSlashCommandsWithOptions } from '../../../slash-commands.js'; |
| 4 | -import { SlashCommandParser } from '../../../slash-commands/SlashCommandParser.js'; | |
| 5 | 4 | import { SlashCommandScope } from '../../../slash-commands/SlashCommandScope.js'; |
| 6 | 5 | import { debounceAsync, log, warnSlashCommandParser } from '../index../../slash-commands/SlashCommandParser.js'; |
| 6 | +import { debounceAsync, warn } from '../index.js'; | |
| 7 | 7 | import { QuickReply } from './QuickReply.js'; |
| 8 | 8 | |
| 9 | 9 | export class QuickReplySet { |
| 10 | 10 | /**@type {QuickReplySet[]}*/ static list = []; |
| 11 | 11 | |
| 12 | - | |
| 13 | 12 | static from(props) { |
| 14 | 13 | props.qrList = []; //props.qrList?.map(it=>QuickReply.from(it)); |
| 15 | 14 | const instance = Object.assign(new this(), props); |
| @@ -24,9 +23,6 @@ export class QuickReplySet { | ||
| 24 | 23 | return this.list.find(it=>it.name == name); |
| 25 | 24 | } |
| 26 | 25 | |
| 27 | - | |
| 28 | - | |
| 29 | - | |
| 30 | 26 | /**@type {string}*/ name; |
| 31 | 27 | /**@type {boolean}*/ disableSend = false; |
| 32 | 28 | /**@type {boolean}*/ placeBeforeInput = false; |
| @@ -34,19 +30,12 @@ export class QuickReplySet { | ||
| 34 | 30 | /**@type {string}*/ color = 'transparent'; |
| 35 | 31 | /**@type {boolean}*/ onlyBorderColor = false; |
| 36 | 32 | /**@type {QuickReply[]}*/ qrList = []; |
| 37 | - | |
| 38 | 33 | /**@type {number}*/ idIndex = 0; |
| 39 | - | |
| 40 | 34 | /**@type {boolean}*/ isDeleted = false; |
| 41 | - | |
| 42 | 35 | /**@type {function}*/ save; |
| 43 | - | |
| 44 | 36 | /**@type {HTMLElement}*/ dom; |
| 45 | 37 | /**@type {HTMLElement}*/ settingsDom; |
| 46 | 38 | |
| 47 | - | |
| 48 | - | |
| 49 | - | |
| 50 | 39 | constructor() { |
| 51 | 40 | this.save = debounceAsync(()=>this.performSave(), 200); |
| 52 | 41 | } |
| @@ -55,9 +44,6 @@ export class QuickReplySet { | ||
| 55 | 44 | this.qrList.forEach(qr=>this.hookQuickReply(qr)); |
| 56 | 45 | } |
| 57 | 46 | |
| 58 | - | |
| 59 | - | |
| 60 | - | |
| 61 | 47 | unrender() { |
| 62 | 48 | this.dom?.remove(); |
| 63 | 49 | this.dom = null; |
| @@ -100,9 +86,6 @@ export class QuickReplySet { | ||
| 100 | 86 | } |
| 101 | 87 | } |
| 102 | 88 | |
| 103 | - | |
| 104 | - | |
| 105 | - | |
| 106 | 89 | renderSettings() { |
| 107 | 90 | if (!this.settingsDom) { |
| 108 | 91 | this.settingsDom = document.createElement('div'); { |
| @@ -123,9 +106,6 @@ export class QuickReplySet { | ||
| 123 | 106 | this.settingsDom.append(qr.renderSettings(idx)); |
| 124 | 107 | } |
| 125 | 108 | |
| 126 | - | |
| 127 | - | |
| 128 | - | |
| 129 | 109 | /** |
| 130 | 110 | * |
| 131 | 111 | * @param {QuickReply} qr |
| @@ -138,6 +118,7 @@ export class QuickReplySet { | ||
| 138 | 118 | closure.scope.setMacro('arg::*', ''); |
| 139 | 119 | return (await closure.execute())?.pipe; |
| 140 | 120 | } |
| 121 | + | |
| 141 | 122 | /** |
| 142 | 123 | * |
| 143 | 124 | * @param {QuickReply} qr The QR to execute. |
| @@ -207,6 +188,7 @@ export class QuickReplySet { | ||
| 207 | 188 | document.querySelector('#send_but').click(); |
| 208 | 189 | } |
| 209 | 190 | } |
| 191 | + | |
| 210 | 192 | /** |
| 211 | 193 | * @param {QuickReply} qr |
| 212 | 194 | * @param {string} [message] - optional altered message to be used |
| @@ -220,9 +202,6 @@ export class QuickReplySet { | ||
| 220 | 202 | }); |
| 221 | 203 | } |
| 222 | 204 | |
| 223 | - | |
| 224 | - | |
| 225 | - | |
| 226 | 205 | addQuickReply(data = {}) { |
| 227 | 206 | const id = Math.max(this.idIndex, this.qrList.reduce((max,qr)=>Math.max(max,qr.id),0)) + 1; |
| 228 | 207 | data.id = |
| @@ -239,6 +218,7 @@ export class QuickReplySet { | ||
| 239 | 218 | this.save(); |
| 240 | 219 | return qr; |
| 241 | 220 | } |
| 221 | + | |
| 242 | 222 | addQuickReplyFromText(qrJson) { |
| 243 | 223 | let data; |
| 244 | 224 | if (qrJson) { |
| @@ -371,7 +351,6 @@ export class QuickReplySet { | ||
| 371 | 351 | this.save(); |
| 372 | 352 | } |
| 373 | 353 | |
| 374 | - | |
| 375 | 354 | toJSON() { |
| 376 | 355 | return { |
| 377 | 356 | version: 2, |
| @@ -386,7 +365,6 @@ export class QuickReplySet { | ||
| 386 | 365 | }; |
| 387 | 366 | } |
| 388 | 367 | |
| 389 | - | |
| 390 | 368 | async performSave() { |
| 391 | 369 | const response = await fetch('/api/quick-replies/save', { |
| 392 | 370 | method: 'POST', |
| @@ -883,6 +883,10 @@ export class SlashCommandHandler { | ||
| 883 | 883 | } |
| 884 | 884 | } |
| 885 | 885 | getQuickReply(args) { |
| 886 | + if (!args.id && !args.label) { | |
| 887 | + toastr.error('Please provide a valid id or label.'); | |
| 888 | + return ''; | |
| 889 | + } | |
| 886 | 890 | try { |
| 887 | 891 | return JSON.stringify(this.api.getQrByLabel(args.set, args.id !== undefined ? Number(args.id) : args.label)); |
| 888 | 892 | } catch (ex) { |
| @@ -346,7 +346,7 @@ export class SettingsUi { | ||
| 346 | 346 | } |
| 347 | 347 | |
| 348 | 348 | async addQrSet() { |
| 349 | 349 | const name = await Popup.show.input('Create a new WorldQuick InfoReply Set', 'Enter a name for the new Quick Reply Set:'); |
| 350 | 350 | if (name && name.length > 0) { |
| 351 | 351 | const oldQrs = QuickReplySet.get(name); |
| 352 | 352 | if (oldQrs) { |
| @@ -94,6 +94,12 @@ | ||
| 94 | 94 | <span data-i18n="World Info">World Info</span> |
| 95 | 95 | </label> |
| 96 | 96 | </div> |
| 97 | + <div data-i18n="[title]ext_regex_reasoning_desc" title="Reasoning block contents. When 'Only Format Prompt' is checked, it will also affect the reasoning contents added to the prompt."> | |
| 98 | + <label class="checkbox flex-container"> | |
| 99 | + <input type="checkbox" name="replace_position" value="6"> | |
| 100 | + <span data-i18n="Reasoning">Reasoning</span> | |
| 101 | + </label> | |
| 102 | + </div> | |
| 97 | 103 | <div class="flex-container wide100p marginTop5"> |
| 98 | 104 | <div class="flex1 flex-container flexNoGap"> |
| 99 | 105 | <small data-i18n="[title]ext_regex_min_depth_desc" title="When applied to prompts or display, only affect messages that are at least N levels deep. 0 = last message, 1 = penultimate message, etc. Only counts WI entries @Depth and usable messages, i.e. not hidden or system."> |
| @@ -20,6 +20,7 @@ const regex_placement = { | ||
| 20 | 20 | SLASH_COMMAND: 3, |
| 21 | 21 | // 4 - sendAs (legacy) |
| 22 | 22 | WORLD_INFO: 5, |
| 23 | + REASONING: 6, | |
| 23 | 24 | }; |
| 24 | 25 | |
| 25 | 26 | export const substitute_find_regex = { |
| @@ -94,7 +95,7 @@ function getRegexedString(rawString, placement, { characterOverride, isMarkdown, | ||
| 94 | 95 | // Script applies to Generate and input is Generate |
| 95 | 96 | (script.promptOnly && isPrompt) || |
| 96 | 97 | // Script applies to all cases when neither "only"s are true, but there's no need to do it when `isMarkdown`, the as source (chat history) should already be changed beforehand |
| 97 | 98 | (!script.markdownOnly && !script.promptOnly && !isMarkdown && !isPrompt) |
| 98 | 99 | ) { |
| 99 | 100 | if (isEdit && !script.runOnEdit) { |
| 100 | 101 | console.debug(`getRegexedString: Skipping script ${script.scriptName} because it does not run on edit`); |
| @@ -10,6 +10,7 @@ import { SlashCommandParser } from '../../slash-commands/SlashCommandParser.js'; | ||
| 10 | 10 | import { download, getFileText, getSortableDelay, uuidv4 } from '../../utils.js'; |
| 11 | 11 | import { regex_placement, runRegexScript, substitute_find_regex } from './engine.js'; |
| 12 | 12 | import { t } from '../../i18n.js'; |
| 13 | +import { accountStorage } from '../../util/AccountStorage.js'; | |
| 13 | 14 | |
| 14 | 15 | /** |
| 15 | 16 | * @typedef {object} RegexScript |
| @@ -18,7 +19,7 @@ import { t } from '../../i18n.js'; | ||
| 18 | 19 | * @property {string} replaceString - The replace string |
| 19 | 20 | * @property {string[]} trimStrings - The trim strings |
| 20 | 21 | * @property {string?} findRegex - The find regex |
| 21 | 22 | * @property {stringnumber?} substituteRegex - The substitute regex |
| 22 | 23 | */ |
| 23 | 24 | |
| 24 | 25 | /** |
| @@ -440,8 +441,8 @@ async function checkEmbeddedRegexScripts() { | ||
| 440 | 441 | if (avatar && !extension_settings.character_allowed_regex.includes(avatar)) { |
| 441 | 442 | const checkKey = `AlertRegex_${characters[chid].avatar}`; |
| 442 | 443 | |
| 443 | 444 | if (!localStorageaccountStorage.getItem(checkKey)) { |
| 444 | 445 | localStorageaccountStorage.setItem(checkKey, 'true'); |
| 445 | 446 | const template = await renderExtensionTemplateAsync('regex', 'embeddedScripts', {}); |
| 446 | 447 | const result = await callGenericPopup(template, POPUP_TYPE.CONFIRM, '', { okButton: 'Yes' }); |
| 447 | 448 | |
| @@ -81,6 +81,7 @@ const sources = { | ||
| 81 | 81 | huggingface: 'huggingface', |
| 82 | 82 | nanogpt: 'nanogpt', |
| 83 | 83 | bfl: 'bfl', |
| 84 | + falai: 'falai', | |
| 84 | 85 | }; |
| 85 | 86 | |
| 86 | 87 | const initiators = { |
| @@ -1169,6 +1170,10 @@ async function onBflKeyClick() { | ||
| 1169 | 1170 | return onApiKeyClick('BFL API Key:', SECRET_KEYS.BFL); |
| 1170 | 1171 | } |
| 1171 | 1172 | |
| 1173 | +async function onFalaiKeyClick() { | |
| 1174 | + return onApiKeyClick('FALAI API Key:', SECRET_KEYS.FALAI); | |
| 1175 | +} | |
| 1176 | + | |
| 1172 | 1177 | function onBflUpsamplingInput() { |
| 1173 | 1178 | extension_settings.sd.bfl_upsampling = !!$('#sd_bfl_upsampling').prop('checked'); |
| 1174 | 1179 | saveSettingsDebounced(); |
| @@ -1299,6 +1304,7 @@ async function onModelChange() { | ||
| 1299 | 1304 | sources.huggingface, |
| 1300 | 1305 | sources.nanogpt, |
| 1301 | 1306 | sources.bfl, |
| 1307 | + sources.falai, | |
| 1302 | 1308 | ]; |
| 1303 | 1309 | |
| 1304 | 1310 | if (cloudSources.includes(extension_settings.sd.source)) { |
| @@ -1707,6 +1713,9 @@ async function loadModels() { | ||
| 1707 | 1713 | case sources.bfl: |
| 1708 | 1714 | models = await loadBflModels(); |
| 1709 | 1715 | break; |
| 1716 | + case sources.falai: | |
| 1717 | + models = await loadFalaiModels(); | |
| 1718 | + break; | |
| 1710 | 1719 | } |
| 1711 | 1720 | |
| 1712 | 1721 | for (const model of models) { |
| @@ -1744,6 +1753,21 @@ async function loadBflModels() { | ||
| 1744 | 1753 | ]; |
| 1745 | 1754 | } |
| 1746 | 1755 | |
| 1756 | +async function loadFalaiModels() { | |
| 1757 | + $('#sd_falai_key').toggleClass('success', !!secret_state[SECRET_KEYS.FALAI]); | |
| 1758 | + | |
| 1759 | + const result = await fetch('/api/sd/falai/models', { | |
| 1760 | + method: 'POST', | |
| 1761 | + headers: getRequestHeaders(), | |
| 1762 | + }); | |
| 1763 | + | |
| 1764 | + if (result.ok) { | |
| 1765 | + return await result.json(); | |
| 1766 | + } | |
| 1767 | + | |
| 1768 | + return []; | |
| 1769 | +} | |
| 1770 | + | |
| 1747 | 1771 | async function loadPollinationsModels() { |
| 1748 | 1772 | const result = await fetch('/api/sd/pollinations/models', { |
| 1749 | 1773 | method: 'POST', |
| @@ -2081,6 +2105,9 @@ async function loadSchedulers() { | ||
| 2081 | 2105 | case sources.bfl: |
| 2082 | 2106 | schedulers = ['N/A']; |
| 2083 | 2107 | break; |
| 2108 | + case sources.falai: | |
| 2109 | + schedulers = ['N/A']; | |
| 2110 | + break; | |
| 2084 | 2111 | } |
| 2085 | 2112 | |
| 2086 | 2113 | for (const scheduler of schedulers) { |
| @@ -2735,6 +2762,9 @@ async function sendGenerationRequest(generationType, prompt, additionalNegativeP | ||
| 2735 | 2762 | case sources.bfl: |
| 2736 | 2763 | result = await generateBflImage(prefixedPrompt, signal); |
| 2737 | 2764 | break; |
| 2765 | + case sources.falai: | |
| 2766 | + result = await generateFalaiImage(prefixedPrompt, negativePrompt, signal); | |
| 2767 | + break; | |
| 2738 | 2768 | } |
| 2739 | 2769 | |
| 2740 | 2770 | if (!result.data) { |
| @@ -3496,6 +3526,40 @@ async function generateBflImage(prompt, signal) { | ||
| 3496 | 3526 | } |
| 3497 | 3527 | } |
| 3498 | 3528 | |
| 3529 | +/** | |
| 3530 | + * Generates an image using the FAL.AI API. | |
| 3531 | + * @param {string} prompt - The main instruction used to guide the image generation. | |
| 3532 | + * @param {string} negativePrompt - The negative prompt used to guide the image generation. | |
| 3533 | + * @param {AbortSignal} signal - An AbortSignal object that can be used to cancel the request. | |
| 3534 | + * @returns {Promise<{format: string, data: string}>} - A promise that resolves when the image generation and processing are complete. | |
| 3535 | + */ | |
| 3536 | +async function generateFalaiImage(prompt, negativePrompt, signal) { | |
| 3537 | + const result = await fetch('/api/sd/falai/generate', { | |
| 3538 | + method: 'POST', | |
| 3539 | + headers: getRequestHeaders(), | |
| 3540 | + signal: signal, | |
| 3541 | + body: JSON.stringify({ | |
| 3542 | + prompt: prompt, | |
| 3543 | + negative_prompt: negativePrompt, | |
| 3544 | + model: extension_settings.sd.model, | |
| 3545 | + steps: clamp(extension_settings.sd.steps, 1, 50), | |
| 3546 | + guidance: clamp(extension_settings.sd.scale, 1.5, 5), | |
| 3547 | + width: clamp(extension_settings.sd.width, 256, 1440), | |
| 3548 | + height: clamp(extension_settings.sd.height, 256, 1440), | |
| 3549 | + seed: extension_settings.sd.seed >= 0 ? extension_settings.sd.seed : undefined, | |
| 3550 | + }), | |
| 3551 | + }); | |
| 3552 | + | |
| 3553 | + if (result.ok) { | |
| 3554 | + const data = await result.json(); | |
| 3555 | + return { format: 'jpg', data: data.image }; | |
| 3556 | + } else { | |
| 3557 | + const text = await result.text(); | |
| 3558 | + console.log(text); | |
| 3559 | + throw new Error(text); | |
| 3560 | + } | |
| 3561 | +} | |
| 3562 | + | |
| 3499 | 3563 | async function onComfyOpenWorkflowEditorClick() { |
| 3500 | 3564 | let workflow = await (await fetch('/api/sd/comfy/workflow', { |
| 3501 | 3565 | method: 'POST', |
| @@ -3782,6 +3846,8 @@ function isValidState() { | ||
| 3782 | 3846 | return secret_state[SECRET_KEYS.NANOGPT]; |
| 3783 | 3847 | case sources.bfl: |
| 3784 | 3848 | return secret_state[SECRET_KEYS.BFL]; |
| 3849 | + case sources.falai: | |
| 3850 | + return secret_state[SECRET_KEYS.FALAI]; | |
| 3785 | 3851 | } |
| 3786 | 3852 | } |
| 3787 | 3853 | |
| @@ -4443,6 +4509,7 @@ jQuery(async () => { | ||
| 4443 | 4509 | $('#sd_function_tool').on('input', onFunctionToolInput); |
| 4444 | 4510 | $('#sd_bfl_key').on('click', onBflKeyClick); |
| 4445 | 4511 | $('#sd_bfl_upsampling').on('input', onBflUpsamplingInput); |
| 4512 | + $('#sd_falai_key').on('click', onFalaiKeyClick); | |
| 4446 | 4513 | |
| 4447 | 4514 | if (!CSS.supports('field-sizing', 'content')) { |
| 4448 | 4515 | $('.sd_settings .inline-drawer-toggle').on('click', function () { |
| @@ -41,7 +41,8 @@ | ||
| 41 | 41 | <option value="blockentropy">Block Entropy</option> |
| 42 | 42 | <option value="comfy">ComfyUI</option> |
| 43 | 43 | <option value="drawthings">DrawThings HTTP API</option> |
| 44 | 44 | <option value="extras">Extras API (local / remotedeprecated)</option> |
| 45 | + <option value="falai">FAL.AI</option> | |
| 45 | 46 | <option value="huggingface">HuggingFace Inference API (serverless)</option> |
| 46 | 47 | <option value="nanogpt">NanoGPT</option> |
| 47 | 48 | <option value="novel">NovelAI Diffusion</option> |
| @@ -256,6 +257,20 @@ | ||
| 256 | 257 | </label> |
| 257 | 258 | </div> |
| 258 | 259 | |
| 260 | + <div data-sd-source="falai"> | |
| 261 | + <div class="flex-container flexnowrap alignItemsBaseline marginBot5"> | |
| 262 | + <a href="https://fal.ai/dashboard" target="_blank" rel="noopener noreferrer"> | |
| 263 | + <strong data-i18n="API Key">API Key</strong> | |
| 264 | + <i class="fa-solid fa-share-from-square"></i> | |
| 265 | + </a> | |
| 266 | + <span class="expander"></span> | |
| 267 | + <div id="sd_falai_key" class="menu_button menu_button_icon"> | |
| 268 | + <i class="fa-fw fa-solid fa-key"></i> | |
| 269 | + <span data-i18n="Click to set">Click to set</span> | |
| 270 | + </div> | |
| 271 | + </div> | |
| 272 | + </div> | |
| 273 | + | |
| 259 | 274 | <div class="flex-container"> |
| 260 | 275 | <div class="flex1"> |
| 261 | 276 | <label for="sd_model" data-i18n="Model">Model</label> |
| @@ -6,6 +6,8 @@ import { getFriendlyTokenizerName, getTextTokens, getTokenCountAsync, tokenizers | ||
| 6 | 6 | import { resetScrollHeight, debounce } from '../../utils.js'; |
| 7 | 7 | import { debounce_timeout } from '../../constants.js'; |
| 8 | 8 | import { POPUP_TYPE, callGenericPopup } from '../../popup.js'; |
| 9 | +import { renderExtensionTemplateAsync } from '../../extensions.js'; | |
| 10 | +import { t } from '../../i18n.js'; | |
| 9 | 11 | |
| 10 | 12 | function rgb2hex(rgb) { |
| 11 | 13 | rgb = rgb.match(/^rgba?[\s+]?\([\s+]?(\d+)[\s+]?,[\s+]?(\d+)[\s+]?,[\s+]?(\d+)[\s+]?/i); |
| @@ -22,23 +24,7 @@ $('button').click(function () { | ||
| 22 | 24 | |
| 23 | 25 | async function doTokenCounter() { |
| 24 | 26 | const { tokenizerName, tokenizerId } = getFriendlyTokenizerName(main_api); |
| 25 | - const html = ` | |
| 27 | + const html = await renderExtensionTemplateAsync('token-counter', 'window', {tokenizerName}); | |
| 26 | - <div class="wide100p"> | |
| 27 | - <h3>Token Counter</h3> | |
| 28 | - <div class="justifyLeft flex-container flexFlowColumn"> | |
| 29 | - <h4>Type / paste in the box below to see the number of tokens in the text.</h4> | |
| 30 | - <p>Selected tokenizer: ${tokenizerName}</p> | |
| 31 | - <div>Input:</div> | |
| 32 | - <textarea id="token_counter_textarea" class="wide100p textarea_compact" rows="1"></textarea> | |
| 33 | - <div>Tokens: <span id="token_counter_result">0</span></div> | |
| 34 | - <hr> | |
| 35 | - <div>Tokenized text:</div> | |
| 36 | - <div id="tokenized_chunks_display" class="wide100p">—</div> | |
| 37 | - <hr> | |
| 38 | - <div>Token IDs:</div> | |
| 39 | - <textarea id="token_counter_ids" class="wide100p textarea_compact" readonly rows="1">—</textarea> | |
| 40 | - </div> | |
| 41 | - </div>`; | |
| 42 | 28 | |
| 43 | 29 | const dialog = $(html); |
| 44 | 30 | const countDebounced = debounce(async () => { |
| @@ -131,9 +117,9 @@ async function doCount() { | ||
| 131 | 117 | jQuery(() => { |
| 132 | 118 | const buttonHtml = ` |
| 133 | 119 | <div id="token_counter" class="list-group-item flex-container flexGap5"> |
| 134 | 120 | <div class="fa-solid fa-1 extensionsMenuExtensionButton" /></div>` + |
| 135 | 121 | t`Token Counter` + |
| 136 | 122 | '</div>`'; |
| 137 | 123 | $('#token_counter_wand_container').append(buttonHtml); |
| 138 | 124 | $('#token_counter').on('click', doTokenCounter); |
| 139 | 125 | SlashCommandParser.addCommandObject(SlashCommand.fromProps({ |
| @@ -0,0 +1,16 @@ | ||
| 1 | +<div class="wide100p"> | |
| 2 | + <h3 data-i18n="Token Counter">Token Counter</h3> | |
| 3 | + <div class="justifyLeft flex-container flexFlowColumn"> | |
| 4 | + <h4 data-i18n="Type / paste in the box below to see the number of tokens in the text.">Type / paste in the box below to see the number of tokens in the text.</h4> | |
| 5 | + <p><span data-i18n="Selected tokenizer:">Selected tokenizer:</span> {{tokenizerName}}</p> | |
| 6 | + <div data-i18n="Input:">Input:</div> | |
| 7 | + <textarea id="token_counter_textarea" class="wide100p textarea_compact" rows="1"></textarea> | |
| 8 | + <div><span data-i18n="Tokens:">Tokens:</span> <span id="token_counter_result">0</span></div> | |
| 9 | + <hr> | |
| 10 | + <div data-i18n="Tokenized text:">Tokenized text:</div> | |
| 11 | + <div id="tokenized_chunks_display" class="wide100p">—</div> | |
| 12 | + <hr> | |
| 13 | + <div data-i18n="Token IDs:">Token IDs:</div> | |
| 14 | + <textarea id="token_counter_ids" class="wide100p textarea_compact" readonly rows="1">—</textarea> | |
| 15 | + </div> | |
| 16 | +</div> | |
| 16 | \ No newline at end of file | |
| @@ -605,7 +605,7 @@ const handleOutgoingMessage = createEventHandler(translateOutgoingMessage, () => | ||
| 605 | 605 | const handleImpersonateReady = createEventHandler(translateImpersonate, () => shouldTranslate(incomingTypes)); |
| 606 | 606 | const handleMessageEdit = createEventHandler(translateMessageEdit, () => true); |
| 607 | 607 | |
| 608 | 608 | window['globalThis.translate'] = translate; |
| 609 | 609 | |
| 610 | 610 | jQuery(async () => { |
| 611 | 611 | const html = await renderExtensionTemplateAsync('translate', 'index'); |
| @@ -388,7 +388,7 @@ class AllTalkTtsProvider { | ||
| 388 | 388 | } |
| 389 | 389 | |
| 390 | 390 | async fetchRvcVoiceObjects() { |
| 391 | 391 | if (this.settings.server_version == 'v2v1') { |
| 392 | 392 | console.log('Skipping RVC voices fetch for V1 server'); |
| 393 | 393 | return []; |
| 394 | 394 | } |
| @@ -1031,14 +1031,18 @@ class AllTalkTtsProvider { | ||
| 1031 | 1031 | console.error('fetchTtsGeneration Error Response Text:', errorText); |
| 1032 | 1032 | throw new Error(`HTTP ${response.status}: ${errorText}`); |
| 1033 | 1033 | } |
| 1034 | + | |
| 1034 | 1035 | const data = await response.json(); |
| 1035 | 1036 | |
| 1036 | - // Handle V1/V2 URL differences | |
| 1037 | + // V1 returns a complete URL, V2 returns a relative path | |
| 1037 | 1038 | const outputUrl =if (this.settings.server_version === 'v1') { |
| 1038 | - ? data.output_file_url // V1 returns full URL | |
| 1039 | + // V1: Use the complete URL directly from the response | |
| 1039 | - : `${this.settings.provider_endpoint}${data.output_file_url}`; // V2 returns relative path | |
| 1040 | + return data.output_file_url; | |
| 1041 | + } else { | |
| 1042 | + // V2: Combine the endpoint with the relative path | |
| 1043 | + return `${this.settings.provider_endpoint}${data.output_file_url}`; | |
| 1044 | + } | |
| 1040 | 1045 | |
| 1041 | - return outputUrl; | |
| 1042 | 1046 | } catch (error) { |
| 1043 | 1047 | console.error('[fetchTtsGeneration] Exception caught:', error); |
| 1044 | 1048 | throw error; |
| @@ -27,13 +27,12 @@ import { SlashCommandEnumValue, enumTypes } from '../../slash-commands/SlashComm | ||
| 27 | 27 | import { enumIcons } from '../../slash-commands/SlashCommandCommonEnumsProvider.js'; |
| 28 | 28 | import { POPUP_TYPE, callGenericPopup } from '../../popup.js'; |
| 29 | 29 | import { GoogleTranslateTtsProvider } from './google-translate.js'; |
| 30 | -export { talkingAnimation }; | |
| 31 | 30 | |
| 32 | 31 | const UPDATE_INTERVAL = 1000; |
| 32 | +const wrapper = new ModuleWorkerWrapper(moduleWorker); | |
| 33 | 33 | |
| 34 | 34 | let voiceMapEntries = []; |
| 35 | 35 | let voiceMap = {}; // {charName:voiceid, charName2:voiceid2} |
| 36 | -let talkingHeadState = false; | |
| 37 | 36 | let lastChatId = null; |
| 38 | 37 | let lastMessage = null; |
| 39 | 38 | let lastMessageHash = null; |
| @@ -120,7 +119,7 @@ async function onNarrateOneMessage() { | ||
| 120 | 119 | } |
| 121 | 120 | |
| 122 | 121 | resetTtsPlayback(); |
| 123 | 122 | ttsJobQueue.pushprocessAndQueueTtsMessage(message); |
| 124 | 123 | moduleWorker(); |
| 125 | 124 | } |
| 126 | 125 | |
| @@ -147,7 +146,7 @@ async function onNarrateText(args, text) { | ||
| 147 | 146 | } |
| 148 | 147 | |
| 149 | 148 | resetTtsPlayback(); |
| 150 | 149 | ttsJobQueue.pushprocessAndQueueTtsMessage({ mes: text, name: name }); |
| 151 | 150 | await moduleWorker(); |
| 152 | 151 | |
| 153 | 152 | // Return back to the chat voices |
| @@ -165,27 +164,6 @@ async function moduleWorker() { | ||
| 165 | 164 | updateUiAudioPlayState(); |
| 166 | 165 | } |
| 167 | 166 | |
| 168 | -function talkingAnimation(switchValue) { | |
| 169 | - if (!modules.includes('talkinghead')) { | |
| 170 | - console.debug('Talking Animation module not loaded'); | |
| 171 | - return; | |
| 172 | - } | |
| 173 | - | |
| 174 | - const apiUrl = getApiUrl(); | |
| 175 | - const animationType = switchValue ? 'start' : 'stop'; | |
| 176 | - | |
| 177 | - if (switchValue !== talkingHeadState) { | |
| 178 | - try { | |
| 179 | - console.log(animationType + ' Talking Animation'); | |
| 180 | - doExtrasFetch(`${apiUrl}/api/talkinghead/${animationType}_talking`); | |
| 181 | - talkingHeadState = switchValue; | |
| 182 | - } catch (error) { | |
| 183 | - // Handle the error here or simply ignore it to prevent logging | |
| 184 | - } | |
| 185 | - } | |
| 186 | - updateUiAudioPlayState(); | |
| 187 | -} | |
| 188 | - | |
| 189 | 167 | function resetTtsPlayback() { |
| 190 | 168 | // Stop system TTS utterance |
| 191 | 169 | cancelTtsPlay(); |
| @@ -220,6 +198,36 @@ function isTtsProcessing() { | ||
| 220 | 198 | return processing; |
| 221 | 199 | } |
| 222 | 200 | |
| 201 | +/** | |
| 202 | + * Splits a message into lines and adds each non-empty line to the TTS job queue. | |
| 203 | + * @param {Object} message - The message object to be processed. | |
| 204 | + * @param {string} message.mes - The text of the message to be split into lines. | |
| 205 | + * @param {string} message.name - The name associated with the message. | |
| 206 | + * @returns {void} | |
| 207 | + */ | |
| 208 | +function processAndQueueTtsMessage(message) { | |
| 209 | + if (!extension_settings.tts.narrate_by_paragraphs) { | |
| 210 | + ttsJobQueue.push(message); | |
| 211 | + return; | |
| 212 | + } | |
| 213 | + | |
| 214 | + const lines = message.mes.split('\n'); | |
| 215 | + | |
| 216 | + for (let i = 0; i < lines.length; i++) { | |
| 217 | + const line = lines[i]; | |
| 218 | + | |
| 219 | + if (line.length === 0) { | |
| 220 | + continue; | |
| 221 | + } | |
| 222 | + | |
| 223 | + ttsJobQueue.push( | |
| 224 | + Object.assign({}, message, { | |
| 225 | + mes: line, | |
| 226 | + }), | |
| 227 | + ); | |
| 228 | + } | |
| 229 | +} | |
| 230 | + | |
| 223 | 231 | function debugTtsPlayback() { |
| 224 | 232 | console.log(JSON.stringify( |
| 225 | 233 | { |
| @@ -347,10 +355,9 @@ function onAudioControlClicked() { | ||
| 347 | 355 | // Not pausing, doing a full stop to anything TTS is doing. Better UX as pause is not as useful |
| 348 | 356 | if (!audioElement.paused || isTtsProcessing()) { |
| 349 | 357 | resetTtsPlayback(); |
| 350 | - talkingAnimation(false); | |
| 351 | 358 | } else { |
| 352 | 359 | // Default play behavior if not processing or playing is to play the last message. |
| 353 | 360 | ttsJobQueue.pushprocessAndQueueTtsMessage(context.chat[context.chat.length - 1]); |
| 354 | 361 | } |
| 355 | 362 | updateUiAudioPlayState(); |
| 356 | 363 | } |
| @@ -374,8 +381,8 @@ function addAudioControl() { | ||
| 374 | 381 | function completeCurrentAudioJob() { |
| 375 | 382 | audioQueueProcessorReady = true; |
| 376 | 383 | currentAudioJob = null; |
| 377 | - talkingAnimation(false); //stop lip animation | |
| 378 | 384 | // updateUiPlayState(); |
| 385 | + wrapper.update(); | |
| 379 | 386 | } |
| 380 | 387 | |
| 381 | 388 | /** |
| @@ -404,7 +411,6 @@ async function processAudioJobQueue() { | ||
| 404 | 411 | audioQueueProcessorReady = false; |
| 405 | 412 | currentAudioJob = audioJobQueue.shift(); |
| 406 | 413 | playAudioData(currentAudioJob); |
| 407 | - talkingAnimation(true); | |
| 408 | 414 | } catch (error) { |
| 409 | 415 | toastr.error(error.toString()); |
| 410 | 416 | console.error(error); |
| @@ -569,6 +575,7 @@ function loadSettings() { | ||
| 569 | 575 | $('#tts_narrate_quoted').prop('checked', extension_settings.tts.narrate_quoted_only); |
| 570 | 576 | $('#tts_auto_generation').prop('checked', extension_settings.tts.auto_generation); |
| 571 | 577 | $('#tts_periodic_auto_generation').prop('checked', extension_settings.tts.periodic_auto_generation); |
| 578 | + $('#tts_narrate_by_paragraphs').prop('checked', extension_settings.tts.narrate_by_paragraphs); | |
| 572 | 579 | $('#tts_narrate_translated_only').prop('checked', extension_settings.tts.narrate_translated_only); |
| 573 | 580 | $('#tts_narrate_user').prop('checked', extension_settings.tts.narrate_user); |
| 574 | 581 | $('#tts_pass_asterisks').prop('checked', extension_settings.tts.pass_asterisks); |
| @@ -638,6 +645,11 @@ function onPeriodicAutoGenerationClick() { | ||
| 638 | 645 | saveSettingsDebounced(); |
| 639 | 646 | } |
| 640 | 647 | |
| 648 | +function onNarrateByParagraphsClick() { | |
| 649 | + extension_settings.tts.narrate_by_paragraphs = !!$('#tts_narrate_by_paragraphs').prop('checked'); | |
| 650 | + saveSettingsDebounced(); | |
| 651 | +} | |
| 652 | + | |
| 641 | 653 | |
| 642 | 654 | function onNarrateDialoguesClick() { |
| 643 | 655 | extension_settings.tts.narrate_dialogues_only = !!$('#tts_narrate_dialogues').prop('checked'); |
| @@ -816,7 +828,12 @@ async function onMessageEvent(messageId, lastCharIndex) { | ||
| 816 | 828 | lastChatId = context.chatId; |
| 817 | 829 | |
| 818 | 830 | console.debug(`Adding message from ${message.name} for TTS processing: "${message.mes}"`); |
| 831 | + | |
| 832 | + if (extension_settings.tts.periodic_auto_generation) { | |
| 819 | 833 | ttsJobQueue.push(message); |
| 834 | + } else { | |
| 835 | + processAndQueueTtsMessage(message); | |
| 836 | + } | |
| 820 | 837 | } |
| 821 | 838 | |
| 822 | 839 | async function onMessageDeleted() { |
| @@ -1156,6 +1173,7 @@ jQuery(async function () { | ||
| 1156 | 1173 | $('#tts_pass_asterisks').on('click', onPassAsterisksClick); |
| 1157 | 1174 | $('#tts_auto_generation').on('click', onAutoGenerationClick); |
| 1158 | 1175 | $('#tts_periodic_auto_generation').on('click', onPeriodicAutoGenerationClick); |
| 1176 | + $('#tts_narrate_by_paragraphs').on('click', onNarrateByParagraphsClick); | |
| 1159 | 1177 | $('#tts_narrate_user').on('click', onNarrateUserClick); |
| 1160 | 1178 | |
| 1161 | 1179 | $('#playback_rate').on('input', function () { |
| @@ -1177,7 +1195,6 @@ jQuery(async function () { | ||
| 1177 | 1195 | loadSettings(); // Depends on Extension Controls and loadTtsProvider |
| 1178 | 1196 | loadTtsProvider(extension_settings.tts.currentProvider); // No dependencies |
| 1179 | 1197 | addAudioControl(); // Depends on Extension Controls |
| 1180 | - const wrapper = new ModuleWorkerWrapper(moduleWorker); | |
| 1181 | 1198 | setInterval(wrapper.update.bind(wrapper), UPDATE_INTERVAL); // Init depends on all the things |
| 1182 | 1199 | eventSource.on(event_types.MESSAGE_SWIPED, resetTtsPlayback); |
| 1183 | 1200 | eventSource.on(event_types.CHAT_CHANGED, onChatChanged); |
| @@ -25,7 +25,7 @@ class OpenAICompatibleTtsProvider { | ||
| 25 | 25 | <label for="openai_compatible_tts_endpoint">Provider Endpoint:</label> |
| 26 | 26 | <div class="flex-container alignItemsCenter"> |
| 27 | 27 | <div class="flex1"> |
| 28 | 28 | <input id="openai_compatible_tts_endpoint" type="text" class="text_pole" maxlength="250500" value="${this.defaultSettings.provider_endpoint}"/> |
| 29 | 29 | </div> |
| 30 | 30 | <div id="openai_compatible_tts_key" class="menu_button menu_button_icon"> |
| 31 | 31 | <i class="fa-solid fa-key"></i> |
| @@ -33,9 +33,9 @@ class OpenAICompatibleTtsProvider { | ||
| 33 | 33 | </div> |
| 34 | 34 | </div> |
| 35 | 35 | <label for="openai_compatible_model">Model:</label> |
| 36 | 36 | <input id="openai_compatible_model" type="text" class="text_pole" maxlength="250500" value="${this.defaultSettings.model}"/> |
| 37 | 37 | <label for="openai_compatible_tts_voices">Available Voices (comma separated):</label> |
| 38 | 38 | <input id="openai_compatible_tts_voices" type="text" class="text_pole" maxlength="250" value="${this.defaultSettings.available_voices.join()}"/> |
| 39 | 39 | <label for="openai_compatible_tts_speed">Speed: <span id="openai_compatible_tts_speed_output"></span></label> |
| 40 | 40 | <input type="range" id="openai_compatible_tts_speed" value="1" min="0.25" max="4" step="0.05">`; |
| 41 | 41 | return html; |
| @@ -30,6 +30,10 @@ | ||
| 30 | 30 | <input type="checkbox" id="tts_periodic_auto_generation"> |
| 31 | 31 | <small data-i18n="Narrate by paragraphs (when streaming)">Narrate by paragraphs (when streaming)</small> |
| 32 | 32 | </label> |
| 33 | + <label class="checkbox_label" for="tts_narrate_by_paragraphs"> | |
| 34 | + <input type="checkbox" id="tts_narrate_by_paragraphs"> | |
| 35 | + <small data-i18n="Narrate by paragraphs (when not streaming)">Narrate by paragraphs (when not streaming)</small> | |
| 36 | + </label> | |
| 33 | 37 | <label class="checkbox_label" for="tts_narrate_quoted"> |
| 34 | 38 | <input type="checkbox" id="tts_narrate_quoted"> |
| 35 | 39 | <small data-i18n="Only narrate quotes">Only narrate "quotes"</small> |
| @@ -1,6 +1,5 @@ | ||
| 1 | 1 | import { isMobile } from '../../RossAscends-mods.js'; |
| 2 | 2 | import { getPreviewString } from './index.js'; |
| 3 | -import { talkingAnimation } from './index.js'; | |
| 4 | 3 | import { saveTtsProviderSettings } from './index.js'; |
| 5 | 4 | export { SystemTtsProvider }; |
| 6 | 5 | |
| @@ -70,7 +69,6 @@ var speechUtteranceChunker = function (utt, settings, callback) { | ||
| 70 | 69 | //placing the speak invocation inside a callback fixes ordering and onend issues. |
| 71 | 70 | setTimeout(function () { |
| 72 | 71 | speechSynthesis.speak(newUtt); |
| 73 | - talkingAnimation(true); | |
| 74 | 72 | }, 0); |
| 75 | 73 | }; |
| 76 | 74 | |
| @@ -240,7 +238,6 @@ class SystemTtsProvider { | ||
| 240 | 238 | //some code to execute when done |
| 241 | 239 | resolve(silence); |
| 242 | 240 | console.log('System TTS done'); |
| 243 | - talkingAnimation(false); | |
| 244 | 241 | }); |
| 245 | 242 | }); |
| 246 | 243 | } |
| @@ -561,9 +561,9 @@ async function retrieveFileChunks(queryText, collectionId) { | ||
| 561 | 561 | */ |
| 562 | 562 | async function vectorizeFile(fileText, fileName, collectionId, chunkSize, overlapPercent) { |
| 563 | 563 | try { |
| 564 | 564 | if (settings.translate_files && typeof window['globalThis.translate'] === 'function') { |
| 565 | 565 | console.log(`Vectors: Translating file ${fileName} to English...`); |
| 566 | 566 | const translatedText = await window['globalThis.translate'](fileText, 'en'); |
| 567 | 567 | fileText = translatedText; |
| 568 | 568 | } |
| 569 | 569 | |
| @@ -746,74 +746,65 @@ async function getQueryText(chat, initiator) { | ||
| 746 | 746 | } |
| 747 | 747 | |
| 748 | 748 | /** |
| 749 | 749 | * Gets thecommon savedbody hashesparameters for avector collectionrequests. |
| 750 | 750 | * @paramreturns {stringobject} collectionId |
| 751 | -* @returns {Promise<number[]>} Saved hashes | |
| 752 | 751 | */ |
| 753 | 752 | async function getSavedHashesgetVectorsRequestBody(collectionId) { |
| 754 | - const response = await fetch('/api/vector/list', { | |
| 753 | + const body = {}; | |
| 755 | - method: 'POST', | |
| 756 | - headers: getVectorHeaders(), | |
| 757 | - body: JSON.stringify({ | |
| 758 | - collectionId: collectionId, | |
| 759 | - source: settings.source, | |
| 760 | - }), | |
| 761 | - }); | |
| 762 | - | |
| 763 | - if (!response.ok) { | |
| 764 | - throw new Error(`Failed to get saved hashes for collection ${collectionId}`); | |
| 765 | - } | |
| 766 | - | |
| 767 | - const hashes = await response.json(); | |
| 768 | - return hashes; | |
| 769 | -} | |
| 770 | - | |
| 771 | -function getVectorHeaders() { | |
| 772 | - const headers = getRequestHeaders(); | |
| 773 | 754 | switch (settings.source) { |
| 774 | 755 | case 'extras': |
| 775 | - Object.assign(headers, { | |
| 756 | + body.extrasUrl = extension_settings.apiUrl; | |
| 776 | - 'X-Extras-Url': extension_settings.apiUrl, | |
| 757 | + body.extrasKey = extension_settings.apiKey; | |
| 777 | - 'X-Extras-Key': extension_settings.apiKey, | |
| 778 | - }); | |
| 779 | 758 | break; |
| 780 | 759 | case 'togetherai': |
| 781 | - Object.assign(headers, { | |
| 760 | + body.model = extension_settings.vectors.togetherai_model; | |
| 782 | - 'X-Togetherai-Model': extension_settings.vectors.togetherai_model, | |
| 783 | - }); | |
| 784 | 761 | break; |
| 785 | 762 | case 'openai': |
| 786 | - Object.assign(headers, { | |
| 763 | + body.model = extension_settings.vectors.openai_model; | |
| 787 | - 'X-OpenAI-Model': extension_settings.vectors.openai_model, | |
| 788 | - }); | |
| 789 | 764 | break; |
| 790 | 765 | case 'cohere': |
| 791 | - Object.assign(headers, { | |
| 766 | + body.model = extension_settings.vectors.cohere_model; | |
| 792 | - 'X-Cohere-Model': extension_settings.vectors.cohere_model, | |
| 793 | - }); | |
| 794 | 767 | break; |
| 795 | 768 | case 'ollama': |
| 796 | - Object.assign(headers, { | |
| 769 | + body.model = extension_settings.vectors.ollama_model; | |
| 797 | - 'X-Ollama-Model': extension_settings.vectors.ollama_model, | |
| 770 | + body.apiUrl = textgenerationwebui_settings.server_urls[textgen_types.OLLAMA]; | |
| 798 | - 'X-Ollama-URL': textgenerationwebui_settings.server_urls[textgen_types.OLLAMA], | |
| 771 | + body.keep = !!extension_settings.vectors.ollama_keep; | |
| 799 | - 'X-Ollama-Keep': !!extension_settings.vectors.ollama_keep, | |
| 800 | - }); | |
| 801 | 772 | break; |
| 802 | 773 | case 'llamacpp': |
| 803 | - Object.assign(headers, { | |
| 774 | + body.apiUrl = textgenerationwebui_settings.server_urls[textgen_types.LLAMACPP]; | |
| 804 | - 'X-LlamaCpp-URL': textgenerationwebui_settings.server_urls[textgen_types.LLAMACPP], | |
| 805 | - }); | |
| 806 | 775 | break; |
| 807 | 776 | case 'vllm': |
| 808 | - Object.assign(headers, { | |
| 777 | + body.apiUrl = textgenerationwebui_settings.server_urls[textgen_types.VLLM]; | |
| 809 | - 'X-Vllm-URL': textgenerationwebui_settings.server_urls[textgen_types.VLLM], | |
| 778 | + body.model = extension_settings.vectors.vllm_model; | |
| 810 | - 'X-Vllm-Model': extension_settings.vectors.vllm_model, | |
| 811 | - }); | |
| 812 | 779 | break; |
| 813 | 780 | default: |
| 814 | 781 | break; |
| 815 | 782 | } |
| 816 | 783 | return headersbody; |
| 784 | +} | |
| 785 | + | |
| 786 | +/** | |
| 787 | + * Gets the saved hashes for a collection | |
| 788 | +* @param {string} collectionId | |
| 789 | +* @returns {Promise<number[]>} Saved hashes | |
| 790 | +*/ | |
| 791 | +async function getSavedHashes(collectionId) { | |
| 792 | + const response = await fetch('/api/vector/list', { | |
| 793 | + method: 'POST', | |
| 794 | + headers: getRequestHeaders(), | |
| 795 | + body: JSON.stringify({ | |
| 796 | + ...getVectorsRequestBody(), | |
| 797 | + collectionId: collectionId, | |
| 798 | + source: settings.source, | |
| 799 | + }), | |
| 800 | + }); | |
| 801 | + | |
| 802 | + if (!response.ok) { | |
| 803 | + throw new Error(`Failed to get saved hashes for collection ${collectionId}`); | |
| 804 | + } | |
| 805 | + | |
| 806 | + const hashes = await response.json(); | |
| 807 | + return hashes; | |
| 817 | 808 | } |
| 818 | 809 | |
| 819 | 810 | /** |
| @@ -825,12 +816,11 @@ function getVectorHeaders() { | ||
| 825 | 816 | async function insertVectorItems(collectionId, items) { |
| 826 | 817 | throwIfSourceInvalid(); |
| 827 | 818 | |
| 828 | - const headers = getVectorHeaders(); | |
| 829 | - | |
| 830 | 819 | const response = await fetch('/api/vector/insert', { |
| 831 | 820 | method: 'POST', |
| 832 | 821 | headers: headersgetRequestHeaders(), |
| 833 | 822 | body: JSON.stringify({ |
| 823 | + ...getVectorsRequestBody(), | |
| 834 | 824 | collectionId: collectionId, |
| 835 | 825 | items: items, |
| 836 | 826 | source: settings.source, |
| @@ -879,8 +869,9 @@ function throwIfSourceInvalid() { | ||
| 879 | 869 | async function deleteVectorItems(collectionId, hashes) { |
| 880 | 870 | const response = await fetch('/api/vector/delete', { |
| 881 | 871 | method: 'POST', |
| 882 | 872 | headers: getVectorHeadersgetRequestHeaders(), |
| 883 | 873 | body: JSON.stringify({ |
| 874 | + ...getVectorsRequestBody(), | |
| 884 | 875 | collectionId: collectionId, |
| 885 | 876 | hashes: hashes, |
| 886 | 877 | source: settings.source, |
| @@ -899,12 +890,11 @@ async function deleteVectorItems(collectionId, hashes) { | ||
| 899 | 890 | * @returns {Promise<{ hashes: number[], metadata: object[]}>} - Hashes of the results |
| 900 | 891 | */ |
| 901 | 892 | async function queryCollection(collectionId, searchText, topK) { |
| 902 | - const headers = getVectorHeaders(); | |
| 903 | - | |
| 904 | 893 | const response = await fetch('/api/vector/query', { |
| 905 | 894 | method: 'POST', |
| 906 | 895 | headers: headersgetRequestHeaders(), |
| 907 | 896 | body: JSON.stringify({ |
| 897 | + ...getVectorsRequestBody(), | |
| 908 | 898 | collectionId: collectionId, |
| 909 | 899 | searchText: searchText, |
| 910 | 900 | topK: topK, |
| @@ -929,12 +919,11 @@ async function queryCollection(collectionId, searchText, topK) { | ||
| 929 | 919 | * @returns {Promise<Record<string, { hashes: number[], metadata: object[] }>>} - Results mapped to collection IDs |
| 930 | 920 | */ |
| 931 | 921 | async function queryMultipleCollections(collectionIds, searchText, topK, threshold) { |
| 932 | - const headers = getVectorHeaders(); | |
| 933 | - | |
| 934 | 922 | const response = await fetch('/api/vector/query-multi', { |
| 935 | 923 | method: 'POST', |
| 936 | 924 | headers: headersgetRequestHeaders(), |
| 937 | 925 | body: JSON.stringify({ |
| 926 | + ...getVectorsRequestBody(), | |
| 938 | 927 | collectionIds: collectionIds, |
| 939 | 928 | searchText: searchText, |
| 940 | 929 | topK: topK, |
| @@ -965,8 +954,9 @@ async function purgeFileVectorIndex(fileUrl) { | ||
| 965 | 954 | |
| 966 | 955 | const response = await fetch('/api/vector/purge', { |
| 967 | 956 | method: 'POST', |
| 968 | 957 | headers: getVectorHeadersgetRequestHeaders(), |
| 969 | 958 | body: JSON.stringify({ |
| 959 | + ...getVectorsRequestBody(), | |
| 970 | 960 | collectionId: collectionId, |
| 971 | 961 | }), |
| 972 | 962 | }); |
| @@ -994,8 +984,9 @@ async function purgeVectorIndex(collectionId) { | ||
| 994 | 984 | |
| 995 | 985 | const response = await fetch('/api/vector/purge', { |
| 996 | 986 | method: 'POST', |
| 997 | 987 | headers: getVectorHeadersgetRequestHeaders(), |
| 998 | 988 | body: JSON.stringify({ |
| 989 | + ...getVectorsRequestBody(), | |
| 999 | 990 | collectionId: collectionId, |
| 1000 | 991 | }), |
| 1001 | 992 | }); |
| @@ -1019,7 +1010,10 @@ async function purgeAllVectorIndexes() { | ||
| 1019 | 1010 | try { |
| 1020 | 1011 | const response = await fetch('/api/vector/purge-all', { |
| 1021 | 1012 | method: 'POST', |
| 1022 | 1013 | headers: getVectorHeadersgetRequestHeaders(), |
| 1014 | + body: JSON.stringify({ | |
| 1015 | + ...getVectorsRequestBody(), | |
| 1016 | + }), | |
| 1023 | 1017 | }); |
| 1024 | 1018 | |
| 1025 | 1019 | if (!response.ok) { |
| @@ -1638,14 +1632,12 @@ jQuery(async () => { | ||
| 1638 | 1632 | } |
| 1639 | 1633 | return textResult; |
| 1640 | 1634 | }; |
| 1641 | - | |
| 1642 | 1635 | if (args.return === 'chunks') { |
| 1643 | 1636 | return getChunksText(); |
| 1644 | 1637 | } |
| 1645 | 1638 | |
| 1646 | 1639 | // @ts-ignore |
| 1647 | 1640 | return slashCommandReturnHelper.doReturn(args.return ?? 'object', urls, { objectToStringFunc: list => list.join('\n') }); |
| 1648 | - | |
| 1649 | 1641 | }, |
| 1650 | 1642 | aliases: ['databank-search', 'data-bank-search'], |
| 1651 | 1643 | helpString: 'Search the Data Bank for a specific query using vector similarity. Returns a list of file URLs with the most relevant content.', |
| @@ -1660,10 +1652,10 @@ jQuery(async () => { | ||
| 1660 | 1652 | defaultValue: 'object', |
| 1661 | 1653 | enumList: [ |
| 1662 | 1654 | new SlashCommandEnumValue('chunks', 'Return the actual content chunks', enumTypes.enum, '{}'), |
| 1663 | 1655 | ...slashCommandReturnHelper.enumList({ allowObject: true }), |
| 1664 | 1656 | ], |
| 1665 | 1657 | forceEnum: true, |
| 1666 | 1658 | }), |
| 1667 | 1659 | ], |
| 1668 | 1660 | unnamedArgumentList: [ |
| 1669 | 1661 | new SlashCommandArgument('Query to search by.', ARGUMENT_TYPE.STRING, true, false), |
| @@ -11,7 +11,7 @@ | ||
| 11 | 11 | </label> |
| 12 | 12 | <select id="vectors_source" class="text_pole"> |
| 13 | 13 | <option value="cohere">Cohere</option> |
| 14 | 14 | <option value="extras">Extras (deprecated)</option> |
| 15 | 15 | <option value="palm">Google AI Studio</option> |
| 16 | 16 | <option value="llamacpp">llama.cpp</option> |
| 17 | 17 | <option value="transformers" data-i18n="Local (Transformers)">Local (Transformers)</option> |
| @@ -1,18 +1,30 @@ | ||
| 1 | 1 | ////////////////// LOCAL STORAGE HANDLING ///////////////////// |
| 2 | 2 | |
| 3 | +/** | |
| 4 | + * @deprecated THIS FUNCTION IS OBSOLETE. DO NOT USE | |
| 5 | + */ | |
| 3 | 6 | export function SaveLocal(target, val) { |
| 4 | 7 | localStorage.setItem(target, val); |
| 5 | 8 | console.debug('SaveLocal -- ' + target + ' : ' + val); |
| 6 | 9 | } |
| 10 | +/** | |
| 11 | + * @deprecated THIS FUNCTION IS OBSOLETE. DO NOT USE | |
| 12 | + */ | |
| 7 | 13 | export function LoadLocal(target) { |
| 8 | 14 | console.debug('LoadLocal -- ' + target); |
| 9 | 15 | return localStorage.getItem(target); |
| 10 | 16 | |
| 11 | 17 | } |
| 18 | +/** | |
| 19 | + * @deprecated THIS FUNCTION IS OBSOLETE. DO NOT USE | |
| 20 | + */ | |
| 12 | 21 | export function LoadLocalBool(target) { |
| 13 | 22 | let result = localStorage.getItem(target) === 'true'; |
| 14 | 23 | return result; |
| 15 | 24 | } |
| 25 | +/** | |
| 26 | + * @deprecated THIS FUNCTION IS OBSOLETE. DO NOT USE | |
| 27 | + */ | |
| 16 | 28 | export function CheckLocal() { |
| 17 | 29 | console.log('----------local storage---------'); |
| 18 | 30 | var i; |
| @@ -22,6 +34,9 @@ export function CheckLocal() { | ||
| 22 | 34 | console.log('------------------------------'); |
| 23 | 35 | } |
| 24 | 36 | |
| 37 | +/** | |
| 38 | + * @deprecated THIS FUNCTION IS OBSOLETE. DO NOT USE | |
| 39 | + */ | |
| 25 | 40 | export function ClearLocal() { localStorage.clear(); console.log('Removed All Local Storage'); } |
| 26 | 41 | |
| 27 | 42 | ///////////////////////////////////////////////////////////////////////// |
| @@ -78,6 +78,7 @@ import { FILTER_TYPES, FilterHelper } from './filters.js'; | ||
| 78 | 78 | import { isExternalMediaAllowed } from './chats.js'; |
| 79 | 79 | import { POPUP_TYPE, Popup, callGenericPopup } from './popup.js'; |
| 80 | 80 | import { t } from './i18n.js'; |
| 81 | +import { accountStorage } from './util/AccountStorage.js'; | |
| 81 | 82 | |
| 82 | 83 | export { |
| 83 | 84 | selected_group, |
| @@ -292,10 +293,11 @@ export function getGroupNames() { | ||
| 292 | 293 | |
| 293 | 294 | /** |
| 294 | 295 | * Finds the character ID for a group member. |
| 295 | 296 | * @param {number|string} arg 0-based member index or character name |
| 296 | - * @returns {number} 0-based character ID | |
| 297 | + * @param {Boolean} full Whether to return a key-value object containing extra data | |
| 298 | + * @returns {number|Object} 0-based character ID or key-value object if full is true | |
| 297 | 299 | */ |
| 298 | 300 | export function findGroupMemberId(arg, full = false) { |
| 299 | 301 | arg = arg?.trim(); |
| 300 | 302 | |
| 301 | 303 | if (!arg) { |
| @@ -311,15 +313,19 @@ export function findGroupMemberId(arg) { | ||
| 311 | 313 | } |
| 312 | 314 | |
| 313 | 315 | const index = parseInt(arg); |
| 314 | 316 | const searchByNamesearchByString = isNaN(index); |
| 315 | 317 | |
| 316 | 318 | if (searchByNamesearchByString) { |
| 317 | - const memberNames = group.members.map(x => ({ name: characters.find(y => y.avatar === x)?.name, index: characters.findIndex(y => y.avatar === x) })); | |
| 319 | + const memberNames = group.members.map(x => ({ | |
| 318 | - const fuse = new Fuse(memberNames, { keys: ['name'] }); | |
| 320 | + avatar: x, | |
| 321 | + name: characters.find(y => y.avatar === x)?.name, | |
| 322 | + index: characters.findIndex(y => y.avatar === x), | |
| 323 | + })); | |
| 324 | + const fuse = new Fuse(memberNames, { keys: ['avatar', 'name'] }); | |
| 319 | 325 | const result = fuse.search(arg); |
| 320 | 326 | |
| 321 | 327 | if (!result.length) { |
| 322 | 328 | console.warn(`WARN: No group member found withusing namestring ${arg}`); |
| 323 | 329 | return; |
| 324 | 330 | } |
| 325 | 331 | |
| @@ -330,9 +336,11 @@ export function findGroupMemberId(arg) { | ||
| 330 | 336 | return; |
| 331 | 337 | } |
| 332 | 338 | |
| 333 | 339 | console.log(`TriggeringTargeting group member ${chid} (${arg}) from search result`, result[0]); |
| 334 | - return chid; | |
| 340 | + | |
| 335 | - } else { | |
| 341 | + return !full ? chid : { ...{ id: chid }, ...result[0].item }; | |
| 342 | + } | |
| 343 | + else { | |
| 336 | 344 | const memberAvatar = group.members[index]; |
| 337 | 345 | |
| 338 | 346 | if (memberAvatar === undefined) { |
| @@ -347,8 +355,14 @@ export function findGroupMemberId(arg) { | ||
| 347 | 355 | return; |
| 348 | 356 | } |
| 349 | 357 | |
| 350 | 358 | console.log(`TriggeringTargeting group member ${memberAvatar} at index ${index}`); |
| 351 | - return chid; | |
| 359 | + | |
| 360 | + return !full ? chid : { | |
| 361 | + id: chid, | |
| 362 | + avatar: memberAvatar, | |
| 363 | + name: characters.find(y => y.avatar === memberAvatar)?.name, | |
| 364 | + index: index, | |
| 365 | + }; | |
| 352 | 366 | } |
| 353 | 367 | } |
| 354 | 368 | |
| @@ -805,7 +819,6 @@ async function generateGroupWrapper(by_auto_mode, type = null, params = {}) { | ||
| 805 | 819 | |
| 806 | 820 | /** @type {any} Caution: JS war crimes ahead */ |
| 807 | 821 | let textResult = ''; |
| 808 | - let typingIndicator = $('#chat .typing_indicator'); | |
| 809 | 822 | const group = groups.find((x) => x.id === selected_group); |
| 810 | 823 | |
| 811 | 824 | if (!group || !Array.isArray(group.members) || !group.members.length) { |
| @@ -821,14 +834,6 @@ async function generateGroupWrapper(by_auto_mode, type = null, params = {}) { | ||
| 821 | 834 | setCharacterId(undefined); |
| 822 | 835 | const userInput = String($('#send_textarea').val()); |
| 823 | 836 | |
| 824 | - if (typingIndicator.length === 0 && !isStreamingEnabled()) { | |
| 825 | - typingIndicator = $( | |
| 826 | - '#typing_indicator_template .typing_indicator', | |
| 827 | - ).clone(); | |
| 828 | - typingIndicator.hide(); | |
| 829 | - $('#chat').append(typingIndicator); | |
| 830 | - } | |
| 831 | - | |
| 832 | 837 | // id of this specific batch for regeneration purposes |
| 833 | 838 | group_generation_id = Date.now(); |
| 834 | 839 | const lastMessage = chat[chat.length - 1]; |
| @@ -906,14 +911,6 @@ async function generateGroupWrapper(by_auto_mode, type = null, params = {}) { | ||
| 906 | 911 | } |
| 907 | 912 | await eventSource.emit(event_types.GROUP_MEMBER_DRAFTED, chId); |
| 908 | 913 | |
| 909 | - if (type !== 'swipe' && type !== 'impersonate' && !isStreamingEnabled()) { | |
| 910 | - // update indicator and scroll down | |
| 911 | - typingIndicator | |
| 912 | - .find('.typing_indicator_name') | |
| 913 | - .text(characters[chId].name); | |
| 914 | - typingIndicator.show(); | |
| 915 | - } | |
| 916 | - | |
| 917 | 914 | // Wait for generation to finish |
| 918 | 915 | textResult = await Generate(generateType, { automatic_trigger: by_auto_mode, ...(params || {}) }); |
| 919 | 916 | let messageChunk = textResult?.messageChunk; |
| @@ -930,8 +927,6 @@ async function generateGroupWrapper(by_auto_mode, type = null, params = {}) { | ||
| 930 | 927 | } |
| 931 | 928 | } |
| 932 | 929 | } finally { |
| 933 | - typingIndicator.hide(); | |
| 934 | - | |
| 935 | 930 | is_group_generating = false; |
| 936 | 931 | setSendButtonState(false); |
| 937 | 932 | setCharacterId(undefined); |
| @@ -1315,10 +1310,10 @@ function printGroupCandidates() { | ||
| 1315 | 1310 | formatNavigator: PAGINATION_TEMPLATE, |
| 1316 | 1311 | showNavigator: true, |
| 1317 | 1312 | showSizeChanger: true, |
| 1318 | 1313 | pageSize: Number(localStorageaccountStorage.getItem(storageKey)) || 5, |
| 1319 | 1314 | sizeChangerOptions: [5, 10, 25, 50, 100, 200, 500, 1000], |
| 1320 | 1315 | afterSizeSelectorChange: function (e) { |
| 1321 | 1316 | localStorageaccountStorage.setItem(storageKey, e.target.value); |
| 1322 | 1317 | }, |
| 1323 | 1318 | callback: function (data) { |
| 1324 | 1319 | $('#rm_group_add_members').empty(); |
| @@ -1342,10 +1337,10 @@ function printGroupMembers() { | ||
| 1342 | 1337 | formatNavigator: PAGINATION_TEMPLATE, |
| 1343 | 1338 | showNavigator: true, |
| 1344 | 1339 | showSizeChanger: true, |
| 1345 | 1340 | pageSize: Number(localStorageaccountStorage.getItem(storageKey)) || 5, |
| 1346 | 1341 | sizeChangerOptions: [5, 10, 25, 50, 100, 200, 500, 1000], |
| 1347 | 1342 | afterSizeSelectorChange: function (e) { |
| 1348 | 1343 | localStorageaccountStorage.setItem(storageKey, e.target.value); |
| 1349 | 1344 | }, |
| 1350 | 1345 | callback: function (data) { |
| 1351 | 1346 | $('.rm_group_members').empty(); |
| @@ -1669,12 +1664,12 @@ function updateFavButtonState(state) { | ||
| 1669 | 1664 | export async function openGroupById(groupId) { |
| 1670 | 1665 | if (isChatSaving) { |
| 1671 | 1666 | toastr.info(t`Please wait until the chat is saved before switching characters.`, t`Your chat is still saving...`); |
| 1672 | 1667 | return false; |
| 1673 | 1668 | } |
| 1674 | 1669 | |
| 1675 | 1670 | if (!groups.find(x => x.id === groupId)) { |
| 1676 | 1671 | console.log('Group not found', groupId); |
| 1677 | 1672 | return false; |
| 1678 | 1673 | } |
| 1679 | 1674 | |
| 1680 | 1675 | if (!is_send_press && !is_group_generating) { |
| @@ -1691,8 +1686,11 @@ export async function openGroupById(groupId) { | ||
| 1691 | 1686 | updateChatMetadata({}, true); |
| 1692 | 1687 | chat.length = 0; |
| 1693 | 1688 | await getGroupChat(groupId); |
| 1689 | + return true; | |
| 1694 | 1690 | } |
| 1695 | 1691 | } |
| 1692 | + | |
| 1693 | + return false; | |
| 1696 | 1694 | } |
| 1697 | 1695 | |
| 1698 | 1696 | function openCharacterDefinition(characterSelect) { |
| @@ -27,21 +27,42 @@ export async function hideLoader() { | ||
| 27 | 27 | } |
| 28 | 28 | |
| 29 | 29 | return new Promise((resolve) => { |
| 30 | - // Spinner blurs/fades out | |
| 30 | + const spinner = $('#load-spinner'); | |
| 31 | - $('#load-spinner').on('transitionend webkitTransitionEnd oTransitionEnd MSTransitionEnd', function () { | |
| 31 | + if (!spinner.length) { | |
| 32 | + console.warn('Spinner element not found, skipping animation'); | |
| 33 | + cleanup(); | |
| 34 | + return; | |
| 35 | + } | |
| 36 | + | |
| 37 | + // Check if transitions are enabled | |
| 38 | + const transitionDuration = spinner[0] ? getComputedStyle(spinner[0]).transitionDuration : '0s'; | |
| 39 | + const hasTransitions = parseFloat(transitionDuration) > 0; | |
| 40 | + | |
| 41 | + if (hasTransitions) { | |
| 42 | + Promise.race([ | |
| 43 | + new Promise((r) => setTimeout(r, 500)), // Fallback timeout | |
| 44 | + new Promise((r) => spinner.one('transitionend webkitTransitionEnd oTransitionEnd MSTransitionEnd', r)), | |
| 45 | + ]).finally(cleanup); | |
| 46 | + } else { | |
| 47 | + cleanup(); | |
| 48 | + } | |
| 49 | + | |
| 50 | + function cleanup() { | |
| 32 | 51 | $('#loader').remove(); |
| 33 | 52 | // Yoink preloader entirely; it only exists to cover up unstyled content while loading JS |
| 34 | 53 | // If it's present, we remove it once and then it's gone. |
| 35 | 54 | yoinkPreloader(); |
| 36 | 55 | |
| 37 | 56 | loaderPopup.complete(POPUP_RESULT.AFFIRMATIVE).then(() => { |
| 57 | + .catch((err) => console.error('Error completing loaderPopup:', err)) | |
| 58 | + .finally(() => { | |
| 38 | 59 | loaderPopup = null; |
| 39 | 60 | resolve(); |
| 40 | 61 | }); |
| 41 | 62 | }); |
| 42 | 63 | |
| 43 | - $('#load-spinner') | |
| 64 | + // Apply the styles | |
| 44 | 65 | spinner.css({ |
| 45 | 66 | 'filter': 'blur(15px)', |
| 46 | 67 | 'opacity': '0', |
| 47 | 68 | }); |
| @@ -73,6 +73,7 @@ import { SlashCommandEnumValue } from './slash-commands/SlashCommandEnumValue.js | ||
| 73 | 73 | import { Popup, POPUP_RESULT } from './popup.js'; |
| 74 | 74 | import { t } from './i18n.js'; |
| 75 | 75 | import { ToolManager } from './tool-calling.js'; |
| 76 | +import { accountStorage } from './util/AccountStorage.js'; | |
| 76 | 77 | |
| 77 | 78 | export { |
| 78 | 79 | openai_messages_count, |
| @@ -82,7 +83,6 @@ export { | ||
| 82 | 83 | setOpenAIMessageExamples, |
| 83 | 84 | setupChatCompletionPromptManager, |
| 84 | 85 | sendOpenAIRequest, |
| 85 | - getChatCompletionModel, | |
| 86 | 86 | TokenHandler, |
| 87 | 87 | IdentifierNotFoundError, |
| 88 | 88 | Message, |
| @@ -258,8 +258,8 @@ const default_settings = { | ||
| 258 | 258 | ai21_model: 'jamba-1.5-large', |
| 259 | 259 | mistralai_model: 'mistral-large-latest', |
| 260 | 260 | cohere_model: 'command-r-plus', |
| 261 | 261 | perplexity_model: 'llama-3.1-70bsonar-instructpro', |
| 262 | 262 | groq_model: 'llama-3.13-70b-versatile', |
| 263 | 263 | nanogpt_model: 'gpt-4o-mini', |
| 264 | 264 | zerooneai_model: 'yi-large', |
| 265 | 265 | blockentropy_model: 'be-70b-base-llama3.1', |
| @@ -298,7 +298,8 @@ const default_settings = { | ||
| 298 | 298 | names_behavior: character_names_behavior.DEFAULT, |
| 299 | 299 | continue_postfix: continue_postfix_types.SPACE, |
| 300 | 300 | custom_prompt_post_processing: custom_prompt_post_processing_types.NONE, |
| 301 | 301 | show_thoughts: falsetrue, |
| 302 | + reasoning_effort: 'medium', | |
| 302 | 303 | seed: -1, |
| 303 | 304 | n: 1, |
| 304 | 305 | }; |
| @@ -337,7 +338,7 @@ const oai_settings = { | ||
| 337 | 338 | ai21_model: 'jamba-1.5-large', |
| 338 | 339 | mistralai_model: 'mistral-large-latest', |
| 339 | 340 | cohere_model: 'command-r-plus', |
| 340 | 341 | perplexity_model: 'llama-3.1-70bsonar-instructpro', |
| 341 | 342 | groq_model: 'llama-3.1-70b-versatile', |
| 342 | 343 | nanogpt_model: 'gpt-4o-mini', |
| 343 | 344 | zerooneai_model: 'yi-large', |
| @@ -377,7 +378,8 @@ const oai_settings = { | ||
| 377 | 378 | names_behavior: character_names_behavior.DEFAULT, |
| 378 | 379 | continue_postfix: continue_postfix_types.SPACE, |
| 379 | 380 | custom_prompt_post_processing: custom_prompt_post_processing_types.NONE, |
| 380 | 381 | show_thoughts: falsetrue, |
| 382 | + reasoning_effort: 'medium', | |
| 381 | 383 | seed: -1, |
| 382 | 384 | n: 1, |
| 383 | 385 | }; |
| @@ -412,7 +414,7 @@ async function validateReverseProxy() { | ||
| 412 | 414 | throw err; |
| 413 | 415 | } |
| 414 | 416 | const rememberKey = `Proxy_SkipConfirm_${getStringHash(oai_settings.reverse_proxy)}`; |
| 415 | 417 | const skipConfirm = localStorageaccountStorage.getItem(rememberKey) === 'true'; |
| 416 | 418 | |
| 417 | 419 | const confirmation = skipConfirm || await Popup.show.confirm(t`Connecting To Proxy`, await renderTemplateAsync('proxyConnectionWarning', { proxyURL: DOMPurify.sanitize(oai_settings.reverse_proxy) })); |
| 418 | 420 | |
| @@ -423,7 +425,7 @@ async function validateReverseProxy() { | ||
| 423 | 425 | throw new Error('Proxy connection denied.'); |
| 424 | 426 | } |
| 425 | 427 | |
| 426 | 428 | localStorageaccountStorage.setItem(rememberKey, String(true)); |
| 427 | 429 | } |
| 428 | 430 | |
| 429 | 431 | /** |
| @@ -1443,9 +1445,7 @@ async function sendWindowAIRequest(messages, signal, stream) { | ||
| 1443 | 1445 | } |
| 1444 | 1446 | |
| 1445 | 1447 | const onStreamResult = (res, err) => { |
| 1446 | 1448 | if (err) {return; |
| 1447 | - return; | |
| 1448 | - } | |
| 1449 | 1449 | |
| 1450 | 1450 | const thisContent = res?.message?.content; |
| 1451 | 1451 | |
| @@ -1497,7 +1497,7 @@ async function sendWindowAIRequest(messages, signal, stream) { | ||
| 1497 | 1497 | } |
| 1498 | 1498 | } |
| 1499 | 1499 | |
| 1500 | 1500 | export function getChatCompletionModel() { |
| 1501 | 1501 | switch (oai_settings.chat_completion_source) { |
| 1502 | 1502 | case chat_completion_sources.CLAUDE: |
| 1503 | 1503 | return oai_settings.claude_model; |
| @@ -1869,7 +1869,7 @@ async function sendOpenAIRequest(type, messages, signal) { | ||
| 1869 | 1869 | const isQuiet = type === 'quiet'; |
| 1870 | 1870 | const isImpersonate = type === 'impersonate'; |
| 1871 | 1871 | const isContinue = type === 'continue'; |
| 1872 | 1872 | const stream = oai_settings.stream_openai && !isQuiet && !isScale && !(isGoogleisOAI && oai_settings.google_model.includes(['bisono1-2024-12-17')) &&, !'o1'].includes(isOAI && oai_settings.openai_model.startsWith('o1-')); |
| 1873 | 1873 | const useLogprobs = !!power_user.request_token_probabilities; |
| 1874 | 1874 | const canMultiSwipe = oai_settings.n > 1 && !isContinue && !isImpersonate && !isQuiet && (isOAI || isCustom); |
| 1875 | 1875 | |
| @@ -1913,9 +1913,14 @@ async function sendOpenAIRequest(type, messages, signal) { | ||
| 1913 | 1913 | 'user_name': name1, |
| 1914 | 1914 | 'char_name': name2, |
| 1915 | 1915 | 'group_names': getGroupNames(), |
| 1916 | 1916 | 'show_thoughtsinclude_reasoning': Boolean(oai_settings.show_thoughts), |
| 1917 | + 'reasoning_effort': String(oai_settings.reasoning_effort), | |
| 1917 | 1918 | }; |
| 1918 | 1919 | |
| 1920 | + if (!canMultiSwipe && ToolManager.canPerformToolCalls(type)) { | |
| 1921 | + await ToolManager.registerFunctionToolsOpenAI(generate_data); | |
| 1922 | + } | |
| 1923 | + | |
| 1919 | 1924 | // Empty array will produce a validation error |
| 1920 | 1925 | if (!Array.isArray(generate_data.stop) || !generate_data.stop.length) { |
| 1921 | 1926 | delete generate_data.stop; |
| @@ -2039,6 +2044,8 @@ async function sendOpenAIRequest(type, messages, signal) { | ||
| 2039 | 2044 | delete generate_data.top_logprobs; |
| 2040 | 2045 | delete generate_data.logprobs; |
| 2041 | 2046 | delete generate_data.logit_bias; |
| 2047 | + delete generate_data.tools; | |
| 2048 | + delete generate_data.tool_choice; | |
| 2042 | 2049 | } |
| 2043 | 2050 | } |
| 2044 | 2051 | |
| @@ -2046,11 +2053,7 @@ async function sendOpenAIRequest(type, messages, signal) { | ||
| 2046 | 2053 | generate_data['seed'] = oai_settings.seed; |
| 2047 | 2054 | } |
| 2048 | 2055 | |
| 2049 | - if (!canMultiSwipe && ToolManager.canPerformToolCalls(type)) { | |
| 2056 | + if (isOAI && (oai_settings.openai_model.startsWith('o1') || oai_settings.openai_model.startsWith('o3'))) { | |
| 2050 | - await ToolManager.registerFunctionToolsOpenAI(generate_data); | |
| 2051 | - } | |
| 2052 | - | |
| 2053 | - if (isOAI && oai_settings.openai_model.startsWith('o1-')) { | |
| 2054 | 2057 | generate_data.messages.forEach((msg) => { |
| 2055 | 2058 | if (msg.role === 'system') { |
| 2056 | 2059 | msg.role = 'user'; |
| @@ -2058,7 +2061,6 @@ async function sendOpenAIRequest(type, messages, signal) { | ||
| 2058 | 2061 | }); |
| 2059 | 2062 | generate_data.max_completion_tokens = generate_data.max_tokens; |
| 2060 | 2063 | delete generate_data.max_tokens; |
| 2061 | - delete generate_data.stream; | |
| 2062 | 2064 | delete generate_data.logprobs; |
| 2063 | 2065 | delete generate_data.top_logprobs; |
| 2064 | 2066 | delete generate_data.n; |
| @@ -2069,8 +2071,7 @@ async function sendOpenAIRequest(type, messages, signal) { | ||
| 2069 | 2071 | delete generate_data.tools; |
| 2070 | 2072 | delete generate_data.tool_choice; |
| 2071 | 2073 | delete generate_data.stop; |
| 2072 | - // It does support logit_bias, but the tokenizer used and its effect is yet unknown. | |
| 2074 | + delete generate_data.logit_bias; | |
| 2073 | - // delete generate_data.logit_bias; | |
| 2074 | 2075 | } |
| 2075 | 2076 | |
| 2076 | 2077 | await eventSource.emit(event_types.CHAT_COMPLETION_SETTINGS_READY, generate_data); |
| @@ -2166,6 +2167,14 @@ function getStreamingReply(data, state) { | ||
| 2166 | 2167 | state.reasoning += (data.choices?.filter(x => x?.delta?.reasoning)?.[0]?.delta?.reasoning || ''); |
| 2167 | 2168 | } |
| 2168 | 2169 | return data.choices?.[0]?.delta?.content ?? data.choices?.[0]?.message?.content ?? data.choices?.[0]?.text ?? ''; |
| 2170 | + } else if (oai_settings.chat_completion_source === chat_completion_sources.CUSTOM) { | |
| 2171 | + if (oai_settings.show_thoughts) { | |
| 2172 | + state.reasoning += | |
| 2173 | + data.choices?.filter(x => x?.delta?.reasoning_content)?.[0]?.delta?.reasoning_content ?? | |
| 2174 | + data.choices?.filter(x => x?.delta?.reasoning)?.[0]?.delta?.reasoning ?? | |
| 2175 | + ''; | |
| 2176 | + } | |
| 2177 | + return data.choices?.[0]?.delta?.content ?? data.choices?.[0]?.message?.content ?? data.choices?.[0]?.text ?? ''; | |
| 2169 | 2178 | } else { |
| 2170 | 2179 | return data.choices?.[0]?.delta?.content ?? data.choices?.[0]?.message?.content ?? data.choices?.[0]?.text ?? ''; |
| 2171 | 2180 | } |
| @@ -3124,6 +3133,7 @@ function loadOpenAISettings(data, settings) { | ||
| 3124 | 3133 | oai_settings.inline_image_quality = settings.inline_image_quality ?? default_settings.inline_image_quality; |
| 3125 | 3134 | oai_settings.bypass_status_check = settings.bypass_status_check ?? default_settings.bypass_status_check; |
| 3126 | 3135 | oai_settings.show_thoughts = settings.show_thoughts ?? default_settings.show_thoughts; |
| 3136 | + oai_settings.reasoning_effort = settings.reasoning_effort ?? default_settings.reasoning_effort; | |
| 3127 | 3137 | oai_settings.seed = settings.seed ?? default_settings.seed; |
| 3128 | 3138 | oai_settings.n = settings.n ?? default_settings.n; |
| 3129 | 3139 | |
| @@ -3253,6 +3263,9 @@ function loadOpenAISettings(data, settings) { | ||
| 3253 | 3263 | $('#n_openai').val(oai_settings.n); |
| 3254 | 3264 | $('#openai_show_thoughts').prop('checked', oai_settings.show_thoughts); |
| 3255 | 3265 | |
| 3266 | + $('#openai_reasoning_effort').val(oai_settings.reasoning_effort); | |
| 3267 | + $(`#openai_reasoning_effort option[value="${oai_settings.reasoning_effort}"]`).prop('selected', true); | |
| 3268 | + | |
| 3256 | 3269 | if (settings.reverse_proxy !== undefined) oai_settings.reverse_proxy = settings.reverse_proxy; |
| 3257 | 3270 | $('#openai_reverse_proxy').val(oai_settings.reverse_proxy); |
| 3258 | 3271 | |
| @@ -3513,6 +3526,7 @@ async function saveOpenAIPreset(name, settings, triggerUi = true) { | ||
| 3513 | 3526 | continue_postfix: settings.continue_postfix, |
| 3514 | 3527 | function_calling: settings.function_calling, |
| 3515 | 3528 | show_thoughts: settings.show_thoughts, |
| 3529 | + reasoning_effort: settings.reasoning_effort, | |
| 3516 | 3530 | seed: settings.seed, |
| 3517 | 3531 | n: settings.n, |
| 3518 | 3532 | }; |
| @@ -3971,6 +3985,7 @@ function onSettingsPresetChange() { | ||
| 3971 | 3985 | continue_postfix: ['#continue_postfix', 'continue_postfix', false], |
| 3972 | 3986 | function_calling: ['#openai_function_calling', 'function_calling', true], |
| 3973 | 3987 | show_thoughts: ['#openai_show_thoughts', 'show_thoughts', true], |
| 3988 | + reasoning_effort: ['#openai_reasoning_effort', 'reasoning_effort', false], | |
| 3974 | 3989 | seed: ['#seed_openai', 'seed', false], |
| 3975 | 3990 | n: ['#n_openai', 'n', false], |
| 3976 | 3991 | }; |
| @@ -4027,7 +4042,7 @@ function getMaxContextOpenAI(value) { | ||
| 4027 | 4042 | if (oai_settings.max_context_unlocked) { |
| 4028 | 4043 | return unlocked_max; |
| 4029 | 4044 | } |
| 4030 | 4045 | else if (value.startsWith('o1-') || value.startsWith('o3')) { |
| 4031 | 4046 | return max_128k; |
| 4032 | 4047 | } |
| 4033 | 4048 | else if (value.includes('chatgpt-4o-latest') || value.includes('gpt-4-turbo') || value.includes('gpt-4o') || value.includes('gpt-4-1106') || value.includes('gpt-4-0125') || value.includes('gpt-4-vision')) { |
| @@ -4100,6 +4115,40 @@ function getMaxContextWindowAI(value) { | ||
| 4100 | 4115 | } |
| 4101 | 4116 | } |
| 4102 | 4117 | |
| 4118 | +/** | |
| 4119 | + * Get the maximum context size for the Groq model | |
| 4120 | + * @param {string} model Model identifier | |
| 4121 | + * @param {boolean} isUnlocked Whether context limits are unlocked | |
| 4122 | + * @returns {number} Maximum context size in tokens | |
| 4123 | + */ | |
| 4124 | +function getGroqMaxContext(model, isUnlocked) { | |
| 4125 | + if (isUnlocked) { | |
| 4126 | + return unlocked_max; | |
| 4127 | + } | |
| 4128 | + | |
| 4129 | + const contextMap = { | |
| 4130 | + 'gemma2-9b-it': max_8k, | |
| 4131 | + 'llama-3.3-70b-versatile': max_128k, | |
| 4132 | + 'llama-3.1-8b-instant': max_128k, | |
| 4133 | + 'llama3-70b-8192': max_8k, | |
| 4134 | + 'llama3-8b-8192': max_8k, | |
| 4135 | + 'llama-guard-3-8b': max_8k, | |
| 4136 | + 'mixtral-8x7b-32768': max_32k, | |
| 4137 | + 'deepseek-r1-distill-llama-70b': max_128k, | |
| 4138 | + 'llama-3.3-70b-specdec': max_8k, | |
| 4139 | + 'llama-3.2-1b-preview': max_128k, | |
| 4140 | + 'llama-3.2-3b-preview': max_128k, | |
| 4141 | + 'llama-3.2-11b-vision-preview': max_128k, | |
| 4142 | + 'llama-3.2-90b-vision-preview': max_128k, | |
| 4143 | + 'qwen-2.5-32b': max_128k, | |
| 4144 | + 'deepseek-r1-distill-qwen-32b': max_128k, | |
| 4145 | + 'deepseek-r1-distill-llama-70b-specdec': max_128k, | |
| 4146 | + }; | |
| 4147 | + | |
| 4148 | + // Return context size if model found, otherwise default to 128k | |
| 4149 | + return Object.entries(contextMap).find(([key]) => model.includes(key))?.[1] || max_128k; | |
| 4150 | +} | |
| 4151 | + | |
| 4103 | 4152 | async function onModelChange() { |
| 4104 | 4153 | biasCache = undefined; |
| 4105 | 4154 | let value = String($(this).val() || ''); |
| @@ -4232,9 +4281,9 @@ async function onModelChange() { | ||
| 4232 | 4281 | $('#openai_max_context').attr('max', max_2mil); |
| 4233 | 4282 | } else if (value.includes('gemini-exp-1114') || value.includes('gemini-exp-1121') || value.includes('gemini-2.0-flash-thinking-exp-1219')) { |
| 4234 | 4283 | $('#openai_max_context').attr('max', max_32k); |
| 4235 | 4284 | } else if (value.includes('gemini-1.5-pro') || value.includes('gemini-exp-1206') || value.includes('gemini-2.0-pro')) { |
| 4236 | 4285 | $('#openai_max_context').attr('max', max_2mil); |
| 4237 | 4286 | } else if (value.includes('gemini-1.5-flash') || value.includes('gemini-2.0-flash-exp') || value.includes('gemini-2.0-flash-thinking-exp')) { |
| 4238 | 4287 | $('#openai_max_context').attr('max', max_1mil); |
| 4239 | 4288 | } else if (value.includes('gemini-1.0-pro') || value === 'gemini-pro') { |
| 4240 | 4289 | $('#openai_max_context').attr('max', max_32k); |
| @@ -4380,28 +4429,19 @@ async function onModelChange() { | ||
| 4380 | 4429 | if (oai_settings.max_context_unlocked) { |
| 4381 | 4430 | $('#openai_max_context').attr('max', unlocked_max); |
| 4382 | 4431 | } |
| 4432 | + else if (['sonar', 'sonar-reasoning', 'sonar-reasoning-pro', 'r1-1776'].includes(oai_settings.perplexity_model)) { | |
| 4433 | + $('#openai_max_context').attr('max', 127000); | |
| 4434 | + } | |
| 4435 | + else if (['sonar-pro'].includes(oai_settings.perplexity_model)) { | |
| 4436 | + $('#openai_max_context').attr('max', 200000); | |
| 4437 | + } | |
| 4383 | 4438 | else if (oai_settings.perplexity_model.includes('llama-3.1')) { |
| 4384 | 4439 | const isOnline = oai_settings.perplexity_model.includes('online'); |
| 4385 | 4440 | const contextSize = isOnline ? 128 * 1024 - 4000 : 128 * 1024; |
| 4386 | 4441 | $('#openai_max_context').attr('max', contextSize); |
| 4387 | 4442 | } |
| 4388 | - else if (['llama-3-sonar-small-32k-chat', 'llama-3-sonar-large-32k-chat'].includes(oai_settings.perplexity_model)) { | |
| 4389 | - $('#openai_max_context').attr('max', max_32k); | |
| 4390 | - } | |
| 4391 | - else if (['llama-3-sonar-small-32k-online', 'llama-3-sonar-large-32k-online'].includes(oai_settings.perplexity_model)) { | |
| 4392 | - $('#openai_max_context').attr('max', 28000); | |
| 4393 | - } | |
| 4394 | - else if (['sonar-small-chat', 'sonar-medium-chat', 'codellama-70b-instruct', 'mistral-7b-instruct', 'mixtral-8x7b-instruct', 'mixtral-8x22b-instruct'].includes(oai_settings.perplexity_model)) { | |
| 4395 | - $('#openai_max_context').attr('max', max_16k); | |
| 4396 | - } | |
| 4397 | - else if (['llama-3-8b-instruct', 'llama-3-70b-instruct'].includes(oai_settings.perplexity_model)) { | |
| 4398 | - $('#openai_max_context').attr('max', max_8k); | |
| 4399 | - } | |
| 4400 | - else if (['sonar-small-online', 'sonar-medium-online'].includes(oai_settings.perplexity_model)) { | |
| 4401 | - $('#openai_max_context').attr('max', 12000); | |
| 4402 | - } | |
| 4403 | 4443 | else { |
| 4404 | 4444 | $('#openai_max_context').attr('max', max_4kmax_128k); |
| 4405 | 4445 | } |
| 4406 | 4446 | oai_settings.openai_max_context = Math.min(Number($('#openai_max_context').attr('max')), oai_settings.openai_max_context); |
| 4407 | 4447 | $('#openai_max_context').val(oai_settings.openai_max_context).trigger('input'); |
| @@ -4410,27 +4450,8 @@ async function onModelChange() { | ||
| 4410 | 4450 | } |
| 4411 | 4451 | |
| 4412 | 4452 | if (oai_settings.chat_completion_source == chat_completion_sources.GROQ) { |
| 4413 | - if (oai_settings.max_context_unlocked) { | |
| 4453 | + const maxContext = getGroqMaxContext(oai_settings.groq_model, oai_settings.max_context_unlocked); | |
| 4414 | 4454 | $('#openai_max_context').attr('max', unlocked_maxmaxContext); |
| 4415 | - } | |
| 4416 | - else if (oai_settings.groq_model.includes('llama-3.2') && oai_settings.groq_model.includes('-preview')) { | |
| 4417 | - $('#openai_max_context').attr('max', max_8k); | |
| 4418 | - } | |
| 4419 | - else if (oai_settings.groq_model.includes('llama-3.3') || oai_settings.groq_model.includes('llama-3.2') || oai_settings.groq_model.includes('llama-3.1')) { | |
| 4420 | - $('#openai_max_context').attr('max', max_128k); | |
| 4421 | - } | |
| 4422 | - else if (oai_settings.groq_model.includes('llama3-groq')) { | |
| 4423 | - $('#openai_max_context').attr('max', max_8k); | |
| 4424 | - } | |
| 4425 | - else if (['llama3-8b-8192', 'llama3-70b-8192', 'gemma-7b-it', 'gemma2-9b-it'].includes(oai_settings.groq_model)) { | |
| 4426 | - $('#openai_max_context').attr('max', max_8k); | |
| 4427 | - } | |
| 4428 | - else if (['mixtral-8x7b-32768'].includes(oai_settings.groq_model)) { | |
| 4429 | - $('#openai_max_context').attr('max', max_32k); | |
| 4430 | - } | |
| 4431 | - else { | |
| 4432 | - $('#openai_max_context').attr('max', max_4k); | |
| 4433 | - } | |
| 4434 | 4455 | oai_settings.openai_max_context = Math.min(Number($('#openai_max_context').attr('max')), oai_settings.openai_max_context); |
| 4435 | 4456 | $('#openai_max_context').val(oai_settings.openai_max_context).trigger('input'); |
| 4436 | 4457 | oai_settings.temp_openai = Math.min(oai_max_temp, oai_settings.temp_openai); |
| @@ -4930,6 +4951,12 @@ export function isImageInliningSupported() { | ||
| 4930 | 4951 | // gultra just isn't being offered as multimodal, thanks google. |
| 4931 | 4952 | const visionSupportedModels = [ |
| 4932 | 4953 | 'gpt-4-vision', |
| 4954 | + 'gemini-2.0-pro-exp', | |
| 4955 | + 'gemini-2.0-pro-exp-02-05', | |
| 4956 | + 'gemini-2.0-flash-lite-preview', | |
| 4957 | + 'gemini-2.0-flash-lite-preview-02-05', | |
| 4958 | + 'gemini-2.0-flash', | |
| 4959 | + 'gemini-2.0-flash-001', | |
| 4933 | 4960 | 'gemini-2.0-flash-thinking-exp-1219', |
| 4934 | 4961 | 'gemini-2.0-flash-thinking-exp-01-21', |
| 4935 | 4962 | 'gemini-2.0-flash-thinking-exp', |
| @@ -4957,6 +4984,8 @@ export function isImageInliningSupported() { | ||
| 4957 | 4984 | 'gpt-4-turbo', |
| 4958 | 4985 | 'gpt-4o', |
| 4959 | 4986 | 'gpt-4o-mini', |
| 4987 | + 'o1', | |
| 4988 | + 'o1-2024-12-17', | |
| 4960 | 4989 | 'chatgpt-4o-latest', |
| 4961 | 4990 | 'yi-vision', |
| 4962 | 4991 | 'pixtral-latest', |
| @@ -5515,6 +5544,11 @@ export function initOpenAI() { | ||
| 5515 | 5544 | saveSettingsDebounced(); |
| 5516 | 5545 | }); |
| 5517 | 5546 | |
| 5547 | + $('#openai_reasoning_effort').on('input', function () { | |
| 5548 | + oai_settings.reasoning_effort = String($(this).val()); | |
| 5549 | + saveSettingsDebounced(); | |
| 5550 | + }); | |
| 5551 | + | |
| 5518 | 5552 | if (!CSS.supports('field-sizing', 'content')) { |
| 5519 | 5553 | $(document).on('input', '#openai_settings .autoSetHeight', function () { |
| 5520 | 5554 | resetScrollHeight($(this)); |
| @@ -30,6 +30,7 @@ import { t } from './i18n.js'; | ||
| 30 | 30 | import { openWorldInfoEditor, world_names } from './world-info.js'; |
| 31 | 31 | import { renderTemplateAsync } from './templates.js'; |
| 32 | 32 | import { saveMetadataDebounced } from './extensions.js'; |
| 33 | +import { accountStorage } from './util/AccountStorage.js'; | |
| 33 | 34 | |
| 34 | 35 | /** |
| 35 | 36 | * @typedef {object} PersonaConnection A connection between a character and a character or group entity |
| @@ -67,7 +68,7 @@ export function isPersonaPanelOpen() { | ||
| 67 | 68 | } |
| 68 | 69 | |
| 69 | 70 | function switchPersonaGridView() { |
| 70 | 71 | const state = localStorageaccountStorage.getItem(GRID_STORAGE_KEY) === 'true'; |
| 71 | 72 | $('#user_avatar_block').toggleClass('gridView', state); |
| 72 | 73 | } |
| 73 | 74 | |
| @@ -218,7 +219,7 @@ export async function getUserAvatars(doRender = true, openPageAt = '') { | ||
| 218 | 219 | |
| 219 | 220 | const storageKey = 'Personas_PerPage'; |
| 220 | 221 | const listId = '#user_avatar_block'; |
| 221 | 222 | const perPage = Number(localStorageaccountStorage.getItem(storageKey)) || 5; |
| 222 | 223 | |
| 223 | 224 | $('#persona_pagination_container').pagination({ |
| 224 | 225 | dataSource: entities, |
| @@ -241,7 +242,7 @@ export async function getUserAvatars(doRender = true, openPageAt = '') { | ||
| 241 | 242 | updatePersonaUIStates(); |
| 242 | 243 | }, |
| 243 | 244 | afterSizeSelectorChange: function (e) { |
| 244 | 245 | localStorageaccountStorage.setItem(storageKey, e.target.value); |
| 245 | 246 | }, |
| 246 | 247 | afterPaging: function (e) { |
| 247 | 248 | savePersonasPage = e; |
| @@ -1631,8 +1632,8 @@ export function initPersonas() { | ||
| 1631 | 1632 | saveSettingsDebounced(); |
| 1632 | 1633 | }); |
| 1633 | 1634 | $('#persona_grid_toggle').on('click', () => { |
| 1634 | 1635 | const state = localStorageaccountStorage.getItem(GRID_STORAGE_KEY) === 'true'; |
| 1635 | 1636 | localStorageaccountStorage.setItem(GRID_STORAGE_KEY, String(!state)); |
| 1636 | 1637 | switchPersonaGridView(); |
| 1637 | 1638 | }); |
| 1638 | 1639 | |
| @@ -24,6 +24,15 @@ export const POPUP_RESULT = { | ||
| 24 | 24 | AFFIRMATIVE: 1, |
| 25 | 25 | NEGATIVE: 0, |
| 26 | 26 | CANCELLED: null, |
| 27 | + CUSTOM1: 1001, | |
| 28 | + CUSTOM2: 1002, | |
| 29 | + CUSTOM3: 1003, | |
| 30 | + CUSTOM4: 1004, | |
| 31 | + CUSTOM5: 1005, | |
| 32 | + CUSTOM6: 1006, | |
| 33 | + CUSTOM7: 1007, | |
| 34 | + CUSTOM8: 1008, | |
| 35 | + CUSTOM9: 1009, | |
| 27 | 36 | }; |
| 28 | 37 | |
| 29 | 38 | /** |
| @@ -37,6 +46,7 @@ export const POPUP_RESULT = { | ||
| 37 | 46 | * @property {boolean?} [transparent=false] - Whether to display the popup in transparent mode (no background, border, shadow or anything, only its content) |
| 38 | 47 | * @property {boolean?} [allowHorizontalScrolling=false] - Whether to allow horizontal scrolling in the popup |
| 39 | 48 | * @property {boolean?} [allowVerticalScrolling=false] - Whether to allow vertical scrolling in the popup |
| 49 | + * @property {boolean?} [leftAlign=false] - Whether the popup content should be left-aligned by default | |
| 40 | 50 | * @property {'slow'|'fast'|'none'?} [animation='slow'] - Animation speed for the popup (opening, closing, ...) |
| 41 | 51 | * @property {POPUP_RESULT|number?} [defaultResult=POPUP_RESULT.AFFIRMATIVE] - The default result of this popup when Enter is pressed. Can be changed from `POPUP_RESULT.AFFIRMATIVE`. |
| 42 | 52 | * @property {CustomPopupButton[]|string[]?} [customButtons=null] - Custom buttons to add to the popup. If only strings are provided, the buttons will be added with default options, and their result will be in order from `2` onward. |
| @@ -164,7 +174,7 @@ export class Popup { | ||
| 164 | 174 | * @param {string} [inputValue=''] - The initial value of the input field |
| 165 | 175 | * @param {PopupOptions} [options={}] - Additional options for the popup |
| 166 | 176 | */ |
| 167 | 177 | constructor(content, type, inputValue = '', { okButton = null, cancelButton = null, rows = 1, wide = false, wider = false, large = false, transparent = false, allowHorizontalScrolling = false, allowVerticalScrolling = false, leftAlign = false, animation = 'fast', defaultResult = POPUP_RESULT.AFFIRMATIVE, customButtons = null, customInputs = null, onClosing = null, onClose = null, cropAspect = null, cropImage = null } = {}) { |
| 168 | 178 | Popup.util.popups.push(this); |
| 169 | 179 | |
| 170 | 180 | // Make this popup uniquely identifiable |
| @@ -209,6 +219,7 @@ export class Popup { | ||
| 209 | 219 | if (transparent) this.dlg.classList.add('transparent_dialogue_popup'); |
| 210 | 220 | if (allowHorizontalScrolling) this.dlg.classList.add('horizontal_scrolling_dialogue_popup'); |
| 211 | 221 | if (allowVerticalScrolling) this.dlg.classList.add('vertical_scrolling_dialogue_popup'); |
| 222 | + if (leftAlign) this.dlg.classList.add('left_aligned_dialogue_popup'); | |
| 212 | 223 | if (animation) this.dlg.classList.add('popup--animation-' + animation); |
| 213 | 224 | |
| 214 | 225 | // If custom button captions are provided, we set them beforehand |
| @@ -54,6 +54,7 @@ import { commonEnumProviders, enumIcons } from './slash-commands/SlashCommandCom | ||
| 54 | 54 | import { POPUP_TYPE, callGenericPopup } from './popup.js'; |
| 55 | 55 | import { loadSystemPrompts } from './sysprompt.js'; |
| 56 | 56 | import { fuzzySearchCategories } from './filters.js'; |
| 57 | +import { accountStorage } from './util/AccountStorage.js'; | |
| 57 | 58 | |
| 58 | 59 | export { |
| 59 | 60 | loadPowerUserSettings, |
| @@ -254,7 +255,10 @@ let power_user = { | ||
| 254 | 255 | }, |
| 255 | 256 | |
| 256 | 257 | reasoning: { |
| 258 | + auto_parse: false, | |
| 257 | 259 | add_to_prompts: false, |
| 260 | + auto_expand: false, | |
| 261 | + show_hidden: false, | |
| 258 | 262 | prefix: '<think>\n', |
| 259 | 263 | suffix: '\n</think>', |
| 260 | 264 | separator: '\n\n', |
| @@ -1843,14 +1847,15 @@ async function loadContextSettings() { | ||
| 1843 | 1847 | |
| 1844 | 1848 | /** |
| 1845 | 1849 | * Common function to perform fuzzy search with optional caching |
| 1850 | + * @template T | |
| 1846 | 1851 | * @param {string} type - Type of search from fuzzySearchCategories |
| 1847 | 1852 | * @param {anyT[]} data - Data array to search in |
| 1848 | 1853 | * @param {Array<{name: string, weight: number, getFn?: (obj: anyT) => string}>} keys - Fuse.js keys configuration |
| 1849 | 1854 | * @param {string} searchValue - The search term |
| 1850 | 1855 | * @param {Object.<string, { resultMap: Map<string, any> }>} [fuzzySearchCaches=null] - Optional fuzzy search caches |
| 1851 | 1856 | * @returns {import('fuse.js').FuseResult<anyT>[]} Results as items with their score |
| 1852 | 1857 | */ |
| 1853 | 1858 | export function performFuzzySearch(type, data, keys, searchValue, fuzzySearchCaches = null) { |
| 1854 | 1859 | // Check cache if provided |
| 1855 | 1860 | if (fuzzySearchCaches) { |
| 1856 | 1861 | const cache = fuzzySearchCaches[type]; |
| @@ -2019,7 +2024,7 @@ export function renderStoryString(params) { | ||
| 2019 | 2024 | */ |
| 2020 | 2025 | function validateStoryString(storyString, params) { |
| 2021 | 2026 | /** @type {{hashCache: {[hash: string]: {fieldsWarned: {[key: string]: boolean}}}}} */ |
| 2022 | 2027 | const cache = JSON.parse(localStorageaccountStorage.getItem(storage_keys.storyStringValidationCache)) ?? { hashCache: {} }; |
| 2023 | 2028 | |
| 2024 | 2029 | const hash = getStringHash(storyString); |
| 2025 | 2030 | |
| @@ -2056,7 +2061,7 @@ function validateStoryString(storyString, params) { | ||
| 2056 | 2061 | toastr.warning(`The story string does not contain the following fields, but they would contain content: ${fieldsList}`, 'Story String Validation'); |
| 2057 | 2062 | } |
| 2058 | 2063 | |
| 2059 | 2064 | localStorageaccountStorage.setItem(storage_keys.storyStringValidationCache, JSON.stringify(cache)); |
| 2060 | 2065 | } |
| 2061 | 2066 | |
| 2062 | 2067 | |
| @@ -2451,7 +2456,7 @@ async function resetMovablePanels(type) { | ||
| 2451 | 2456 | } |
| 2452 | 2457 | |
| 2453 | 2458 | saveSettingsDebounced(); |
| 2454 | 2459 | await eventSource.emit(event_types.MOVABLE_PANELS_RESET); |
| 2455 | 2460 | |
| 2456 | 2461 | eventSource.once(event_types.SETTINGS_UPDATED, () => { |
| 2457 | 2462 | $('.resizing').removeClass('resizing'); |
| @@ -2919,6 +2924,46 @@ export function flushEphemeralStoppingStrings() { | ||
| 2919 | 2924 | } |
| 2920 | 2925 | |
| 2921 | 2926 | /** |
| 2927 | + * Checks if the generated text should be filtered based on the auto-swipe settings. | |
| 2928 | + * @param {string} text The text to check | |
| 2929 | + * @returns {boolean} If the generated text should be filtered | |
| 2930 | + */ | |
| 2931 | +export function generatedTextFiltered(text) { | |
| 2932 | + /** | |
| 2933 | + * Checks if the given text contains any of the blacklisted words. | |
| 2934 | + * @param {string} text The text to check | |
| 2935 | + * @param {string[]} blacklist The list of blacklisted words | |
| 2936 | + * @param {number} threshold The number of blacklisted words that need to be present to trigger the check | |
| 2937 | + * @returns {boolean} Whether the text contains blacklisted words | |
| 2938 | + */ | |
| 2939 | + function containsBlacklistedWords(text, blacklist, threshold) { | |
| 2940 | + const regex = new RegExp(`\\b(${blacklist.join('|')})\\b`, 'gi'); | |
| 2941 | + const matches = text.match(regex) || []; | |
| 2942 | + return matches.length >= threshold; | |
| 2943 | + } | |
| 2944 | + | |
| 2945 | + // Make sure a generated text is non-empty | |
| 2946 | + // Otherwise we might get in a loop with a broken API | |
| 2947 | + text = text.trim(); | |
| 2948 | + if (text.length > 0) { | |
| 2949 | + if (power_user.auto_swipe_minimum_length) { | |
| 2950 | + if (text.length < power_user.auto_swipe_minimum_length) { | |
| 2951 | + console.log('Generated text size too small'); | |
| 2952 | + return true; | |
| 2953 | + } | |
| 2954 | + } | |
| 2955 | + if (power_user.auto_swipe_blacklist.length && power_user.auto_swipe_blacklist_threshold) { | |
| 2956 | + if (containsBlacklistedWords(text, power_user.auto_swipe_blacklist, power_user.auto_swipe_blacklist_threshold)) { | |
| 2957 | + console.log('Generated text has blacklisted words'); | |
| 2958 | + return true; | |
| 2959 | + } | |
| 2960 | + } | |
| 2961 | + } | |
| 2962 | + | |
| 2963 | + return false; | |
| 2964 | +} | |
| 2965 | + | |
| 2966 | +/** | |
| 2922 | 2967 | * Gets the custom stopping strings from the power user settings. |
| 2923 | 2968 | * @param {number | undefined} limit Number of strings to return. If 0 or undefined, returns all strings. |
| 2924 | 2969 | * @returns {string[]} An array of custom stopping strings |
| @@ -3899,9 +3944,9 @@ $(document).ready(() => { | ||
| 3899 | 3944 | helpString: 'Start a new chat with a random character. If an argument is provided, only considers characters that have the specified tag.', |
| 3900 | 3945 | })); |
| 3901 | 3946 | SlashCommandParser.addCommandObject(SlashCommand.fromProps({ |
| 3902 | 3947 | name: 'delmodedel', |
| 3903 | 3948 | callback: doDelMode, |
| 3904 | 3949 | aliases: ['deldelete', 'delmode'], |
| 3905 | 3950 | unnamedArgumentList: [ |
| 3906 | 3951 | new SlashCommandArgument( |
| 3907 | 3952 | 'optional number', [ARGUMENT_TYPE.NUMBER], false, |
| @@ -4084,4 +4129,45 @@ $(document).ready(() => { | ||
| 4084 | 4129 | ], |
| 4085 | 4130 | helpString: 'activates a movingUI preset by name', |
| 4086 | 4131 | })); |
| 4132 | + SlashCommandParser.addCommandObject(SlashCommand.fromProps({ | |
| 4133 | + name: 'stop-strings', | |
| 4134 | + aliases: ['stopping-strings', 'custom-stopping-strings', 'custom-stop-strings'], | |
| 4135 | + helpString: ` | |
| 4136 | + <div> | |
| 4137 | + Sets a list of custom stopping strings. Gets the list if no value is provided. | |
| 4138 | + </div> | |
| 4139 | + <div> | |
| 4140 | + <strong>Examples:</strong> | |
| 4141 | + </div> | |
| 4142 | + <ul> | |
| 4143 | + <li>Value must be a JSON-serialized array: <pre><code class="language-stscript">/stop-strings ["goodbye", "farewell"]</code></pre></li> | |
| 4144 | + <li>Pipe characters must be escaped with a backslash: <pre><code class="language-stscript">/stop-strings ["left\\|right"]</code></pre></li> | |
| 4145 | + </ul> | |
| 4146 | + `, | |
| 4147 | + returns: ARGUMENT_TYPE.LIST, | |
| 4148 | + unnamedArgumentList: [ | |
| 4149 | + SlashCommandArgument.fromProps({ | |
| 4150 | + description: 'list of strings', | |
| 4151 | + typeList: [ARGUMENT_TYPE.LIST], | |
| 4152 | + acceptsMultiple: false, | |
| 4153 | + isRequired: false, | |
| 4154 | + }), | |
| 4155 | + ], | |
| 4156 | + callback: (_, value) => { | |
| 4157 | + if (String(value ?? '').trim()) { | |
| 4158 | + const parsedValue = ((x) => { try { return JSON.parse(x.toString()); } catch { return null; } })(value); | |
| 4159 | + if (!parsedValue || !Array.isArray(parsedValue)) { | |
| 4160 | + throw new Error('Invalid list format. The value must be a JSON-serialized array of strings.'); | |
| 4161 | + } | |
| 4162 | + parsedValue.forEach((item, index) => { | |
| 4163 | + parsedValue[index] = String(item); | |
| 4164 | + }); | |
| 4165 | + power_user.custom_stopping_strings = JSON.stringify(parsedValue); | |
| 4166 | + $('#custom_stopping_strings').val(power_user.custom_stopping_strings); | |
| 4167 | + saveSettingsDebounced(); | |
| 4168 | + } | |
| 4169 | + | |
| 4170 | + return power_user.custom_stopping_strings; | |
| 4171 | + }, | |
| 4172 | + })); | |
| 4087 | 4173 | }); |
| @@ -586,6 +586,9 @@ class PresetManager { | ||
| 586 | 586 | 'tabby_model', |
| 587 | 587 | 'derived', |
| 588 | 588 | 'generic_model', |
| 589 | + 'include_reasoning', | |
| 590 | + 'global_banned_tokens', | |
| 591 | + 'send_banned_tokens', | |
| 589 | 592 | ]; |
| 590 | 593 | const settings = Object.assign({}, getSettingsByApiId(this.apiId)); |
| 591 | 594 | |
| @@ -1,13 +1,32 @@ | ||
| 1 | -import { chat, closeMessageEditor, saveChatConditional, saveSettingsDebounced, substituteParams, updateMessageBlock } from '../script.js'; | |
| 1 | +import { | |
| 2 | -import { t } from './i18n.js'; | |
| 2 | + moment, | |
| 3 | +} from '../lib.js'; | |
| 4 | +import { chat, closeMessageEditor, event_types, eventSource, main_api, messageFormatting, saveChatConditional, saveSettingsDebounced, substituteParams, updateMessageBlock } from '../script.js'; | |
| 5 | +import { getRegexedString, regex_placement } from './extensions/regex/engine.js'; | |
| 6 | +import { getCurrentLocale, t, translate } from './i18n.js'; | |
| 3 | 7 | import { MacrosParser } from './macros.js'; |
| 8 | +import { chat_completion_sources, getChatCompletionModel, oai_settings } from './openai.js'; | |
| 4 | 9 | import { Popup } from './popup.js'; |
| 5 | 10 | import { power_user } from './power-user.js'; |
| 6 | 11 | import { SlashCommand } from './slash-commands/SlashCommand.js'; |
| 7 | 12 | import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from './slash-commands/SlashCommandArgument.js'; |
| 8 | 13 | import { commonEnumProviders, enumIcons } from './slash-commands/SlashCommandCommonEnumsProvider.js'; |
| 14 | +import { enumTypes, SlashCommandEnumValue } from './slash-commands/SlashCommandEnumValue.js'; | |
| 9 | 15 | import { SlashCommandParser } from './slash-commands/SlashCommandParser.js'; |
| 10 | 16 | import { copyTexttextgen_types, textgenerationwebui_settings } from './utilstextgen-settings.js'; |
| 17 | +import { copyText, escapeRegex, isFalseBoolean, setDatasetProperty, trimSpaces } from './utils.js'; | |
| 18 | + | |
| 19 | +/** | |
| 20 | + * Enum representing the type of the reasoning for a message (where it came from) | |
| 21 | + * @enum {string} | |
| 22 | + * @readonly | |
| 23 | + */ | |
| 24 | +export const ReasoningType = { | |
| 25 | + Model: 'model', | |
| 26 | + Parsed: 'parsed', | |
| 27 | + Manual: 'manual', | |
| 28 | + Edited: 'edited', | |
| 29 | +}; | |
| 11 | 30 | |
| 12 | 31 | /** |
| 13 | 32 | * Gets a message from a jQuery element. |
| @@ -22,12 +41,473 @@ function getMessageFromJquery(element) { | ||
| 22 | 41 | } |
| 23 | 42 | |
| 24 | 43 | /** |
| 44 | + * Toggles the auto-expand state of reasoning blocks. | |
| 45 | + */ | |
| 46 | +function toggleReasoningAutoExpand() { | |
| 47 | + const reasoningBlocks = document.querySelectorAll('details.mes_reasoning_details'); | |
| 48 | + reasoningBlocks.forEach((block) => { | |
| 49 | + if (block instanceof HTMLDetailsElement) { | |
| 50 | + block.open = power_user.reasoning.auto_expand; | |
| 51 | + } | |
| 52 | + }); | |
| 53 | +} | |
| 54 | + | |
| 55 | +/** | |
| 56 | + * Extracts the reasoning from the response data. | |
| 57 | + * @param {object} data Response data | |
| 58 | + * @returns {string} Extracted reasoning | |
| 59 | + */ | |
| 60 | +export function extractReasoningFromData(data) { | |
| 61 | + switch (main_api) { | |
| 62 | + case 'textgenerationwebui': | |
| 63 | + switch (textgenerationwebui_settings.type) { | |
| 64 | + case textgen_types.OPENROUTER: | |
| 65 | + return data?.choices?.[0]?.reasoning ?? ''; | |
| 66 | + } | |
| 67 | + break; | |
| 68 | + | |
| 69 | + case 'openai': | |
| 70 | + if (!oai_settings.show_thoughts) break; | |
| 71 | + | |
| 72 | + switch (oai_settings.chat_completion_source) { | |
| 73 | + case chat_completion_sources.DEEPSEEK: | |
| 74 | + return data?.choices?.[0]?.message?.reasoning_content ?? ''; | |
| 75 | + case chat_completion_sources.OPENROUTER: | |
| 76 | + return data?.choices?.[0]?.message?.reasoning ?? ''; | |
| 77 | + case chat_completion_sources.MAKERSUITE: | |
| 78 | + return data?.responseContent?.parts?.filter(part => part.thought)?.map(part => part.text)?.join('\n\n') ?? ''; | |
| 79 | + case chat_completion_sources.CUSTOM: { | |
| 80 | + return data?.choices?.[0]?.message?.reasoning_content | |
| 81 | + ?? data?.choices?.[0]?.message?.reasoning | |
| 82 | + ?? ''; | |
| 83 | + } | |
| 84 | + } | |
| 85 | + break; | |
| 86 | + } | |
| 87 | + | |
| 88 | + return ''; | |
| 89 | +} | |
| 90 | + | |
| 91 | +/** | |
| 92 | + * Check if the model supports reasoning, but does not send back the reasoning | |
| 93 | + * @returns {boolean} True if the model supports reasoning | |
| 94 | + */ | |
| 95 | +export function isHiddenReasoningModel() { | |
| 96 | + if (main_api !== 'openai') { | |
| 97 | + return false; | |
| 98 | + } | |
| 99 | + | |
| 100 | + /** @typedef {{ (currentModel: string, supportedModel: string): boolean }} MatchingFunc */ | |
| 101 | + /** @type {Record.<string, MatchingFunc>} */ | |
| 102 | + const FUNCS = { | |
| 103 | + equals: (currentModel, supportedModel) => currentModel === supportedModel, | |
| 104 | + startsWith: (currentModel, supportedModel) => currentModel.startsWith(supportedModel), | |
| 105 | + }; | |
| 106 | + | |
| 107 | + /** @type {{ name: string; func: MatchingFunc; }[]} */ | |
| 108 | + const hiddenReasoningModels = [ | |
| 109 | + { name: 'o1', func: FUNCS.startsWith }, | |
| 110 | + { name: 'o3', func: FUNCS.startsWith }, | |
| 111 | + { name: 'gemini-2.0-flash-thinking-exp', func: FUNCS.startsWith }, | |
| 112 | + { name: 'gemini-2.0-pro-exp', func: FUNCS.startsWith }, | |
| 113 | + ]; | |
| 114 | + | |
| 115 | + const model = getChatCompletionModel() || ''; | |
| 116 | + | |
| 117 | + const isHidden = hiddenReasoningModels.some(({ name, func }) => func(model, name)); | |
| 118 | + return isHidden; | |
| 119 | +} | |
| 120 | + | |
| 121 | +/** | |
| 122 | + * Updates the Reasoning UI for a specific message | |
| 123 | + * @param {number|JQuery<HTMLElement>|HTMLElement} messageIdOrElement The message ID or the message element | |
| 124 | + * @param {Object} [options={}] - Optional arguments | |
| 125 | + * @param {boolean} [options.reset=false] - Whether to reset state, and not take the current mess properties (for example when swiping) | |
| 126 | + */ | |
| 127 | +export function updateReasoningUI(messageIdOrElement, { reset = false } = {}) { | |
| 128 | + const handler = new ReasoningHandler(); | |
| 129 | + handler.initHandleMessage(messageIdOrElement, { reset }); | |
| 130 | +} | |
| 131 | + | |
| 132 | + | |
| 133 | +/** | |
| 134 | + * Enum for representing the state of reasoning | |
| 135 | + * @enum {string} | |
| 136 | + * @readonly | |
| 137 | + */ | |
| 138 | +export const ReasoningState = { | |
| 139 | + None: 'none', | |
| 140 | + Thinking: 'thinking', | |
| 141 | + Done: 'done', | |
| 142 | + Hidden: 'hidden', | |
| 143 | +}; | |
| 144 | + | |
| 145 | +/** | |
| 146 | + * Handles reasoning-specific logic and DOM updates for messages. | |
| 147 | + * This class is used inside the {@link StreamingProcessor} to manage reasoning states and UI updates. | |
| 148 | + */ | |
| 149 | +export class ReasoningHandler { | |
| 150 | + /** @type {boolean} True if the model supports reasoning, but hides the reasoning output */ | |
| 151 | + #isHiddenReasoningModel; | |
| 152 | + /** @type {boolean} True if the handler is currently handling a manual parse of reasoning blocks */ | |
| 153 | + #isParsingReasoning = false; | |
| 154 | + /** @type {number?} When reasoning is being parsed manually, and the reasoning has ended, this will be the index at which the actual messages starts */ | |
| 155 | + #parsingReasoningMesStartIndex = null; | |
| 156 | + | |
| 157 | + /** | |
| 158 | + * @param {Date?} [timeStarted=null] - When the generation started | |
| 159 | + */ | |
| 160 | + constructor(timeStarted = null) { | |
| 161 | + /** @type {ReasoningState} The current state of the reasoning process */ | |
| 162 | + this.state = ReasoningState.None; | |
| 163 | + /** @type {ReasoningType?} The type of the reasoning (where it came from) */ | |
| 164 | + this.type = null; | |
| 165 | + /** @type {string} The reasoning output */ | |
| 166 | + this.reasoning = ''; | |
| 167 | + /** @type {Date} When the reasoning started */ | |
| 168 | + this.startTime = null; | |
| 169 | + /** @type {Date} When the reasoning ended */ | |
| 170 | + this.endTime = null; | |
| 171 | + | |
| 172 | + /** @type {Date} Initial starting time of the generation */ | |
| 173 | + this.initialTime = timeStarted ?? new Date(); | |
| 174 | + | |
| 175 | + this.#isHiddenReasoningModel = isHiddenReasoningModel(); | |
| 176 | + | |
| 177 | + // Cached DOM elements for reasoning | |
| 178 | + /** @type {HTMLElement} Main message DOM element `.mes` */ | |
| 179 | + this.messageDom = null; | |
| 180 | + /** @type {HTMLDetailsElement} Reasoning details DOM element `.mes_reasoning_details` */ | |
| 181 | + this.messageReasoningDetailsDom = null; | |
| 182 | + /** @type {HTMLElement} Reasoning content DOM element `.mes_reasoning` */ | |
| 183 | + this.messageReasoningContentDom = null; | |
| 184 | + /** @type {HTMLElement} Reasoning header DOM element `.mes_reasoning_header_title` */ | |
| 185 | + this.messageReasoningHeaderDom = null; | |
| 186 | + } | |
| 187 | + | |
| 188 | + /** | |
| 189 | + * Initializes the reasoning handler for a specific message. | |
| 190 | + * | |
| 191 | + * Can be used to update the DOM elements or read other reasoning states. | |
| 192 | + * It will internally take the message-saved data and write the states back into the handler, as if during streaming of the message. | |
| 193 | + * The state will always be either done/hidden or none. | |
| 194 | + * | |
| 195 | + * @param {number|JQuery<HTMLElement>|HTMLElement} messageIdOrElement - The message ID or the message element | |
| 196 | + * @param {Object} [options={}] - Optional arguments | |
| 197 | + * @param {boolean} [options.reset=false] - Whether to reset state of the handler, and not take the current mess properties (for example when swiping) | |
| 198 | + */ | |
| 199 | + initHandleMessage(messageIdOrElement, { reset = false } = {}) { | |
| 200 | + /** @type {HTMLElement} */ | |
| 201 | + const messageElement = typeof messageIdOrElement === 'number' | |
| 202 | + ? document.querySelector(`#chat [mesid="${messageIdOrElement}"]`) | |
| 203 | + : messageIdOrElement instanceof HTMLElement | |
| 204 | + ? messageIdOrElement | |
| 205 | + : $(messageIdOrElement)[0]; | |
| 206 | + const messageId = Number(messageElement.getAttribute('mesid')); | |
| 207 | + | |
| 208 | + if (isNaN(messageId) || !chat[messageId]) return; | |
| 209 | + | |
| 210 | + if (!chat[messageId].extra) { | |
| 211 | + chat[messageId].extra = {}; | |
| 212 | + } | |
| 213 | + const extra = chat[messageId].extra; | |
| 214 | + | |
| 215 | + if (extra.reasoning) { | |
| 216 | + this.state = ReasoningState.Done; | |
| 217 | + } else if (extra.reasoning_duration) { | |
| 218 | + this.state = ReasoningState.Hidden; | |
| 219 | + } | |
| 220 | + | |
| 221 | + this.type = extra?.reasoning_type; | |
| 222 | + this.reasoning = extra?.reasoning ?? ''; | |
| 223 | + | |
| 224 | + if (this.state !== ReasoningState.None) { | |
| 225 | + this.initialTime = new Date(chat[messageId].gen_started); | |
| 226 | + this.startTime = this.initialTime; | |
| 227 | + this.endTime = new Date(this.startTime.getTime() + (extra?.reasoning_duration ?? 0)); | |
| 228 | + } | |
| 229 | + | |
| 230 | + // Prefill main dom element, as message might not have been rendered yet | |
| 231 | + this.messageDom = messageElement; | |
| 232 | + | |
| 233 | + // Make sure reset correctly clears all relevant states | |
| 234 | + if (reset) { | |
| 235 | + this.state = this.#isHiddenReasoningModel ? ReasoningState.Thinking : ReasoningState.None; | |
| 236 | + this.type = null; | |
| 237 | + this.reasoning = ''; | |
| 238 | + this.initialTime = new Date(); | |
| 239 | + this.startTime = null; | |
| 240 | + this.endTime = null; | |
| 241 | + } | |
| 242 | + | |
| 243 | + this.updateDom(messageId); | |
| 244 | + | |
| 245 | + if (power_user.reasoning.auto_expand && this.state !== ReasoningState.Hidden) { | |
| 246 | + this.messageReasoningDetailsDom.open = true; | |
| 247 | + } | |
| 248 | + } | |
| 249 | + | |
| 250 | + /** | |
| 251 | + * Gets the duration of the reasoning in milliseconds. | |
| 252 | + * | |
| 253 | + * @returns {number?} The duration in milliseconds, or null if the start or end time is not set | |
| 254 | + */ | |
| 255 | + getDuration() { | |
| 256 | + if (this.startTime && this.endTime) { | |
| 257 | + return this.endTime.getTime() - this.startTime.getTime(); | |
| 258 | + } | |
| 259 | + return null; | |
| 260 | + } | |
| 261 | + | |
| 262 | + /** | |
| 263 | + * Updates the reasoning text/string for a message. | |
| 264 | + * | |
| 265 | + * @param {number} messageId - The ID of the message to update | |
| 266 | + * @param {string?} [reasoning=null] - The reasoning text to update - If null or empty, uses the current reasoning | |
| 267 | + * @param {Object} [options={}] - Optional arguments | |
| 268 | + * @param {boolean} [options.persist=false] - Whether to persist the reasoning to the message object | |
| 269 | + * @param {boolean} [options.allowReset=false] - Whether to allow empty reasoning provided to reset the reasoning, instead of just taking the existing one | |
| 270 | + * @returns {boolean} - Returns true if the reasoning was changed, otherwise false | |
| 271 | + */ | |
| 272 | + updateReasoning(messageId, reasoning = null, { persist = false, allowReset = false } = {}) { | |
| 273 | + if (messageId == -1 || !chat[messageId]) { | |
| 274 | + return false; | |
| 275 | + } | |
| 276 | + | |
| 277 | + reasoning = allowReset ? reasoning ?? this.reasoning : reasoning || this.reasoning; | |
| 278 | + reasoning = trimSpaces(reasoning); | |
| 279 | + | |
| 280 | + // Ensure the chat extra exists | |
| 281 | + if (!chat[messageId].extra) { | |
| 282 | + chat[messageId].extra = {}; | |
| 283 | + } | |
| 284 | + const extra = chat[messageId].extra; | |
| 285 | + | |
| 286 | + const reasoningChanged = extra.reasoning !== reasoning; | |
| 287 | + this.reasoning = getRegexedString(reasoning ?? '', regex_placement.REASONING); | |
| 288 | + | |
| 289 | + this.type = (this.#isParsingReasoning || this.#parsingReasoningMesStartIndex) ? ReasoningType.Parsed : ReasoningType.Model; | |
| 290 | + | |
| 291 | + if (persist) { | |
| 292 | + // Build and save the reasoning data to message extras | |
| 293 | + extra.reasoning = this.reasoning; | |
| 294 | + extra.reasoning_duration = this.getDuration(); | |
| 295 | + extra.reasoning_type = (this.#isParsingReasoning || this.#parsingReasoningMesStartIndex) ? ReasoningType.Parsed : ReasoningType.Model; | |
| 296 | + } | |
| 297 | + | |
| 298 | + return reasoningChanged; | |
| 299 | + } | |
| 300 | + | |
| 301 | + | |
| 302 | + /** | |
| 303 | + * Handles processing of reasoning for a message. | |
| 304 | + * | |
| 305 | + * This is usually called by the message processor when a message is changed. | |
| 306 | + * | |
| 307 | + * @param {number} messageId - The ID of the message to process | |
| 308 | + * @param {boolean} mesChanged - Whether the message has changed | |
| 309 | + * @returns {Promise<void>} | |
| 310 | + */ | |
| 311 | + async process(messageId, mesChanged) { | |
| 312 | + mesChanged = this.#autoParseReasoningFromMessage(messageId, mesChanged); | |
| 313 | + | |
| 314 | + if (!this.reasoning && !this.#isHiddenReasoningModel) | |
| 315 | + return; | |
| 316 | + | |
| 317 | + // Ensure reasoning string is updated and regexes are applied correctly | |
| 318 | + const reasoningChanged = this.updateReasoning(messageId, null, { persist: true }); | |
| 319 | + | |
| 320 | + if ((this.#isHiddenReasoningModel || reasoningChanged) && this.state === ReasoningState.None) { | |
| 321 | + this.state = ReasoningState.Thinking; | |
| 322 | + this.startTime = this.initialTime; | |
| 323 | + } | |
| 324 | + if ((this.#isHiddenReasoningModel || !reasoningChanged) && mesChanged && this.state === ReasoningState.Thinking) { | |
| 325 | + this.endTime = new Date(); | |
| 326 | + await this.finish(messageId); | |
| 327 | + } | |
| 328 | + } | |
| 329 | + | |
| 330 | + #autoParseReasoningFromMessage(messageId, mesChanged) { | |
| 331 | + if (!power_user.reasoning.auto_parse) | |
| 332 | + return; | |
| 333 | + if (!power_user.reasoning.prefix || !power_user.reasoning.suffix) | |
| 334 | + return mesChanged; | |
| 335 | + | |
| 336 | + /** @type {{ mes: string, [key: string]: any}} */ | |
| 337 | + const message = chat[messageId]; | |
| 338 | + if (!message) return mesChanged; | |
| 339 | + | |
| 340 | + // If we are done with reasoning parse, we just split the message correctly so the reasoning doesn't show up inside of it. | |
| 341 | + if (this.#parsingReasoningMesStartIndex) { | |
| 342 | + message.mes = trimSpaces(message.mes.slice(this.#parsingReasoningMesStartIndex)); | |
| 343 | + return mesChanged; | |
| 344 | + } | |
| 345 | + | |
| 346 | + if (this.state === ReasoningState.None || this.#isHiddenReasoningModel) { | |
| 347 | + // If streamed message starts with the opening, cut it out and put all inside reasoning | |
| 348 | + if (message.mes.startsWith(power_user.reasoning.prefix) && message.mes.length > power_user.reasoning.prefix.length) { | |
| 349 | + this.#isParsingReasoning = true; | |
| 350 | + | |
| 351 | + // Manually set starting state here, as we might already have received the ending suffix | |
| 352 | + this.state = ReasoningState.Thinking; | |
| 353 | + this.startTime = this.startTime ?? this.initialTime; | |
| 354 | + this.endTime = null; | |
| 355 | + } | |
| 356 | + } | |
| 357 | + | |
| 358 | + if (!this.#isParsingReasoning) | |
| 359 | + return mesChanged; | |
| 360 | + | |
| 361 | + // If we are in manual parsing mode, all currently streaming mes tokens will go the the reasoning block | |
| 362 | + const originalMes = message.mes; | |
| 363 | + this.reasoning = originalMes.slice(power_user.reasoning.prefix.length); | |
| 364 | + message.mes = ''; | |
| 365 | + | |
| 366 | + // If the reasoning contains the ending suffix, we cut that off and continue as message streaming | |
| 367 | + if (this.reasoning.includes(power_user.reasoning.suffix)) { | |
| 368 | + this.reasoning = this.reasoning.slice(0, this.reasoning.indexOf(power_user.reasoning.suffix)); | |
| 369 | + this.#parsingReasoningMesStartIndex = originalMes.indexOf(power_user.reasoning.suffix) + power_user.reasoning.suffix.length; | |
| 370 | + message.mes = trimSpaces(originalMes.slice(this.#parsingReasoningMesStartIndex)); | |
| 371 | + this.#isParsingReasoning = false; | |
| 372 | + } | |
| 373 | + | |
| 374 | + // Only return the original mesChanged value if we haven't cut off the complete message | |
| 375 | + return message.mes.length ? mesChanged : false; | |
| 376 | + } | |
| 377 | + | |
| 378 | + /** | |
| 379 | + * Completes the reasoning process for a message. | |
| 380 | + * | |
| 381 | + * Records the finish time if it was not set during streaming and updates the reasoning state. | |
| 382 | + * Emits an event to signal the completion of reasoning and updates the DOM elements accordingly. | |
| 383 | + * | |
| 384 | + * @param {number} messageId - The ID of the message to complete reasoning for | |
| 385 | + * @returns {Promise<void>} | |
| 386 | + */ | |
| 387 | + async finish(messageId) { | |
| 388 | + if (this.state === ReasoningState.None) return; | |
| 389 | + | |
| 390 | + // Make sure the finish time is recorded if a reasoning was in process and it wasn't ended correctly during streaming | |
| 391 | + if (this.startTime !== null && this.endTime === null) { | |
| 392 | + this.endTime = new Date(); | |
| 393 | + } | |
| 394 | + | |
| 395 | + if (this.state === ReasoningState.Thinking) { | |
| 396 | + this.state = this.#isHiddenReasoningModel ? ReasoningState.Hidden : ReasoningState.Done; | |
| 397 | + this.updateReasoning(messageId, null, { persist: true }); | |
| 398 | + await eventSource.emit(event_types.STREAM_REASONING_DONE, this.reasoning, this.getDuration(), messageId, this.state); | |
| 399 | + } | |
| 400 | + | |
| 401 | + this.updateDom(messageId); | |
| 402 | + } | |
| 403 | + | |
| 404 | + /** | |
| 405 | + * Updates the reasoning UI elements for a message. | |
| 406 | + * | |
| 407 | + * Toggles the CSS class, updates states, reasoning message, and duration. | |
| 408 | + * | |
| 409 | + * @param {number} messageId - The ID of the message to update | |
| 410 | + */ | |
| 411 | + updateDom(messageId) { | |
| 412 | + this.#checkDomElements(messageId); | |
| 413 | + | |
| 414 | + // Main CSS class to show this message includes reasoning | |
| 415 | + this.messageDom.classList.toggle('reasoning', this.state !== ReasoningState.None); | |
| 416 | + | |
| 417 | + // Update states to the relevant DOM elements | |
| 418 | + setDatasetProperty(this.messageDom, 'reasoningState', this.state !== ReasoningState.None ? this.state : null); | |
| 419 | + setDatasetProperty(this.messageReasoningDetailsDom, 'state', this.state); | |
| 420 | + setDatasetProperty(this.messageReasoningDetailsDom, 'type', this.type); | |
| 421 | + | |
| 422 | + // Update the reasoning message | |
| 423 | + const reasoning = trimSpaces(this.reasoning); | |
| 424 | + const displayReasoning = messageFormatting(reasoning, '', false, false, messageId, {}, true); | |
| 425 | + this.messageReasoningContentDom.innerHTML = displayReasoning; | |
| 426 | + | |
| 427 | + // Update tooltip for hidden reasoning edit | |
| 428 | + /** @type {HTMLElement} */ | |
| 429 | + const button = this.messageDom.querySelector('.mes_edit_add_reasoning'); | |
| 430 | + button.title = this.state === ReasoningState.Hidden ? t`Hidden reasoning - Add reasoning block` : t`Add reasoning block`; | |
| 431 | + | |
| 432 | + // Make sure that hidden reasoning headers are collapsed by default, to not show a useless edit button | |
| 433 | + if (this.state === ReasoningState.Hidden) { | |
| 434 | + this.messageReasoningDetailsDom.open = false; | |
| 435 | + } | |
| 436 | + | |
| 437 | + // Update the reasoning duration in the UI | |
| 438 | + this.#updateReasoningTimeUI(); | |
| 439 | + } | |
| 440 | + | |
| 441 | + /** | |
| 442 | + * Finds and caches reasoning-related DOM elements for the given message. | |
| 443 | + * | |
| 444 | + * @param {number} messageId - The ID of the message to cache the DOM elements for | |
| 445 | + */ | |
| 446 | + #checkDomElements(messageId) { | |
| 447 | + // Make sure we reset dom elements if we are checking for a different message (shouldn't happen, but be sure) | |
| 448 | + if (this.messageDom !== null && this.messageDom.getAttribute('mesid') !== messageId.toString()) { | |
| 449 | + this.messageDom = null; | |
| 450 | + } | |
| 451 | + | |
| 452 | + // Cache the DOM elements once | |
| 453 | + if (this.messageDom === null) { | |
| 454 | + this.messageDom = document.querySelector(`#chat .mes[mesid="${messageId}"]`); | |
| 455 | + if (this.messageDom === null) throw new Error('message dom does not exist'); | |
| 456 | + } | |
| 457 | + if (this.messageReasoningDetailsDom === null) { | |
| 458 | + this.messageReasoningDetailsDom = this.messageDom.querySelector('.mes_reasoning_details'); | |
| 459 | + } | |
| 460 | + if (this.messageReasoningContentDom === null) { | |
| 461 | + this.messageReasoningContentDom = this.messageDom.querySelector('.mes_reasoning'); | |
| 462 | + } | |
| 463 | + if (this.messageReasoningHeaderDom === null) { | |
| 464 | + this.messageReasoningHeaderDom = this.messageDom.querySelector('.mes_reasoning_header_title'); | |
| 465 | + } | |
| 466 | + } | |
| 467 | + | |
| 468 | + /** | |
| 469 | + * Updates the reasoning time display in the UI. | |
| 470 | + * | |
| 471 | + * Shows the duration in a human-readable format with a tooltip for exact seconds. | |
| 472 | + * Displays "Thinking..." if still processing, or a generic message otherwise. | |
| 473 | + */ | |
| 474 | + #updateReasoningTimeUI() { | |
| 475 | + const element = this.messageReasoningHeaderDom; | |
| 476 | + const duration = this.getDuration(); | |
| 477 | + let data = null; | |
| 478 | + let title = ''; | |
| 479 | + if (duration) { | |
| 480 | + const seconds = moment.duration(duration).asSeconds(); | |
| 481 | + | |
| 482 | + const durationStr = moment.duration(duration).locale(getCurrentLocale()).humanize({ s: 50, ss: 3 }); | |
| 483 | + element.textContent = t`Thought for ${durationStr}`; | |
| 484 | + data = String(seconds); | |
| 485 | + title = `${seconds} seconds`; | |
| 486 | + } else if ([ReasoningState.Done, ReasoningState.Hidden].includes(this.state)) { | |
| 487 | + element.textContent = t`Thought for some time`; | |
| 488 | + data = 'unknown'; | |
| 489 | + } else { | |
| 490 | + element.textContent = t`Thinking...`; | |
| 491 | + data = null; | |
| 492 | + } | |
| 493 | + | |
| 494 | + if (this.type !== ReasoningType.Model) { | |
| 495 | + title += ` [${translate(this.type)}]`; | |
| 496 | + title = title.trim(); | |
| 497 | + } | |
| 498 | + element.title = title; | |
| 499 | + | |
| 500 | + setDatasetProperty(this.messageReasoningDetailsDom, 'duration', data); | |
| 501 | + setDatasetProperty(element, 'duration', data); | |
| 502 | + } | |
| 503 | +} | |
| 504 | + | |
| 505 | +/** | |
| 25 | 506 | * Helper class for adding reasoning to messages. |
| 26 | 507 | * Keeps track of the number of reasoning additions. |
| 27 | 508 | */ |
| 28 | 509 | export class PromptReasoning { |
| 29 | 510 | static REASONING_PLACEHOLDER = '\u200B'; |
| 30 | - static REASONING_PLACEHOLDER_REGEX = new RegExp(`${PromptReasoning.REASONING_PLACEHOLDER}$`); | |
| 31 | 511 | |
| 32 | 512 | constructor() { |
| 33 | 513 | this.counter = 0; |
| @@ -49,15 +529,16 @@ export class PromptReasoning { | ||
| 49 | 529 | * Add reasoning to a message according to the power user settings. |
| 50 | 530 | * @param {string} content Message content |
| 51 | 531 | * @param {string} reasoning Message reasoning |
| 532 | + * @param {boolean} isPrefix Whether this is the last message prefix | |
| 52 | 533 | * @returns {string} Message content with reasoning |
| 53 | 534 | */ |
| 54 | 535 | addToMessage(content, reasoning, isPrefix) { |
| 55 | 536 | // Disabled or reached limit of additions |
| 56 | 537 | if (!isPrefix && (!power_user.reasoning.add_to_prompts || this.counter >= power_user.reasoning.max_additions)) { |
| 57 | 538 | return content; |
| 58 | 539 | } |
| 59 | 540 | |
| 60 | 541 | // No reasoning provided or a legacy placeholder |
| 61 | 542 | if (!reasoning || reasoning === PromptReasoning.REASONING_PLACEHOLDER) { |
| 62 | 543 | return content; |
| 63 | 544 | } |
| @@ -70,6 +551,11 @@ export class PromptReasoning { | ||
| 70 | 551 | const separator = substituteParams(power_user.reasoning.separator || ''); |
| 71 | 552 | const suffix = substituteParams(power_user.reasoning.suffix || ''); |
| 72 | 553 | |
| 554 | + // Combine parts with reasoning only | |
| 555 | + if (isPrefix && !content) { | |
| 556 | + return `${prefix}${reasoning}`; | |
| 557 | + } | |
| 558 | + | |
| 73 | 559 | // Combine parts with reasoning and content |
| 74 | 560 | return `${prefix}${reasoning}${suffix}${separator}${content}`; |
| 75 | 561 | } |
| @@ -105,11 +591,34 @@ function loadReasoningSettings() { | ||
| 105 | 591 | power_user.reasoning.max_additions = Number($(this).val()); |
| 106 | 592 | saveSettingsDebounced(); |
| 107 | 593 | }); |
| 594 | + | |
| 595 | + $('#reasoning_auto_parse').prop('checked', power_user.reasoning.auto_parse); | |
| 596 | + $('#reasoning_auto_parse').on('change', function () { | |
| 597 | + power_user.reasoning.auto_parse = !!$(this).prop('checked'); | |
| 598 | + saveSettingsDebounced(); | |
| 599 | + }); | |
| 600 | + | |
| 601 | + $('#reasoning_auto_expand').prop('checked', power_user.reasoning.auto_expand); | |
| 602 | + $('#reasoning_auto_expand').on('change', function () { | |
| 603 | + power_user.reasoning.auto_expand = !!$(this).prop('checked'); | |
| 604 | + toggleReasoningAutoExpand(); | |
| 605 | + saveSettingsDebounced(); | |
| 606 | + }); | |
| 607 | + toggleReasoningAutoExpand(); | |
| 608 | + | |
| 609 | + $('#reasoning_show_hidden').prop('checked', power_user.reasoning.show_hidden); | |
| 610 | + $('#reasoning_show_hidden').on('change', function () { | |
| 611 | + power_user.reasoning.show_hidden = !!$(this).prop('checked'); | |
| 612 | + $('#chat').attr('data-show-hidden-reasoning', power_user.reasoning.show_hidden ? 'true' : null); | |
| 613 | + saveSettingsDebounced(); | |
| 614 | + }); | |
| 615 | + $('#chat').attr('data-show-hidden-reasoning', power_user.reasoning.show_hidden ? 'true' : null); | |
| 108 | 616 | } |
| 109 | 617 | |
| 110 | 618 | function registerReasoningSlashCommands() { |
| 111 | 619 | SlashCommandParser.addCommandObject(SlashCommand.fromProps({ |
| 112 | 620 | name: 'reasoning-get', |
| 621 | + aliases: ['get-reasoning'], | |
| 113 | 622 | returns: ARGUMENT_TYPE.STRING, |
| 114 | 623 | helpString: t`Get the contents of a reasoning block of a message. Returns an empty string if the message does not have a reasoning block.`, |
| 115 | 624 | unnamedArgumentList: [ |
| @@ -120,15 +629,16 @@ function registerReasoningSlashCommands() { | ||
| 120 | 629 | }), |
| 121 | 630 | ], |
| 122 | 631 | callback: (_args, value) => { |
| 123 | 632 | const messageId = !isNaN(NumberparseInt(value.toString())) ? NumberparseInt(value.toString()) : chat.length - 1; |
| 124 | 633 | const message = chat[messageId]; |
| 125 | 634 | const reasoning = String(message?.extra?.reasoning ?? ''); |
| 126 | - return reasoning.replace(PromptReasoning.REASONING_PLACEHOLDER_REGEX, ''); | |
| 635 | + return reasoning; | |
| 127 | 636 | }, |
| 128 | 637 | })); |
| 129 | 638 | |
| 130 | 639 | SlashCommandParser.addCommandObject(SlashCommand.fromProps({ |
| 131 | 640 | name: 'reasoning-set', |
| 641 | + aliases: ['set-reasoning'], | |
| 132 | 642 | returns: ARGUMENT_TYPE.STRING, |
| 133 | 643 | helpString: t`Set the reasoning block of a message. Returns the reasoning block content.`, |
| 134 | 644 | namedArgumentList: [ |
| @@ -146,13 +656,18 @@ function registerReasoningSlashCommands() { | ||
| 146 | 656 | }), |
| 147 | 657 | ], |
| 148 | 658 | callback: async (args, value) => { |
| 149 | 659 | const messageId = !isNaN(Number(args[0].at)) ? Number(args[0].at) : chat.length - 1; |
| 150 | 660 | const message = chat[messageId]; |
| 151 | 661 | if (!message?.extra) { |
| 152 | 662 | return ''; |
| 153 | 663 | } |
| 664 | + // Make sure the message has an extra object | |
| 665 | + if (!message.extra || typeof message.extra !== 'object') { | |
| 666 | + message.extra = {}; | |
| 667 | + } | |
| 154 | 668 | |
| 155 | 669 | message.extra.reasoning = String(value ?? ''); |
| 670 | + message.extra.reasoning_type = ReasoningType.Manual; | |
| 156 | 671 | await saveChatConditional(); |
| 157 | 672 | |
| 158 | 673 | closeMessageEditor('reasoning'); |
| @@ -160,6 +675,77 @@ function registerReasoningSlashCommands() { | ||
| 160 | 675 | return message.extra.reasoning; |
| 161 | 676 | }, |
| 162 | 677 | })); |
| 678 | + | |
| 679 | + SlashCommandParser.addCommandObject(SlashCommand.fromProps({ | |
| 680 | + name: 'reasoning-parse', | |
| 681 | + aliases: ['parse-reasoning'], | |
| 682 | + returns: 'reasoning string', | |
| 683 | + helpString: t`Extracts the reasoning block from a string using the Reasoning Formatting settings.`, | |
| 684 | + namedArgumentList: [ | |
| 685 | + SlashCommandNamedArgument.fromProps({ | |
| 686 | + name: 'regex', | |
| 687 | + description: 'Whether to apply regex scripts to the reasoning content.', | |
| 688 | + typeList: [ARGUMENT_TYPE.BOOLEAN], | |
| 689 | + defaultValue: 'true', | |
| 690 | + isRequired: false, | |
| 691 | + enumList: commonEnumProviders.boolean('trueFalse')(), | |
| 692 | + }), | |
| 693 | + SlashCommandNamedArgument.fromProps({ | |
| 694 | + name: 'return', | |
| 695 | + description: 'Whether to return the parsed reasoning or the content without reasoning', | |
| 696 | + typeList: [ARGUMENT_TYPE.STRING], | |
| 697 | + defaultValue: 'reasoning', | |
| 698 | + isRequired: false, | |
| 699 | + enumList: [ | |
| 700 | + new SlashCommandEnumValue('reasoning', null, enumTypes.enum, enumIcons.reasoning), | |
| 701 | + new SlashCommandEnumValue('content', null, enumTypes.enum, enumIcons.message), | |
| 702 | + ], | |
| 703 | + }), | |
| 704 | + SlashCommandNamedArgument.fromProps({ | |
| 705 | + name: 'strict', | |
| 706 | + description: 'Whether to require the reasoning block to be at the beginning of the string (excluding whitespaces).', | |
| 707 | + typeList: [ARGUMENT_TYPE.BOOLEAN], | |
| 708 | + defaultValue: 'true', | |
| 709 | + isRequired: false, | |
| 710 | + enumList: commonEnumProviders.boolean('trueFalse')(), | |
| 711 | + }), | |
| 712 | + ], | |
| 713 | + unnamedArgumentList: [ | |
| 714 | + SlashCommandArgument.fromProps({ | |
| 715 | + description: 'input string', | |
| 716 | + typeList: [ARGUMENT_TYPE.STRING], | |
| 717 | + }), | |
| 718 | + ], | |
| 719 | + callback: (args, value) => { | |
| 720 | + if (!value || typeof value !== 'string') { | |
| 721 | + return ''; | |
| 722 | + } | |
| 723 | + | |
| 724 | + if (!power_user.reasoning.prefix || !power_user.reasoning.suffix) { | |
| 725 | + toastr.warning(t`Both prefix and suffix must be set in the Reasoning Formatting settings.`, t`Reasoning Parse`); | |
| 726 | + return value; | |
| 727 | + } | |
| 728 | + if (typeof args.return !== 'string' || !['reasoning', 'content'].includes(args.return)) { | |
| 729 | + toastr.warning(t`Invalid return type '${args.return}', defaulting to 'reasoning'.`, t`Reasoning Parse`); | |
| 730 | + } | |
| 731 | + | |
| 732 | + const returnMessage = args.return === 'content'; | |
| 733 | + | |
| 734 | + const parsedReasoning = parseReasoningFromString(value, { strict: !isFalseBoolean(String(args.strict ?? '')) }); | |
| 735 | + if (!parsedReasoning) { | |
| 736 | + return returnMessage ? value : ''; | |
| 737 | + } | |
| 738 | + | |
| 739 | + if (returnMessage) { | |
| 740 | + return parsedReasoning.content; | |
| 741 | + } | |
| 742 | + | |
| 743 | + const applyRegex = !isFalseBoolean(String(args.regex ?? '')); | |
| 744 | + return applyRegex | |
| 745 | + ? getRegexedString(parsedReasoning.reasoning, regex_placement.REASONING) | |
| 746 | + : parsedReasoning.reasoning; | |
| 747 | + }, | |
| 748 | + })); | |
| 163 | 749 | } |
| 164 | 750 | |
| 165 | 751 | function registerReasoningMacros() { |
| @@ -169,6 +755,31 @@ function registerReasoningMacros() { | ||
| 169 | 755 | } |
| 170 | 756 | |
| 171 | 757 | function setReasoningEventHandlers() { |
| 758 | + $(document).on('click', '.mes_reasoning_details', function (e) { | |
| 759 | + if (!e.target.closest('.mes_reasoning_actions') && !e.target.closest('.mes_reasoning_header')) { | |
| 760 | + e.preventDefault(); | |
| 761 | + } | |
| 762 | + }); | |
| 763 | + | |
| 764 | + $(document).on('click', '.mes_reasoning_header', function (e) { | |
| 765 | + const details = $(this).closest('.mes_reasoning_details'); | |
| 766 | + // Along with the CSS rules to mark blocks not toggle-able when they are empty, prevent them from actually being toggled, or being edited | |
| 767 | + if (details.find('.mes_reasoning').is(':empty')) { | |
| 768 | + e.preventDefault(); | |
| 769 | + return; | |
| 770 | + } | |
| 771 | + | |
| 772 | + // If we are in message edit mode and reasoning area is closed, a click opens and edits it | |
| 773 | + const mes = $(this).closest('.mes'); | |
| 774 | + const mesEditArea = mes.find('#curEditTextarea'); | |
| 775 | + if (mesEditArea.length) { | |
| 776 | + const summary = $(mes).find('.mes_reasoning_summary'); | |
| 777 | + if (!summary.attr('open')) { | |
| 778 | + summary.find('.mes_reasoning_edit').trigger('click'); | |
| 779 | + } | |
| 780 | + } | |
| 781 | + }); | |
| 782 | + | |
| 172 | 783 | $(document).on('click', '.mes_reasoning_copy', (e) => { |
| 173 | 784 | e.stopPropagation(); |
| 174 | 785 | e.preventDefault(); |
| @@ -187,7 +798,7 @@ function setReasoningEventHandlers(){ | ||
| 187 | 798 | const textarea = document.createElement('textarea'); |
| 188 | 799 | const reasoningBlock = messageBlock.find('.mes_reasoning'); |
| 189 | 800 | textarea.classList.add('reasoning_edit_textarea'); |
| 190 | 801 | textarea.value = reasoning.replace(PromptReasoning.REASONING_PLACEHOLDER_REGEX, ''); |
| 191 | 802 | $(textarea).insertBefore(reasoningBlock); |
| 192 | 803 | |
| 193 | 804 | if (!CSS.supports('field-sizing', 'content')) { |
| @@ -224,11 +835,14 @@ function setReasoningEventHandlers(){ | ||
| 224 | 835 | } |
| 225 | 836 | |
| 226 | 837 | const textarea = messageBlock.find('.reasoning_edit_textarea'); |
| 227 | 838 | const reasoning = getRegexedString(String(textarea.val()), regex_placement.REASONING, { isEdit: true }); |
| 228 | 839 | message.extra.reasoning = reasoning; |
| 840 | + message.extra.reasoning_type = message.extra.reasoning_type ? ReasoningType.Edited : ReasoningType.Manual; | |
| 229 | 841 | await saveChatConditional(); |
| 230 | 842 | updateMessageBlock(messageId, message); |
| 231 | 843 | textarea.remove(); |
| 844 | + | |
| 845 | + messageBlock.find('.mes_edit_done:visible').trigger('click'); | |
| 232 | 846 | }); |
| 233 | 847 | |
| 234 | 848 | $(document).on('click', '.mes_reasoning_edit_cancel', function (e) { |
| @@ -238,10 +852,14 @@ function setReasoningEventHandlers(){ | ||
| 238 | 852 | const { messageBlock } = getMessageFromJquery(this); |
| 239 | 853 | const textarea = messageBlock.find('.reasoning_edit_textarea'); |
| 240 | 854 | textarea.remove(); |
| 855 | + | |
| 856 | + messageBlock.find('.mes_reasoning_edit_cancel:visible').trigger('click'); | |
| 857 | + | |
| 858 | + updateReasoningUI(messageBlock); | |
| 241 | 859 | }); |
| 242 | 860 | |
| 243 | 861 | $(document).on('click', '.mes_edit_add_reasoning', async function () { |
| 244 | 862 | const { message, messageIdmessageBlock } = getMessageFromJquery(this); |
| 245 | 863 | if (!message?.extra) { |
| 246 | 864 | return; |
| 247 | 865 | } |
| @@ -251,34 +869,46 @@ function setReasoningEventHandlers(){ | ||
| 251 | 869 | return; |
| 252 | 870 | } |
| 253 | 871 | |
| 254 | - message.extra.reasoning = PromptReasoning.REASONING_PLACEHOLDER; | |
| 872 | + messageBlock.addClass('reasoning'); | |
| 873 | + | |
| 874 | + // To make hidden reasoning blocks editable, we just set them to "Done" here already. | |
| 875 | + // They will be done on save anyway - and on cancel the reasoning block gets rerendered too. | |
| 876 | + if (messageBlock.attr('data-reasoning-state') === ReasoningState.Hidden) { | |
| 877 | + messageBlock.attr('data-reasoning-state', ReasoningState.Done); | |
| 878 | + } | |
| 879 | + | |
| 880 | + // Open the reasoning area so we can actually edit it | |
| 881 | + messageBlock.find('.mes_reasoning_details').attr('open', ''); | |
| 882 | + messageBlock.find('.mes_reasoning_edit').trigger('click'); | |
| 255 | 883 | await saveChatConditional(); |
| 256 | - closeMessageEditor(); | |
| 257 | - updateMessageBlock(messageId, message); | |
| 258 | 884 | }); |
| 259 | 885 | |
| 260 | 886 | $(document).on('click', '.mes_reasoning_delete', async function (e) { |
| 261 | 887 | e.stopPropagation(); |
| 262 | 888 | e.preventDefault(); |
| 263 | 889 | |
| 264 | 890 | const confirm = await Popup.show.confirm(t`Remove Reasoning`, t`Are you sure you want to clear the reasoning?`,<br t`/>Visible message contents will stay intact.`); |
| 265 | 891 | |
| 266 | 892 | if (!confirm) { |
| 267 | 893 | return; |
| 268 | 894 | } |
| 269 | 895 | |
| 270 | 896 | const { message, messageId, messageBlock } = getMessageFromJquery(this); |
| 271 | 897 | if (!message?.extra) { |
| 272 | 898 | return; |
| 273 | 899 | } |
| 274 | 900 | message.extra.reasoning = ''; |
| 901 | + delete message.extra.reasoning_type; | |
| 902 | + delete message.extra.reasoning_duration; | |
| 275 | 903 | await saveChatConditional(); |
| 276 | 904 | updateMessageBlock(messageId, message); |
| 905 | + const textarea = messageBlock.find('.reasoning_edit_textarea'); | |
| 906 | + textarea.remove(); | |
| 277 | 907 | }); |
| 278 | 908 | |
| 279 | 909 | $(document).on('pointerup', '.mes_reasoning_copy', async function () { |
| 280 | 910 | const { message } = getMessageFromJquery(this); |
| 281 | 911 | const reasoning = String(message?.extra?.reasoning ?? '').replace(PromptReasoning.REASONING_PLACEHOLDER_REGEX, ''); |
| 282 | 912 | |
| 283 | 913 | if (!reasoning) { |
| 284 | 914 | return; |
| @@ -289,9 +919,123 @@ function setReasoningEventHandlers(){ | ||
| 289 | 919 | }); |
| 290 | 920 | } |
| 291 | 921 | |
| 922 | +/** | |
| 923 | + * Removes reasoning from a string if auto-parsing is enabled. | |
| 924 | + * @param {string} str Input string | |
| 925 | + * @returns {string} Output string | |
| 926 | + */ | |
| 927 | +export function removeReasoningFromString(str) { | |
| 928 | + if (!power_user.reasoning.auto_parse) { | |
| 929 | + return str; | |
| 930 | + } | |
| 931 | + | |
| 932 | + const parsedReasoning = parseReasoningFromString(str); | |
| 933 | + return parsedReasoning?.content ?? str; | |
| 934 | +} | |
| 935 | + | |
| 936 | +/** | |
| 937 | + * Parses reasoning from a string using the power user reasoning settings. | |
| 938 | + * @typedef {Object} ParsedReasoning | |
| 939 | + * @property {string} reasoning Reasoning block | |
| 940 | + * @property {string} content Message content | |
| 941 | + * @param {string} str Content of the message | |
| 942 | + * @param {Object} options Optional arguments | |
| 943 | + * @param {boolean} [options.strict=true] Whether the reasoning block **has** to be at the beginning of the provided string (excluding whitespaces), or can be anywhere in it | |
| 944 | + * @returns {ParsedReasoning|null} Parsed reasoning block and message content | |
| 945 | + */ | |
| 946 | +function parseReasoningFromString(str, { strict = true } = {}) { | |
| 947 | + // Both prefix and suffix must be defined | |
| 948 | + if (!power_user.reasoning.prefix || !power_user.reasoning.suffix) { | |
| 949 | + return null; | |
| 950 | + } | |
| 951 | + | |
| 952 | + try { | |
| 953 | + const regex = new RegExp(`${(strict ? '^\\s*?' : '')}${escapeRegex(power_user.reasoning.prefix)}(.*?)${escapeRegex(power_user.reasoning.suffix)}`, 's'); | |
| 954 | + | |
| 955 | + let didReplace = false; | |
| 956 | + let reasoning = ''; | |
| 957 | + let content = String(str).replace(regex, (_match, captureGroup) => { | |
| 958 | + didReplace = true; | |
| 959 | + reasoning = captureGroup; | |
| 960 | + return ''; | |
| 961 | + }); | |
| 962 | + | |
| 963 | + if (didReplace) { | |
| 964 | + reasoning = trimSpaces(reasoning); | |
| 965 | + content = trimSpaces(content); | |
| 966 | + } | |
| 967 | + | |
| 968 | + return { reasoning, content }; | |
| 969 | + } catch (error) { | |
| 970 | + console.error('[Reasoning] Error parsing reasoning block', error); | |
| 971 | + return null; | |
| 972 | + } | |
| 973 | +} | |
| 974 | + | |
| 975 | +function registerReasoningAppEvents() { | |
| 976 | + eventSource.makeFirst(event_types.MESSAGE_RECEIVED, (/** @type {number} */ idx) => { | |
| 977 | + if (!power_user.reasoning.auto_parse) { | |
| 978 | + return; | |
| 979 | + } | |
| 980 | + | |
| 981 | + console.debug('[Reasoning] Auto-parsing reasoning block for message', idx); | |
| 982 | + const message = chat[idx]; | |
| 983 | + | |
| 984 | + if (!message) { | |
| 985 | + console.warn('[Reasoning] Message not found', idx); | |
| 986 | + return null; | |
| 987 | + } | |
| 988 | + | |
| 989 | + if (!message.mes || message.mes === '...') { | |
| 990 | + console.debug('[Reasoning] Message content is empty or a placeholder', idx); | |
| 991 | + return null; | |
| 992 | + } | |
| 993 | + | |
| 994 | + if (message.extra?.reasoning) { | |
| 995 | + console.debug('[Reasoning] Message already has reasoning', idx); | |
| 996 | + return null; | |
| 997 | + } | |
| 998 | + | |
| 999 | + const parsedReasoning = parseReasoningFromString(message.mes); | |
| 1000 | + | |
| 1001 | + // No reasoning block found | |
| 1002 | + if (!parsedReasoning) { | |
| 1003 | + return; | |
| 1004 | + } | |
| 1005 | + | |
| 1006 | + // Make sure the message has an extra object | |
| 1007 | + if (!message.extra || typeof message.extra !== 'object') { | |
| 1008 | + message.extra = {}; | |
| 1009 | + } | |
| 1010 | + | |
| 1011 | + const contentUpdated = !!parsedReasoning.reasoning || parsedReasoning.content !== message.mes; | |
| 1012 | + | |
| 1013 | + // If reasoning was found, add it to the message | |
| 1014 | + if (parsedReasoning.reasoning) { | |
| 1015 | + message.extra.reasoning = getRegexedString(parsedReasoning.reasoning, regex_placement.REASONING); | |
| 1016 | + message.extra.reasoning_type = ReasoningType.Parsed; | |
| 1017 | + } | |
| 1018 | + | |
| 1019 | + // Update the message text if it was changed | |
| 1020 | + if (parsedReasoning.content !== message.mes) { | |
| 1021 | + message.mes = parsedReasoning.content; | |
| 1022 | + } | |
| 1023 | + | |
| 1024 | + // Find if a message already exists in DOM and must be updated | |
| 1025 | + if (contentUpdated) { | |
| 1026 | + const messageRendered = document.querySelector(`.mes[mesid="${idx}"]`) !== null; | |
| 1027 | + if (messageRendered) { | |
| 1028 | + console.debug('[Reasoning] Updating message block', idx); | |
| 1029 | + updateMessageBlock(idx, message); | |
| 1030 | + } | |
| 1031 | + } | |
| 1032 | + }); | |
| 1033 | +} | |
| 1034 | + | |
| 292 | 1035 | export function initReasoning() { |
| 293 | 1036 | loadReasoningSettings(); |
| 294 | 1037 | setReasoningEventHandlers(); |
| 295 | 1038 | registerReasoningSlashCommands(); |
| 296 | 1039 | registerReasoningMacros(); |
| 1040 | + registerReasoningAppEvents(); | |
| 297 | 1041 | } |
| @@ -40,6 +40,8 @@ export const SECRET_KEYS = { | ||
| 40 | 40 | BFL: 'api_key_bfl', |
| 41 | 41 | GENERIC: 'api_key_generic', |
| 42 | 42 | DEEPSEEK: 'api_key_deepseek', |
| 43 | + SERPER: 'api_key_serper', | |
| 44 | + FALAI: 'api_key_falai', | |
| 43 | 45 | }; |
| 44 | 46 | |
| 45 | 47 | const INPUT_MAP = { |
| @@ -59,7 +59,7 @@ import { autoSelectPersona, isPersonaLocked, retriggerFirstMessageOnEmptyChat, s | ||
| 59 | 59 | import { addEphemeralStoppingString, chat_styles, flushEphemeralStoppingStrings, power_user } from './power-user.js'; |
| 60 | 60 | import { SERVER_INPUTS, textgen_types, textgenerationwebui_settings } from './textgen-settings.js'; |
| 61 | 61 | import { decodeTextTokens, getAvailableTokenizers, getFriendlyTokenizerName, getTextTokens, getTokenCountAsync, selectTokenizer } from './tokenizers.js'; |
| 62 | 62 | import { debounce, delay, equalsIgnoreCaseAndAccents, findChar, getCharIndex, isFalseBoolean, isTrueBoolean, onlyUnique, regexFromString, showFontAwesomePicker, stringToRange, trimToEndSentence, trimToStartSentence, waitUntilCondition } from './utils.js'; |
| 63 | 63 | import { registerVariableCommands, resolveVariable } from './variables.js'; |
| 64 | 64 | import { background_settings } from './backgrounds.js'; |
| 65 | 65 | import { SlashCommandClosure } from './slash-commands/SlashCommandClosure.js'; |
| @@ -76,6 +76,7 @@ import { SlashCommandBreakController } from './slash-commands/SlashCommandBreakC | ||
| 76 | 76 | import { SlashCommandExecutionError } from './slash-commands/SlashCommandExecutionError.js'; |
| 77 | 77 | import { slashCommandReturnHelper } from './slash-commands/SlashCommandReturnHelper.js'; |
| 78 | 78 | import { t } from './i18n.js'; |
| 79 | +import { accountStorage } from './util/AccountStorage.js'; | |
| 79 | 80 | export { |
| 80 | 81 | executeSlashCommands, executeSlashCommandsWithOptions, getSlashCommandsHelp, registerSlashCommand, |
| 81 | 82 | }; |
| @@ -283,7 +284,6 @@ export function initDefaultSlashCommands() { | ||
| 283 | 284 | description: 'Character name - or unique character identifier (avatar key)', |
| 284 | 285 | typeList: [ARGUMENT_TYPE.STRING], |
| 285 | 286 | enumProvider: commonEnumProviders.characters('character'), |
| 286 | - forceEnum: false, | |
| 287 | 287 | }), |
| 288 | 288 | ], |
| 289 | 289 | helpString: ` |
| @@ -322,7 +322,6 @@ export function initDefaultSlashCommands() { | ||
| 322 | 322 | typeList: [ARGUMENT_TYPE.STRING], |
| 323 | 323 | isRequired: true, |
| 324 | 324 | enumProvider: commonEnumProviders.characters('character'), |
| 325 | - forceEnum: false, | |
| 326 | 325 | }), |
| 327 | 326 | SlashCommandNamedArgument.fromProps({ |
| 328 | 327 | name: 'avatar', |
| @@ -566,7 +565,6 @@ export function initDefaultSlashCommands() { | ||
| 566 | 565 | typeList: [ARGUMENT_TYPE.STRING], |
| 567 | 566 | isRequired: true, |
| 568 | 567 | enumProvider: commonEnumProviders.characters('all'), |
| 569 | - forceEnum: true, | |
| 570 | 568 | }), |
| 571 | 569 | ], |
| 572 | 570 | helpString: 'Opens up a chat with the character or group by its name', |
| @@ -782,6 +780,57 @@ export function initDefaultSlashCommands() { | ||
| 782 | 780 | helpString: 'Unhides a message from the prompt.', |
| 783 | 781 | })); |
| 784 | 782 | SlashCommandParser.addCommandObject(SlashCommand.fromProps({ |
| 783 | + name: 'member-get', | |
| 784 | + aliases: ['getmember', 'memberget'], | |
| 785 | + callback: (async ({ field = 'name' }, arg) => { | |
| 786 | + if (!selected_group) { | |
| 787 | + toastr.warning('Cannot run /member-get command outside of a group chat.'); | |
| 788 | + return ''; | |
| 789 | + } | |
| 790 | + if (field === '') { | |
| 791 | + toastr.warning('\'/member-get field=\' argument required!'); | |
| 792 | + return ''; | |
| 793 | + } | |
| 794 | + field = field.toString(); | |
| 795 | + arg = arg.toString(); | |
| 796 | + if (!['name', 'index', 'id', 'avatar'].includes(field)) { | |
| 797 | + toastr.warning('\'/member-get field=\' argument required!'); | |
| 798 | + return ''; | |
| 799 | + } | |
| 800 | + const isId = !isNaN(parseInt(arg)); | |
| 801 | + const groupMember = findGroupMemberId(arg, true); | |
| 802 | + if (!groupMember) { | |
| 803 | + toastr.warn(`No group member found using ${isId ? 'id' : 'string'} ${arg}`); | |
| 804 | + return ''; | |
| 805 | + } | |
| 806 | + return groupMember[field]; | |
| 807 | + }), | |
| 808 | + namedArgumentList: [ | |
| 809 | + SlashCommandNamedArgument.fromProps({ | |
| 810 | + name: 'field', | |
| 811 | + description: 'Whether to retrieve the name, index, id, or avatar.', | |
| 812 | + typeList: [ARGUMENT_TYPE.STRING], | |
| 813 | + isRequired: true, | |
| 814 | + defaultValue: 'name', | |
| 815 | + enumList: [ | |
| 816 | + new SlashCommandEnumValue('name', 'Character name'), | |
| 817 | + new SlashCommandEnumValue('index', 'Group member index'), | |
| 818 | + new SlashCommandEnumValue('avatar', 'Character avatar'), | |
| 819 | + new SlashCommandEnumValue('id', 'Character index'), | |
| 820 | + ], | |
| 821 | + }), | |
| 822 | + ], | |
| 823 | + unnamedArgumentList: [ | |
| 824 | + SlashCommandArgument.fromProps({ | |
| 825 | + description: 'member index (starts with 0), name, or avatar', | |
| 826 | + typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.STRING], | |
| 827 | + isRequired: true, | |
| 828 | + enumProvider: commonEnumProviders.groupMembers(), | |
| 829 | + }), | |
| 830 | + ], | |
| 831 | + helpString: 'Retrieves a group member\'s name, index, id, or avatar.', | |
| 832 | + })); | |
| 833 | + SlashCommandParser.addCommandObject(SlashCommand.fromProps({ | |
| 785 | 834 | name: 'member-disable', |
| 786 | 835 | callback: disableGroupMemberCallback, |
| 787 | 836 | aliases: ['disable', 'disablemember', 'memberdisable'], |
| @@ -891,7 +940,8 @@ export function initDefaultSlashCommands() { | ||
| 891 | 940 | helpString: 'Moves a group member down in the group chat list.', |
| 892 | 941 | })); |
| 893 | 942 | SlashCommandParser.addCommandObject(SlashCommand.fromProps({ |
| 894 | 943 | name: 'member-peek', |
| 944 | + aliases: ['peek', 'memberpeek', 'peekmember'], | |
| 895 | 945 | callback: peekCallback, |
| 896 | 946 | unnamedArgumentList: [ |
| 897 | 947 | SlashCommandArgument.fromProps({ |
| @@ -1057,7 +1107,6 @@ export function initDefaultSlashCommands() { | ||
| 1057 | 1107 | typeList: [ARGUMENT_TYPE.STRING], |
| 1058 | 1108 | defaultValue: 'System', |
| 1059 | 1109 | enumProvider: () => [...commonEnumProviders.characters('character')(), new SlashCommandEnumValue('System', null, enumTypes.enum, enumIcons.assistant)], |
| 1060 | - forceEnum: false, | |
| 1061 | 1110 | }), |
| 1062 | 1111 | new SlashCommandNamedArgument( |
| 1063 | 1112 | 'length', 'API response length in tokens', [ARGUMENT_TYPE.NUMBER], false, |
| @@ -1951,7 +2000,7 @@ export function initDefaultSlashCommands() { | ||
| 1951 | 2000 | returns: 'uppercase string', |
| 1952 | 2001 | unnamedArgumentList: [ |
| 1953 | 2002 | new SlashCommandArgument( |
| 1954 | 2003 | 'stringtext to affect', [ARGUMENT_TYPE.STRING], true, false, |
| 1955 | 2004 | ), |
| 1956 | 2005 | ], |
| 1957 | 2006 | helpString: 'Converts the provided string to uppercase.', |
| @@ -1963,7 +2012,7 @@ export function initDefaultSlashCommands() { | ||
| 1963 | 2012 | returns: 'lowercase string', |
| 1964 | 2013 | unnamedArgumentList: [ |
| 1965 | 2014 | new SlashCommandArgument( |
| 1966 | 2015 | 'stringtext to affect', [ARGUMENT_TYPE.STRING], true, false, |
| 1967 | 2016 | ), |
| 1968 | 2017 | ], |
| 1969 | 2018 | helpString: 'Converts the provided string to lowercase.', |
| @@ -1983,7 +2032,7 @@ export function initDefaultSlashCommands() { | ||
| 1983 | 2032 | ], |
| 1984 | 2033 | unnamedArgumentList: [ |
| 1985 | 2034 | new SlashCommandArgument( |
| 1986 | 2035 | 'stringtext to affect', [ARGUMENT_TYPE.STRING], true, false, |
| 1987 | 2036 | ), |
| 1988 | 2037 | ], |
| 1989 | 2038 | helpString: ` |
| @@ -2047,6 +2096,62 @@ export function initDefaultSlashCommands() { | ||
| 2047 | 2096 | return ''; |
| 2048 | 2097 | }, |
| 2049 | 2098 | })); |
| 2099 | + SlashCommandParser.addCommandObject(SlashCommand.fromProps({ | |
| 2100 | + name: 'replace', | |
| 2101 | + aliases: ['re'], | |
| 2102 | + callback: (async ({ mode = 'literal', pattern, replacer = '' }, text) => { | |
| 2103 | + if (pattern === '') | |
| 2104 | + throw new Error('Argument of \'pattern=\' cannot be empty'); | |
| 2105 | + switch (mode) { | |
| 2106 | + case 'literal': | |
| 2107 | + return text.replaceAll(pattern, replacer); | |
| 2108 | + case 'regex': | |
| 2109 | + return text.replace(regexFromString(pattern), replacer); | |
| 2110 | + default: | |
| 2111 | + throw new Error('Invalid \'/replace mode=\' argument specified!'); | |
| 2112 | + } | |
| 2113 | + }), | |
| 2114 | + returns: 'replaced text', | |
| 2115 | + namedArgumentList: [ | |
| 2116 | + SlashCommandNamedArgument.fromProps({ | |
| 2117 | + name: 'mode', | |
| 2118 | + description: 'Replaces occurrence(s) of a pattern', | |
| 2119 | + typeList: [ARGUMENT_TYPE.STRING], | |
| 2120 | + defaultValue: 'literal', | |
| 2121 | + enumList: ['literal', 'regex'], | |
| 2122 | + }), | |
| 2123 | + new SlashCommandNamedArgument( | |
| 2124 | + 'pattern', 'pattern to search with', [ARGUMENT_TYPE.STRING], true, false, | |
| 2125 | + ), | |
| 2126 | + new SlashCommandNamedArgument( | |
| 2127 | + 'replacer', 'replacement text for matches', [ARGUMENT_TYPE.STRING], false, false, '', | |
| 2128 | + ), | |
| 2129 | + ], | |
| 2130 | + unnamedArgumentList: [ | |
| 2131 | + new SlashCommandArgument( | |
| 2132 | + 'text to affect', [ARGUMENT_TYPE.STRING], true, false, | |
| 2133 | + ), | |
| 2134 | + ], | |
| 2135 | + helpString: ` | |
| 2136 | + <div> | |
| 2137 | + Replaces text within the provided string based on the pattern. | |
| 2138 | + </div> | |
| 2139 | + <div> | |
| 2140 | + If <code>mode</code> is <code>literal</code> (or omitted), <code>pattern</code> is a literal search string (case-sensitive).<br /> | |
| 2141 | + If <code>mode</code> is <code>regex</code>, <code>pattern</code> is parsed as an ECMAScript Regular Expression.<br /> | |
| 2142 | + The <code>replacer</code> replaces based on the <code>pattern</code> in the input text.<br /> | |
| 2143 | + If <code>replacer</code> is omitted, the replacement(s) will be an empty string.<br /> | |
| 2144 | + </div> | |
| 2145 | + <div> | |
| 2146 | + <strong>Example:</strong> | |
| 2147 | + <pre>/let x Blue house and blue car || </pre> | |
| 2148 | + <pre>/replace pattern="blue" {{var::x}} | /echo |/# Blue house and car ||</pre> | |
| 2149 | + <pre>/replace pattern="blue" replacer="red" {{var::x}} | /echo |/# Blue house and red car ||</pre> | |
| 2150 | + <pre>/replace mode=regex pattern="/blue/i" replacer="red" {{var::x}} | /echo |/# red house and blue car ||</pre> | |
| 2151 | + <pre>/replace mode=regex pattern="/blue/gi" replacer="red" {{var::x}} | /echo |/# red house and red car ||</pre> | |
| 2152 | + </div> | |
| 2153 | + `, | |
| 2154 | + })); | |
| 2050 | 2155 | |
| 2051 | 2156 | registerVariableCommands(); |
| 2052 | 2157 | } |
| @@ -3039,7 +3144,7 @@ function performGroupMemberAction(chid, action) { | ||
| 3039 | 3144 | |
| 3040 | 3145 | async function disableGroupMemberCallback(_, arg) { |
| 3041 | 3146 | if (!selected_group) { |
| 3042 | 3147 | toastr.warning('Cannot run /member-disable command outside of a group chat.'); |
| 3043 | 3148 | return ''; |
| 3044 | 3149 | } |
| 3045 | 3150 | |
| @@ -3056,7 +3161,7 @@ async function disableGroupMemberCallback(_, arg) { | ||
| 3056 | 3161 | |
| 3057 | 3162 | async function enableGroupMemberCallback(_, arg) { |
| 3058 | 3163 | if (!selected_group) { |
| 3059 | 3164 | toastr.warning('Cannot run /member-enable command outside of a group chat.'); |
| 3060 | 3165 | return ''; |
| 3061 | 3166 | } |
| 3062 | 3167 | |
| @@ -3073,7 +3178,7 @@ async function enableGroupMemberCallback(_, arg) { | ||
| 3073 | 3178 | |
| 3074 | 3179 | async function moveGroupMemberUpCallback(_, arg) { |
| 3075 | 3180 | if (!selected_group) { |
| 3076 | 3181 | toastr.warning('Cannot run /memberupmember-up command outside of a group chat.'); |
| 3077 | 3182 | return ''; |
| 3078 | 3183 | } |
| 3079 | 3184 | |
| @@ -3090,7 +3195,7 @@ async function moveGroupMemberUpCallback(_, arg) { | ||
| 3090 | 3195 | |
| 3091 | 3196 | async function moveGroupMemberDownCallback(_, arg) { |
| 3092 | 3197 | if (!selected_group) { |
| 3093 | 3198 | toastr.warning('Cannot run /memberdownmember-down command outside of a group chat.'); |
| 3094 | 3199 | return ''; |
| 3095 | 3200 | } |
| 3096 | 3201 | |
| @@ -3107,12 +3212,12 @@ async function moveGroupMemberDownCallback(_, arg) { | ||
| 3107 | 3212 | |
| 3108 | 3213 | async function peekCallback(_, arg) { |
| 3109 | 3214 | if (!selected_group) { |
| 3110 | 3215 | toastr.warning('Cannot run /member-peek command outside of a group chat.'); |
| 3111 | 3216 | return ''; |
| 3112 | 3217 | } |
| 3113 | 3218 | |
| 3114 | 3219 | if (is_group_generating) { |
| 3115 | 3220 | toastr.warning('Cannot run /member-peek command while the group reply is generating.'); |
| 3116 | 3221 | return ''; |
| 3117 | 3222 | } |
| 3118 | 3223 | |
| @@ -3129,12 +3234,7 @@ async function peekCallback(_, arg) { | ||
| 3129 | 3234 | |
| 3130 | 3235 | async function removeGroupMemberCallback(_, arg) { |
| 3131 | 3236 | if (!selected_group) { |
| 3132 | 3237 | toastr.warning('Cannot run /memberremovemember-remove command outside of a group chat.'); |
| 3133 | - return ''; | |
| 3134 | - } | |
| 3135 | - | |
| 3136 | - if (is_group_generating) { | |
| 3137 | - toastr.warning('Cannot run /memberremove command while the group reply is generating.'); | |
| 3138 | 3238 | return ''; |
| 3139 | 3239 | } |
| 3140 | 3240 | |
| @@ -3242,12 +3342,7 @@ function findPersonaByName(name) { | ||
| 3242 | 3342 | } |
| 3243 | 3343 | |
| 3244 | 3344 | async function sendUserMessageCallback(args, text) { |
| 3245 | - if (!text) { | |
| 3345 | + text = String(text ?? '').trim(); | |
| 3246 | - toastr.warning('You must specify text to send'); | |
| 3247 | - return; | |
| 3248 | - } | |
| 3249 | - | |
| 3250 | - text = text.trim(); | |
| 3251 | 3346 | const compact = isTrueBoolean(args?.compact); |
| 3252 | 3347 | const bias = extractMessageBias(text); |
| 3253 | 3348 | |
| @@ -3562,24 +3657,18 @@ export function getNameAndAvatarForMessage(character, name = null) { | ||
| 3562 | 3657 | } |
| 3563 | 3658 | |
| 3564 | 3659 | export async function sendMessageAs(args, text) { |
| 3565 | - if (!text) { | |
| 3566 | - toastr.warning('You must specify text to send as'); | |
| 3567 | - return ''; | |
| 3568 | - } | |
| 3569 | - | |
| 3570 | 3660 | let name = args.name?.trim(); |
| 3571 | - let mesText; | |
| 3572 | 3661 | |
| 3573 | 3662 | if (!name) { |
| 3574 | 3663 | const namelessWarningKey = 'sendAsNamelessWarningShown'; |
| 3575 | 3664 | if (localStorageaccountStorage.getItem(namelessWarningKey) !== 'true') { |
| 3576 | 3665 | toastr.warning('To avoid confusion, please use /sendas name="Character Name"', 'Name defaulted to {{char}}', { timeOut: 10000 }); |
| 3577 | 3666 | localStorageaccountStorage.setItem(namelessWarningKey, 'true'); |
| 3578 | 3667 | } |
| 3579 | 3668 | name = name2; |
| 3580 | 3669 | } |
| 3581 | 3670 | |
| 3582 | 3671 | let mesText = String(text ?? '').trim(); |
| 3583 | 3672 | |
| 3584 | 3673 | // Requires a regex check after the slash command is pushed to output |
| 3585 | 3674 | mesText = getRegexedString(mesText, regex_placement.SLASH_COMMAND, { characterOverride: name }); |
| @@ -3657,11 +3746,7 @@ export async function sendMessageAs(args, text) { | ||
| 3657 | 3746 | } |
| 3658 | 3747 | |
| 3659 | 3748 | export async function sendNarratorMessage(args, text) { |
| 3660 | - if (!text) { | |
| 3749 | + text = String(text ?? ''); | |
| 3661 | - toastr.warning('You must specify text to send'); | |
| 3662 | - return ''; | |
| 3663 | - } | |
| 3664 | - | |
| 3665 | 3750 | const name = chat_metadata[NARRATOR_NAME_KEY] || NARRATOR_NAME_DEFAULT; |
| 3666 | 3751 | // Messages that do nothing but set bias will be hidden from the context |
| 3667 | 3752 | const bias = extractMessageBias(text); |
| @@ -3752,18 +3837,13 @@ export async function promptQuietForLoudResponse(who, text) { | ||
| 3752 | 3837 | } |
| 3753 | 3838 | |
| 3754 | 3839 | async function sendCommentMessage(args, text) { |
| 3755 | - if (!text) { | |
| 3756 | - toastr.warning('You must specify text to send'); | |
| 3757 | - return ''; | |
| 3758 | - } | |
| 3759 | - | |
| 3760 | 3840 | const compact = isTrueBoolean(args?.compact); |
| 3761 | 3841 | const message = { |
| 3762 | 3842 | name: COMMENT_NAME_DEFAULT, |
| 3763 | 3843 | is_user: false, |
| 3764 | 3844 | is_system: true, |
| 3765 | 3845 | send_date: getMessageTimeStamp(), |
| 3766 | 3846 | mes: substituteParams(String(text ?? '').trim()), |
| 3767 | 3847 | force_avatar: comment_avatar, |
| 3768 | 3848 | extra: { |
| 3769 | 3849 | type: system_message_types.COMMENT, |
| @@ -34,6 +34,7 @@ export const enumIcons = { | ||
| 34 | 34 | preset: '⚙️', |
| 35 | 35 | file: '📄', |
| 36 | 36 | message: '💬', |
| 37 | + reasoning: '💡', | |
| 37 | 38 | voice: '🎤', |
| 38 | 39 | server: '🖥️', |
| 39 | 40 | popup: '🗔', |
| @@ -68,10 +68,14 @@ import { tag_map, tags } from './tags.js'; | ||
| 68 | 68 | import { textgenerationwebui_settings } from './textgen-settings.js'; |
| 69 | 69 | import { tokenizers, getTextTokens, getTokenCount, getTokenCountAsync, getTokenizerModel } from './tokenizers.js'; |
| 70 | 70 | import { ToolManager } from './tool-calling.js'; |
| 71 | +import { accountStorage } from './util/AccountStorage.js'; | |
| 71 | 72 | import { timestampToMoment, uuidv4 } from './utils.js'; |
| 73 | +import { getGlobalVariable, getLocalVariable, setGlobalVariable, setLocalVariable } from './variables.js'; | |
| 74 | +import { convertCharacterBook, loadWorldInfo, saveWorldInfo, updateWorldInfoList } from './world-info.js'; | |
| 72 | 75 | |
| 73 | 76 | export function getContext() { |
| 74 | 77 | return { |
| 78 | + accountStorage, | |
| 75 | 79 | chat, |
| 76 | 80 | characters, |
| 77 | 81 | groups, |
| @@ -175,6 +179,20 @@ export function getContext() { | ||
| 175 | 179 | humanizedDateTime, |
| 176 | 180 | updateMessageBlock, |
| 177 | 181 | appendMediaToMessage, |
| 182 | + variables: { | |
| 183 | + local: { | |
| 184 | + get: getLocalVariable, | |
| 185 | + set: setLocalVariable, | |
| 186 | + }, | |
| 187 | + global: { | |
| 188 | + get: getGlobalVariable, | |
| 189 | + set: setGlobalVariable, | |
| 190 | + }, | |
| 191 | + }, | |
| 192 | + loadWorldInfo, | |
| 193 | + saveWorldInfo, | |
| 194 | + updateWorldInfoList, | |
| 195 | + convertCharacterBook, | |
| 178 | 196 | }; |
| 179 | 197 | } |
| 180 | 198 | |
| @@ -146,5 +146,5 @@ | ||
| 146 | 146 | </div> |
| 147 | 147 | <hr> |
| 148 | 148 | <div id="rawPromptPopup" class="list-group"> |
| 149 | 149 | <div id="rawPromptWrapper" class="tokenItemizingSubclasstokenItemizingMaintext"></div> |
| 150 | 150 | </div> |
| @@ -6,6 +6,7 @@ import { tokenizers } from './tokenizers.js'; | ||
| 6 | 6 | import { renderTemplateAsync } from './templates.js'; |
| 7 | 7 | import { POPUP_TYPE, callGenericPopup } from './popup.js'; |
| 8 | 8 | import { t } from './i18n.js'; |
| 9 | +import { accountStorage } from './util/AccountStorage.js'; | |
| 9 | 10 | |
| 10 | 11 | let mancerModels = []; |
| 11 | 12 | let togetherModels = []; |
| @@ -54,6 +55,17 @@ const OPENROUTER_PROVIDERS = [ | ||
| 54 | 55 | 'xAI', |
| 55 | 56 | 'Cloudflare', |
| 56 | 57 | 'SF Compute', |
| 58 | + 'Minimax', | |
| 59 | + 'Nineteen', | |
| 60 | + 'Liquid', | |
| 61 | + 'InferenceNet', | |
| 62 | + 'Friendli', | |
| 63 | + 'AionLabs', | |
| 64 | + 'Alibaba', | |
| 65 | + 'Nebius', | |
| 66 | + 'Chutes', | |
| 67 | + 'Kluster', | |
| 68 | + 'Targon', | |
| 57 | 69 | '01.AI', |
| 58 | 70 | 'HuggingFace', |
| 59 | 71 | 'Mancer', |
| @@ -330,7 +342,7 @@ export async function loadFeatherlessModels(data) { | ||
| 330 | 342 | populateClassSelection(data); |
| 331 | 343 | |
| 332 | 344 | // Retrieve the stored number of items per page or default to 10 |
| 333 | 345 | const perPage = Number(localStorageaccountStorage.getItem(storageKey)) || 10; |
| 334 | 346 | |
| 335 | 347 | // Initialize pagination |
| 336 | 348 | applyFiltersAndSort(); |
| @@ -406,7 +418,7 @@ export async function loadFeatherlessModels(data) { | ||
| 406 | 418 | }, |
| 407 | 419 | afterSizeSelectorChange: function (e) { |
| 408 | 420 | const newPerPage = e.target.value; |
| 409 | 421 | localStorageaccountStorage.setItem('Models_PerPage'storageKey, newPerPage); |
| 410 | 422 | setupPagination(models, Number(newPerPage), featherlessCurrentPage); // Use the stored current page number |
| 411 | 423 | }, |
| 412 | 424 | }); |
| @@ -507,7 +519,7 @@ export async function loadFeatherlessModels(data) { | ||
| 507 | 519 | const currentModelIndex = filteredModels.findIndex(x => x.id === textgen_settings.featherless_model); |
| 508 | 520 | featherlessCurrentPage = currentModelIndex >= 0 ? (currentModelIndex / perPage) + 1 : 1; |
| 509 | 521 | |
| 510 | 522 | setupPagination(filteredModels, Number(localStorageaccountStorage.getItem(storageKey)) || perPage, featherlessCurrentPage); |
| 511 | 523 | } |
| 512 | 524 | |
| 513 | 525 | // Required to keep the /model command function |
| @@ -10,6 +10,7 @@ import { | ||
| 10 | 10 | setOnlineStatus, |
| 11 | 11 | substituteParams, |
| 12 | 12 | } from '../script.js'; |
| 13 | +import { t } from './i18n.js'; | |
| 13 | 14 | import { BIAS_CACHE, createNewLogitBiasEntry, displayLogitBias, getLogitBiasListResult } from './logit-bias.js'; |
| 14 | 15 | |
| 15 | 16 | import { power_user, registerDebugFunction } from './power-user.js'; |
| @@ -172,6 +173,7 @@ const settings = { | ||
| 172 | 173 | //truncation_length: 2048, |
| 173 | 174 | ban_eos_token: false, |
| 174 | 175 | skip_special_tokens: true, |
| 176 | + include_reasoning: true, | |
| 175 | 177 | streaming: false, |
| 176 | 178 | mirostat_mode: 0, |
| 177 | 179 | mirostat_tau: 5, |
| @@ -181,6 +183,8 @@ const settings = { | ||
| 181 | 183 | grammar_string: '', |
| 182 | 184 | json_schema: {}, |
| 183 | 185 | banned_tokens: '', |
| 186 | + global_banned_tokens: '', | |
| 187 | + send_banned_tokens: true, | |
| 184 | 188 | sampler_priority: OOBA_DEFAULT_ORDER, |
| 185 | 189 | samplers: LLAMACPP_DEFAULT_ORDER, |
| 186 | 190 | samplers_priorities: APHRODITE_DEFAULT_ORDER, |
| @@ -263,6 +267,7 @@ export const setting_names = [ | ||
| 263 | 267 | 'add_bos_token', |
| 264 | 268 | 'ban_eos_token', |
| 265 | 269 | 'skip_special_tokens', |
| 270 | + 'include_reasoning', | |
| 266 | 271 | 'streaming', |
| 267 | 272 | 'mirostat_mode', |
| 268 | 273 | 'mirostat_tau', |
| @@ -272,6 +277,8 @@ export const setting_names = [ | ||
| 272 | 277 | 'grammar_string', |
| 273 | 278 | 'json_schema', |
| 274 | 279 | 'banned_tokens', |
| 280 | + 'global_banned_tokens', | |
| 281 | + 'send_banned_tokens', | |
| 275 | 282 | 'ignore_eos_token', |
| 276 | 283 | 'spaces_between_special_tokens', |
| 277 | 284 | 'speculative_ngram', |
| @@ -304,7 +311,7 @@ export function validateTextGenUrl() { | ||
| 304 | 311 | const formattedUrl = formatTextGenURL(url); |
| 305 | 312 | |
| 306 | 313 | if (!formattedUrl) { |
| 307 | 314 | toastr.error('t`Enter a valid API URL'`, 'Text Completion API'); |
| 308 | 315 | return; |
| 309 | 316 | } |
| 310 | 317 | |
| @@ -392,7 +399,7 @@ function getTokenizerForTokenIds() { | ||
| 392 | 399 | * @returns {TokenBanResult} String with comma-separated banned token IDs |
| 393 | 400 | */ |
| 394 | 401 | function getCustomTokenBans() { |
| 395 | 402 | if (!settings.send_banned_tokens || (!settings.banned_tokens && !settings.global_banned_tokens && !textgenerationwebui_banned_in_macros.length)) { |
| 396 | 403 | return { |
| 397 | 404 | banned_tokens: '', |
| 398 | 405 | banned_strings: [], |
| @@ -402,8 +409,9 @@ function getCustomTokenBans() { | ||
| 402 | 409 | const tokenizer = getTokenizerForTokenIds(); |
| 403 | 410 | const banned_tokens = []; |
| 404 | 411 | const banned_strings = []; |
| 405 | 412 | const sequences = settings.banned_tokens[] |
| 406 | 413 | .concat(settings.banned_tokens.split('\n')) |
| 414 | + .concat(settings.global_banned_tokens.split('\n')) | |
| 407 | 415 | .concat(textgenerationwebui_banned_in_macros) |
| 408 | 416 | .filter(x => x.length > 0) |
| 409 | 417 | .filter(onlyUnique); |
| @@ -451,6 +459,18 @@ function getCustomTokenBans() { | ||
| 451 | 459 | } |
| 452 | 460 | |
| 453 | 461 | /** |
| 462 | + * Sets the banned strings kill switch toggle. | |
| 463 | + * @param {boolean} isEnabled Kill switch state | |
| 464 | + * @param {string} title Label title | |
| 465 | + */ | |
| 466 | +function toggleBannedStringsKillSwitch(isEnabled, title) { | |
| 467 | + $('#send_banned_tokens_textgenerationwebui').prop('checked', isEnabled); | |
| 468 | + $('#send_banned_tokens_label').find('.menu_button').toggleClass('toggleEnabled', isEnabled).prop('title', title); | |
| 469 | + settings.send_banned_tokens = isEnabled; | |
| 470 | + saveSettingsDebounced(); | |
| 471 | +} | |
| 472 | + | |
| 473 | +/** | |
| 454 | 474 | * Calculates logit bias object from the logit bias list. |
| 455 | 475 | * @returns {object} Logit bias object |
| 456 | 476 | */ |
| @@ -501,7 +521,7 @@ export function loadTextGenSettings(data, loadedSettings) { | ||
| 501 | 521 | for (const [type, selector] of Object.entries(SERVER_INPUTS)) { |
| 502 | 522 | const control = $(selector); |
| 503 | 523 | control.val(settings.server_urls[type] ?? '').on('input', function () { |
| 504 | 524 | settings.server_urls[type] = String($(this).val()).trim(); |
| 505 | 525 | saveSettingsDebounced(); |
| 506 | 526 | }); |
| 507 | 527 | } |
| @@ -592,6 +612,14 @@ function sortAphroditeItemsByOrder(orderArray) { | ||
| 592 | 612 | } |
| 593 | 613 | |
| 594 | 614 | jQuery(function () { |
| 615 | + $('#send_banned_tokens_textgenerationwebui').on('change', function () { | |
| 616 | + const checked = !!$(this).prop('checked'); | |
| 617 | + toggleBannedStringsKillSwitch(checked, | |
| 618 | + checked | |
| 619 | + ? t`Banned tokens/strings are being sent in the request.` | |
| 620 | + : t`Banned tokens/strings are NOT being sent in the request.`); | |
| 621 | + }); | |
| 622 | + | |
| 595 | 623 | $('#koboldcpp_order').sortable({ |
| 596 | 624 | delay: getSortableDelay(), |
| 597 | 625 | stop: function () { |
| @@ -740,6 +768,7 @@ jQuery(function () { | ||
| 740 | 768 | 'add_bos_token_textgenerationwebui': true, |
| 741 | 769 | 'temperature_last_textgenerationwebui': true, |
| 742 | 770 | 'skip_special_tokens_textgenerationwebui': true, |
| 771 | + 'include_reasoning_textgenerationwebui': true, | |
| 743 | 772 | 'top_a_textgenerationwebui': 0, |
| 744 | 773 | 'top_a_counter_textgenerationwebui': 0, |
| 745 | 774 | 'mirostat_mode_textgenerationwebui': 0, |
| @@ -929,6 +958,10 @@ function setSettingByName(setting, value, trigger) { | ||
| 929 | 958 | if (isCheckbox) { |
| 930 | 959 | const val = Boolean(value); |
| 931 | 960 | $(`#${setting}_textgenerationwebui`).prop('checked', val); |
| 961 | + | |
| 962 | + if ('send_banned_tokens' === setting) { | |
| 963 | + $(`#${setting}_textgenerationwebui`).trigger('change'); | |
| 964 | + } | |
| 932 | 965 | } |
| 933 | 966 | else if (isText) { |
| 934 | 967 | $(`#${setting}_textgenerationwebui`).val(value); |
| @@ -986,7 +1019,7 @@ export async function generateTextGenWithStreaming(generate_data, signal) { | ||
| 986 | 1019 | let logprobs = null; |
| 987 | 1020 | const swipes = []; |
| 988 | 1021 | const toolCalls = []; |
| 989 | 1022 | const state = { reasoning: '' }; |
| 990 | 1023 | while (true) { |
| 991 | 1024 | const { done, value } = await reader.read(); |
| 992 | 1025 | if (done) return; |
| @@ -1003,6 +1036,7 @@ export async function generateTextGenWithStreaming(generate_data, signal) { | ||
| 1003 | 1036 | const newText = data?.choices?.[0]?.text || data?.content || ''; |
| 1004 | 1037 | text += newText; |
| 1005 | 1038 | logprobs = parseTextgenLogprobs(newText, data.choices?.[0]?.logprobs || data?.completion_probabilities); |
| 1039 | + state.reasoning += data?.choices?.[0]?.reasoning ?? ''; | |
| 1006 | 1040 | } |
| 1007 | 1041 | |
| 1008 | 1042 | yield { text, swipes, logprobs, toolCalls, state }; |
| @@ -1153,7 +1187,7 @@ export function getTextGenModel() { | ||
| 1153 | 1187 | return settings.aphrodite_model; |
| 1154 | 1188 | case OLLAMA: |
| 1155 | 1189 | if (!settings.ollama_model) { |
| 1156 | 1190 | toastr.error('t`No Ollama model selected.'`, 'Text Completion API'); |
| 1157 | 1191 | throw new Error('No Ollama model selected'); |
| 1158 | 1192 | } |
| 1159 | 1193 | return settings.ollama_model; |
| @@ -1217,7 +1251,7 @@ function replaceMacrosInList(str) { | ||
| 1217 | 1251 | } |
| 1218 | 1252 | } |
| 1219 | 1253 | |
| 1220 | 1254 | export async function getTextGenGenerationData(finalPrompt, maxTokens, isImpersonate, isContinue, cfgValues, type) { |
| 1221 | 1255 | const canMultiSwipe = !isContinue && !isImpersonate && type !== 'quiet'; |
| 1222 | 1256 | const dynatemp = isDynamicTemperatureSupported(); |
| 1223 | 1257 | const { banned_tokens, banned_strings } = getCustomTokenBans(); |
| @@ -1266,6 +1300,7 @@ export function getTextGenGenerationData(finalPrompt, maxTokens, isImpersonate, | ||
| 1266 | 1300 | 'truncation_length': max_context, |
| 1267 | 1301 | 'ban_eos_token': settings.ban_eos_token, |
| 1268 | 1302 | 'skip_special_tokens': settings.skip_special_tokens, |
| 1303 | + 'include_reasoning': settings.include_reasoning, | |
| 1269 | 1304 | 'top_a': settings.top_a, |
| 1270 | 1305 | 'tfs': settings.tfs, |
| 1271 | 1306 | 'epsilon_cutoff': [OOBA, MANCER].includes(settings.type) ? settings.epsilon_cutoff : undefined, |
| @@ -1444,7 +1479,7 @@ export function getTextGenGenerationData(finalPrompt, maxTokens, isImpersonate, | ||
| 1444 | 1479 | } |
| 1445 | 1480 | } |
| 1446 | 1481 | |
| 1447 | 1482 | await eventSource.emitAndWaitemit(event_types.TEXT_COMPLETION_SETTINGS_READY, params); |
| 1448 | 1483 | |
| 1449 | 1484 | // Grammar conflicts with with json_schema |
| 1450 | 1485 | if (settings.type === LLAMACPP) { |
| @@ -679,6 +679,9 @@ export function getTokenizerModel() { | ||
| 679 | 679 | } |
| 680 | 680 | |
| 681 | 681 | if (oai_settings.chat_completion_source === chat_completion_sources.PERPLEXITY) { |
| 682 | + if (oai_settings.perplexity_model.includes('sonar-reasoning') || oai_settings.perplexity_model.includes('r1-1776')) { | |
| 683 | + return deepseekTokenizer; | |
| 684 | + } | |
| 682 | 685 | if (oai_settings.perplexity_model.includes('llama-3') || oai_settings.perplexity_model.includes('llama3')) { |
| 683 | 686 | return llama3Tokenizer; |
| 684 | 687 | } |
| @@ -691,6 +694,9 @@ export function getTokenizerModel() { | ||
| 691 | 694 | } |
| 692 | 695 | |
| 693 | 696 | if (oai_settings.chat_completion_source === chat_completion_sources.GROQ) { |
| 697 | + if (oai_settings.groq_model.includes('qwen')) { | |
| 698 | + return qwen2Tokenizer; | |
| 699 | + } | |
| 694 | 700 | if (oai_settings.groq_model.includes('llama-3') || oai_settings.groq_model.includes('llama3')) { |
| 695 | 701 | return llama3Tokenizer; |
| 696 | 702 | } |
| @@ -563,6 +563,7 @@ export class ToolManager { | ||
| 563 | 563 | chat_completion_sources.OPENROUTER, |
| 564 | 564 | chat_completion_sources.GROQ, |
| 565 | 565 | chat_completion_sources.COHERE, |
| 566 | + chat_completion_sources.DEEPSEEK, | |
| 566 | 567 | ]; |
| 567 | 568 | return supportedSources.includes(oai_settings.chat_completion_source); |
| 568 | 569 | } |
| @@ -9,6 +9,9 @@ import { ensureImageFormatSupported, getBase64Async, humanFileSize } from './uti | ||
| 9 | 9 | export let currentUser = null; |
| 10 | 10 | export let accountsEnabled = false; |
| 11 | 11 | |
| 12 | +// Extend the session every 30 minutes | |
| 13 | +const SESSION_EXTEND_INTERVAL = 30 * 60 * 1000; | |
| 14 | + | |
| 12 | 15 | /** |
| 13 | 16 | * Enable or disable user account controls in the UI. |
| 14 | 17 | * @param {boolean} isEnabled User account controls enabled |
| @@ -44,6 +47,14 @@ export function isAdmin() { | ||
| 44 | 47 | } |
| 45 | 48 | |
| 46 | 49 | /** |
| 50 | + * Gets the handle string of the current user. | |
| 51 | + * @returns {string} User handle | |
| 52 | + */ | |
| 53 | +export function getCurrentUserHandle() { | |
| 54 | + return currentUser?.handle || 'default-user'; | |
| 55 | +} | |
| 56 | + | |
| 57 | +/** | |
| 47 | 58 | * Get the current user. |
| 48 | 59 | * @returns {Promise<void>} |
| 49 | 60 | */ |
| @@ -886,6 +897,24 @@ async function slugify(text) { | ||
| 886 | 897 | } |
| 887 | 898 | } |
| 888 | 899 | |
| 900 | +/** | |
| 901 | + * Pings the server to extend the user session. | |
| 902 | + */ | |
| 903 | +async function extendUserSession() { | |
| 904 | + try { | |
| 905 | + const response = await fetch('/api/ping?extend=1', { | |
| 906 | + method: 'GET', | |
| 907 | + headers: getRequestHeaders(), | |
| 908 | + }); | |
| 909 | + | |
| 910 | + if (!response.ok) { | |
| 911 | + throw new Error('Ping did not succeed', { cause: response.status }); | |
| 912 | + } | |
| 913 | + } catch (error) { | |
| 914 | + console.error('Failed to extend user session', error); | |
| 915 | + } | |
| 916 | +} | |
| 917 | + | |
| 889 | 918 | jQuery(() => { |
| 890 | 919 | $('#logout_button').on('click', () => { |
| 891 | 920 | logout(); |
| @@ -896,4 +925,9 @@ jQuery(() => { | ||
| 896 | 925 | $('#account_button').on('click', () => { |
| 897 | 926 | openUserProfile(); |
| 898 | 927 | }); |
| 928 | + setInterval(async () => { | |
| 929 | + if (currentUser) { | |
| 930 | + await extendUserSession(); | |
| 931 | + } | |
| 932 | + }, SESSION_EXTEND_INTERVAL); | |
| 899 | 933 | }); |
| @@ -0,0 +1,139 @@ | ||
| 1 | +import { saveSettingsDebounced } from '../../script.js'; | |
| 2 | + | |
| 3 | +const MIGRATED_MARKER = '__migrated'; | |
| 4 | +const MIGRATABLE_KEYS = [ | |
| 5 | + /^AlertRegex_/, | |
| 6 | + /^AlertWI_/, | |
| 7 | + /^Assets_SkipConfirm_/, | |
| 8 | + /^Characters_PerPage$/, | |
| 9 | + /^DataBank_sortField$/, | |
| 10 | + /^DataBank_sortOrder$/, | |
| 11 | + /^extension_update_nag$/, | |
| 12 | + /^extensions_sortByName$/, | |
| 13 | + /^FeatherlessModels_PerPage$/, | |
| 14 | + /^GroupMembers_PerPage$/, | |
| 15 | + /^GroupCandidates_PerPage$/, | |
| 16 | + /^LNavLockOn$/, | |
| 17 | + /^LNavOpened$/, | |
| 18 | + /^mediaWarningShown:/, | |
| 19 | + /^NavLockOn$/, | |
| 20 | + /^NavOpened$/, | |
| 21 | + /^Personas_PerPage$/, | |
| 22 | + /^Personas_GridView$/, | |
| 23 | + /^Proxy_SkipConfirm_/, | |
| 24 | + /^qr--executeShortcut$/, | |
| 25 | + /^qr--syntax$/, | |
| 26 | + /^qr--tabSize$/, | |
| 27 | + /^qr--wrap$/, | |
| 28 | + /^RegenerateWithCtrlEnter$/, | |
| 29 | + /^SelectedNavTab$/, | |
| 30 | + /^sendAsNamelessWarningShown$/, | |
| 31 | + /^StoryStringValidationCache$/, | |
| 32 | + /^WINavOpened$/, | |
| 33 | + /^WI_PerPage$/, | |
| 34 | + /^world_info_sort_order$/, | |
| 35 | +]; | |
| 36 | + | |
| 37 | +/** | |
| 38 | + * Provides access to account storage of arbitrary key-value pairs. | |
| 39 | + */ | |
| 40 | +class AccountStorage { | |
| 41 | + /** | |
| 42 | + * @type {Record<string, string>} Storage state | |
| 43 | + */ | |
| 44 | + #state = {}; | |
| 45 | + | |
| 46 | + /** | |
| 47 | + * @type {boolean} If the storage was initialized | |
| 48 | + */ | |
| 49 | + #ready = false; | |
| 50 | + | |
| 51 | + #migrateLocalStorage() { | |
| 52 | + const localStorageKeys = []; | |
| 53 | + for (let i = 0; i < globalThis.localStorage.length; i++) { | |
| 54 | + localStorageKeys.push(globalThis.localStorage.key(i)); | |
| 55 | + } | |
| 56 | + for (const key of localStorageKeys) { | |
| 57 | + if (MIGRATABLE_KEYS.some(k => k.test(key))) { | |
| 58 | + const value = globalThis.localStorage.getItem(key); | |
| 59 | + this.#state[key] = value; | |
| 60 | + globalThis.localStorage.removeItem(key); | |
| 61 | + } | |
| 62 | + } | |
| 63 | + } | |
| 64 | + | |
| 65 | + /** | |
| 66 | + * Initialize the account storage. | |
| 67 | + * @param {Object} state Initial state | |
| 68 | + */ | |
| 69 | + init(state) { | |
| 70 | + if (state && typeof state === 'object') { | |
| 71 | + this.#state = Object.assign(this.#state, state); | |
| 72 | + } | |
| 73 | + | |
| 74 | + if (!Object.hasOwn(this.#state, MIGRATED_MARKER)) { | |
| 75 | + this.#migrateLocalStorage(); | |
| 76 | + this.#state[MIGRATED_MARKER] = '1'; | |
| 77 | + saveSettingsDebounced(); | |
| 78 | + } | |
| 79 | + | |
| 80 | + this.#ready = true; | |
| 81 | + } | |
| 82 | + | |
| 83 | + /** | |
| 84 | + * Get the value of a key in account storage. | |
| 85 | + * @param {string} key Key to get | |
| 86 | + * @returns {string|null} Value of the key | |
| 87 | + */ | |
| 88 | + getItem(key) { | |
| 89 | + if (!this.#ready) { | |
| 90 | + console.warn(`AccountStorage not ready (trying to read from ${key})`); | |
| 91 | + } | |
| 92 | + | |
| 93 | + return Object.hasOwn(this.#state, key) ? String(this.#state[key]) : null; | |
| 94 | + } | |
| 95 | + | |
| 96 | + /** | |
| 97 | + * Set a key in account storage. | |
| 98 | + * @param {string} key Key to set | |
| 99 | + * @param {string} value Value to set | |
| 100 | + */ | |
| 101 | + setItem(key, value) { | |
| 102 | + if (!this.#ready) { | |
| 103 | + console.warn(`AccountStorage not ready (trying to write to ${key})`); | |
| 104 | + } | |
| 105 | + | |
| 106 | + this.#state[key] = String(value); | |
| 107 | + saveSettingsDebounced(); | |
| 108 | + } | |
| 109 | + | |
| 110 | + /** | |
| 111 | + * Remove a key from account storage. | |
| 112 | + * @param {string} key Key to remove | |
| 113 | + */ | |
| 114 | + removeItem(key) { | |
| 115 | + if (!this.#ready) { | |
| 116 | + console.warn(`AccountStorage not ready (trying to remove ${key})`); | |
| 117 | + } | |
| 118 | + | |
| 119 | + if (!Object.hasOwn(this.#state, key)) { | |
| 120 | + return; | |
| 121 | + } | |
| 122 | + | |
| 123 | + delete this.#state[key]; | |
| 124 | + saveSettingsDebounced(); | |
| 125 | + } | |
| 126 | + | |
| 127 | + /** | |
| 128 | + * Gets a snapshot of the storage state. | |
| 129 | + * @returns {Record<string, string>} A deep clone of the storage state | |
| 130 | + */ | |
| 131 | + getState() { | |
| 132 | + return structuredClone(this.#state); | |
| 133 | + } | |
| 134 | +} | |
| 135 | + | |
| 136 | +/** | |
| 137 | + * Account storage instance. | |
| 138 | + */ | |
| 139 | +export const accountStorage = new AccountStorage(); | |
| @@ -8,7 +8,7 @@ import { | ||
| 8 | 8 | import { getContext } from './extensions.js'; |
| 9 | 9 | import { characters, getRequestHeaders, this_chid } from '../script.js'; |
| 10 | 10 | import { isMobile } from './RossAscends-mods.js'; |
| 11 | 11 | import { collapseNewlines, power_user } from './power-user.js'; |
| 12 | 12 | import { debounce_timeout } from './constants.js'; |
| 13 | 13 | import { Popup, POPUP_RESULT, POPUP_TYPE } from './popup.js'; |
| 14 | 14 | import { SlashCommandClosure } from './slash-commands/SlashCommandClosure.js'; |
| @@ -677,6 +677,19 @@ export function sortByCssOrder(a, b) { | ||
| 677 | 677 | } |
| 678 | 678 | |
| 679 | 679 | /** |
| 680 | + * Trims leading and trailing whitespace from the input string based on a configuration setting. | |
| 681 | + * @param {string} input - The string to be trimmed | |
| 682 | + * @returns {string} The trimmed string if trimming is enabled; otherwise, returns the original string | |
| 683 | + */ | |
| 684 | + | |
| 685 | +export function trimSpaces(input) { | |
| 686 | + if (!input || typeof input !== 'string') { | |
| 687 | + return input; | |
| 688 | + } | |
| 689 | + return power_user.trim_spaces ? input.trim() : input; | |
| 690 | +} | |
| 691 | + | |
| 692 | +/** | |
| 680 | 693 | * Trims a string to the end of a nearest sentence. |
| 681 | 694 | * @param {string} input The string to trim. |
| 682 | 695 | * @returns {string} The trimmed string. |
| @@ -994,13 +1007,18 @@ export function getImageSizeFromDataURL(dataUrl) { | ||
| 994 | 1007 | }); |
| 995 | 1008 | } |
| 996 | 1009 | |
| 997 | -export function getCharaFilename(chid) { | |
| 1010 | +/** | |
| 1011 | + * Gets the filename of the character avatar without extension | |
| 1012 | + * @param {number?} [chid=null] - Character ID. If not provided, uses the current character ID | |
| 1013 | + * @param {object} [options={}] - Options arguments | |
| 1014 | + * @param {string?} [options.manualAvatarKey=null] - Manually take the following avatar key, instead of using the chid to determine the name | |
| 1015 | + * @returns {string?} The filename of the character avatar without extension, or null if the character ID is invalid | |
| 1016 | + */ | |
| 1017 | +export function getCharaFilename(chid = null, { manualAvatarKey = null } = {}) { | |
| 998 | 1018 | const context = getContext(); |
| 999 | 1019 | const fileName = manualAvatarKey ?? context.characters[chid ?? context.characterId]?.avatar; |
| 1000 | 1020 | |
| 1001 | - if (fileName) { | |
| 1021 | + return fileName?.replace(/\.[^/.]+$/, '') ?? null; | |
| 1002 | - return fileName.replace(/\.[^/.]+$/, ''); | |
| 1003 | - } | |
| 1004 | 1022 | } |
| 1005 | 1023 | |
| 1006 | 1024 | /** |
| @@ -1733,17 +1751,17 @@ export function hasAnimation(control) { | ||
| 1733 | 1751 | |
| 1734 | 1752 | /** |
| 1735 | 1753 | * Run an action once an animation on a control ends. If the control has no animation, the action will be executed immediately. |
| 1736 | - * | |
| 1754 | + * The action will be executed after the animation ends or after the timeout, whichever comes first. | |
| 1737 | 1755 | * @param {HTMLElement} control - The control element to listen for animation end event |
| 1738 | 1756 | * @param {(control:*?) => void} callback - The callback function to be executed when the animation ends |
| 1757 | + * @param {number} [timeout=500] - The timeout in milliseconds to wait for the animation to end before executing the callback | |
| 1739 | 1758 | */ |
| 1740 | 1759 | export function runAfterAnimation(control, callback, timeout = 500) { |
| 1741 | 1760 | if (hasAnimation(control)) { |
| 1742 | - const onAnimationEnd = () => { | |
| 1761 | + Promise.race([ | |
| 1743 | - control.removeEventListener('animationend', onAnimationEnd); | |
| 1762 | + new Promise((r) => setTimeout(r, timeout)), // Fallback timeout | |
| 1744 | - callback(control); | |
| 1763 | + new Promise((r) => control.addEventListener('animationend', r, { once: true })), | |
| 1745 | - }; | |
| 1764 | + ]).finally(() => callback(control)); | |
| 1746 | - control.addEventListener('animationend', onAnimationEnd); | |
| 1747 | 1765 | } else { |
| 1748 | 1766 | callback(control); |
| 1749 | 1767 | } |
| @@ -2059,6 +2077,23 @@ export function toggleDrawer(drawer, expand = true) { | ||
| 2059 | 2077 | } |
| 2060 | 2078 | } |
| 2061 | 2079 | |
| 2080 | +/** | |
| 2081 | + * Sets or removes a dataset property on an HTMLElement | |
| 2082 | + * | |
| 2083 | + * Utility function to make it easier to reset dataset properties on null, without them being "null" as value. | |
| 2084 | + * | |
| 2085 | + * @param {HTMLElement} element - The element to modify | |
| 2086 | + * @param {string} name - The name of the dataset property | |
| 2087 | + * @param {string|null} value - The value to set - If null, the dataset property will be removed | |
| 2088 | + */ | |
| 2089 | +export function setDatasetProperty(element, name, value) { | |
| 2090 | + if (value === null) { | |
| 2091 | + delete element.dataset[name]; | |
| 2092 | + } else { | |
| 2093 | + element.dataset[name] = value; | |
| 2094 | + } | |
| 2095 | +} | |
| 2096 | + | |
| 2062 | 2097 | export async function fetchFaFile(name) { |
| 2063 | 2098 | const style = document.createElement('style'); |
| 2064 | 2099 | style.innerHTML = await (await fetch(`/css/${name}`)).text(); |
| @@ -19,7 +19,7 @@ import { isFalseBoolean, convertValueType, isTrueBoolean } from './utils.js'; | ||
| 19 | 19 | |
| 20 | 20 | const MAX_LOOPS = 100; |
| 21 | 21 | |
| 22 | 22 | export function getLocalVariable(name, args = {}) { |
| 23 | 23 | if (!chat_metadata.variables) { |
| 24 | 24 | chat_metadata.variables = {}; |
| 25 | 25 | } |
| @@ -45,7 +45,7 @@ function getLocalVariable(name, args = {}) { | ||
| 45 | 45 | return (localVariable?.trim?.() === '' || isNaN(Number(localVariable))) ? (localVariable || '') : Number(localVariable); |
| 46 | 46 | } |
| 47 | 47 | |
| 48 | 48 | export function setLocalVariable(name, value, args = {}) { |
| 49 | 49 | if (!name) { |
| 50 | 50 | throw new Error('Variable name cannot be empty or undefined.'); |
| 51 | 51 | } |
| @@ -80,7 +80,7 @@ function setLocalVariable(name, value, args = {}) { | ||
| 80 | 80 | return value; |
| 81 | 81 | } |
| 82 | 82 | |
| 83 | 83 | export function getGlobalVariable(name, args = {}) { |
| 84 | 84 | let globalVariable = extension_settings.variables.global[args.key ?? name]; |
| 85 | 85 | if (args.index !== undefined) { |
| 86 | 86 | try { |
| @@ -102,7 +102,7 @@ function getGlobalVariable(name, args = {}) { | ||
| 102 | 102 | return (globalVariable?.trim?.() === '' || isNaN(Number(globalVariable))) ? (globalVariable || '') : Number(globalVariable); |
| 103 | 103 | } |
| 104 | 104 | |
| 105 | 105 | export function setGlobalVariable(name, value, args = {}) { |
| 106 | 106 | if (!name) { |
| 107 | 107 | throw new Error('Variable name cannot be empty or undefined.'); |
| 108 | 108 | } |
| @@ -21,6 +21,7 @@ import { callGenericPopup, Popup, POPUP_TYPE } from './popup.js'; | ||
| 21 | 21 | import { StructuredCloneMap } from './util/StructuredCloneMap.js'; |
| 22 | 22 | import { renderTemplateAsync } from './templates.js'; |
| 23 | 23 | import { t } from './i18n.js'; |
| 24 | +import { accountStorage } from './util/AccountStorage.js'; | |
| 24 | 25 | |
| 25 | 26 | export const world_info_insertion_strategy = { |
| 26 | 27 | evenly: 0, |
| @@ -400,6 +401,12 @@ class WorldInfoTimedEffects { | ||
| 400 | 401 | #entries = []; |
| 401 | 402 | |
| 402 | 403 | /** |
| 404 | + * Is this a dry run? | |
| 405 | + * @type {boolean} | |
| 406 | + */ | |
| 407 | + #isDryRun = false; | |
| 408 | + | |
| 409 | + /** | |
| 403 | 410 | * Buffer for active timed effects. |
| 404 | 411 | * @type {Record<TimedEffectType, WIScanEntry[]>} |
| 405 | 412 | */ |
| @@ -448,10 +455,12 @@ class WorldInfoTimedEffects { | ||
| 448 | 455 | * Initialize the timed effects with the given messages. |
| 449 | 456 | * @param {string[]} chat Array of chat messages |
| 450 | 457 | * @param {WIScanEntry[]} entries Array of entries |
| 458 | + * @param {boolean} isDryRun Whether the operation is a dry run | |
| 451 | 459 | */ |
| 452 | 460 | constructor(chat, entries, isDryRun = false) { |
| 453 | 461 | this.#chat = chat; |
| 454 | 462 | this.#entries = entries; |
| 463 | + this.#isDryRun = isDryRun; | |
| 455 | 464 | this.#ensureChatMetadata(); |
| 456 | 465 | } |
| 457 | 466 | |
| @@ -583,8 +592,10 @@ class WorldInfoTimedEffects { | ||
| 583 | 592 | * Checks for timed effects on chat messages. |
| 584 | 593 | */ |
| 585 | 594 | checkTimedEffects() { |
| 595 | + if (!this.#isDryRun) { | |
| 586 | 596 | this.#checkTimedEffectOfType('sticky', this.#buffer.sticky, this.#onEnded.sticky.bind(this)); |
| 587 | 597 | this.#checkTimedEffectOfType('cooldown', this.#buffer.cooldown, this.#onEnded.cooldown.bind(this)); |
| 598 | + } | |
| 588 | 599 | this.#checkDelayEffect(this.#buffer.delay); |
| 589 | 600 | } |
| 590 | 601 | |
| @@ -629,6 +640,7 @@ class WorldInfoTimedEffects { | ||
| 629 | 640 | * @param {WIScanEntry[]} activatedEntries Entries that were activated |
| 630 | 641 | */ |
| 631 | 642 | setTimedEffects(activatedEntries) { |
| 643 | + if (this.#isDryRun) return; | |
| 632 | 644 | for (const entry of activatedEntries) { |
| 633 | 645 | this.#setTimedEffectOfType('sticky', entry); |
| 634 | 646 | this.#setTimedEffectOfType('cooldown', entry); |
| @@ -645,6 +657,9 @@ class WorldInfoTimedEffects { | ||
| 645 | 657 | if (!this.isValidEffectType(type)) { |
| 646 | 658 | return; |
| 647 | 659 | } |
| 660 | + if (this.#isDryRun && type !== 'delay') { | |
| 661 | + return; | |
| 662 | + } | |
| 648 | 663 | |
| 649 | 664 | const key = this.#getEntryKey(entry); |
| 650 | 665 | delete chat_metadata.timedWorldInfo[type][key]; |
| @@ -858,7 +873,7 @@ export function setWorldInfoSettings(settings, data) { | ||
| 858 | 873 | $('#world_editor_select').append(`<option value='${i}'>${item}</option>`); |
| 859 | 874 | }); |
| 860 | 875 | |
| 861 | 876 | $('#world_info_sort_order').val(localStorageaccountStorage.getItem(SORT_ORDER_KEY) || '0'); |
| 862 | 877 | $('#world_info').trigger('change'); |
| 863 | 878 | $('#world_editor_select').trigger('change'); |
| 864 | 879 | |
| @@ -1708,7 +1723,7 @@ export async function loadWorldInfo(name) { | ||
| 1708 | 1723 | return null; |
| 1709 | 1724 | } |
| 1710 | 1725 | |
| 1711 | 1726 | export async function updateWorldInfoList() { |
| 1712 | 1727 | const result = await fetch('/api/settings/get', { |
| 1713 | 1728 | method: 'POST', |
| 1714 | 1729 | headers: getRequestHeaders(), |
| @@ -1933,13 +1948,13 @@ function displayWorldEntries(name, data, navigation = navigation_option.none, fl | ||
| 1933 | 1948 | if (typeof navigation === 'number' && Number(navigation) >= 0) { |
| 1934 | 1949 | const data = getDataArray(); |
| 1935 | 1950 | const uidIndex = data.findIndex(x => x.uid === navigation); |
| 1936 | 1951 | const perPage = Number(localStorageaccountStorage.getItem(storageKey)) || perPageDefault; |
| 1937 | 1952 | startPage = Math.floor(uidIndex / perPage) + 1; |
| 1938 | 1953 | } |
| 1939 | 1954 | |
| 1940 | 1955 | $('#world_info_pagination').pagination({ |
| 1941 | 1956 | dataSource: getDataArray, |
| 1942 | 1957 | pageSize: Number(localStorageaccountStorage.getItem(storageKey)) || perPageDefault, |
| 1943 | 1958 | sizeChangerOptions: [10, 25, 50, 100, 500, 1000], |
| 1944 | 1959 | showSizeChanger: true, |
| 1945 | 1960 | pageRange: 1, |
| @@ -1969,7 +1984,7 @@ function displayWorldEntries(name, data, navigation = navigation_option.none, fl | ||
| 1969 | 1984 | worldEntriesList.append(blocks); |
| 1970 | 1985 | }, |
| 1971 | 1986 | afterSizeSelectorChange: function (e) { |
| 1972 | 1987 | localStorageaccountStorage.setItem(storageKey, e.target.value); |
| 1973 | 1988 | }, |
| 1974 | 1989 | afterPaging: function () { |
| 1975 | 1990 | $('#world_popup_entries_list textarea[name="comment"]').each(function () { |
| @@ -2174,7 +2189,7 @@ function verifyWorldInfoSearchSortRule() { | ||
| 2174 | 2189 | // If search got cleared, we make sure to hide the option and go back to the one before |
| 2175 | 2190 | if (!searchTerm && !isHidden) { |
| 2176 | 2191 | searchOption.attr('hidden', ''); |
| 2177 | 2192 | selector.val(localStorageaccountStorage.getItem(SORT_ORDER_KEY) || '0'); |
| 2178 | 2193 | } |
| 2179 | 2194 | } |
| 2180 | 2195 | |
| @@ -2423,7 +2438,9 @@ export async function getWorldEntry(name, data, entry) { | ||
| 2423 | 2438 | setWIOriginalDataValue(data, uid, originalDataValueName, data.entries[uid][entryPropName]); |
| 2424 | 2439 | await saveWorldInfo(name, data); |
| 2425 | 2440 | } |
| 2441 | + $(this).toggleClass('empty', !data.entries[uid][entryPropName].length); | |
| 2426 | 2442 | }); |
| 2443 | + input.toggleClass('empty', !entry[entryPropName].length); | |
| 2427 | 2444 | input.on('select2:select', /** @type {function(*):void} */ event => updateWorldEntryKeyOptionsCache([event.params.data])); |
| 2428 | 2445 | input.on('select2:unselect', /** @type {function(*):void} */ event => updateWorldEntryKeyOptionsCache([event.params.data], { remove: true })); |
| 2429 | 2446 | |
| @@ -2458,6 +2475,7 @@ export async function getWorldEntry(name, data, entry) { | ||
| 2458 | 2475 | data.entries[uid][entryPropName] = splitKeywordsAndRegexes(value); |
| 2459 | 2476 | setWIOriginalDataValue(data, uid, originalDataValueName, data.entries[uid][entryPropName]); |
| 2460 | 2477 | await saveWorldInfo(name, data); |
| 2478 | + $(this).toggleClass('empty', !data.entries[uid][entryPropName].length); | |
| 2461 | 2479 | } |
| 2462 | 2480 | }); |
| 2463 | 2481 | input.val(entry[entryPropName].join(', ')).trigger('input', { skipReset: true }); |
| @@ -3435,7 +3453,7 @@ async function _save(name, data) { | ||
| 3435 | 3453 | headers: getRequestHeaders(), |
| 3436 | 3454 | body: JSON.stringify({ name: name, data: data }), |
| 3437 | 3455 | }); |
| 3438 | 3456 | await eventSource.emit(event_types.WORLDINFO_UPDATED, name, data); |
| 3439 | 3457 | } |
| 3440 | 3458 | |
| 3441 | 3459 | |
| @@ -3847,7 +3865,7 @@ export async function checkWorldInfo(chat, maxContext, isDryRun) { | ||
| 3847 | 3865 | const context = getContext(); |
| 3848 | 3866 | const buffer = new WorldInfoBuffer(chat); |
| 3849 | 3867 | |
| 3850 | 3868 | console.debug(`[WI] --- START WI SCAN (on ${chat.length} messages)${isDryRun ? ' (DRY RUN)' : ''} ---`); |
| 3851 | 3869 | |
| 3852 | 3870 | // Combine the chat |
| 3853 | 3871 | |
| @@ -3879,9 +3897,9 @@ export async function checkWorldInfo(chat, maxContext, isDryRun) { | ||
| 3879 | 3897 | |
| 3880 | 3898 | console.debug(`[WI] Context size: ${maxContext}; WI budget: ${budget} (max% = ${world_info_budget}%, cap = ${world_info_budget_cap})`); |
| 3881 | 3899 | const sortedEntries = await getSortedEntries(); |
| 3882 | 3900 | const timedEffects = new WorldInfoTimedEffects(chat, sortedEntries, isDryRun); |
| 3883 | 3901 | |
| 3884 | 3902 | !isDryRun && timedEffects.checkTimedEffects(); |
| 3885 | 3903 | |
| 3886 | 3904 | if (sortedEntries.length === 0) { |
| 3887 | 3905 | return { worldInfoBefore: '', worldInfoAfter: '', WIDepthEntries: [], EMEntries: [], allActivatedEntries: new Set() }; |
| @@ -4324,12 +4342,12 @@ export async function checkWorldInfo(chat, maxContext, isDryRun) { | ||
| 4324 | 4342 | context.setExtensionPrompt(NOTE_MODULE_NAME, ANWithWI, chat_metadata[metadata_keys.position], chat_metadata[metadata_keys.depth], extension_settings.note.allowWIScan, chat_metadata[metadata_keys.role]); |
| 4325 | 4343 | } |
| 4326 | 4344 | |
| 4327 | 4345 | !isDryRun && timedEffects.setTimedEffects(Array.from(allActivatedEntries.values())); |
| 4328 | 4346 | buffer.resetExternalEffects(); |
| 4329 | 4347 | timedEffects.cleanUp(); |
| 4330 | 4348 | |
| 4331 | 4349 | console.log(`[WI] ${isDryRun ? 'Hypothetically adding' : 'Adding'} ${allActivatedEntries.size} entries to prompt`, Array.from(allActivatedEntries.values())); |
| 4332 | 4350 | console.debug('`[WI] --- DONE${isDryRun ? ' (DRY RUN)' : ''} ---'`); |
| 4333 | 4351 | |
| 4334 | 4352 | return { worldInfoBefore, worldInfoAfter, EMEntries, WIDepthEntries, allActivatedEntries: new Set(allActivatedEntries.values()) }; |
| 4335 | 4353 | } |
| @@ -4658,7 +4676,7 @@ function convertNovelLorebook(inputObj) { | ||
| 4658 | 4676 | return outputObj; |
| 4659 | 4677 | } |
| 4660 | 4678 | |
| 4661 | 4679 | export function convertCharacterBook(characterBook) { |
| 4662 | 4680 | const result = { entries: {}, originalData: characterBook }; |
| 4663 | 4681 | |
| 4664 | 4682 | characterBook.entries.forEach((entry, index) => { |
| @@ -4736,8 +4754,8 @@ export function checkEmbeddedWorld(chid) { | ||
| 4736 | 4754 | // Only show the alert once per character |
| 4737 | 4755 | const checkKey = `AlertWI_${characters[chid].avatar}`; |
| 4738 | 4756 | const worldName = characters[chid]?.data?.extensions?.world; |
| 4739 | 4757 | if (!localStorageaccountStorage.getItem(checkKey) && (!worldName || !world_names.includes(worldName))) { |
| 4740 | 4758 | localStorageaccountStorage.setItem(checkKey, 'true'); |
| 4741 | 4759 | |
| 4742 | 4760 | if (power_user.world_import_dialog) { |
| 4743 | 4761 | const html = `<h3>This character has an embedded World/Lorebook.</h3> |
| @@ -5181,7 +5199,7 @@ jQuery(() => { | ||
| 5181 | 5199 | $('#world_info_sort_order').on('change', function () { |
| 5182 | 5200 | const value = String($(this).find(':selected').val()); |
| 5183 | 5201 | // Save sort order, but do not save search sorting, as this is a temporary sorting option |
| 5184 | 5202 | if (value !== 'search') localStorageaccountStorage.setItem(SORT_ORDER_KEY, value); |
| 5185 | 5203 | updateEditor(navigation_option.none); |
| 5186 | 5204 | }); |
| 5187 | 5205 | |
| @@ -55,6 +55,10 @@ | ||
| 55 | 55 | --interactable-outline-color: var(--white100); |
| 56 | 56 | --interactable-outline-color-faint: var(--white20a); |
| 57 | 57 | |
| 58 | + --reasoning-body-color: var(--SmartThemeEmColor); | |
| 59 | + --reasoning-em-color: color-mix(in srgb, var(--SmartThemeEmColor) 67%, var(--SmartThemeBlurTintColor) 33%); | |
| 60 | + --reasoning-saturation: 0.5; | |
| 61 | + | |
| 58 | 62 | |
| 59 | 63 | /*Default Theme, will be changed by ToolCool Color Picker*/ |
| 60 | 64 | --SmartThemeBodyColor: rgb(220, 220, 210); |
| @@ -106,6 +110,8 @@ | ||
| 106 | 110 | --tool-cool-color-picker-btn-bg: transparent; |
| 107 | 111 | --tool-cool-color-picker-btn-border-color: transparent; |
| 108 | 112 | |
| 113 | + --mes-right-spacing: 30px; | |
| 114 | + | |
| 109 | 115 | --avatar-base-height: 50px; |
| 110 | 116 | --avatar-base-width: 50px; |
| 111 | 117 | --avatar-base-border-radius: 2px; |
| @@ -291,6 +297,10 @@ input[type='checkbox']:focus-visible { | ||
| 291 | 297 | color: var(--SmartThemeEmColor); |
| 292 | 298 | } |
| 293 | 299 | |
| 300 | +.tokenItemizingMaintext { | |
| 301 | + font-size: calc(var(--mainFontSize) * 0.8); | |
| 302 | +} | |
| 303 | + | |
| 294 | 304 | .tokenGraph { |
| 295 | 305 | border-radius: 10px; |
| 296 | 306 | border: 1px solid var(--SmartThemeBorderColor); |
| @@ -373,18 +383,56 @@ input[type='checkbox']:focus-visible { | ||
| 373 | 383 | |
| 374 | 384 | .mes_reasoning { |
| 375 | 385 | display: block; |
| 376 | 386 | border-left: 1px2px solid var(--SmartThemeBorderColorreasoning-body-color); |
| 377 | - background-color: var(--black30a); | |
| 387 | + border-radius: 2px; | |
| 378 | - border-radius: 5px; | |
| 379 | 388 | padding: 5px; |
| 380 | 389 | marginpadding-left: 5px 014px; |
| 390 | + margin-bottom: 0.5em; | |
| 381 | 391 | overflow-y: auto; |
| 392 | + color: hsl(from var(--reasoning-body-color) h calc(s * var(--reasoning-saturation)) l); | |
| 393 | +} | |
| 394 | + | |
| 395 | +.mes_reasoning_details { | |
| 396 | + margin-right: var(--mes-right-spacing); | |
| 382 | 397 | } |
| 383 | 398 | |
| 384 | 399 | .mes_reasoning_details .mes_reasoning_summary { |
| 400 | + list-style: none; | |
| 401 | + margin-right: calc(var(--mes-right-spacing) * -1); | |
| 402 | +} | |
| 403 | + | |
| 404 | +.mes_reasoning_details summary::-webkit-details-marker { | |
| 405 | + display: none; | |
| 406 | +} | |
| 407 | + | |
| 408 | +.mes_reasoning *:last-child { | |
| 409 | + margin-bottom: 0; | |
| 410 | +} | |
| 411 | + | |
| 412 | +.mes_reasoning_header_block { | |
| 413 | + flex-grow: 1; | |
| 414 | +} | |
| 415 | + | |
| 416 | +.mes_reasoning_header { | |
| 385 | 417 | cursor: pointer; |
| 386 | 418 | position: relative; |
| 387 | 419 | marginuser-select: 2pxnone; |
| 420 | + margin: 0.5em 2px; | |
| 421 | + padding: 7px 14px; | |
| 422 | + padding-right: calc(0.7em + 14px); | |
| 423 | + border-radius: 5px; | |
| 424 | + background-color: var(--grey30); | |
| 425 | + font-size: calc(var(--mainFontSize) * 0.9); | |
| 426 | + align-items: baseline; | |
| 427 | +} | |
| 428 | + | |
| 429 | +.mes:has(.mes_reasoning:empty) .mes_reasoning_header { | |
| 430 | + cursor: default; | |
| 431 | +} | |
| 432 | + | |
| 433 | +/* TWIMC: Remove with custom CSS to show the icon */ | |
| 434 | +.mes_reasoning_header>.icon-svg { | |
| 435 | + display: none; | |
| 388 | 436 | } |
| 389 | 437 | |
| 390 | 438 | @supports not selector(:has(*)) { |
| @@ -394,29 +442,41 @@ input[type='checkbox']:focus-visible { | ||
| 394 | 442 | } |
| 395 | 443 | |
| 396 | 444 | .mes_bias:empty, |
| 397 | -.mes_reasoning:empty, | |
| 445 | +.mes:not(.reasoning) .mes_reasoning_details, | |
| 398 | -.mes_reasoning_details:has(.mes_reasoning:empty), | |
| 399 | -.mes_block:has(.edit_textarea) .mes_reasoning_details, | |
| 400 | 446 | .mes_reasoning_details:not([open]) .mes_reasoning_actions, |
| 401 | 447 | .mes_reasoning_details:has(.reasoning_edit_textarea) .mes_reasoning, |
| 402 | 448 | .mes_reasoning_details:not(:has(.reasoning_edit_textarea)) .mes_reasoning_actions .mes_button.mes_reasoning_edit_donemes_reasoning_header, |
| 403 | 449 | .mes_reasoning_details:not(:has(.reasoning_edit_textarea)) .mes_reasoning_actions .mes_button:not(.mes_reasoning_edit_canceledit_button), |
| 404 | 450 | .mes_reasoning_details:not(:has(.reasoning_edit_textarea)) .mes_reasoning_actions .mes_button:not(.mes_reasoning_edit_doneedit_button, .mes_reasoning_edit_cancel) { |
| 451 | +.mes_block:has(.edit_textarea):has(.reasoning_edit_textarea) .mes_reasoning_actions, | |
| 452 | +.mes.reasoning:not([data-reasoning-state="hidden"]) .mes_edit_add_reasoning, | |
| 453 | +.mes:has(.mes_reasoning:empty) .mes_reasoning_arrow, | |
| 454 | +.mes:has(.mes_reasoning:empty) .mes_reasoning, | |
| 455 | +.mes:has(.mes_reasoning:empty) .mes_reasoning_copy { | |
| 405 | 456 | display: none; |
| 406 | 457 | } |
| 407 | 458 | |
| 408 | -.mes_reasoning_actions { | |
| 459 | +.mes[data-reasoning-state="hidden"] .mes_edit_add_reasoning { | |
| 460 | + background-color: color-mix(in srgb, var(--SmartThemeQuoteColor) 33%, var(--SmartThemeBlurTintColor) 66%); | |
| 461 | +} | |
| 462 | + | |
| 463 | +/** If hidden reasoning should not be shown, we hide all blocks that don't have content */ | |
| 464 | +#chat:not([data-show-hidden-reasoning="true"]):not(:has(.reasoning_edit_textarea)) .mes:has(.mes_reasoning:empty) .mes_reasoning_details { | |
| 465 | + display: none; | |
| 466 | +} | |
| 467 | + | |
| 468 | +.mes_reasoning_details .mes_reasoning_arrow { | |
| 409 | 469 | position: absolute; |
| 410 | 470 | righttop: 050%; |
| 411 | 471 | topright: 07px; |
| 472 | + transform: translateY(-50%); | |
| 473 | + font-size: calc(var(--mainFontSize) * 0.7); | |
| 474 | + width: calc(var(--mainFontSize) * 0.7); | |
| 475 | + height: calc(var(--mainFontSize) * 0.7); | |
| 476 | +} | |
| 412 | 477 | |
| 413 | - display: flex; | |
| 478 | +.mes_reasoning_details:not([open]) .mes_reasoning_arrow { | |
| 414 | - gap: 4px; | |
| 479 | + transform: translateY(-50%) rotate(180deg); | |
| 415 | - flex-wrap: nowrap; | |
| 416 | - justify-content: flex-end; | |
| 417 | - transition: all 200ms; | |
| 418 | - overflow-x: hidden; | |
| 419 | - padding: 1px; | |
| 420 | 480 | } |
| 421 | 481 | |
| 422 | 482 | .mes_reasoning_summary>span { |
| @@ -424,21 +484,36 @@ input[type='checkbox']:focus-visible { | ||
| 424 | 484 | } |
| 425 | 485 | |
| 426 | 486 | .mes_text i, |
| 427 | 487 | .mes_text em, { |
| 488 | + color: var(--SmartThemeEmColor); | |
| 489 | +} | |
| 428 | 490 | .mes_reasoning i, |
| 429 | 491 | .mes_reasoning em { |
| 430 | - color: var(--SmartThemeEmColor); | |
| 492 | + color: hsl(from var(--reasoning-em-color) h calc(s * var(--reasoning-saturation)) l); | |
| 431 | 493 | } |
| 432 | 494 | |
| 433 | 495 | .mes_text uq i, |
| 434 | 496 | .mes_reasoningmes_text uq em { |
| 497 | + color: inherit; | |
| 498 | +} | |
| 499 | +.mes_reasoning q i, | |
| 500 | +.mes_reasoning q em { | |
| 501 | + color: hsl(from var(--SmartThemeQuoteColor) h calc(s * var(--reasoning-saturation)) l); | |
| 502 | +} | |
| 503 | + | |
| 504 | +.mes_text u { | |
| 435 | 505 | color: var(--SmartThemeUnderlineColor); |
| 436 | 506 | } |
| 507 | +.mes_reasoning u { | |
| 508 | + color: hsl(from var(--SmartThemeUnderlineColor) h calc(s * var(--reasoning-saturation)) l); | |
| 509 | +} | |
| 437 | 510 | |
| 438 | 511 | .mes_text q, { |
| 439 | -.mes_reasoning q { | |
| 440 | 512 | color: var(--SmartThemeQuoteColor); |
| 441 | 513 | } |
| 514 | +.mes_reasoning q { | |
| 515 | + color: hsl(from var(--SmartThemeQuoteColor) h calc(s * var(--reasoning-saturation)) l); | |
| 516 | +} | |
| 442 | 517 | |
| 443 | 518 | .mes_text font[color] em, |
| 444 | 519 | .mes_text font[color] i, |
| @@ -1126,13 +1201,8 @@ body .panelControlBar { | ||
| 1126 | 1201 | /*only affects bubblechat to make it sit nicely at the bottom*/ |
| 1127 | 1202 | } |
| 1128 | 1203 | |
| 1129 | 1204 | .last_mes:has(.mes_text:empty):has(.mes_reasoning_details[open]) .mes_reasoning:not(:empty) { |
| 1130 | 1205 | margin-bottom: 30pxvar(--mes-right-spacing); |
| 1131 | -} | |
| 1132 | - | |
| 1133 | -.last_mes .mes_reasoning, | |
| 1134 | -.last_mes .mes_text { | |
| 1135 | - padding-right: 30px; | |
| 1136 | 1206 | } |
| 1137 | 1207 | |
| 1138 | 1208 | /* SWIPE RELATED STYLES*/ |
| @@ -1363,6 +1433,7 @@ body.swipeAllMessages .mes:not(.last_mes) .swipes-counter { | ||
| 1363 | 1433 | padding-left: 0; |
| 1364 | 1434 | padding-top: 5px; |
| 1365 | 1435 | padding-bottom: 5px; |
| 1436 | + padding-right: var(--mes-right-spacing); | |
| 1366 | 1437 | } |
| 1367 | 1438 | |
| 1368 | 1439 | br { |
| @@ -2849,9 +2920,8 @@ select option:not(:checked) { | ||
| 2849 | 2920 | color: var(--active) !important; |
| 2850 | 2921 | } |
| 2851 | 2922 | |
| 2852 | 2923 | #instruct_enabled_label .menu_button.togglable:not(.toggleEnabled), { |
| 2853 | -#sysprompt_enabled_label .menu_button:not(.toggleEnabled) { | |
| 2924 | + color: red; | |
| 2854 | - color: Red; | |
| 2855 | 2925 | } |
| 2856 | 2926 | |
| 2857 | 2927 | .displayBlock { |
| @@ -3048,6 +3118,7 @@ input[type=search]:focus::-webkit-search-cancel-button { | ||
| 3048 | 3118 | .mes_block .ch_name { |
| 3049 | 3119 | max-width: 100%; |
| 3050 | 3120 | min-height: 22px; |
| 3121 | + align-items: flex-start; | |
| 3051 | 3122 | } |
| 3052 | 3123 | |
| 3053 | 3124 | /*applies to both groups and solos chars in the char list*/ |
| @@ -4275,7 +4346,13 @@ input[type="range"]::-webkit-slider-thumb { | ||
| 4275 | 4346 | transition: 0.3s ease-in-out; |
| 4276 | 4347 | } |
| 4277 | 4348 | |
| 4278 | 4349 | .mes_edit_buttons .menu_buttonmes_reasoning_actions { |
| 4350 | + margin: 0; | |
| 4351 | + margin-top: 0.5em; | |
| 4352 | +} | |
| 4353 | + | |
| 4354 | +.mes_edit_buttons .menu_button, | |
| 4355 | +.mes_reasoning_actions .edit_button { | |
| 4279 | 4356 | opacity: 0.5; |
| 4280 | 4357 | padding: 0px; |
| 4281 | 4358 | font-size: 1rem; |
| @@ -4288,6 +4365,12 @@ input[type="range"]::-webkit-slider-thumb { | ||
| 4288 | 4365 | align-items: center; |
| 4289 | 4366 | } |
| 4290 | 4367 | |
| 4368 | +.mes_reasoning_actions .edit_button { | |
| 4369 | + margin-bottom: 0.5em; | |
| 4370 | + opacity: 1; | |
| 4371 | + filter: brightness(0.7); | |
| 4372 | +} | |
| 4373 | + | |
| 4291 | 4374 | .mes_reasoning_edit_cancel, |
| 4292 | 4375 | .mes_edit_cancel.menu_button { |
| 4293 | 4376 | background-color: var(--crimson70a); |
| @@ -4314,6 +4397,14 @@ input[type="range"]::-webkit-slider-thumb { | ||
| 4314 | 4397 | field-sizing: content; |
| 4315 | 4398 | } |
| 4316 | 4399 | |
| 4400 | +body[data-generating="true"] #send_but, | |
| 4401 | +body[data-generating="true"] #mes_continue, | |
| 4402 | +body[data-generating="true"] #mes_impersonate, | |
| 4403 | +body[data-generating="true"] #chat .last_mes .mes_buttons, | |
| 4404 | +body[data-generating="true"] #chat .last_mes .mes_reasoning_actions { | |
| 4405 | + display: none; | |
| 4406 | +} | |
| 4407 | + | |
| 4317 | 4408 | #anchor_order { |
| 4318 | 4409 | margin-bottom: 15px; |
| 4319 | 4410 | } |
| @@ -4653,23 +4744,6 @@ body .ui-widget-content li:hover { | ||
| 4653 | 4744 | opacity: 1; |
| 4654 | 4745 | } |
| 4655 | 4746 | |
| 4656 | -.typing_indicator { | |
| 4657 | - position: sticky; | |
| 4658 | - bottom: 10px; | |
| 4659 | - margin: 10px; | |
| 4660 | - opacity: 0.85; | |
| 4661 | - text-shadow: 0px 0px calc(var(--shadowWidth) * 1px) var(--SmartThemeShadowColor); | |
| 4662 | - order: 9999; | |
| 4663 | -} | |
| 4664 | - | |
| 4665 | -.typing_indicator:after { | |
| 4666 | - display: inline-block; | |
| 4667 | - vertical-align: bottom; | |
| 4668 | - animation: ellipsis steps(4, end) 1500ms infinite; | |
| 4669 | - content: ""; | |
| 4670 | - width: 0px; | |
| 4671 | -} | |
| 4672 | - | |
| 4673 | 4747 | #group_avatar_preview .missing-avatar { |
| 4674 | 4748 | display: inline; |
| 4675 | 4749 | vertical-align: middle; |
| @@ -5758,11 +5832,13 @@ body:not(.movingUI) .drawer-content.maximized { | ||
| 5758 | 5832 | overflow-wrap: anywhere; |
| 5759 | 5833 | } |
| 5760 | 5834 | |
| 5835 | +#SystemPromptColumn summary, | |
| 5761 | 5836 | #InstructSequencesColumn summary { |
| 5762 | 5837 | font-size: 0.95em; |
| 5763 | 5838 | cursor: pointer; |
| 5764 | 5839 | } |
| 5765 | 5840 | |
| 5841 | +#SystemPromptColumn details, | |
| 5766 | 5842 | #InstructSequencesColumn details:not(:last-of-type) { |
| 5767 | 5843 | margin-bottom: 5px; |
| 5768 | 5844 | } |
| @@ -5927,6 +6003,18 @@ body:not(.movingUI) .drawer-content.maximized { | ||
| 5927 | 6003 | flex: 1; |
| 5928 | 6004 | } |
| 5929 | 6005 | |
| 6006 | +.oneline-dropdown label { | |
| 6007 | + margin-top: 3px; | |
| 6008 | + margin-bottom: 5px; | |
| 6009 | + flex-grow: 1; | |
| 6010 | + text-align: left; | |
| 6011 | +} | |
| 6012 | + | |
| 6013 | +.oneline-dropdown select { | |
| 6014 | + min-width: fit-content; | |
| 6015 | + width: 40%; | |
| 6016 | +} | |
| 6017 | + | |
| 5930 | 6018 | .multiline { |
| 5931 | 6019 | white-space: pre-wrap; |
| 5932 | 6020 | } |
| @@ -4,6 +4,7 @@ | ||
| 4 | 4 | import fs from 'node:fs'; |
| 5 | 5 | import http from 'node:http'; |
| 6 | 6 | import https from 'node:https'; |
| 7 | +import os from 'os'; | |
| 7 | 8 | import path from 'node:path'; |
| 8 | 9 | import util from 'node:util'; |
| 9 | 10 | import net from 'node:net'; |
| @@ -29,6 +30,7 @@ import bodyParser from 'body-parser'; | ||
| 29 | 30 | |
| 30 | 31 | // net related library imports |
| 31 | 32 | import fetch from 'node-fetch'; |
| 33 | +import ipRegex from 'ip-regex'; | |
| 32 | 34 | |
| 33 | 35 | // Unrestrict console logs display limit |
| 34 | 36 | util.inspect.defaultOptions.maxArrayLength = null; |
| @@ -56,8 +58,10 @@ import { | ||
| 56 | 58 | import getWebpackServeMiddleware from './src/middleware/webpack-serve.js'; |
| 57 | 59 | import basicAuthMiddleware from './src/middleware/basicAuth.js'; |
| 58 | 60 | import whitelistMiddleware from './src/middleware/whitelist.js'; |
| 61 | +import accessLoggerMiddleware, { getAccessLogPath, migrateAccessLog } from './src/middleware/accessLogWriter.js'; | |
| 59 | 62 | import multerMonkeyPatch from './src/middleware/multerMonkeyPatch.js'; |
| 60 | 63 | import initRequestProxy from './src/request-proxy.js'; |
| 64 | +import getCacheBusterMiddleware from './src/middleware/cacheBuster.js'; | |
| 61 | 65 | import { |
| 62 | 66 | getVersion, |
| 63 | 67 | getConfigValue, |
| @@ -65,7 +69,11 @@ import { | ||
| 65 | 69 | forwardFetchResponse, |
| 66 | 70 | removeColorFormatting, |
| 67 | 71 | getSeparator, |
| 72 | + stringToBool, | |
| 73 | + urlHostnameToIPv6, | |
| 74 | + canResolve, | |
| 68 | 75 | safeReadFileSync, |
| 76 | + setupLogLevel, | |
| 69 | 77 | } from './src/util.js'; |
| 70 | 78 | import { UPLOADS_DIRECTORY } from './src/constants.js'; |
| 71 | 79 | import { ensureThumbnailCache } from './src/endpoints/thumbnails.js'; |
| @@ -125,6 +133,8 @@ if (process.versions && process.versions.node && process.versions.node.match(/20 | ||
| 125 | 133 | const DEFAULT_PORT = 8000; |
| 126 | 134 | const DEFAULT_AUTORUN = false; |
| 127 | 135 | const DEFAULT_LISTEN = false; |
| 136 | +const DEFAULT_LISTEN_ADDRESS_IPV6 = '[::]'; | |
| 137 | +const DEFAULT_LISTEN_ADDRESS_IPV4 = '0.0.0.0'; | |
| 128 | 138 | const DEFAULT_CORS_PROXY = false; |
| 129 | 139 | const DEFAULT_WHITELIST = true; |
| 130 | 140 | const DEFAULT_ACCOUNTS = false; |
| @@ -149,11 +159,11 @@ const DEFAULT_PROXY_BYPASS = []; | ||
| 149 | 159 | const cliArguments = yargs(hideBin(process.argv)) |
| 150 | 160 | .usage('Usage: <your-start-script> <command> [options]') |
| 151 | 161 | .option('enableIPv6', { |
| 152 | 162 | type: 'booleanstring', |
| 153 | 163 | default: null, |
| 154 | 164 | describe: `Enables IPv6.\n[config default: ${DEFAULT_ENABLE_IPV6}]`, |
| 155 | 165 | }).option('enableIPv4', { |
| 156 | 166 | type: 'booleanstring', |
| 157 | 167 | default: null, |
| 158 | 168 | describe: `Enables IPv4.\n[config default: ${DEFAULT_ENABLE_IPV4}]`, |
| 159 | 169 | }).option('port', { |
| @@ -180,6 +190,14 @@ const cliArguments = yargs(hideBin(process.argv)) | ||
| 180 | 190 | type: 'boolean', |
| 181 | 191 | default: null, |
| 182 | 192 | describe: `SillyTavern is listening on all network interfaces (Wi-Fi, LAN, localhost). If false, will limit it only to internal localhost (127.0.0.1).\nIf not provided falls back to yaml config 'listen'.\n[config default: ${DEFAULT_LISTEN}]`, |
| 193 | + }).option('listenAddressIPv6', { | |
| 194 | + type: 'string', | |
| 195 | + default: null, | |
| 196 | + describe: 'Set SillyTavern to listen to a specific IPv6 address. If not set, it will fallback to listen to all.\n[config default: [::] ]', | |
| 197 | + }).option('listenAddressIPv4', { | |
| 198 | + type: 'string', | |
| 199 | + default: null, | |
| 200 | + describe: 'Set SillyTavern to listen to a specific IPv4 address. If not set, it will fallback to listen to all.\n[config default: 0.0.0.0 ]', | |
| 183 | 201 | }).option('corsProxy', { |
| 184 | 202 | type: 'boolean', |
| 185 | 203 | default: null, |
| @@ -226,7 +244,6 @@ const cliArguments = yargs(hideBin(process.argv)) | ||
| 226 | 244 | describe: 'Request proxy URL (HTTP or SOCKS protocols)', |
| 227 | 245 | }).option('requestProxyBypass', { |
| 228 | 246 | type: 'array', |
| 229 | - default: null, | |
| 230 | 247 | describe: 'Request proxy bypass list (space separated list of hosts)', |
| 231 | 248 | }).parseSync(); |
| 232 | 249 | |
| @@ -242,27 +259,46 @@ app.use(helmet({ | ||
| 242 | 259 | app.use(compression()); |
| 243 | 260 | app.use(responseTime()); |
| 244 | 261 | |
| 262 | + | |
| 263 | +/** @type {number} */ | |
| 245 | 264 | const server_port = cliArguments.port ?? process.env.SILLY_TAVERN_PORT ?? getConfigValue('port', DEFAULT_PORT); |
| 265 | +/** @type {boolean} */ | |
| 246 | 266 | const autorun = (cliArguments.autorun ?? getConfigValue('autorun', DEFAULT_AUTORUN)) && !cliArguments.ssl; |
| 267 | +/** @type {boolean} */ | |
| 247 | 268 | const listen = cliArguments.listen ?? getConfigValue('listen', DEFAULT_LISTEN); |
| 269 | +/** @type {string} */ | |
| 270 | +const listenAddressIPv6 = cliArguments.listenAddressIPv6 ?? getConfigValue('listenAddress.ipv6', DEFAULT_LISTEN_ADDRESS_IPV6); | |
| 271 | +/** @type {string} */ | |
| 272 | +const listenAddressIPv4 = cliArguments.listenAddressIPv4 ?? getConfigValue('listenAddress.ipv4', DEFAULT_LISTEN_ADDRESS_IPV4); | |
| 273 | +/** @type {boolean} */ | |
| 248 | 274 | const enableCorsProxy = cliArguments.corsProxy ?? getConfigValue('enableCorsProxy', DEFAULT_CORS_PROXY); |
| 249 | 275 | const enableWhitelist = cliArguments.whitelist ?? getConfigValue('whitelistMode', DEFAULT_WHITELIST); |
| 276 | +/** @type {string} */ | |
| 250 | 277 | const dataRoot = cliArguments.dataRoot ?? getConfigValue('dataRoot', './data'); |
| 278 | +/** @type {boolean} */ | |
| 251 | 279 | const disableCsrf = cliArguments.disableCsrf ?? getConfigValue('disableCsrfProtection', DEFAULT_CSRF_DISABLED); |
| 252 | 280 | const basicAuthMode = cliArguments.basicAuthMode ?? getConfigValue('basicAuthMode', DEFAULT_BASIC_AUTH); |
| 253 | 281 | const perUserBasicAuth = getConfigValue('perUserBasicAuth', DEFAULT_PER_USER_BASIC_AUTH); |
| 282 | +/** @type {boolean} */ | |
| 254 | 283 | const enableAccounts = getConfigValue('enableUserAccounts', DEFAULT_ACCOUNTS); |
| 255 | 284 | |
| 256 | 285 | const uploadsPath = path.join(dataRoot, UPLOADS_DIRECTORY); |
| 257 | 286 | |
| 258 | -const enableIPv6 = cliArguments.enableIPv6 ?? getConfigValue('protocol.ipv6', DEFAULT_ENABLE_IPV6); | |
| 259 | -const enableIPv4 = cliArguments.enableIPv4 ?? getConfigValue('protocol.ipv4', DEFAULT_ENABLE_IPV4); | |
| 260 | 287 | |
| 288 | +/** @type {boolean | "auto"} */ | |
| 289 | +let enableIPv6 = stringToBool(cliArguments.enableIPv6) ?? getConfigValue('protocol.ipv6', DEFAULT_ENABLE_IPV6); | |
| 290 | +/** @type {boolean | "auto"} */ | |
| 291 | +let enableIPv4 = stringToBool(cliArguments.enableIPv4) ?? getConfigValue('protocol.ipv4', DEFAULT_ENABLE_IPV4); | |
| 292 | + | |
| 293 | +/** @type {string} */ | |
| 261 | 294 | const autorunHostname = cliArguments.autorunHostname ?? getConfigValue('autorunHostname', DEFAULT_AUTORUN_HOSTNAME); |
| 295 | +/** @type {number} */ | |
| 262 | 296 | const autorunPortOverride = cliArguments.autorunPortOverride ?? getConfigValue('autorunPortOverride', DEFAULT_AUTORUN_PORT); |
| 263 | 297 | |
| 298 | +/** @type {boolean} */ | |
| 264 | 299 | const dnsPreferIPv6 = cliArguments.dnsPreferIPv6 ?? getConfigValue('dnsPreferIPv6', DEFAULT_PREFER_IPV6); |
| 265 | 300 | |
| 301 | +/** @type {boolean} */ | |
| 266 | 302 | const avoidLocalhost = cliArguments.avoidLocalhost ?? getConfigValue('avoidLocalhost', DEFAULT_AVOID_LOCALHOST); |
| 267 | 303 | |
| 268 | 304 | const proxyEnabled = cliArguments.requestProxyEnabled ?? getConfigValue('requestProxy.enabled', DEFAULT_PROXY_ENABLED); |
| @@ -279,7 +315,19 @@ if (dnsPreferIPv6) { | ||
| 279 | 315 | console.log('Preferring IPv4 for DNS resolution'); |
| 280 | 316 | } |
| 281 | 317 | |
| 282 | -if (!enableIPv6 && !enableIPv4) { | |
| 318 | + | |
| 319 | +const ipOptions = [true, 'auto', false]; | |
| 320 | + | |
| 321 | +if (!ipOptions.includes(enableIPv6)) { | |
| 322 | + console.warn(color.red('`protocol: ipv6` option invalid'), '\n use:', ipOptions, '\n setting to:', DEFAULT_ENABLE_IPV6); | |
| 323 | + enableIPv6 = DEFAULT_ENABLE_IPV6; | |
| 324 | +} | |
| 325 | +if (!ipOptions.includes(enableIPv4)) { | |
| 326 | + console.warn(color.red('`protocol: ipv4` option invalid'), '\n use:', ipOptions, '\n setting to:', DEFAULT_ENABLE_IPV4); | |
| 327 | + enableIPv4 = DEFAULT_ENABLE_IPV4; | |
| 328 | +} | |
| 329 | + | |
| 330 | +if (enableIPv6 === false && enableIPv4 === false) { | |
| 283 | 331 | console.error('error: You can\'t disable all internet protocols: at least IPv6 or IPv4 must be enabled.'); |
| 284 | 332 | process.exit(1); |
| 285 | 333 | } |
| @@ -292,9 +340,17 @@ const CORS = cors({ | ||
| 292 | 340 | |
| 293 | 341 | app.use(CORS); |
| 294 | 342 | |
| 295 | 343 | if (listen && basicAuthMode) app.use(basicAuthMiddleware);{ |
| 344 | + app.use(basicAuthMiddleware); | |
| 345 | +} | |
| 296 | 346 | |
| 297 | -app.use(whitelistMiddleware(enableWhitelist, listen)); | |
| 347 | +if (enableWhitelist) { | |
| 348 | + app.use(whitelistMiddleware()); | |
| 349 | +} | |
| 350 | + | |
| 351 | +if (listen) { | |
| 352 | + app.use(accessLoggerMiddleware()); | |
| 353 | +} | |
| 298 | 354 | |
| 299 | 355 | if (enableCorsProxy) { |
| 300 | 356 | app.use(bodyParser.json({ |
| @@ -364,6 +420,55 @@ function getSessionCookieAge() { | ||
| 364 | 420 | return undefined; |
| 365 | 421 | } |
| 366 | 422 | |
| 423 | +/** | |
| 424 | + * Checks the network interfaces to determine the presence of IPv6 and IPv4 addresses. | |
| 425 | + * | |
| 426 | + * @returns {Promise<[boolean, boolean, boolean, boolean]>} A promise that resolves to an array containing: | |
| 427 | + * - [0]: `hasIPv6` (boolean) - Whether the computer has any IPv6 address, including (`::1`). | |
| 428 | + * - [1]: `hasIPv4` (boolean) - Whether the computer has any IPv4 address, including (`127.0.0.1`). | |
| 429 | + * - [2]: `hasIPv6Local` (boolean) - Whether the computer has local IPv6 address (`::1`). | |
| 430 | + * - [3]: `hasIPv4Local` (boolean) - Whether the computer has local IPv4 address (`127.0.0.1`). | |
| 431 | + */ | |
| 432 | +async function getHasIP() { | |
| 433 | + let hasIPv6 = false; | |
| 434 | + let hasIPv6Local = false; | |
| 435 | + | |
| 436 | + let hasIPv4 = false; | |
| 437 | + let hasIPv4Local = false; | |
| 438 | + | |
| 439 | + const interfaces = os.networkInterfaces(); | |
| 440 | + | |
| 441 | + for (const iface of Object.values(interfaces)) { | |
| 442 | + if (iface === undefined) { | |
| 443 | + continue; | |
| 444 | + } | |
| 445 | + | |
| 446 | + for (const info of iface) { | |
| 447 | + if (info.family === 'IPv6') { | |
| 448 | + hasIPv6 = true; | |
| 449 | + if (info.address === '::1') { | |
| 450 | + hasIPv6Local = true; | |
| 451 | + } | |
| 452 | + } | |
| 453 | + | |
| 454 | + if (info.family === 'IPv4') { | |
| 455 | + hasIPv4 = true; | |
| 456 | + if (info.address === '127.0.0.1') { | |
| 457 | + hasIPv4Local = true; | |
| 458 | + } | |
| 459 | + } | |
| 460 | + if (hasIPv6 && hasIPv4 && hasIPv6Local && hasIPv4Local) break; | |
| 461 | + } | |
| 462 | + if (hasIPv6 && hasIPv4 && hasIPv6Local && hasIPv4Local) break; | |
| 463 | + } | |
| 464 | + return [ | |
| 465 | + hasIPv6, | |
| 466 | + hasIPv4, | |
| 467 | + hasIPv6Local, | |
| 468 | + hasIPv4Local, | |
| 469 | + ]; | |
| 470 | +} | |
| 471 | + | |
| 367 | 472 | app.use(cookieSession({ |
| 368 | 473 | name: getCookieSessionName(), |
| 369 | 474 | sameSite: 'strict', |
| @@ -419,7 +524,7 @@ if (!disableCsrf) { | ||
| 419 | 524 | |
| 420 | 525 | // Static files |
| 421 | 526 | // Host index page |
| 422 | 527 | app.get('/', getCacheBusterMiddleware(), (request, response) => { |
| 423 | 528 | if (shouldRedirectToLogin(request)) { |
| 424 | 529 | const query = request.url.split('?')[1]; |
| 425 | 530 | const redirectUrl = query ? `/login?${query}` : '/login'; |
| @@ -459,7 +564,13 @@ app.use('/api/users', usersPublicRouter); | ||
| 459 | 564 | |
| 460 | 565 | // Everything below this line requires authentication |
| 461 | 566 | app.use(requireLoginMiddleware); |
| 462 | 567 | app.get('/api/ping', (_request, response) => response.sendStatus(204));{ |
| 568 | + if (request.query.extend && request.session) { | |
| 569 | + request.session.touch = Date.now(); | |
| 570 | + } | |
| 571 | + | |
| 572 | + response.sendStatus(204); | |
| 573 | +}); | |
| 463 | 574 | |
| 464 | 575 | // File uploads |
| 465 | 576 | app.use(multer({ dest: uploadsPath, limits: { fieldSize: 10 * 1024 * 1024 } }).single('avatar')); |
| @@ -627,13 +738,13 @@ app.use('/api/azure', azureRouter); | ||
| 627 | 738 | |
| 628 | 739 | const tavernUrlV6 = new URL( |
| 629 | 740 | (cliArguments.ssl ? 'https://' : 'http://') + |
| 630 | 741 | (listen ? (ipRegex.v6({ exact: true }).test(listenAddressIPv6) ? listenAddressIPv6 : '[::]') : '[::1]') + |
| 631 | 742 | (':' + server_port), |
| 632 | 743 | ); |
| 633 | 744 | |
| 634 | 745 | const tavernUrl = new URL( |
| 635 | 746 | (cliArguments.ssl ? 'https://' : 'http://') + |
| 636 | 747 | (listen ? (ipRegex.v4({ exact: true }).test(listenAddressIPv4) ? listenAddressIPv4 : '0.0.0.0') : '127.0.0.1') + |
| 637 | 748 | (':' + server_port), |
| 638 | 749 | ); |
| 639 | 750 | |
| @@ -657,6 +768,7 @@ const preSetupTasks = async function () { | ||
| 657 | 768 | await checkForNewContent(directories); |
| 658 | 769 | await ensureThumbnailCache(); |
| 659 | 770 | cleanUploads(); |
| 771 | + migrateAccessLog(); | |
| 660 | 772 | |
| 661 | 773 | await settingsInit(); |
| 662 | 774 | await statsInit(); |
| @@ -693,20 +805,23 @@ const preSetupTasks = async function () { | ||
| 693 | 805 | |
| 694 | 806 | /** |
| 695 | 807 | * Gets the hostname to use for autorun in the browser. |
| 696 | 808 | * @returnsparam {stringboolean} The hostnameuseIPv6 toIf use for autorunIPv6 |
| 809 | + * @param {boolean} useIPv4 If use IPv4 | |
| 810 | + * @returns Promise<string> The hostname to use for autorun | |
| 697 | 811 | */ |
| 698 | 812 | async function getAutorunHostname(useIPv6, useIPv4) { |
| 699 | 813 | if (autorunHostname === 'auto') { |
| 700 | - if (enableIPv6 && enableIPv4) { | |
| 814 | + let localhostResolve = await canResolve('localhost', useIPv6, useIPv4); | |
| 701 | - if (avoidLocalhost) return '[::1]'; | |
| 815 | + | |
| 702 | - return 'localhost'; | |
| 816 | + if (useIPv6 && useIPv4) { | |
| 817 | + return (avoidLocalhost || !localhostResolve) ? '[::1]' : 'localhost'; | |
| 703 | 818 | } |
| 704 | 819 | |
| 705 | 820 | if (enableIPv6useIPv6) { |
Diff truncated