Merge pull request #3493 from SillyTavern/staging Staging

75aec772719006bdefcdfa2f5a0fb0ac0d257158

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

Signed
148 files changed, +3832 -1165Showing whitespace changes
.github/readme.md+1 -1
@@ -274,7 +274,7 @@ You will need two mandatory directory mappings and a port mapping to allow Silly
2741. Open your Command Line2741. Open your Command Line
2752. Run the following command2752. Run the following command
276276
277`docker create --name='sillytavern' --net='[DockerNet]' -p '8000:8000/tcp' -v '[plugins]':'/home/node/app/plugins':'rw' -v '[config]':'/home/node/app/config':'rw' -v '[data]':'/home/node/app/data':'rw' -v '[extensions]':'/home/node/app/public/scripts/extensions/third-party':'rw' 'ghcr.io/sillytavern/sillytavern:[version]'`277`docker run --name='sillytavern' --net='[DockerNet]' -p '8000:8000/tcp' -v '[plugins]':'/home/node/app/plugins':'rw' -v '[config]':'/home/node/app/config':'rw' -v '[data]':'/home/node/app/data':'rw' -v '[extensions]':'/home/node/app/public/scripts/extensions/third-party':'rw' 'ghcr.io/sillytavern/sillytavern:[version]'`
278278
279> Note that 8000 is a default listening port. Don't forget to use an appropriate port if you change it in the config.279> Note that 8000 is a default listening port. Don't forget to use an appropriate port if you change it in the config.
280280
.gitignore+3 -0
@@ -45,6 +45,7 @@ access.log
45/vectors/45/vectors/
46/cache/46/cache/
47public/css/user.css47public/css/user.css
48public/error/
48/plugins/49/plugins/
49/data50/data
50/default/scaffold51/default/scaffold
@@ -52,3 +53,5 @@ public/scripts/extensions/third-party
52/certs53/certs
53.aider*54.aider*
54.env55.env
56/StartDev.bat
57
default/config.yaml+30 -14
@@ -6,7 +6,13 @@ cardsCacheCapacity: 100
6# -- SERVER CONFIGURATION --6# -- SERVER CONFIGURATION --
7# Listen for incoming connections7# Listen for incoming connections
8listen: false8listen: false
9# Listen on a specific address, supports IPv4 and IPv6
10listenAddress:
11 ipv4: 0.0.0.0
12 ipv6: '[::]'
9# Enables IPv6 and/or IPv4 protocols. Need to have at least one enabled!13# Enables IPv6 and/or IPv4 protocols. Need to have at least one enabled!
14# - Use option "auto" to automatically detect support
15# - Use true or false (no qoutes) to enable or disable each protocol
10protocol:16protocol:
11 ipv4: true17 ipv4: true
12 ipv6: false18 ipv6: false
@@ -65,12 +71,14 @@ autheliaAuth: false
65# the username and passwords for basic auth are the same as those71# the username and passwords for basic auth are the same as those
66# for the individual accounts72# for the individual accounts
67perUserBasicAuth: false73perUserBasicAuth: false
74# Minimum log level to display in the terminal (DEBUG = 0, INFO = 1, WARN = 2, ERROR = 3)
75minLogLevel: 0
6876
69# User session timeout *in seconds* (defaults to 24 hours).77# User session timeout *in seconds* (defaults to 24 hours).
70## Set to a positive number to expire session after a certain time of inactivity78## Set to a positive number to expire session after a certain time of inactivity
71## Set to 0 to expire session when the browser is closed79## Set to 0 to expire session when the browser is closed
72## Set to a negative number to disable session expiration80## Set to a negative number to disable session expiration
73sessionTimeout: 8640081sessionTimeout: -1
74# Used to sign session cookies. Will be auto-generated if not set82# Used to sign session cookies. Will be auto-generated if not set
75cookieSecret: ''83cookieSecret: ''
76# Disable CSRF protection - NOT RECOMMENDED84# Disable CSRF protection - NOT RECOMMENDED
@@ -133,24 +141,26 @@ whitelistImportDomains:
133## headers:141## headers:
134## User-Agent: "Googlebot/2.1 (+http://www.google.com/bot.html)"142## User-Agent: "Googlebot/2.1 (+http://www.google.com/bot.html)"
135requestOverrides: []143requestOverrides: []
136# -- EXTENSIONS CONFIGURATION --144
145# EXTENSIONS CONFIGURATION
146extensions:
137 # Enable UI extensions147 # Enable UI extensions
138enableExtensions: true148 enabled: true
139 # Automatically update extensions when a release version changes149 # Automatically update extensions when a release version changes
140enableExtensionsAutoUpdate: true150 autoUpdate: true
151 models:
152 # Enables automatic model download from HuggingFace
153 autoDownload: true
154 # Additional models for extensions. Expects model IDs from HuggingFace model hub in ONNX format
155 classification: Cohee/distilbert-base-uncased-go-emotions-onnx
156 captioning: Xenova/vit-gpt2-image-captioning
157 embedding: Cohee/jina-embeddings-v2-base-en
158 speechToText: Xenova/whisper-small
159 textToSpeech: Xenova/speecht5_tts
160
141# Additional model tokenizers can be downloaded on demand.161# Additional model tokenizers can be downloaded on demand.
142# Disabling will fallback to another locally available tokenizer.162# Disabling will fallback to another locally available tokenizer.
143enableDownloadableTokenizers: true163enableDownloadableTokenizers: true
144# Extension settings
145extras:
146 # Disables automatic model download from HuggingFace
147 disableAutoDownload: false
148 # Extra models for plugins. Expects model IDs from HuggingFace model hub in ONNX format
149 classificationModel: Cohee/distilbert-base-uncased-go-emotions-onnx
150 captioningModel: Xenova/vit-gpt2-image-captioning
151 embeddingModel: Cohee/jina-embeddings-v2-base-en
152 speechToTextModel: Xenova/whisper-small
153 textToSpeechModel: Xenova/speecht5_tts
154# -- OPENAI CONFIGURATION --164# -- OPENAI CONFIGURATION --
155# A placeholder message to use in strict prompt post-processing mode when the prompt doesn't start with a user message165# A placeholder message to use in strict prompt post-processing mode when the prompt doesn't start with a user message
156promptPlaceholder: "[Start a new chat]"166promptPlaceholder: "[Start a new chat]"
@@ -177,6 +187,10 @@ ollama:
177 # * 0: Unload the model immediately after the request187 # * 0: Unload the model immediately after the request
178 # * N (any positive number): Keep the model loaded for N seconds after the request.188 # * N (any positive number): Keep the model loaded for N seconds after the request.
179 keepAlive: -1189 keepAlive: -1
190 # Controls the "num_batch" (batch size) parameter of the generation request
191 # * -1: Use the default value of the model
192 # * N (positive number): Use the specified value. Must be a power of 2, e.g. 128, 256, 512, etc.
193 batchSize: -1
180# -- ANTHROPIC CLAUDE API CONFIGURATION --194# -- ANTHROPIC CLAUDE API CONFIGURATION --
181claude:195claude:
182 # Enables caching of the system prompt (if supported).196 # Enables caching of the system prompt (if supported).
@@ -196,3 +210,5 @@ claude:
196 cachingAtDepth: -1210 cachingAtDepth: -1
197# -- SERVER PLUGIN CONFIGURATION --211# -- SERVER PLUGIN CONFIGURATION --
198enableServerPlugins: false212enableServerPlugins: false
213# Attempt to automatically update server plugins on startup
214enableServerPluginsAutoUpdate: true
default/content/index.json+8 -4
@@ -672,10 +672,6 @@
672 "type": "moving_ui"672 "type": "moving_ui"
673 },673 },
674 {674 {
675 "filename": "presets/moving-ui/Black Magic Time.json",
676 "type": "moving_ui"
677 },
678 {
679 "filename": "presets/quick-replies/Default.json",675 "filename": "presets/quick-replies/Default.json",
680 "type": "quick_replies"676 "type": "quick_replies"
681 },677 },
@@ -782,5 +778,13 @@
782 {778 {
783 "filename": "presets/context/Mistral V7.json",779 "filename": "presets/context/Mistral V7.json",
784 "type": "context"780 "type": "context"
781 },
782 {
783 "filename": "presets/instruct/DeepSeek-V2.5.json",
784 "type": "instruct"
785 },
786 {
787 "filename": "presets/context/DeepSeek-V2.5.json",
788 "type": "context"
785 }789 }
786]790]
default/content/presets/context/DeepSeek-V2.5.json+11 -0
@@ -0,0 +1,11 @@
1{
2 "story_string": "{{#if system}}{{system}}\n{{/if}}{{#if wiBefore}}{{wiBefore}}\n{{/if}}{{#if description}}{{description}}\n{{/if}}{{#if personality}}{{char}}'s personality: {{personality}}\n{{/if}}{{#if scenario}}Scenario: {{scenario}}\n{{/if}}{{#if wiAfter}}{{wiAfter}}\n{{/if}}{{#if persona}}{{persona}}\n{{/if}}{{trim}}\n",
3 "example_separator": "",
4 "chat_start": "",
5 "use_stop_strings": false,
6 "allow_jailbreak": false,
7 "always_force_name2": true,
8 "trim_sentences": false,
9 "single_line": false,
10 "name": "DeepSeek-V2.5"
11}
default/content/presets/instruct/DeepSeek-V2.5.json+22 -0
@@ -0,0 +1,22 @@
1{
2 "input_sequence": "<|User|>",
3 "output_sequence": "<|Assistant|>",
4 "first_output_sequence": "",
5 "last_output_sequence": "",
6 "system_sequence_prefix": "",
7 "system_sequence_suffix": "",
8 "stop_sequence": "",
9 "wrap": false,
10 "macro": true,
11 "names_behavior": "force",
12 "activation_regex": "",
13 "skip_examples": false,
14 "output_suffix": "<|end▁of▁sentence|>",
15 "input_suffix": "",
16 "system_sequence": "",
17 "system_suffix": "",
18 "user_alignment_message": "",
19 "last_system_sequence": "",
20 "system_same_as_user": true,
21 "name": "DeepSeek-V2.5"
22}
default/content/presets/moving-ui/Black Magic Time.json+0 -45
@@ -1,45 +0,0 @@
1{
2 "name": "Black Magic Time",
3 "movingUIState": {
4 "sheld": {
5 "top": 488,
6 "left": 1407,
7 "right": 1,
8 "bottom": 4,
9 "margin": "unset",
10 "width": 471,
11 "height": 439
12 },
13 "floatingPrompt": {
14 "width": 369,
15 "height": 441
16 },
17 "right-nav-panel": {
18 "top": 0,
19 "left": 1400,
20 "right": 111,
21 "bottom": 446,
22 "margin": "unset",
23 "width": 479,
24 "height": 487
25 },
26 "WorldInfo": {
27 "top": 41,
28 "left": 369,
29 "right": 642,
30 "bottom": 51,
31 "margin": "unset",
32 "width": 1034,
33 "height": 858
34 },
35 "left-nav-panel": {
36 "top": 442,
37 "left": 0,
38 "right": 1546,
39 "bottom": 25,
40 "margin": "unset",
41 "width": 368,
42 "height": 483
43 }
44 }
45}
45 \ No newline at end of file \ No newline at end of file
default/user.css → default/public/css/user.css+0 -0
default/public/error/forbidden-by-whitelist.html+22 -0
@@ -0,0 +1,22 @@
1<!DOCTYPE html>
2<html>
3
4<head>
5 <title>Forbidden</title>
6</head>
7
8<body>
9 <h1>Forbidden</h1>
10 <p>
11 If you are the system administrator, add your IP address to the
12 whitelist or disable whitelist mode by editing
13 <code>config.yaml</code> in the root directory of your installation.
14 </p>
15 <hr />
16 <p>
17 <em>Connection from {{ipDetails}} has been blocked. This attempt
18 has been logged.</em>
19 </p>
20</body>
21
22</html>
default/public/error/unauthorized.html+17 -0
@@ -0,0 +1,17 @@
1<!DOCTYPE html>
2<html>
3
4<head>
5 <title>Unauthorized</title>
6</head>
7
8<body>
9 <h1>Unauthorized</h1>
10 <p>
11 If you are the system administrator, you can configure the
12 <code>basicAuthUser</code> credentials by editing
13 <code>config.yaml</code> in the root directory of your installation.
14 </p>
15</body>
16
17</html>
default/public/error/url-not-found.html+15 -0
@@ -0,0 +1,15 @@
1<!DOCTYPE html>
2<html>
3
4<head>
5 <title>Not found</title>
6</head>
7
8<body>
9 <h1>Not found</h1>
10 <p>
11 The requested URL was not found on this server.
12 </p>
13</body>
14
15</html>
index.d.ts+18 -8
@@ -1,6 +1,24 @@
1import { UserDirectoryList, User } from "./src/users";1import { UserDirectoryList, User } from "./src/users";
2import { CsrfSyncedToken } from "csrf-sync";
23
3declare global {4declare global {
5 declare namespace CookieSessionInterfaces {
6 export interface CookieSessionObject {
7 /**
8 * The CSRF token for the session.
9 */
10 csrfToken: CsrfSyncedToken;
11 /**
12 * Authenticated user handle.
13 */
14 handle: string;
15 /**
16 * Last time the session was extended.
17 */
18 touch: number;
19 }
20 }
21
4 namespace Express {22 namespace Express {
5 export interface Request {23 export interface Request {
6 user: {24 user: {
@@ -15,11 +33,3 @@ declare global {
15 */33 */
16 var DATA_ROOT: string;34 var DATA_ROOT: string;
17}35}
18
19declare module 'express-session' {
20 export interface SessionData {
21 handle: string;
22 touch: number;
23 // other properties...
24 }
25 }
jsconfig.json+1 -1
@@ -15,7 +15,7 @@
15 "**/node_modules/**",15 "**/node_modules/**",
16 "**/dist/**",16 "**/dist/**",
17 "**/.git/**",17 "**/.git/**",
18 "public/lib/**",18 "public/**",
19 "backups/**",19 "backups/**",
20 "data/**",20 "data/**",
21 "cache/**",21 "cache/**",
package-lock.json+38 -7
@@ -1,12 +1,12 @@
1{1{
2 "name": "sillytavern",2 "name": "sillytavern",
3 "version": "1.12.11",3 "version": "1.12.12",
4 "lockfileVersion": 3,4 "lockfileVersion": 3,
5 "requires": true,5 "requires": true,
6 "packages": {6 "packages": {
7 "": {7 "": {
8 "name": "sillytavern",8 "name": "sillytavern",
9 "version": "1.12.11",9 "version": "1.12.12",
10 "hasInstallScript": true,10 "hasInstallScript": true,
11 "license": "AGPL-3.0",11 "license": "AGPL-3.0",
12 "dependencies": {12 "dependencies": {
@@ -26,7 +26,7 @@
26 "cookie-parser": "^1.4.6",26 "cookie-parser": "^1.4.6",
27 "cookie-session": "^2.1.0",27 "cookie-session": "^2.1.0",
28 "cors": "^2.8.5",28 "cors": "^2.8.5",
29 "csrf-csrf": "^2.2.3",29 "csrf-sync": "^4.0.3",
30 "diff-match-patch": "^1.0.5",30 "diff-match-patch": "^1.0.5",
31 "dompurify": "^3.1.7",31 "dompurify": "^3.1.7",
32 "droll": "^0.2.1",32 "droll": "^0.2.1",
@@ -41,6 +41,7 @@
41 "html-entities": "^2.5.2",41 "html-entities": "^2.5.2",
42 "iconv-lite": "^0.6.3",42 "iconv-lite": "^0.6.3",
43 "ip-matching": "^2.1.2",43 "ip-matching": "^2.1.2",
44 "ip-regex": "^5.0.0",
44 "ipaddr.js": "^2.0.1",45 "ipaddr.js": "^2.0.1",
45 "jimp": "^0.22.10",46 "jimp": "^0.22.10",
46 "localforage": "^1.10.0",47 "localforage": "^1.10.0",
@@ -86,6 +87,7 @@
86 "@types/cookie-session": "^2.0.49",87 "@types/cookie-session": "^2.0.49",
87 "@types/cors": "^2.8.17",88 "@types/cors": "^2.8.17",
88 "@types/deno": "^2.0.0",89 "@types/deno": "^2.0.0",
90 "@types/dompurify": "^3.0.5",
89 "@types/express": "^4.17.21",91 "@types/express": "^4.17.21",
90 "@types/jquery": "^3.5.29",92 "@types/jquery": "^3.5.29",
91 "@types/jquery-cropper": "^1.0.4",93 "@types/jquery-cropper": "^1.0.4",
@@ -1179,6 +1181,16 @@
1179 "dev": true,1181 "dev": true,
1180 "license": "MIT"1182 "license": "MIT"
1181 },1183 },
1184 "node_modules/@types/dompurify": {
1185 "version": "3.0.5",
1186 "resolved": "https://registry.npmjs.org/@types/dompurify/-/dompurify-3.0.5.tgz",
1187 "integrity": "sha512-1Wg0g3BtQF7sSb27fJQAKck1HECM6zV1EB66j8JH9i3LCjYabJa0FSdiSgsD5K/RbrsR0SiraKacLB+T8ZVYAg==",
1188 "dev": true,
1189 "license": "MIT",
1190 "dependencies": {
1191 "@types/trusted-types": "*"
1192 }
1193 },
1182 "node_modules/@types/estree": {1194 "node_modules/@types/estree": {
1183 "version": "1.0.6",1195 "version": "1.0.6",
1184 "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz",1196 "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz",
@@ -1462,6 +1474,13 @@
1462 "@types/jquery": "*"1474 "@types/jquery": "*"
1463 }1475 }
1464 },1476 },
1477 "node_modules/@types/trusted-types": {
1478 "version": "2.0.7",
1479 "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
1480 "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
1481 "dev": true,
1482 "license": "MIT"
1483 },
1465 "node_modules/@types/write-file-atomic": {1484 "node_modules/@types/write-file-atomic": {
1466 "version": "4.0.3",1485 "version": "4.0.3",
1467 "resolved": "https://registry.npmjs.org/@types/write-file-atomic/-/write-file-atomic-4.0.3.tgz",1486 "resolved": "https://registry.npmjs.org/@types/write-file-atomic/-/write-file-atomic-4.0.3.tgz",
@@ -2987,10 +3006,10 @@
2987 "node": "*"3006 "node": "*"
2988 }3007 }
2989 },3008 },
2990 "node_modules/csrf-csrf": {3009 "node_modules/csrf-sync": {
2991 "version": "2.2.4",3010 "version": "4.0.3",
2992 "resolved": "https://registry.npmjs.org/csrf-csrf/-/csrf-csrf-2.2.4.tgz",3011 "resolved": "https://registry.npmjs.org/csrf-sync/-/csrf-sync-4.0.3.tgz",
2993 "integrity": "sha512-LuhBmy5RfRmEfeqeYqgaAuS1eDpVtKZB/Eiec9xiKQLBynJxrGVRdM2yRT/YMl1Njo/yKh2L9AYsIwSlTPnx2A==",3012 "integrity": "sha512-wXzltBBzt/7imzDt6ZT7G/axQG7jo4Sm0uXDUzFY8hR59qhDHdjqpW2hojS4oAVIZDzwlMQloIVCTJoDDh0wwA==",
2994 "license": "ISC",3013 "license": "ISC",
2995 "dependencies": {3014 "dependencies": {
2996 "http-errors": "^2.0.0"3015 "http-errors": "^2.0.0"
@@ -4610,6 +4629,18 @@
4610 "integrity": "sha512-/ok+VhKMasgR5gvTRViwRFQfc0qYt9Vdowg6TO4/pFlDCob5ZjGPkwuOoQVCd5OrMm20zqh+1vA8KLJZTeWudg==",4629 "integrity": "sha512-/ok+VhKMasgR5gvTRViwRFQfc0qYt9Vdowg6TO4/pFlDCob5ZjGPkwuOoQVCd5OrMm20zqh+1vA8KLJZTeWudg==",
4611 "license": "LGPL-3.0-only"4630 "license": "LGPL-3.0-only"
4612 },4631 },
4632 "node_modules/ip-regex": {
4633 "version": "5.0.0",
4634 "resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-5.0.0.tgz",
4635 "integrity": "sha512-fOCG6lhoKKakwv+C6KdsOnGvgXnmgfmp0myi3bcNwj3qfwPAxRKWEuFhvEFF7ceYIz6+1jRZ+yguLFAmUNPEfw==",
4636 "license": "MIT",
4637 "engines": {
4638 "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
4639 },
4640 "funding": {
4641 "url": "https://github.com/sponsors/sindresorhus"
4642 }
4643 },
4613 "node_modules/ipaddr.js": {4644 "node_modules/ipaddr.js": {
4614 "version": "2.1.0",4645 "version": "2.1.0",
4615 "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.1.0.tgz",4646 "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.1.0.tgz",
package.json+5 -2
@@ -16,7 +16,7 @@
16 "cookie-parser": "^1.4.6",16 "cookie-parser": "^1.4.6",
17 "cookie-session": "^2.1.0",17 "cookie-session": "^2.1.0",
18 "cors": "^2.8.5",18 "cors": "^2.8.5",
19 "csrf-csrf": "^2.2.3",19 "csrf-sync": "^4.0.3",
20 "diff-match-patch": "^1.0.5",20 "diff-match-patch": "^1.0.5",
21 "dompurify": "^3.1.7",21 "dompurify": "^3.1.7",
22 "droll": "^0.2.1",22 "droll": "^0.2.1",
@@ -31,6 +31,7 @@
31 "html-entities": "^2.5.2",31 "html-entities": "^2.5.2",
32 "iconv-lite": "^0.6.3",32 "iconv-lite": "^0.6.3",
33 "ip-matching": "^2.1.2",33 "ip-matching": "^2.1.2",
34 "ip-regex": "^5.0.0",
34 "ipaddr.js": "^2.0.1",35 "ipaddr.js": "^2.0.1",
35 "jimp": "^0.22.10",36 "jimp": "^0.22.10",
36 "localforage": "^1.10.0",37 "localforage": "^1.10.0",
@@ -86,9 +87,10 @@
86 "type": "git",87 "type": "git",
87 "url": "https://github.com/SillyTavern/SillyTavern.git"88 "url": "https://github.com/SillyTavern/SillyTavern.git"
88 },89 },
89 "version": "1.12.11",90 "version": "1.12.12",
90 "scripts": {91 "scripts": {
91 "start": "node server.js",92 "start": "node server.js",
93 "debug": "node server.js --inspect",
92 "start:deno": "deno run --allow-run --allow-net --allow-read --allow-write --allow-sys --allow-env server.js",94 "start:deno": "deno run --allow-run --allow-net --allow-read --allow-write --allow-sys --allow-env server.js",
93 "start:bun": "bun server.js",95 "start:bun": "bun server.js",
94 "start:no-csrf": "node server.js --disableCsrf",96 "start:no-csrf": "node server.js --disableCsrf",
@@ -114,6 +116,7 @@
114 "@types/cookie-session": "^2.0.49",116 "@types/cookie-session": "^2.0.49",
115 "@types/cors": "^2.8.17",117 "@types/cors": "^2.8.17",
116 "@types/deno": "^2.0.0",118 "@types/deno": "^2.0.0",
119 "@types/dompurify": "^3.0.5",
117 "@types/express": "^4.17.21",120 "@types/express": "^4.17.21",
118 "@types/jquery": "^3.5.29",121 "@types/jquery": "^3.5.29",
119 "@types/jquery-cropper": "^1.0.4",122 "@types/jquery-cropper": "^1.0.4",
plugins.js+7 -0
@@ -48,6 +48,13 @@ async function updatePlugins() {
48 console.log(`Updating plugin ${color.green(directory)}...`);48 console.log(`Updating plugin ${color.green(directory)}...`);
49 const pluginPath = path.join(pluginsPath, directory);49 const pluginPath = path.join(pluginsPath, directory);
50 const pluginRepo = git(pluginPath);50 const pluginRepo = git(pluginPath);
51
52 const isRepo = await pluginRepo.checkIsRepo();
53 if (!isRepo) {
54 console.log(`Directory ${color.yellow(directory)} is not a Git repository`);
55 continue;
56 }
57
51 await pluginRepo.fetch();58 await pluginRepo.fetch();
52 const commitHash = await pluginRepo.revparse(['HEAD']);59 const commitHash = await pluginRepo.revparse(['HEAD']);
53 const trackingBranch = await pluginRepo.revparse(['--abbrev-ref', '@{u}']);60 const trackingBranch = await pluginRepo.revparse(['--abbrev-ref', '@{u}']);
post-install.js+91 -11
@@ -64,6 +64,46 @@ const keyMigrationMap = [
64 newKey: 'backups.chat.throttleInterval',64 newKey: 'backups.chat.throttleInterval',
65 migrate: (value) => value,65 migrate: (value) => value,
66 },66 },
67 {
68 oldKey: 'enableExtensions',
69 newKey: 'extensions.enabled',
70 migrate: (value) => value,
71 },
72 {
73 oldKey: 'enableExtensionsAutoUpdate',
74 newKey: 'extensions.autoUpdate',
75 migrate: (value) => value,
76 },
77 {
78 oldKey: 'extras.disableAutoDownload',
79 newKey: 'extensions.models.autoDownload',
80 migrate: (value) => !value,
81 },
82 {
83 oldKey: 'extras.classificationModel',
84 newKey: 'extensions.models.classification',
85 migrate: (value) => value,
86 },
87 {
88 oldKey: 'extras.captioningModel',
89 newKey: 'extensions.models.captioning',
90 migrate: (value) => value,
91 },
92 {
93 oldKey: 'extras.embeddingModel',
94 newKey: 'extensions.models.embedding',
95 migrate: (value) => value,
96 },
97 {
98 oldKey: 'extras.speechToTextModel',
99 newKey: 'extensions.models.speechToText',
100 migrate: (value) => value,
101 },
102 {
103 oldKey: 'extras.textToSpeechModel',
104 newKey: 'extensions.models.textToSpeech',
105 migrate: (value) => value,
106 },
67];107];
68108
69/**109/**
@@ -73,7 +113,7 @@ const keyMigrationMap = [
73 * @returns {string[]} Array of all keys in the object113 * @returns {string[]} Array of all keys in the object
74 */114 */
75function getAllKeys(obj, prefix = '') {115function getAllKeys(obj, prefix = '') {
76 if (typeof obj !== 'object' || Array.isArray(obj)) {116 if (typeof obj !== 'object' || Array.isArray(obj) || obj === null) {
77 return [];117 return [];
78 }118 }
79119
@@ -173,20 +213,60 @@ function addMissingConfigValues() {
173 * Creates the default config files if they don't exist yet.213 * Creates the default config files if they don't exist yet.
174 */214 */
175function createDefaultFiles() {215function createDefaultFiles() {
176 const files = {216 /**
177 config: './config.yaml',217 * @typedef DefaultItem
178 user: './public/css/user.css',218 * @type {object}
179 };219 * @property {'file' | 'directory'} type - Whether the item should be copied as a single file or merged into a directory structure.
220 * @property {string} defaultPath - The path to the default item (typically in `default/`).
221 * @property {string} productionPath - The path to the copied item for production use.
222 */
180223
181 for (const file of Object.values(files)) {224 /** @type {DefaultItem[]} */
225 const defaultItems = [
226 {
227 type: 'file',
228 defaultPath: './default/config.yaml',
229 productionPath: './config.yaml',
230 },
231 {
232 type: 'directory',
233 defaultPath: './default/public/',
234 productionPath: './public/',
235 },
236 ];
237
238 for (const defaultItem of defaultItems) {
182 try {239 try {
183 if (!fs.existsSync(file)) {240 if (defaultItem.type === 'file') {
184 const defaultFilePath = path.join('./default', path.parse(file).base);241 if (!fs.existsSync(defaultItem.productionPath)) {
185 fs.copyFileSync(defaultFilePath, file);242 fs.copyFileSync(
186 console.log(color.green(`Created default file: ${file}`));243 defaultItem.defaultPath,
244 defaultItem.productionPath,
245 );
246 console.log(
247 color.green(`Created default file: ${defaultItem.productionPath}`),
248 );
249 }
250 } else if (defaultItem.type === 'directory') {
251 fs.cpSync(defaultItem.defaultPath, defaultItem.productionPath, {
252 force: false, // Don't overwrite existing files!
253 recursive: true,
254 });
255 console.log(
256 color.green(`Synchronized missing files: ${defaultItem.productionPath}`),
257 );
258 } else {
259 throw new Error(
260 'FATAL: Unexpected default file format in `post-install.js#createDefaultFiles()`.',
261 );
187 }262 }
188 } catch (error) {263 } catch (error) {
189 console.error(color.red(`FATAL: Could not write default file: ${file}`), error);264 console.error(
265 color.red(
266 `FATAL: Could not write default ${defaultItem.type}: ${defaultItem.productionPath}`,
267 ),
268 error,
269 );
190 }270 }
191 }271 }
192}272}
public/css/mobile-styles.css+0 -2
@@ -216,8 +216,6 @@
216216
217 }217 }
218218
219 #showRawPrompt,
220 #copyPromptToClipboard,
221 #groupCurrentMemberPopoutButton,219 #groupCurrentMemberPopoutButton,
222 #summaryExtensionPopoutButton {220 #summaryExtensionPopoutButton {
223 display: none;221 display: none;
public/css/popup.css+4 -0
@@ -72,6 +72,10 @@ dialog {
72 overflow-x: auto;72 overflow-x: auto;
73}73}
7474
75.popup.left_aligned_dialogue_popup .popup-content {
76 text-align: start;
77}
78
75/* Opening animation */79/* Opening animation */
76.popup[opening] {80.popup[opening] {
77 animation: pop-in var(--popup-animation-speed) ease-in-out;81 animation: pop-in var(--popup-animation-speed) ease-in-out;
public/css/select2-overrides.css+7 -0
@@ -100,6 +100,13 @@
100 border: 1px solid var(--SmartThemeBorderColor);100 border: 1px solid var(--SmartThemeBorderColor);
101}101}
102102
103.select2-container .select2-results .select2-results__option--disabled {
104 color: inherit;
105 background-color: inherit;
106 cursor: not-allowed;
107 filter: brightness(0.5);
108}
109
103.select2-container .select2-selection--multiple .select2-selection__choice,110.select2-container .select2-selection--multiple .select2-selection__choice,
104.select2-container .select2-selection--single .select2-selection__choice {111.select2-container .select2-selection--single .select2-selection__choice {
105 border-radius: 5px;112 border-radius: 5px;
public/css/toggle-dependent.css+9 -0
@@ -472,6 +472,11 @@ label[for="trim_spaces"]:has(input:checked) i.warning {
472 display: none;472 display: none;
473}473}
474474
475label[for="trim_spaces"]:not(:has(input:checked)) small {
476 color: var(--warning);
477 opacity: 1;
478}
479
475#claude_function_prefill_warning {480#claude_function_prefill_warning {
476 display: none;481 display: none;
477 color: red;482 color: red;
@@ -488,3 +493,7 @@ label[for="trim_spaces"]:has(input:checked) i.warning {
488#mistralai_other_models:empty {493#mistralai_other_models:empty {
489 display: none;494 display: none;
490}495}
496
497#banned_tokens_block_ooba:not(:has(#send_banned_tokens_textgenerationwebui:checked)) #banned_tokens_controls_ooba {
498 filter: brightness(0.5);
499}
public/index.html+193 -102
@@ -730,7 +730,7 @@
730 <input type="range" id="top_k_openai" name="volume" min="0" max="500" step="1">730 <input type="range" id="top_k_openai" name="volume" min="0" max="500" step="1">
731 </div>731 </div>
732 <div class="range-block-counter">732 <div class="range-block-counter">
733 <input type="number" min="0" max="200" step="1" data-for="top_k_openai" id="top_k_counter_openai">733 <input type="number" min="0" max="500" step="1" data-for="top_k_openai" id="top_k_counter_openai">
734 </div>734 </div>
735 </div>735 </div>
736 </div>736 </div>
@@ -1587,6 +1587,10 @@
1587 <input type="checkbox" id="skip_special_tokens_textgenerationwebui" />1587 <input type="checkbox" id="skip_special_tokens_textgenerationwebui" />
1588 <small data-i18n="Skip Special Tokens">Skip Special Tokens</small>1588 <small data-i18n="Skip Special Tokens">Skip Special Tokens</small>
1589 </label>1589 </label>
1590 <label data-tg-type="openrouter" class="checkbox_label flexGrow flexShrink" for="include_reasoning_textgenerationwebui">
1591 <input type="checkbox" id="include_reasoning_textgenerationwebui" />
1592 <small data-i18n="Request Model Reasoning">Request Model Reasoning</small>
1593 </label>
1590 <label data-tg-type="ooba, aphrodite, tabby" class="checkbox_label flexGrow flexShrink" for="temperature_last_textgenerationwebui">1594 <label data-tg-type="ooba, aphrodite, tabby" class="checkbox_label flexGrow flexShrink" for="temperature_last_textgenerationwebui">
1591 <input type="checkbox" id="temperature_last_textgenerationwebui" />1595 <input type="checkbox" id="temperature_last_textgenerationwebui" />
1592 <label>1596 <label>
@@ -1617,17 +1621,34 @@
1617 </div>1621 </div>
1618 <div data-tg-type-mode="except" data-tg-type="generic" id="banned_tokens_block_ooba" class="wide100p">1622 <div data-tg-type-mode="except" data-tg-type="generic" id="banned_tokens_block_ooba" class="wide100p">
1619 <hr class="width100p">1623 <hr class="width100p">
1620 <h4 class="range-block-title justifyCenter">1624 <div class="range-block-title title_restorable">
1621 <span data-i18n="Banned Tokens">Banned Tokens/Strings</span>1625 <div>
1626 <strong data-i18n="Banned Tokens">Banned Tokens/Strings</strong>
1622 <div class="margin5 fa-solid fa-circle-info opacity50p " data-i18n="[title]LLaMA / Mistral / Yi models only" title="Enter sequences you don't want to appear in the output.&#13;Unquoted text will be tokenized in the back end and banned as tokens.&#13;[token ids] will be banned as-is.&#13;Most tokens have a leading space. Use token counter (with the correct tokenizer selected first!) if you are unsure.&#13;Enclose text in double quotes to ban the entire string as a set.&#13;Quoted Strings and [Token ids] must be on their own line."></div>1627 <div class="margin5 fa-solid fa-circle-info opacity50p " data-i18n="[title]LLaMA / Mistral / Yi models only" title="Enter sequences you don't want to appear in the output.&#13;Unquoted text will be tokenized in the back end and banned as tokens.&#13;[token ids] will be banned as-is.&#13;Most tokens have a leading space. Use token counter (with the correct tokenizer selected first!) if you are unsure.&#13;Enclose text in double quotes to ban the entire string as a set.&#13;Quoted Strings and [Token ids] must be on their own line."></div>
1623 </h4>1628 </div>
1629 <label id="send_banned_tokens_label" for="send_banned_tokens_textgenerationwebui" class="checkbox_label">
1630 <input id="send_banned_tokens_textgenerationwebui" type="checkbox" style="display:none;" />
1631 <small><i class="fa-solid fa-power-off menu_button togglable margin0"></i></small>
1632 </label>
1633 </div>
1634 <div id="banned_tokens_controls_ooba">
1635 <div class="textAlignCenter">
1636 <small data-i18n="Global list">Global list</small>
1637 </div>
1638 <div class="wide100p marginBot10">
1639 <textarea id="global_banned_tokens_textgenerationwebui" class="text_pole textarea_compact" name="global_banned_tokens_textgenerationwebui" rows="3" data-i18n="[placeholder]Example: some text [42, 69, 1337]" placeholder='some text as tokens&#10;[420, 69, 1337]&#10;"Some verbatim string"'></textarea>
1640 </div>
1641 <div class="textAlignCenter">
1642 <small data-i18n="Preset-specific list">Preset-specific list</small>
1643 </div>
1624 <div class="wide100p">1644 <div class="wide100p">
1625 <textarea id="banned_tokens_textgenerationwebui" class="text_pole textarea_compact" name="banned_tokens_textgenerationwebui" rows="3" data-i18n="[placeholder]Example: some text [42, 69, 1337]" placeholder='some text as tokens&#10;[420, 69, 1337]&#10;"Some verbatim string"'></textarea>1645 <textarea id="banned_tokens_textgenerationwebui" class="text_pole textarea_compact" name="banned_tokens_textgenerationwebui" rows="3" data-i18n="[placeholder]Example: some text [42, 69, 1337]" placeholder='some text as tokens&#10;[420, 69, 1337]&#10;"Some verbatim string"'></textarea>
1626 </div>1646 </div>
1627 </div>1647 </div>
1648 </div>
1628 <div class="range-block wide100p">1649 <div class="range-block wide100p">
1629 <div id="logit_bias_textgenerationwebui" class="range-block-title title_restorable">1650 <div id="logit_bias_textgenerationwebui" class="range-block-title title_restorable">
1630 <span data-i18n="Logit Bias">Logit Bias</span>1651 <strong data-i18n="Logit Bias">Logit Bias</strong>
1631 <div id="textgen_logit_bias_new_entry" class="menu_button menu_button_icon">1652 <div id="textgen_logit_bias_new_entry" class="menu_button menu_button_icon">
1632 <i class="fa-xs fa-solid fa-plus"></i>1653 <i class="fa-xs fa-solid fa-plus"></i>
1633 <small data-i18n="Add">Add</small>1654 <small data-i18n="Add">Add</small>
@@ -1930,7 +1951,7 @@
1930 </span>1951 </span>
1931 </div>1952 </div>
1932 </div>1953 </div>
1933 <div class="range-block" data-source="openai,cohere,mistralai,custom,claude,openrouter,groq">1954 <div class="range-block" data-source="openai,cohere,mistralai,custom,claude,openrouter,groq,deepseek">
1934 <label for="openai_function_calling" class="checkbox_label flexWrap widthFreeExpand">1955 <label for="openai_function_calling" class="checkbox_label flexWrap widthFreeExpand">
1935 <input id="openai_function_calling" type="checkbox" />1956 <input id="openai_function_calling" type="checkbox" />
1936 <span data-i18n="Enable function calling">Enable function calling</span>1957 <span data-i18n="Enable function calling">Enable function calling</span>
@@ -1953,6 +1974,7 @@
1953 <span data-i18n="image_inlining_hint_3">menu to attach an image file to the chat.</span>1974 <span data-i18n="image_inlining_hint_3">menu to attach an image file to the chat.</span>
1954 </div>1975 </div>
1955 <div class="flex-container flexFlowColumn wide100p textAlignCenter marginTop10" data-source="openai,custom">1976 <div class="flex-container flexFlowColumn wide100p textAlignCenter marginTop10" data-source="openai,custom">
1977 <div class="flex-container oneline-dropdown">
1956 <label for="openai_inline_image_quality" data-i18n="Inline Image Quality">1978 <label for="openai_inline_image_quality" data-i18n="Inline Image Quality">
1957 Inline Image Quality1979 Inline Image Quality
1958 </label>1980 </label>
@@ -1963,6 +1985,7 @@
1963 </select>1985 </select>
1964 </div>1986 </div>
1965 </div>1987 </div>
1988 </div>
1966 <div class="range-block" data-source="makersuite">1989 <div class="range-block" data-source="makersuite">
1967 <label for="use_makersuite_sysprompt" class="checkbox_label widthFreeExpand">1990 <label for="use_makersuite_sysprompt" class="checkbox_label widthFreeExpand">
1968 <input id="use_makersuite_sysprompt" type="checkbox" />1991 <input id="use_makersuite_sysprompt" type="checkbox" />
@@ -1977,20 +2000,32 @@
1977 </span>2000 </span>
1978 </div>2001 </div>
1979 </div>2002 </div>
1980 <div class="range-block" data-source="makersuite">2003 <div class="range-block" data-source="deepseek,openrouter">
1981 <label for="openai_show_thoughts" class="checkbox_label widthFreeExpand">2004 <label for="openai_show_thoughts" class="checkbox_label widthFreeExpand">
1982 <input id="openai_show_thoughts" type="checkbox" />2005 <input id="openai_show_thoughts" type="checkbox" />
1983 <span>2006 <span>
1984 <span data-i18n="Show model thoughts">Show model thoughts</span>2007 <span data-i18n="Request model reasoning">Request model reasoning</span>
1985 <i class="opacity50p fa-solid fa-circle-info" title="Gemini 2.0 Thinking"></i>2008 <i class="opacity50p fa-solid fa-circle-info" title="DeepSeek Reasoner"></i>
1986 </span>2009 </span>
1987 </label>2010 </label>
1988 <div class="toggle-description justifyLeft marginBot5">2011 <div class="toggle-description justifyLeft marginBot5">
1989 <span data-i18n="Display the model's internal thoughts in the response.">2012 <span data-i18n="Allows the model to return its thinking process.">
1990 Display the model's internal thoughts in the response.2013 Allows the model to return its thinking process.
1991 </span>2014 </span>
1992 </div>2015 </div>
1993 </div>2016 </div>
2017 <div class="flex-container flexFlowColumn wide100p textAlignCenter marginTop10" data-source="openai,custom">
2018 <div class="flex-container oneline-dropdown" title="Constrains effort on reasoning for reasoning models.&#10;Currently supported values are low, medium, and high.&#10;Reducing reasoning effort can result in faster responses and fewer tokens used on reasoning in a response.">
2019 <label for="openai_reasoning_effort" data-i18n="Reasoning Effort">
2020 Reasoning Effort
2021 </label>
2022 <select id="openai_reasoning_effort">
2023 <option data-i18n="openai_reasoning_effort_low" value="low">Low</option>
2024 <option data-i18n="openai_reasoning_effort_medium" value="medium">Medium</option>
2025 <option data-i18n="openai_reasoning_effort_high" value="high">High</option>
2026 </select>
2027 </div>
2028 </div>
1994 <div class="range-block" data-source="claude">2029 <div class="range-block" data-source="claude">
1995 <div class="wide100p">2030 <div class="wide100p">
1996 <div class="flex-container alignItemsCenter">2031 <div class="flex-container alignItemsCenter">
@@ -2692,7 +2727,7 @@
2692 <option value="windowai">Window AI</option>2727 <option value="windowai">Window AI</option>
2693 </optgroup>2728 </optgroup>
2694 </select>2729 </select>
2695 <div class="inline-drawer wide100p" data-source="openai,claude,mistralai,makersuite">2730 <div class="inline-drawer wide100p" data-source="openai,claude,mistralai,makersuite,deepseek">
2696 <div class="inline-drawer-toggle inline-drawer-header">2731 <div class="inline-drawer-toggle inline-drawer-header">
2697 <b data-i18n="Reverse Proxy">Reverse Proxy</b>2732 <b data-i18n="Reverse Proxy">Reverse Proxy</b>
2698 <div class="fa-solid fa-circle-chevron-down inline-drawer-icon down"></div>2733 <div class="fa-solid fa-circle-chevron-down inline-drawer-icon down"></div>
@@ -2755,7 +2790,7 @@
2755 </div>2790 </div>
2756 </div>2791 </div>
2757 </div>2792 </div>
2758 <div id="ReverseProxyWarningMessage" data-source="openai,claude,mistralai,makersuite">2793 <div id="ReverseProxyWarningMessage" data-source="openai,claude,mistralai,makersuite,deepseek">
2759 <div class="reverse_proxy_warning">2794 <div class="reverse_proxy_warning">
2760 <b>2795 <b>
2761 <div data-i18n="Using a proxy that you're not running yourself is a risk to your data privacy.">2796 <div data-i18n="Using a proxy that you're not running yourself is a risk to your data privacy.">
@@ -2805,27 +2840,6 @@
2805 <div>2840 <div>
2806 <h4 data-i18n="OpenAI Model">OpenAI Model</h4>2841 <h4 data-i18n="OpenAI Model">OpenAI Model</h4>
2807 <select id="model_openai_select">2842 <select id="model_openai_select">
2808 <optgroup label="GPT-3.5 Turbo">
2809 <option value="gpt-3.5-turbo">gpt-3.5-turbo</option>
2810 <option value="gpt-3.5-turbo-0125">gpt-3.5-turbo-0125 (2024)</option>
2811 <option value="gpt-3.5-turbo-1106">gpt-3.5-turbo-1106 (2023)</option>
2812 <option value="gpt-3.5-turbo-0613">gpt-3.5-turbo-0613 (2023)</option>
2813 <option value="gpt-3.5-turbo-0301">gpt-3.5-turbo-0301 (2023)</option>
2814 <option value="gpt-3.5-turbo-16k">gpt-3.5-turbo-16k</option>
2815 <option value="gpt-3.5-turbo-16k-0613">gpt-3.5-turbo-16k-0613 (2023)</option>
2816 </optgroup>
2817 <optgroup label="GPT-3.5 Turbo Instruct">
2818 <option value="gpt-3.5-turbo-instruct">gpt-3.5-turbo-instruct</option>
2819 <option value="gpt-3.5-turbo-instruct-0914">gpt-3.5-turbo-instruct-0914</option>
2820 </optgroup>
2821 <optgroup label="GPT-4">
2822 <option value="gpt-4">gpt-4</option>
2823 <option value="gpt-4-0613">gpt-4-0613 (2023)</option>
2824 <option value="gpt-4-0314">gpt-4-0314 (2023)</option>
2825 <option value="gpt-4-32k">gpt-4-32k</option>
2826 <option value="gpt-4-32k-0613">gpt-4-32k-0613 (2023)</option>
2827 <option value="gpt-4-32k-0314">gpt-4-32k-0314 (2023)</option>
2828 </optgroup>
2829 <optgroup label="GPT-4o">2843 <optgroup label="GPT-4o">
2830 <option value="gpt-4o">gpt-4o</option>2844 <option value="gpt-4o">gpt-4o</option>
2831 <option value="gpt-4o-2024-11-20">gpt-4o-2024-11-20</option>2845 <option value="gpt-4o-2024-11-20">gpt-4o-2024-11-20</option>
@@ -2833,29 +2847,44 @@
2833 <option value="gpt-4o-2024-05-13">gpt-4o-2024-05-13</option>2847 <option value="gpt-4o-2024-05-13">gpt-4o-2024-05-13</option>
2834 <option value="chatgpt-4o-latest">chatgpt-4o-latest</option>2848 <option value="chatgpt-4o-latest">chatgpt-4o-latest</option>
2835 </optgroup>2849 </optgroup>
2836 <optgroup label="gpt-4o-mini">2850 <optgroup label="GPT-4o mini">
2837 <option value="gpt-4o-mini">gpt-4o-mini</option>2851 <option value="gpt-4o-mini">gpt-4o-mini</option>
2838 <option value="gpt-4o-mini-2024-07-18">gpt-4o-mini-2024-07-18</option>2852 <option value="gpt-4o-2024-11-20">gpt-4o-2024-11-20</option>
2853 <option value="gpt-4o-2024-08-06">gpt-4o-2024-08-06</option>
2854 <option value="gpt-4o-2024-05-13">gpt-4o-2024-05-13</option>
2855 <option value="chatgpt-4o-latest">chatgpt-4o-latest</option>
2839 </optgroup>2856 </optgroup>
2840 <optgroup label="GPT-4 Turbo">2857 <optgroup label="o1 and o1-mini">
2858 <option value="o1">o1</option>
2859 <option value="o1-2024-12-17">o1-2024-12-17</option>
2860 <option value="o1-mini">o1-mini</option>
2861 <option value="o1-mini-2024-09-12">o1-mini-2024-09-12</option>
2862 <option value="o1-preview">o1-preview</option>
2863 <option value="o1-preview-2024-09-12">o1-preview-2024-09-12</option>
2864 </optgroup>
2865 <optgroup label="o3">
2866 <option value="o3-mini">o3-mini</option>
2867 <option value="o3-mini-2025-01-31">o3-mini-2025-01-31</option>
2868 </optgroup>
2869 <optgroup label="GPT-4 Turbo and GPT-4">
2841 <option value="gpt-4-turbo">gpt-4-turbo</option>2870 <option value="gpt-4-turbo">gpt-4-turbo</option>
2842 <option value="gpt-4-turbo-2024-04-09">gpt-4-turbo-2024-04-09</option>2871 <option value="gpt-4-turbo-2024-04-09">gpt-4-turbo-2024-04-09</option>
2843 <option value="gpt-4-turbo-preview">gpt-4-turbo-preview</option>2872 <option value="gpt-4-turbo-preview">gpt-4-turbo-preview</option>
2844 <option value="gpt-4-vision-preview">gpt-4-vision-preview</option>
2845 <option value="gpt-4-0125-preview">gpt-4-0125-preview (2024)</option>2873 <option value="gpt-4-0125-preview">gpt-4-0125-preview (2024)</option>
2846 <option value="gpt-4-1106-preview">gpt-4-1106-preview (2023)</option>2874 <option value="gpt-4-1106-preview">gpt-4-1106-preview (2023)</option>
2875 <option value="gpt-4">gpt-4</option>
2876 <option value="gpt-4-0613">gpt-4-0613 (2023)</option>
2877 <option value="gpt-4-0314">gpt-4-0314 (2023)</option>
2847 </optgroup>2878 </optgroup>
2848 <optgroup label="o1">2879 <optgroup label="GPT-3.5 Turbo">
2849 <option value="o1-preview">o1-preview</option>2880 <option value="gpt-3.5-turbo">gpt-3.5-turbo</option>
2850 <option value="o1-mini">o1-mini</option>2881 <option value="gpt-3.5-turbo-0125">gpt-3.5-turbo-0125 (2024)</option>
2882 <option value="gpt-3.5-turbo-1106">gpt-3.5-turbo-1106 (2023)</option>
2883 <option value="gpt-3.5-turbo-instruct">gpt-3.5-turbo-instruct</option>
2851 </optgroup>2884 </optgroup>
2852 <optgroup label="Other">2885 <optgroup label="Other">
2853 <option value="text-davinci-003">text-davinci-003</option>2886 <option value="babbage-002">babbage-002</option>
2854 <option value="text-davinci-002">text-davinci-002</option>2887 <option value="davinci-002">davinci-002</option>
2855 <option value="text-curie-001">text-curie-001</option>
2856 <option value="text-babbage-001">text-babbage-001</option>
2857 <option value="text-ada-001">text-ada-001</option>
2858 <option value="code-davinci-002">code-davinci-002</option>
2859 </optgroup>2888 </optgroup>
2860 <optgroup id="openai_external_category" label="External">2889 <optgroup id="openai_external_category" label="External">
2861 </optgroup>2890 </optgroup>
@@ -3054,6 +3083,7 @@
3054 <h4 data-i18n="Google Model">Google Model</h4>3083 <h4 data-i18n="Google Model">Google Model</h4>
3055 <select id="model_google_select">3084 <select id="model_google_select">
3056 <optgroup label="Primary">3085 <optgroup label="Primary">
3086 <option value="gemini-2.0-flash">Gemini 2.0 Flash</option>
3057 <option value="gemini-1.5-pro">Gemini 1.5 Pro</option>3087 <option value="gemini-1.5-pro">Gemini 1.5 Pro</option>
3058 <option value="gemini-1.5-flash">Gemini 1.5 Flash</option>3088 <option value="gemini-1.5-flash">Gemini 1.5 Flash</option>
3059 <option value="gemini-1.0-pro">Gemini 1.0 Pro (Deprecated)</option>3089 <option value="gemini-1.0-pro">Gemini 1.0 Pro (Deprecated)</option>
@@ -3062,7 +3092,14 @@
3062 <option value="gemini-1.0-ultra-latest">Gemini 1.0 Ultra</option>3092 <option value="gemini-1.0-ultra-latest">Gemini 1.0 Ultra</option>
3063 </optgroup>3093 </optgroup>
3064 <optgroup label="Subversions">3094 <optgroup label="Subversions">
3065 <option value="gemini-2.0-flash-thinking-exp-1219">Gemini 2.0 Flash Thinking Experimental</option>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>
3100 <option value="gemini-2.0-flash-thinking-exp">Gemini 2.0 Flash Thinking Experimental</option>
3101 <option value="gemini-2.0-flash-thinking-exp-01-21">Gemini 2.0 Flash Thinking Experimental 2025-01-21</option>
3102 <option value="gemini-2.0-flash-thinking-exp-1219">Gemini 2.0 Flash Thinking Experimental 2024-12-19</option>
3066 <option value="gemini-2.0-flash-exp">Gemini 2.0 Flash Experimental</option>3103 <option value="gemini-2.0-flash-exp">Gemini 2.0 Flash Experimental</option>
3067 <option value="gemini-exp-1114">Gemini Experimental 2024-11-14</option>3104 <option value="gemini-exp-1114">Gemini Experimental 2024-11-14</option>
3068 <option value="gemini-exp-1121">Gemini Experimental 2024-11-21</option>3105 <option value="gemini-exp-1121">Gemini Experimental 2024-11-21</option>
@@ -3149,34 +3186,22 @@
3149 </div>3186 </div>
3150 <h4 data-i18n="Groq Model">Groq Model</h4>3187 <h4 data-i18n="Groq Model">Groq Model</h4>
3151 <select id="model_groq_select">3188 <select id="model_groq_select">
3152 <optgroup label="Llama 3.3">3189 <optgroup label="Production Models">
3190 <option value="gemma2-9b-it">gemma2-9b-it</option>
3153 <option value="llama-3.3-70b-versatile">llama-3.3-70b-versatile</option>3191 <option value="llama-3.3-70b-versatile">llama-3.3-70b-versatile</option>
3192 <option value="llama-3.1-8b-instant">llama-3.1-8b-instant</option>
3193 <option value="llama3-70b-8192">llama3-70b-8192</option>
3194 <option value="llama3-8b-8192">llama3-8b-8192</option>
3195 <option value="mixtral-8x7b-32768">mixtral-8x7b-32768</option>
3154 </optgroup>3196 </optgroup>
3155 <optgroup label="Llama 3.2">3197 <optgroup label="Preview Models">
3198 <option value="deepseek-r1-distill-llama-70b">deepseek-r1-distill-llama-70b</option>
3199 <option value="llama-3.3-70b-specdec">llama-3.3-70b-specdec</option>
3156 <option value="llama-3.2-1b-preview">llama-3.2-1b-preview</option>3200 <option value="llama-3.2-1b-preview">llama-3.2-1b-preview</option>
3157 <option value="llama-3.2-3b-preview">llama-3.2-3b-preview</option>3201 <option value="llama-3.2-3b-preview">llama-3.2-3b-preview</option>
3158 <option value="llama-3.2-11b-vision-preview">llama-3.2-11b-vision-preview</option>3202 <option value="llama-3.2-11b-vision-preview">llama-3.2-11b-vision-preview</option>
3159 <option value="llama-3.2-90b-vision-preview">llama-3.2-90b-vision-preview</option>3203 <option value="llama-3.2-90b-vision-preview">llama-3.2-90b-vision-preview</option>
3160 </optgroup>3204 </optgroup>
3161 <optgroup label="Llama 3.1">
3162 <option value="llama-3.1-8b-instant">llama-3.1-8b-instant</option>
3163 <option value="llama-3.1-70b-versatile">llama-3.1-70b-versatile</option>
3164 <option value="llama-3.1-405b-reasoning">llama-3.1-405b-reasoning</option>
3165 </optgroup>
3166 <optgroup label="Llama 3">
3167 <option value="llama3-groq-8b-8192-tool-use-preview">llama3-groq-8b-8192-tool-use-preview</option>
3168 <option value="llama3-groq-70b-8192-tool-use-preview">llama3-groq-70b-8192-tool-use-preview</option>
3169 <option value="llama3-8b-8192">llama3-8b-8192</option>
3170 <option value="llama3-70b-8192">llama3-70b-8192</option>
3171 </optgroup>
3172 <optgroup label="Gemma">
3173 <option value="gemma-7b-it">gemma-7b-it</option>
3174 <option value="gemma2-9b-it">gemma2-9b-it</option>
3175 </optgroup>
3176 <optgroup label="Other">
3177 <option value="mixtral-8x7b-32768">mixtral-8x7b-32768</option>
3178 <option value="llava-v1.5-7b-4096-preview">llava-v1.5-7b-4096-preview</option>
3179 </optgroup>
3180 </select>3205 </select>
3181 </div>3206 </div>
3182 <div id="nanogpt_form" data-source="nanogpt">3207 <div id="nanogpt_form" data-source="nanogpt">
@@ -3209,6 +3234,7 @@
3209 <select id="model_deepseek_select">3234 <select id="model_deepseek_select">
3210 <option value="deepseek-chat">deepseek-chat</option>3235 <option value="deepseek-chat">deepseek-chat</option>
3211 <option value="deepseek-coder">deepseek-coder</option>3236 <option value="deepseek-coder">deepseek-coder</option>
3237 <option value="deepseek-reasoner">deepseek-reasoner</option>
3212 </select>3238 </select>
3213 </div>3239 </div>
3214 </div>3240 </div>
@@ -3224,32 +3250,19 @@
3224 <h4 data-i18n="Perplexity Model">Perplexity Model</h4>3250 <h4 data-i18n="Perplexity Model">Perplexity Model</h4>
3225 <select id="model_perplexity_select">3251 <select id="model_perplexity_select">
3226 <optgroup label="Perplexity Sonar Models">3252 <optgroup label="Perplexity Sonar Models">
3253 <option value="sonar">sonar</option>
3254 <option value="sonar-pro">sonar-pro</option>
3255 <option value="sonar-reasoning">sonar-reasoning</option>
3256 </optgroup>
3257 <optgroup label="Deprecated Models">
3258 <!-- These are scheduled for deprecation after 2/22/2025 -->
3227 <option value="llama-3.1-sonar-small-128k-online">llama-3.1-sonar-small-128k-online</option>3259 <option value="llama-3.1-sonar-small-128k-online">llama-3.1-sonar-small-128k-online</option>
3228 <option value="llama-3.1-sonar-large-128k-online">llama-3.1-sonar-large-128k-online</option>3260 <option value="llama-3.1-sonar-large-128k-online">llama-3.1-sonar-large-128k-online</option>
3229 <option value="llama-3.1-sonar-huge-128k-online">llama-3.1-sonar-huge-128k-online</option>3261 <option value="llama-3.1-sonar-huge-128k-online">llama-3.1-sonar-huge-128k-online</option>
3230 </optgroup>3262 <!-- These are not listed on the site anymore -->
3231 <optgroup label="Perplexity Chat Models">
3232 <option value="llama-3.1-sonar-small-128k-chat">llama-3.1-sonar-small-128k-chat</option>3263 <option value="llama-3.1-sonar-small-128k-chat">llama-3.1-sonar-small-128k-chat</option>
3233 <option value="llama-3.1-sonar-large-128k-chat">llama-3.1-sonar-large-128k-chat</option>3264 <option value="llama-3.1-sonar-large-128k-chat">llama-3.1-sonar-large-128k-chat</option>
3234 </optgroup>3265 </optgroup>
3235 <optgroup label="Open-Source Models">
3236 <option value="llama-3.1-8b-instruct">llama-3.1-8b-instruct</option>
3237 <option value="llama-3.1-70b-instruct">llama-3.1-70b-instruct</option>
3238 </optgroup>
3239 <optgroup label="Deprecated Models">
3240 <option value="llama-3-sonar-small-32k-chat">llama-3-sonar-small-32k-chat</option>
3241 <option value="llama-3-sonar-small-32k-online">llama-3-sonar-small-32k-online</option>
3242 <option value="llama-3-sonar-large-32k-chat">llama-3-sonar-large-32k-chat</option>
3243 <option value="llama-3-sonar-large-32k-online">llama-3-sonar-large-32k-online</option>
3244 <option value="sonar-small-chat">sonar-small-chat</option>
3245 <option value="sonar-small-online">sonar-small-online</option>
3246 <option value="sonar-medium-chat">sonar-medium-chat</option>
3247 <option value="sonar-medium-online">sonar-medium-online</option>
3248 <option value="llama-3-8b-instruct">llama-3-8b-instruct</option>
3249 <option value="llama-3-70b-instruct">llama-3-70b-instruct</option>
3250 <option value="mistral-7b-instruct">mistral-7b-instruct (v0.2)</option>
3251 <option value="mixtral-8x7b-instruct">mixtral-8x7b-instruct</option>
3252 </optgroup>
3253 </select>3266 </select>
3254 </div>3267 </div>
3255 <form id="cohere_form" data-source="cohere" action="javascript:void(null);" method="post" enctype="multipart/form-data">3268 <form id="cohere_form" data-source="cohere" action="javascript:void(null);" method="post" enctype="multipart/form-data">
@@ -3521,7 +3534,7 @@
3521 </label>3534 </label>
3522 <label id="instruct_enabled_label"for="instruct_enabled" class="checkbox_label flex1" title="Enable Instruct Mode" data-i18n="[title]instruct_enabled">3535 <label id="instruct_enabled_label"for="instruct_enabled" class="checkbox_label flex1" title="Enable Instruct Mode" data-i18n="[title]instruct_enabled">
3523 <input id="instruct_enabled" type="checkbox" style="display:none;" />3536 <input id="instruct_enabled" type="checkbox" style="display:none;" />
3524 <small><i class="fa-solid fa-power-off menu_button margin0"></i></small>3537 <small><i class="fa-solid fa-power-off menu_button togglable margin0"></i></small>
3525 </label>3538 </label>
3526 </div>3539 </div>
3527 </h4>3540 </h4>
@@ -3699,7 +3712,7 @@
3699 <div class="flex-container">3712 <div class="flex-container">
3700 <label id="sysprompt_enabled_label" for="sysprompt_enabled" class="checkbox_label flex1" title="Enable System Prompt" data-i18n="[title]sysprompt_enabled">3713 <label id="sysprompt_enabled_label" for="sysprompt_enabled" class="checkbox_label flex1" title="Enable System Prompt" data-i18n="[title]sysprompt_enabled">
3701 <input id="sysprompt_enabled" type="checkbox" style="display:none;" />3714 <input id="sysprompt_enabled" type="checkbox" style="display:none;" />
3702 <small><i class="fa-solid fa-power-off menu_button margin0"></i></small>3715 <small><i class="fa-solid fa-power-off menu_button togglable margin0"></i></small>
3703 </label>3716 </label>
3704 </div>3717 </div>
3705 </h4>3718 </h4>
@@ -3753,8 +3766,8 @@
3753 </div>3766 </div>
3754 <label class="checkbox_label" for="custom_stopping_strings_macro">3767 <label class="checkbox_label" for="custom_stopping_strings_macro">
3755 <input id="custom_stopping_strings_macro" type="checkbox" checked>3768 <input id="custom_stopping_strings_macro" type="checkbox" checked>
3756 <small data-i18n="Replace Macro in Custom Stopping Strings">3769 <small data-i18n="Replace Macro in Stop Strings">
3757 Replace Macro in Custom Stopping Strings3770 Replace Macro in Stop Strings
3758 </small>3771 </small>
3759 </label>3772 </label>
3760 </div>3773 </div>
@@ -3797,6 +3810,66 @@
3797 </div>3810 </div>
3798 </div>3811 </div>
3799 <div>3812 <div>
3813 <h4 class="standoutHeader">
3814 <span data-i18n="Reasoning">Reasoning</span>
3815 </h4>
3816 <div>
3817 <div class="flex-container alignItemsBaseline">
3818 <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">
3819 <input id="reasoning_auto_parse" type="checkbox" />
3820 <small data-i18n="Auto-Parse">
3821 Auto-Parse
3822 </small>
3823 </label>
3824 <label class="checkbox_label flex1" for="reasoning_auto_expand" title="Automatically expand reasoning blocks." data-i18n="[title]reasoning_auto_expand">
3825 <input id="reasoning_auto_expand" type="checkbox" />
3826 <small data-i18n="Auto-Expand">
3827 Auto-Expand
3828 </small>
3829 </label>
3830 <label class="checkbox_label flex1" for="reasoning_show_hidden" title="Show reasoning time for models with hidden reasoning." data-i18n="[title]reasoning_show_hidden">
3831 <input id="reasoning_show_hidden" type="checkbox" />
3832 <small data-i18n="Show Hidden">
3833 Show Hidden
3834 </small>
3835 </label>
3836 </div>
3837 <div class="flex-container alignItemsBaseline">
3838 <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">
3839 <input id="reasoning_add_to_prompts" type="checkbox" />
3840 <small data-i18n="Add to Prompts">
3841 Add to Prompts
3842 </small>
3843 </label>
3844 <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">
3845 <input id="reasoning_max_additions" class="text_pole textarea_compact widthUnset" type="number" min="0" max="999"></textarea>
3846 <small data-i18n="Max">Max</small>
3847 </div>
3848 </div>
3849 <details>
3850 <summary data-i18n="Reasoning Formatting">
3851 Reasoning Formatting
3852 </summary>
3853 <div class="flex-container">
3854 <div class="flex1" title="Inserted before the reasoning content." data-i18n="[title]reasoning_prefix">
3855 <small data-i18n="Prefix">Prefix</small>
3856 <textarea id="reasoning_prefix" class="text_pole textarea_compact autoSetHeight"></textarea>
3857 </div>
3858 <div class="flex1" title="Inserted after the reasoning content." data-i18n="[title]reasoning_suffix">
3859 <small data-i18n="Suffix">Suffix</small>
3860 <textarea id="reasoning_suffix" class="text_pole textarea_compact autoSetHeight"></textarea>
3861 </div>
3862 </div>
3863 <div class="flex-container">
3864 <div class="flex1" title="Inserted between the reasoning and the message content." data-i18n="[title]reasoning_separator">
3865 <small data-i18n="Separator">Separator</small>
3866 <textarea id="reasoning_separator" class="text_pole textarea_compact autoSetHeight"></textarea>
3867 </div>
3868 </div>
3869 </details>
3870 </div>
3871 </div>
3872 <div>
3800 <h4 class="standoutHeader" data-i18n="Miscellaneous">Miscellaneous</h4>3873 <h4 class="standoutHeader" data-i18n="Miscellaneous">Miscellaneous</h4>
3801 <div>3874 <div>
3802 <small>3875 <small>
@@ -4809,6 +4882,7 @@
4809 </div>4882 </div>
4810 <div id="extensions_settings" class="flex1 wide50p">4883 <div id="extensions_settings" class="flex1 wide50p">
4811 <div id="assets_container" class="extension_container"></div>4884 <div id="assets_container" class="extension_container"></div>
4885 <div id="typing_indicator_container" class="extension_container"></div>
4812 <div id="expressions_container" class="extension_container"></div>4886 <div id="expressions_container" class="extension_container"></div>
4813 <div id="sd_container" class="extension_container"></div>4887 <div id="sd_container" class="extension_container"></div>
4814 <div id="tts_container" class="extension_container"></div>4888 <div id="tts_container" class="extension_container"></div>
@@ -5804,7 +5878,7 @@
5804 <div class="inline-drawer-content flex-container paddingBottom5px wide100p">5878 <div class="inline-drawer-content flex-container paddingBottom5px wide100p">
5805 <div class="flex-container wide100p alignitemscenter">5879 <div class="flex-container wide100p alignitemscenter">
5806 <div name="keywordsAndLogicBlock" class="flex-container wide100p alignitemscenter">5880 <div name="keywordsAndLogicBlock" class="flex-container wide100p alignitemscenter">
5807 <div class="world_entry_form_control flex1">5881 <div class="world_entry_form_control keyprimary flex1">
5808 <small class="displayNone">5882 <small class="displayNone">
5809 <span data-i18n="Comma separated (required)">5883 <span data-i18n="Comma separated (required)">
5810 Comma separated (required)5884 Comma separated (required)
@@ -6218,14 +6292,31 @@
6218 <div class="mes_edit_buttons">6292 <div class="mes_edit_buttons">
6219 <div class="mes_edit_done menu_button fa-solid fa-check" title="Confirm" data-i18n="[title]Confirm"></div>6293 <div class="mes_edit_done menu_button fa-solid fa-check" title="Confirm" data-i18n="[title]Confirm"></div>
6220 <div class="mes_edit_copy menu_button fa-solid fa-copy" title="Copy this message" data-i18n="[title]Copy this message"></div>6294 <div class="mes_edit_copy menu_button fa-solid fa-copy" title="Copy this message" data-i18n="[title]Copy this message"></div>
6221 <div class="mes_edit_delete menu_button fa-solid fa-trash-can" title="Delete this message" data-i18n="[title]Delete this message">6295 <div class="mes_edit_add_reasoning menu_button fa-solid fa-lightbulb" title="Add a reasoning block" data-i18n="[title]Add a reasoning block"></div>
6222 </div>6296 <div class="mes_edit_delete menu_button fa-solid fa-trash-can" title="Delete this message" data-i18n="[title]Delete this message"></div>
6223 <div class="mes_edit_up menu_button fa-solid fa-chevron-up " title="Move message up" data-i18n="[title]Move message up"></div>6297 <div class="mes_edit_up menu_button fa-solid fa-chevron-up " title="Move message up" data-i18n="[title]Move message up"></div>
6224 <div class="mes_edit_down menu_button fa-solid fa-chevron-down" title="Move message down" data-i18n="[title]Move message down">6298 <div class="mes_edit_down menu_button fa-solid fa-chevron-down" title="Move message down" data-i18n="[title]Move message down"></div>
6225 </div>
6226 <div class="mes_edit_cancel menu_button fa-solid fa-xmark" title="Cancel" data-i18n="[title]Cancel"></div>6299 <div class="mes_edit_cancel menu_button fa-solid fa-xmark" title="Cancel" data-i18n="[title]Cancel"></div>
6227 </div>6300 </div>
6228 </div>6301 </div>
6302 <details class="mes_reasoning_details">
6303 <summary class="mes_reasoning_summary flex-container">
6304 <div class="mes_reasoning_header_block flex-container">
6305 <div class="mes_reasoning_header flex-container">
6306 <span class="mes_reasoning_header_title" data-i18n="Thought for some time">Thought for some time</span>
6307 <div class="mes_reasoning_arrow fa-solid fa-chevron-up"></div>
6308 </div>
6309 </div>
6310 <div class="mes_reasoning_actions flex-container">
6311 <div class="mes_reasoning_edit_done menu_button edit_button fa-solid fa-check" title="Confirm" data-i18n="[title]Confirmedit"></div>
6312 <div class="mes_reasoning_delete menu_button edit_button fa-solid fa-trash-can" title="Remove reasoning" data-i18n="[title]Remove reasoning"></div>
6313 <div class="mes_reasoning_edit_cancel menu_button edit_button fa-solid fa-xmark" title="Cancel edit" data-i18n="[title]Cancel edit"></div>
6314 <div class="mes_reasoning_copy mes_button fa-solid fa-copy" title="Copy reasoning" data-i18n="[title]Copy reasoning"></div>
6315 <div class="mes_reasoning_edit mes_button fa-solid fa-pencil" title="Edit reasoning" data-i18n="[title]Edit reasoning"></div>
6316 </div>
6317 </summary>
6318 <div class="mes_reasoning"></div>
6319 </details>
6229 <div class="mes_text"></div>6320 <div class="mes_text"></div>
6230 <div class="mes_img_container">6321 <div class="mes_img_container">
6231 <div class="mes_img_controls">6322 <div class="mes_img_controls">
@@ -6325,7 +6416,10 @@
6325 <img alt="Avatar" src="" />6416 <img alt="Avatar" src="" />
6326 </div>6417 </div>
6327 <div class="group_member_name">6418 <div class="group_member_name">
6328 <div class="ch_name"></div>6419 <div class="character_name_block">
6420 <span class="ch_name"></span>
6421 <small class="ch_additional_info character_version"></small>
6422 </div>
6329 <div class="tags tags_inline"></div>6423 <div class="tags tags_inline"></div>
6330 </div>6424 </div>
6331 <input class="ch_fav" value="" hidden />6425 <input class="ch_fav" value="" hidden />
@@ -6437,9 +6531,6 @@
6437 </div>6531 </div>
64386532
6439 <!-- chat and input bar -->6533 <!-- chat and input bar -->
6440 <div id="typing_indicator_template" class="template_element">
6441 <div class="typing_indicator"><span class="typing_indicator_name">CHAR</span> is typing</div>
6442 </div>
6443 <div id="message_file_template" class="template_element">6534 <div id="message_file_template" class="template_element">
6444 <div class="mes_file_container">6535 <div class="mes_file_container">
6445 <div class="fa-lg fa-solid fa-file-alt mes_file_icon"></div>6536 <div class="fa-lg fa-solid fa-file-alt mes_file_icon"></div>
public/locales/ar-sa.json+2 -2
@@ -482,7 +482,7 @@
482 "separate with commas w/o space between": "فصل بفواصل دون مسافة بينها",482 "separate with commas w/o space between": "فصل بفواصل دون مسافة بينها",
483 "Custom Stopping Strings": "سلاسل توقف مخصصة",483 "Custom Stopping Strings": "سلاسل توقف مخصصة",
484 "JSON serialized array of strings": "مصفوفة سلسلة JSON متسلسلة",484 "JSON serialized array of strings": "مصفوفة سلسلة JSON متسلسلة",
485 "Replace Macro in Custom Stopping Strings": "استبدال الماكرو في سلاسل التوقف المخصصة",485 "Replace Macro in Stop Strings": "استبدال الماكرو في سلاسل التوقف المخصصة",
486 "Auto-Continue": "المتابعة التلقائية",486 "Auto-Continue": "المتابعة التلقائية",
487 "Allow for Chat Completion APIs": "السماح بواجهات برمجة التطبيقات لإكمال الدردشة",487 "Allow for Chat Completion APIs": "السماح بواجهات برمجة التطبيقات لإكمال الدردشة",
488 "Target length (tokens)": "الطول المستهدف (رموز)",488 "Target length (tokens)": "الطول المستهدف (رموز)",
@@ -1376,7 +1376,7 @@
1376 "char_import_2": "Chub Lorebook (رابط مباشر أو معرف)",1376 "char_import_2": "Chub Lorebook (رابط مباشر أو معرف)",
1377 "char_import_3": "حرف JanitorAI (رابط مباشر أو UUID)",1377 "char_import_3": "حرف JanitorAI (رابط مباشر أو UUID)",
1378 "char_import_4": "حرف Pygmalion.chat (رابط مباشر أو UUID)",1378 "char_import_4": "حرف Pygmalion.chat (رابط مباشر أو UUID)",
1379 "char_import_5": "حرف AICharacterCard.com (رابط مباشر أو معرف)",1379 "char_import_5": "حرف AICharacterCards.com (رابط مباشر أو معرف)",
1380 "char_import_6": "رابط PNG المباشر (راجع",1380 "char_import_6": "رابط PNG المباشر (راجع",
1381 "char_import_7": "للمضيفين المسموح بهم)",1381 "char_import_7": "للمضيفين المسموح بهم)",
1382 "char_import_8": "شخصية RisuRealm (رابط مباشر)",1382 "char_import_8": "شخصية RisuRealm (رابط مباشر)",
public/locales/de-de.json+2 -2
@@ -482,7 +482,7 @@
482 "separate with commas w/o space between": "getrennt durch Kommas ohne Leerzeichen dazwischen",482 "separate with commas w/o space between": "getrennt durch Kommas ohne Leerzeichen dazwischen",
483 "Custom Stopping Strings": "Benutzerdefinierte Stoppzeichenfolgen",483 "Custom Stopping Strings": "Benutzerdefinierte Stoppzeichenfolgen",
484 "JSON serialized array of strings": "JSON serialisierte Reihe von Zeichenfolgen",484 "JSON serialized array of strings": "JSON serialisierte Reihe von Zeichenfolgen",
485 "Replace Macro in Custom Stopping Strings": "Makro in benutzerdefinierten Stoppzeichenfolgen ersetzen",485 "Replace Macro in Stop Strings": "Makro in benutzerdefinierten Stoppzeichenfolgen ersetzen",
486 "Auto-Continue": "Automatisch fortsetzen",486 "Auto-Continue": "Automatisch fortsetzen",
487 "Allow for Chat Completion APIs": "Erlaube Chat-Vervollständigungs-APIs",487 "Allow for Chat Completion APIs": "Erlaube Chat-Vervollständigungs-APIs",
488 "Target length (tokens)": "Ziel-Länge (Tokens)",488 "Target length (tokens)": "Ziel-Länge (Tokens)",
@@ -1376,7 +1376,7 @@
1376 "char_import_2": "Chub Lorebook (Direktlink oder ID)",1376 "char_import_2": "Chub Lorebook (Direktlink oder ID)",
1377 "char_import_3": "JanitorAI-Charakter (Direktlink oder UUID)",1377 "char_import_3": "JanitorAI-Charakter (Direktlink oder UUID)",
1378 "char_import_4": "Pygmalion.chat-Charakter (Direktlink oder UUID)",1378 "char_import_4": "Pygmalion.chat-Charakter (Direktlink oder UUID)",
1379 "char_import_5": "AICharacterCard.com-Charakter (Direktlink oder ID)",1379 "char_import_5": "AICharacterCards.com-Charakter (Direktlink oder ID)",
1380 "char_import_6": "Direkter PNG-Link (siehe",1380 "char_import_6": "Direkter PNG-Link (siehe",
1381 "char_import_7": "für erlaubte Hosts)",1381 "char_import_7": "für erlaubte Hosts)",
1382 "char_import_8": "RisuRealm-Charakter (Direktlink)",1382 "char_import_8": "RisuRealm-Charakter (Direktlink)",
public/locales/es-es.json+2 -2
@@ -482,7 +482,7 @@
482 "separate with commas w/o space between": "separe con comas sin espacio entre ellas",482 "separate with commas w/o space between": "separe con comas sin espacio entre ellas",
483 "Custom Stopping Strings": "Cadenas de Detención Personalizadas",483 "Custom Stopping Strings": "Cadenas de Detención Personalizadas",
484 "JSON serialized array of strings": "Arreglo de cadenas serializado en JSON",484 "JSON serialized array of strings": "Arreglo de cadenas serializado en JSON",
485 "Replace Macro in Custom Stopping Strings": "Reemplazar macro en Cadenas de Detención Personalizadas",485 "Replace Macro in Stop Strings": "Reemplazar macro en Cadenas de Detención Personalizadas",
486 "Auto-Continue": "Autocontinuar",486 "Auto-Continue": "Autocontinuar",
487 "Allow for Chat Completion APIs": "Permitir para APIs de Completado de Chat",487 "Allow for Chat Completion APIs": "Permitir para APIs de Completado de Chat",
488 "Target length (tokens)": "Longitud objetivo (tokens)",488 "Target length (tokens)": "Longitud objetivo (tokens)",
@@ -1376,7 +1376,7 @@
1376 "char_import_2": "Chub Lorebook (enlace directo o ID)",1376 "char_import_2": "Chub Lorebook (enlace directo o ID)",
1377 "char_import_3": "Carácter de JanitorAI (enlace directo o UUID)",1377 "char_import_3": "Carácter de JanitorAI (enlace directo o UUID)",
1378 "char_import_4": "Carácter Pygmalion.chat (enlace directo o UUID)",1378 "char_import_4": "Carácter Pygmalion.chat (enlace directo o UUID)",
1379 "char_import_5": "Carácter AICharacterCard.com (enlace directo o ID)",1379 "char_import_5": "Carácter AICharacterCards.com (enlace directo o ID)",
1380 "char_import_6": "Enlace PNG directo (consulte",1380 "char_import_6": "Enlace PNG directo (consulte",
1381 "char_import_7": "para hosts permitidos)",1381 "char_import_7": "para hosts permitidos)",
1382 "char_import_8": "Personaje RisuRealm (Enlace directo)",1382 "char_import_8": "Personaje RisuRealm (Enlace directo)",
public/locales/fr-fr.json+4 -4
@@ -434,7 +434,7 @@
434 "Non-markdown strings": "Chaînes non Markdown",434 "Non-markdown strings": "Chaînes non Markdown",
435 "Custom Stopping Strings": "Chaînes d'arrêt personnalisées",435 "Custom Stopping Strings": "Chaînes d'arrêt personnalisées",
436 "JSON serialized array of strings": "Tableau de chaînes sérialisé JSON",436 "JSON serialized array of strings": "Tableau de chaînes sérialisé JSON",
437 "Replace Macro in Custom Stopping Strings": "Remplacer les macro dans les chaînes d'arrêt personnalisées",437 "Replace Macro in Stop Strings": "Remplacer les macro dans les chaînes d'arrêt personnalisées",
438 "Auto-Continue": "Auto-Continue",438 "Auto-Continue": "Auto-Continue",
439 "Allow for Chat Completion APIs": "Autoriser les APIs de complétion de chat",439 "Allow for Chat Completion APIs": "Autoriser les APIs de complétion de chat",
440 "Target length (tokens)": "Longueur cible (tokens)",440 "Target length (tokens)": "Longueur cible (tokens)",
@@ -1297,7 +1297,7 @@
1297 "char_import_2": "Lorebook de Chub (lien direct ou ID)",1297 "char_import_2": "Lorebook de Chub (lien direct ou ID)",
1298 "char_import_3": "Personnage de JanitorAI (lien direct ou UUID)",1298 "char_import_3": "Personnage de JanitorAI (lien direct ou UUID)",
1299 "char_import_4": "Personnage de Pygmalion.chat (lien direct ou UUID)",1299 "char_import_4": "Personnage de Pygmalion.chat (lien direct ou UUID)",
1300 "char_import_5": "Personnage de AICharacterCard.com (lien direct ou identifiant)",1300 "char_import_5": "Personnage de AICharacterCards.com (lien direct ou identifiant)",
1301 "char_import_6": "Lien PNG direct (voir",1301 "char_import_6": "Lien PNG direct (voir",
1302 "char_import_7": "pour les hôtes autorisés)",1302 "char_import_7": "pour les hôtes autorisés)",
1303 "char_import_8": "Personnage de RisuRealm (lien direct)",1303 "char_import_8": "Personnage de RisuRealm (lien direct)",
@@ -1385,8 +1385,8 @@
1385 "enable_functions_desc_1": "Autorise l'utilisation",1385 "enable_functions_desc_1": "Autorise l'utilisation",
1386 "enable_functions_desc_2": "outils de fonction",1386 "enable_functions_desc_2": "outils de fonction",
1387 "enable_functions_desc_3": "Peut être utilisé par diverses extensions pour fournir des fonctionnalités supplémentaires.",1387 "enable_functions_desc_3": "Peut être utilisé par diverses extensions pour fournir des fonctionnalités supplémentaires.",
1388 "Show model thoughts": "Afficher les pensées du modèle",1388 "Request model reasoning": "Demander les pensées du modèle",
1389 "Display the model's internal thoughts in the response.": "Afficher les pensées internes du modèle dans la réponse.",1389 "Allows the model to return its thinking process.": "Permet au modèle de retourner son processus de réflexion.",
1390 "Confirm token parsing with": "Confirmer l'analyse des tokens avec",1390 "Confirm token parsing with": "Confirmer l'analyse des tokens avec",
1391 "openai_logit_bias_no_items": "Aucun élément",1391 "openai_logit_bias_no_items": "Aucun élément",
1392 "api_no_connection": "Pas de connection...",1392 "api_no_connection": "Pas de connection...",
public/locales/is-is.json+2 -2
@@ -482,7 +482,7 @@
482 "separate with commas w/o space between": "aðskilið með kommum án bila milli",482 "separate with commas w/o space between": "aðskilið með kommum án bila milli",
483 "Custom Stopping Strings": "Eigin stopp-strengir",483 "Custom Stopping Strings": "Eigin stopp-strengir",
484 "JSON serialized array of strings": "JSON raðað fylki af strengjum",484 "JSON serialized array of strings": "JSON raðað fylki af strengjum",
485 "Replace Macro in Custom Stopping Strings": "Skiptu út í macro í sérsniðnum stoppa strengjum",485 "Replace Macro in Stop Strings": "Skiptu út í macro í sérsniðnum stoppa strengjum",
486 "Auto-Continue": "Sjálfvirk Forná",486 "Auto-Continue": "Sjálfvirk Forná",
487 "Allow for Chat Completion APIs": "Leyfa fyrir spjall Loka APIs",487 "Allow for Chat Completion APIs": "Leyfa fyrir spjall Loka APIs",
488 "Target length (tokens)": "Markaðarlengd (texti)",488 "Target length (tokens)": "Markaðarlengd (texti)",
@@ -1376,7 +1376,7 @@
1376 "char_import_2": "Chub Lorebook (beinn hlekkur eða auðkenni)",1376 "char_import_2": "Chub Lorebook (beinn hlekkur eða auðkenni)",
1377 "char_import_3": "JanitorAI karakter (beinn hlekkur eða UUID)",1377 "char_import_3": "JanitorAI karakter (beinn hlekkur eða UUID)",
1378 "char_import_4": "Pygmalion.chat karakter (beinn hlekkur eða UUID)",1378 "char_import_4": "Pygmalion.chat karakter (beinn hlekkur eða UUID)",
1379 "char_import_5": "AICharacterCard.com Karakter (beinn hlekkur eða auðkenni)",1379 "char_import_5": "AICharacterCards.com Karakter (beinn hlekkur eða auðkenni)",
1380 "char_import_6": "Beinn PNG hlekkur (sjá",1380 "char_import_6": "Beinn PNG hlekkur (sjá",
1381 "char_import_7": "fyrir leyfilega gestgjafa)",1381 "char_import_7": "fyrir leyfilega gestgjafa)",
1382 "char_import_8": "RisuRealm karakter (beinn hlekkur)",1382 "char_import_8": "RisuRealm karakter (beinn hlekkur)",
public/locales/it-it.json+2 -2
@@ -482,7 +482,7 @@
482 "separate with commas w/o space between": "separati con virgole senza spazio tra loro",482 "separate with commas w/o space between": "separati con virgole senza spazio tra loro",
483 "Custom Stopping Strings": "Stringhe di Stop Personalizzate",483 "Custom Stopping Strings": "Stringhe di Stop Personalizzate",
484 "JSON serialized array of strings": "Matrice serializzata JSON di stringhe",484 "JSON serialized array of strings": "Matrice serializzata JSON di stringhe",
485 "Replace Macro in Custom Stopping Strings": "Sostituisci Macro in Stringhe di Arresto Personalizzate",485 "Replace Macro in Stop Strings": "Sostituisci Macro in Stringhe di Arresto Personalizzate",
486 "Auto-Continue": "Auto-continua",486 "Auto-Continue": "Auto-continua",
487 "Allow for Chat Completion APIs": "Consenti per API di completamento chat",487 "Allow for Chat Completion APIs": "Consenti per API di completamento chat",
488 "Target length (tokens)": "Lunghezza obiettivo (token)",488 "Target length (tokens)": "Lunghezza obiettivo (token)",
@@ -1376,7 +1376,7 @@
1376 "char_import_2": "Lorebook di Chub (collegamento diretto o ID)",1376 "char_import_2": "Lorebook di Chub (collegamento diretto o ID)",
1377 "char_import_3": "Carattere JanitorAI (collegamento diretto o UUID)",1377 "char_import_3": "Carattere JanitorAI (collegamento diretto o UUID)",
1378 "char_import_4": "Carattere Pygmalion.chat (collegamento diretto o UUID)",1378 "char_import_4": "Carattere Pygmalion.chat (collegamento diretto o UUID)",
1379 "char_import_5": "Carattere AICharacterCard.com (Link diretto o ID)",1379 "char_import_5": "Carattere AICharacterCards.com (Link diretto o ID)",
1380 "char_import_6": "Collegamento PNG diretto (fare riferimento a",1380 "char_import_6": "Collegamento PNG diretto (fare riferimento a",
1381 "char_import_7": "per gli host consentiti)",1381 "char_import_7": "per gli host consentiti)",
1382 "char_import_8": "Personaggio RisuRealm (collegamento diretto)",1382 "char_import_8": "Personaggio RisuRealm (collegamento diretto)",
public/locales/ja-jp.json+2 -2
@@ -482,7 +482,7 @@
482 "separate with commas w/o space between": "間にスペースのないカンマで区切ります",482 "separate with commas w/o space between": "間にスペースのないカンマで区切ります",
483 "Custom Stopping Strings": "カスタム停止文字列",483 "Custom Stopping Strings": "カスタム停止文字列",
484 "JSON serialized array of strings": "文字列のJSONシリアル化配列",484 "JSON serialized array of strings": "文字列のJSONシリアル化配列",
485 "Replace Macro in Custom Stopping Strings": "カスタム停止文字列内のマクロを置換する",485 "Replace Macro in Stop Strings": "カスタム停止文字列内のマクロを置換する",
486 "Auto-Continue": "自動継続",486 "Auto-Continue": "自動継続",
487 "Allow for Chat Completion APIs": "チャット補完APIを許可",487 "Allow for Chat Completion APIs": "チャット補完APIを許可",
488 "Target length (tokens)": "ターゲット長さ(トークン)",488 "Target length (tokens)": "ターゲット長さ(トークン)",
@@ -1378,7 +1378,7 @@
1378 "char_import_2": "Chub ロアブック (直接リンクまたは ID)",1378 "char_import_2": "Chub ロアブック (直接リンクまたは ID)",
1379 "char_import_3": "JanitorAI キャラクター (直接リンクまたは UUID)",1379 "char_import_3": "JanitorAI キャラクター (直接リンクまたは UUID)",
1380 "char_import_4": "Pygmalion.chat キャラクター (直接リンクまたは UUID)",1380 "char_import_4": "Pygmalion.chat キャラクター (直接リンクまたは UUID)",
1381 "char_import_5": "AICharacterCard.com キャラクター (直接リンクまたは ID)",1381 "char_import_5": "AICharacterCards.com キャラクター (直接リンクまたは ID)",
1382 "char_import_6": "直接PNGリンク(参照",1382 "char_import_6": "直接PNGリンク(参照",
1383 "char_import_7": "許可されたホストの場合)",1383 "char_import_7": "許可されたホストの場合)",
1384 "char_import_8": "RisuRealm キャラクター (直接リンク)",1384 "char_import_8": "RisuRealm キャラクター (直接リンク)",
public/locales/ko-kr.json+11 -11
@@ -211,7 +211,7 @@
211 "Sampler Priority": "샘플러 우선 순위",211 "Sampler Priority": "샘플러 우선 순위",
212 "Ooba only. Determines the order of samplers.": "Ooba 전용. 샘플러의 순서를 결정합니다.",212 "Ooba only. Determines the order of samplers.": "Ooba 전용. 샘플러의 순서를 결정합니다.",
213 "Character Names Behavior": "캐릭터 이름 동작",213 "Character Names Behavior": "캐릭터 이름 동작",
214 "[title]character_names_none": "캐릭터 이름 접두사를 추가하지 않습니다. 그룹 채팅에서는 좋지 않을 수 있으므로, 이 설정을 선택할 때는 주의해야 합니다.",214 "character_names_none": "캐릭터 이름 접두사를 추가하지 않습니다. 그룹 채팅에서는 좋지 않을 수 있으므로, 이 설정을 선택할 때는 주의해야 합니다.",
215 "Helps the model to associate messages with characters.": "모델이 메시지를 캐릭터와 연관시키는 데 도움이 됩니다.",215 "Helps the model to associate messages with characters.": "모델이 메시지를 캐릭터와 연관시키는 데 도움이 됩니다.",
216 "None": "없음",216 "None": "없음",
217 "None (not injected)": "없음 (삽입되지 않음)",217 "None (not injected)": "없음 (삽입되지 않음)",
@@ -404,7 +404,7 @@
404 "Custom API Key": "커스텀 API 키",404 "Custom API Key": "커스텀 API 키",
405 "Available Models": "사용 가능한 모델",405 "Available Models": "사용 가능한 모델",
406 "Prompt Post-Processing": "신속한 후처리",406 "Prompt Post-Processing": "신속한 후처리",
407 "[title]API Connections;[no_connection_text]api_no_connection": "연결이 되지 않았습니다...",407 "api_no_connection": "연결이 되지 않았습니다...",
408 "Applies additional processing to the prompt before sending it to the API.": "API로 보내기 전에 프롬프트에 추가 처리를 적용합니다.",408 "Applies additional processing to the prompt before sending it to the API.": "API로 보내기 전에 프롬프트에 추가 처리를 적용합니다.",
409 "Verifies your API connection by sending a short test message. Be aware that you'll be credited for it!": "짧은 테스트 메시지를 보내어 API 연결을 확인합니다. 이에 대해 유료 크레딧이 지불될 수 있음을 인식하세요!",409 "Verifies your API connection by sending a short test message. Be aware that you'll be credited for it!": "짧은 테스트 메시지를 보내어 API 연결을 확인합니다. 이에 대해 유료 크레딧이 지불될 수 있음을 인식하세요!",
410 "Test Message": "테스트 메시지",410 "Test Message": "테스트 메시지",
@@ -492,7 +492,7 @@
492 "separate with commas w/o space between": "쉼표로 구분 (공백 없이)",492 "separate with commas w/o space between": "쉼표로 구분 (공백 없이)",
493 "Custom Stopping Strings": "사용자 정의 중지 문자열",493 "Custom Stopping Strings": "사용자 정의 중지 문자열",
494 "JSON serialized array of strings": "문자열의 JSON 직렬화된 배열",494 "JSON serialized array of strings": "문자열의 JSON 직렬화된 배열",
495 "Replace Macro in Custom Stopping Strings": "사용자 정의 중단 문자열에서 매크로 교체",495 "Replace Macro in Stop Strings": "사용자 정의 중단 문자열에서 매크로 교체",
496 "Auto-Continue": "자동 계속하기",496 "Auto-Continue": "자동 계속하기",
497 "Allow for Chat Completion APIs": "채팅 완성 API 허용",497 "Allow for Chat Completion APIs": "채팅 완성 API 허용",
498 "Target length (tokens)": "대상 길이 (토큰)",498 "Target length (tokens)": "대상 길이 (토큰)",
@@ -625,7 +625,7 @@
625 "Single-row message input area. Mobile only, no effect on PC": "한 줄짜리 메시지 입력 영역. 모바일 전용, PC에는 영향 없음",625 "Single-row message input area. Mobile only, no effect on PC": "한 줄짜리 메시지 입력 영역. 모바일 전용, PC에는 영향 없음",
626 "Compact Input Area (Mobile)": "조그마한 입력 영역 (모바일)",626 "Compact Input Area (Mobile)": "조그마한 입력 영역 (모바일)",
627 "Swipe # for All Messages": "모든 스와이프 메시지에 대해 번호 매기기",627 "Swipe # for All Messages": "모든 스와이프 메시지에 대해 번호 매기기",
628 "[title]Display swipe numbers for all messages, not just the last.": "마지막 메시지만이 아니라 모든 메시지에 대한 스와이프 번호를 표시합니다.",628 "Display swipe numbers for all messages, not just the last.": "마지막 메시지만이 아니라 모든 메시지에 대한 스와이프 번호를 표시합니다.",
629 "In the Character Management panel, show quick selection buttons for favorited characters": "캐릭터 관리 패널에서 즐겨찾는 캐릭터에 대한 빠른 선택 버튼을 표시합니다",629 "In the Character Management panel, show quick selection buttons for favorited characters": "캐릭터 관리 패널에서 즐겨찾는 캐릭터에 대한 빠른 선택 버튼을 표시합니다",
630 "Characters Hotswap": "캐릭터 핫스왑",630 "Characters Hotswap": "캐릭터 핫스왑",
631 "Enable magnification for zoomed avatar display.": "마우스 포인터를 아바타 위에 올려두면 아바타가 확대 됩니다.",631 "Enable magnification for zoomed avatar display.": "마우스 포인터를 아바타 위에 올려두면 아바타가 확대 됩니다.",
@@ -1395,7 +1395,7 @@
1395 "char_import_2": "Chub Lorebook(직접 링크 또는 ID)",1395 "char_import_2": "Chub Lorebook(직접 링크 또는 ID)",
1396 "char_import_3": "JanitorAI 캐릭터(직접 링크 또는 UUID)",1396 "char_import_3": "JanitorAI 캐릭터(직접 링크 또는 UUID)",
1397 "char_import_4": "Pygmalion.chat 문자(직접 링크 또는 UUID)",1397 "char_import_4": "Pygmalion.chat 문자(직접 링크 또는 UUID)",
1398 "char_import_5": "AICharacterCard.com 캐릭터(직접 링크 또는 ID)",1398 "char_import_5": "AICharacterCards.com 캐릭터(직접 링크 또는 ID)",
1399 "char_import_6": "직접 PNG 링크(참조",1399 "char_import_6": "직접 PNG 링크(참조",
1400 "char_import_7": "허용된 호스트의 경우)",1400 "char_import_7": "허용된 호스트의 경우)",
1401 "char_import_8": "RisuRealm 캐릭터 (직접링크)",1401 "char_import_8": "RisuRealm 캐릭터 (직접링크)",
@@ -1538,7 +1538,7 @@
1538 "Only apply color as accent": "색상은 오직 강조로써만 적용됩니다",1538 "Only apply color as accent": "색상은 오직 강조로써만 적용됩니다",
1539 "qr--colorClear": "색상 지우기",1539 "qr--colorClear": "색상 지우기",
1540 "Color": "색상",1540 "Color": "색상",
1541 "[title]world_button_title": "캐릭터 로어. 클릭하여 로드하세요. Shift를 클릭하면 '월드 인포 링크' 팝업이 열립니다.",1541 "world_button_title": "캐릭터 로어. 클릭하여 로드하세요. Shift를 클릭하면 '월드 인포 링크' 팝업이 열립니다.",
1542 "Select TTS Provider": "TTS 공급자 선택",1542 "Select TTS Provider": "TTS 공급자 선택",
1543 "tts_enabled": "활성화",1543 "tts_enabled": "활성화",
1544 "Narrate user messages": "사용자 메시지 나레이션",1544 "Narrate user messages": "사용자 메시지 나레이션",
@@ -1583,15 +1583,15 @@
1583 "Prompt Content": "프롬프트 내용",1583 "Prompt Content": "프롬프트 내용",
1584 "Instruct Sequences": "지시 시퀀스",1584 "Instruct Sequences": "지시 시퀀스",
1585 "Prefer Character Card Instructions": "캐릭터 카드의 지시사항을 선호",1585 "Prefer Character Card Instructions": "캐릭터 카드의 지시사항을 선호",
1586 "[title]If checked and the character card contains a Post-History Instructions override, use that instead": "활성화 된 경우, 캐릭터 카드에 Post-History 지시 무시 항목이 포함되어 있으면, 카드 지시사항의 내용으로 대신 사용합니다.",1586 "If checked and the character card contains a Post-History Instructions override, use that instead": "활성화 된 경우, 캐릭터 카드에 Post-History 지시 무시 항목이 포함되어 있으면, 카드 지시사항의 내용으로 대신 사용합니다.",
1587 "Auto-select Input Text": "입력 텍스트 자동 선택",1587 "Auto-select Input Text": "입력 텍스트 자동 선택",
1588 "[title]Enable auto-select of input text in some text fields when clicking/selecting them. Applies to popup input textboxes, and possible other custom input fields.": "일부 텍스트 필드를 클릭하거나 선택할 때 자동으로 입력된 텍스트가 선택되도록 설정합니다. 팝업 입력창과 기타 커스텀 입력 필드에 적용됩니다.",1588 "Enable auto-select of input text in some text fields when clicking/selecting them. Applies to popup input textboxes, and possible other custom input fields.": "일부 텍스트 필드를 클릭하거나 선택할 때 자동으로 입력된 텍스트가 선택되도록 설정합니다. 팝업 입력창과 기타 커스텀 입력 필드에 적용됩니다.",
1589 "Markdown Hotkeys": "마크다운 입력 단축키",1589 "Markdown Hotkeys": "마크다운 입력 단축키",
1590 "[title]markdown_hotkeys_desc": "특정 텍스트 입력창에서 마크다운 형식 문자를 입력하기 위한 단축키를 활성화합니다. '/help hotkeys'를 참고하세요.",1590 "markdown_hotkeys_desc": "특정 텍스트 입력창에서 마크다운 형식 문자를 입력하기 위한 단축키를 활성화합니다. '/help hotkeys'를 참고하세요.",
1591 "Show group chat queue": "그룹 채팅 대기열 표시",1591 "Show group chat queue": "그룹 채팅 대기열 표시",
1592 "[title]In group chat, highlight the character(s) that are currently queued to generate responses and the order in which they will respond.": "그룹 채팅에서 응답을 생성하기 위해 현재 대기 중인 캐릭터와 응답할 순서를 강조 표시합니다.",1592 "In group chat, highlight the character(s) that are currently queued to generate responses and the order in which they will respond.": "그룹 채팅에서 응답을 생성하기 위해 현재 대기 중인 캐릭터와 응답할 순서를 강조 표시합니다.",
1593 "Quick 'Impersonate' button": "빠른 '사칭' 버튼",1593 "Quick 'Impersonate' button": "빠른 '사칭' 버튼",
1594 "[title]Show a button in the input area to ask the AI to impersonate your character for a single message": "입력 영역에 AI에게 한 메시지 동안 당신의 캐릭터 연기를 사칭하도록 요청하는 버튼을 표시합니다.",1594 "Show a button in the input area to ask the AI to impersonate your character for a single message": "입력 영역에 AI에게 한 메시지 동안 당신의 캐릭터 연기를 사칭하도록 요청하는 버튼을 표시합니다.",
1595 "Injection Template": "삽입 템플릿",1595 "Injection Template": "삽입 템플릿",
1596 "Query messages": "쿼리 메시지 수",1596 "Query messages": "쿼리 메시지 수",
1597 "Score threshold": "점수 임계값",1597 "Score threshold": "점수 임계값",
public/locales/nl-nl.json+2 -2
@@ -482,7 +482,7 @@
482 "separate with commas w/o space between": "gescheiden met komma's zonder spatie ertussen",482 "separate with commas w/o space between": "gescheiden met komma's zonder spatie ertussen",
483 "Custom Stopping Strings": "Aangepaste Stopreeksen",483 "Custom Stopping Strings": "Aangepaste Stopreeksen",
484 "JSON serialized array of strings": "JSON geserialiseerde reeks van strings",484 "JSON serialized array of strings": "JSON geserialiseerde reeks van strings",
485 "Replace Macro in Custom Stopping Strings": "Macro vervangen in aangepaste stopreeksen",485 "Replace Macro in Stop Strings": "Macro vervangen in aangepaste stopreeksen",
486 "Auto-Continue": "Automatisch doorgaan",486 "Auto-Continue": "Automatisch doorgaan",
487 "Allow for Chat Completion APIs": "Chatvervolledigings-API's toestaan",487 "Allow for Chat Completion APIs": "Chatvervolledigings-API's toestaan",
488 "Target length (tokens)": "Doellengte (tokens)",488 "Target length (tokens)": "Doellengte (tokens)",
@@ -1376,7 +1376,7 @@
1376 "char_import_2": "Chub Lorebook (directe link of ID)",1376 "char_import_2": "Chub Lorebook (directe link of ID)",
1377 "char_import_3": "JanitorAI-personage (directe link of UUID)",1377 "char_import_3": "JanitorAI-personage (directe link of UUID)",
1378 "char_import_4": "Pygmalion.chat-teken (directe link of UUID)",1378 "char_import_4": "Pygmalion.chat-teken (directe link of UUID)",
1379 "char_import_5": "AICharacterCard.com-teken (directe link of ID)",1379 "char_import_5": "AICharacterCards.com-teken (directe link of ID)",
1380 "char_import_6": "Directe PNG-link (zie",1380 "char_import_6": "Directe PNG-link (zie",
1381 "char_import_7": "voor toegestane hosts)",1381 "char_import_7": "voor toegestane hosts)",
1382 "char_import_8": "RisuRealm-personage (directe link)",1382 "char_import_8": "RisuRealm-personage (directe link)",
public/locales/pt-pt.json+2 -2
@@ -482,7 +482,7 @@
482 "separate with commas w/o space between": "separe com vírgulas sem espaço entre",482 "separate with commas w/o space between": "separe com vírgulas sem espaço entre",
483 "Custom Stopping Strings": "Cadeias de parada personalizadas",483 "Custom Stopping Strings": "Cadeias de parada personalizadas",
484 "JSON serialized array of strings": "Matriz de strings serializada em JSON",484 "JSON serialized array of strings": "Matriz de strings serializada em JSON",
485 "Replace Macro in Custom Stopping Strings": "Substituir Macro em Strings de Parada Personalizadas",485 "Replace Macro in Stop Strings": "Substituir Macro em Strings de Parada Personalizadas",
486 "Auto-Continue": "Auto-Continuar",486 "Auto-Continue": "Auto-Continuar",
487 "Allow for Chat Completion APIs": "Permitir APIs de Completar Chat",487 "Allow for Chat Completion APIs": "Permitir APIs de Completar Chat",
488 "Target length (tokens)": "Comprimento alvo (tokens)",488 "Target length (tokens)": "Comprimento alvo (tokens)",
@@ -1376,7 +1376,7 @@
1376 "char_import_2": "Chub Lorebook (link direto ou ID)",1376 "char_import_2": "Chub Lorebook (link direto ou ID)",
1377 "char_import_3": "Personagem JanitorAI (Link Direto ou UUID)",1377 "char_import_3": "Personagem JanitorAI (Link Direto ou UUID)",
1378 "char_import_4": "Caractere Pygmalion.chat (Link Direto ou UUID)",1378 "char_import_4": "Caractere Pygmalion.chat (Link Direto ou UUID)",
1379 "char_import_5": "Personagem AICharacterCard.com (link direto ou ID)",1379 "char_import_5": "Personagem AICharacterCards.com (link direto ou ID)",
1380 "char_import_6": "Link PNG direto (consulte",1380 "char_import_6": "Link PNG direto (consulte",
1381 "char_import_7": "para hosts permitidos)",1381 "char_import_7": "para hosts permitidos)",
1382 "char_import_8": "Personagem RisuRealm (link direto)",1382 "char_import_8": "Personagem RisuRealm (link direto)",
public/locales/ru-ru.json+2 -2
@@ -161,7 +161,7 @@
161 "View hidden API keys": "Посмотреть скрытые API-ключи",161 "View hidden API keys": "Посмотреть скрытые API-ключи",
162 "Advanced Formatting": "Расширенное форматирование",162 "Advanced Formatting": "Расширенное форматирование",
163 "Context Template": "Шаблон контекста",163 "Context Template": "Шаблон контекста",
164 "Replace Macro in Custom Stopping Strings": "Заменять макросы в пользовательских стоп-строках",164 "Replace Macro in Stop Strings": "Заменять макросы в пользовательских стоп-строках",
165 "Story String": "Строка истории",165 "Story String": "Строка истории",
166 "Example Separator": "Разделитель примеров сообщений",166 "Example Separator": "Разделитель примеров сообщений",
167 "Chat Start": "Начало чата",167 "Chat Start": "Начало чата",
@@ -966,7 +966,7 @@
966 "char_import_2": "Лорбук с Chub (прямая ссылка или ID)",966 "char_import_2": "Лорбук с Chub (прямая ссылка или ID)",
967 "char_import_3": "Персонаж с JanitorAI (прямая ссылка или UUID)",967 "char_import_3": "Персонаж с JanitorAI (прямая ссылка или UUID)",
968 "char_import_4": "Персонаж с Pygmalion.chat (прямая ссылка или UUID)",968 "char_import_4": "Персонаж с Pygmalion.chat (прямая ссылка или UUID)",
969 "char_import_5": "Персонаж с AICharacterCard.com (прямая ссылка или ID)",969 "char_import_5": "Персонаж с AICharacterCards.com (прямая ссылка или ID)",
970 "char_import_6": "Прямая ссылка на PNG-файл (чтобы узнать список разрешённых хостов, загляните в",970 "char_import_6": "Прямая ссылка на PNG-файл (чтобы узнать список разрешённых хостов, загляните в",
971 "char_import_7": ")",971 "char_import_7": ")",
972 "Grammar String": "Грамматика",972 "Grammar String": "Грамматика",
public/locales/uk-ua.json+2 -2
@@ -482,7 +482,7 @@
482 "separate with commas w/o space between": "розділяйте комами без пропусків між ними",482 "separate with commas w/o space between": "розділяйте комами без пропусків між ними",
483 "Custom Stopping Strings": "Власні рядки зупинки",483 "Custom Stopping Strings": "Власні рядки зупинки",
484 "JSON serialized array of strings": "JSON-серіалізований масив рядків",484 "JSON serialized array of strings": "JSON-серіалізований масив рядків",
485 "Replace Macro in Custom Stopping Strings": "Замінювати макроси у власних рядках зупинки",485 "Replace Macro in Stop Strings": "Замінювати макроси у власних рядках зупинки",
486 "Auto-Continue": "Автоматичне продовження",486 "Auto-Continue": "Автоматичне продовження",
487 "Allow for Chat Completion APIs": "Дозволити для Chat Completion API",487 "Allow for Chat Completion APIs": "Дозволити для Chat Completion API",
488 "Target length (tokens)": "Цільова довжина (токени)",488 "Target length (tokens)": "Цільова довжина (токени)",
@@ -1376,7 +1376,7 @@
1376 "char_import_2": "Chub Lorebook (пряме посилання або ID)",1376 "char_import_2": "Chub Lorebook (пряме посилання або ID)",
1377 "char_import_3": "Символ JanitorAI (пряме посилання або UUID)",1377 "char_import_3": "Символ JanitorAI (пряме посилання або UUID)",
1378 "char_import_4": "Символ Pygmalion.chat (пряме посилання або UUID)",1378 "char_import_4": "Символ Pygmalion.chat (пряме посилання або UUID)",
1379 "char_import_5": "Символ AICharacterCard.com (пряме посилання або ідентифікатор)",1379 "char_import_5": "Символ AICharacterCards.com (пряме посилання або ідентифікатор)",
1380 "char_import_6": "Пряме посилання на PNG (див",1380 "char_import_6": "Пряме посилання на PNG (див",
1381 "char_import_7": "для дозволених хостів)",1381 "char_import_7": "для дозволених хостів)",
1382 "char_import_8": "Персонаж RisuRealm (пряме посилання)",1382 "char_import_8": "Персонаж RisuRealm (пряме посилання)",
public/locales/vi-vn.json+2 -2
@@ -482,7 +482,7 @@
482 "separate with commas w/o space between": "phân tách bằng dấu phẩy không có khoảng trắng giữa",482 "separate with commas w/o space between": "phân tách bằng dấu phẩy không có khoảng trắng giữa",
483 "Custom Stopping Strings": "Chuỗi dừng tùy chỉnh",483 "Custom Stopping Strings": "Chuỗi dừng tùy chỉnh",
484 "JSON serialized array of strings": "Mảng chuỗi được tuần tự hóa JSON",484 "JSON serialized array of strings": "Mảng chuỗi được tuần tự hóa JSON",
485 "Replace Macro in Custom Stopping Strings": "Thay thế Macro trong Chuỗi Dừng Tùy chỉnh",485 "Replace Macro in Stop Strings": "Thay thế Macro trong Chuỗi Dừng Tùy chỉnh",
486 "Auto-Continue": "Tự động Tiếp tục",486 "Auto-Continue": "Tự động Tiếp tục",
487 "Allow for Chat Completion APIs": "Cho phép các API hoàn thành Trò chuyện",487 "Allow for Chat Completion APIs": "Cho phép các API hoàn thành Trò chuyện",
488 "Target length (tokens)": "Độ dài mục tiêu (token)",488 "Target length (tokens)": "Độ dài mục tiêu (token)",
@@ -1376,7 +1376,7 @@
1376 "char_import_2": "Chub (Nhập URL trực tiếp hoặc ID)",1376 "char_import_2": "Chub (Nhập URL trực tiếp hoặc ID)",
1377 "char_import_3": "JanitorAI (Nhập URL trực tiếp hoặc UUID)",1377 "char_import_3": "JanitorAI (Nhập URL trực tiếp hoặc UUID)",
1378 "char_import_4": "Pygmalion.chat (Nhập URL trực tiếp hoặc UUID)",1378 "char_import_4": "Pygmalion.chat (Nhập URL trực tiếp hoặc UUID)",
1379 "char_import_5": "AICharacterCard.com (Nhập URL trực tiếp hoặc ID)",1379 "char_import_5": "AICharacterCards.com (Nhập URL trực tiếp hoặc ID)",
1380 "char_import_6": "Nhập PNG trực tiếp (tham khảo",1380 "char_import_6": "Nhập PNG trực tiếp (tham khảo",
1381 "char_import_7": "đối với các máy chủ được phép)",1381 "char_import_7": "đối với các máy chủ được phép)",
1382 "char_import_8": "RisuRealm (URL trực tiếp)",1382 "char_import_8": "RisuRealm (URL trực tiếp)",
public/locales/zh-cn.json+22 -22
@@ -215,7 +215,7 @@
215 "Classifier Free Guidance. More helpful tip coming soon": "无分类器指导(CFG)。更多有用的提示敬请期待。",215 "Classifier Free Guidance. More helpful tip coming soon": "无分类器指导(CFG)。更多有用的提示敬请期待。",
216 "Scale": "缩放比例",216 "Scale": "缩放比例",
217 "Negative Prompt": "负面提示词",217 "Negative Prompt": "负面提示词",
218 "Used if CFG Scale is unset globally, per chat or character": "如果无分类器指导(CFG)缩放比例未在全局设置,它将作用于每个聊天或每个角色",218 "Used if CFG Scale is unset globally, per chat or character": "如果CFG缩放比例未被全局设置,它将作用于所有聊天或角色",
219 "Add text here that would make the AI generate things you don't want in your outputs.": "请在此处添加文本,以避免生成您不希望出现在输出中的内容。",219 "Add text here that would make the AI generate things you don't want in your outputs.": "请在此处添加文本,以避免生成您不希望出现在输出中的内容。",
220 "Grammar String": "语法字符串",220 "Grammar String": "语法字符串",
221 "GBNF or EBNF, depends on the backend in use. If you're using this you should know which.": "GBNF 或 EBNF,取决于使用的后端。如果您使用这个,您应该知道该用哪一个。",221 "GBNF or EBNF, depends on the backend in use. If you're using this you should know which.": "GBNF 或 EBNF,取决于使用的后端。如果您使用这个,您应该知道该用哪一个。",
@@ -266,8 +266,8 @@
266 "Use system prompt": "使用系统提示词",266 "Use system prompt": "使用系统提示词",
267 "Merges_all_system_messages_desc_1": "合并所有系统消息,直到第一条具有非系统角色的消息,然后通过",267 "Merges_all_system_messages_desc_1": "合并所有系统消息,直到第一条具有非系统角色的消息,然后通过",
268 "Merges_all_system_messages_desc_2": "字段发送。",268 "Merges_all_system_messages_desc_2": "字段发送。",
269 "Show model thoughts": "展示思维链",269 "Request model reasoning": "请求思维链",
270 "Display the model's internal thoughts in the response.": "展示模型在回复时的内部思维链。",270 "Allows the model to return its thinking process.": "允许模型返回其思维过程。",
271 "Assistant Prefill": "AI预填",271 "Assistant Prefill": "AI预填",
272 "Expand the editor": "展开编辑器",272 "Expand the editor": "展开编辑器",
273 "Start Claude's answer with...": "以如下内容开始Claude的回答...",273 "Start Claude's answer with...": "以如下内容开始Claude的回答...",
@@ -559,7 +559,7 @@
559 "Prompt Content": "提示词内容",559 "Prompt Content": "提示词内容",
560 "Custom Stopping Strings": "自定义停止字符串",560 "Custom Stopping Strings": "自定义停止字符串",
561 "JSON serialized array of strings": "JSON序列化的字符串数组",561 "JSON serialized array of strings": "JSON序列化的字符串数组",
562 "Replace Macro in Custom Stopping Strings": "替换自定义停止字符串中的宏",562 "Replace Macro in Stop Strings": "替换自定义停止字符串中的宏",
563 "Token Padding": "词符填充",563 "Token Padding": "词符填充",
564 "Miscellaneous": "杂项",564 "Miscellaneous": "杂项",
565 "Non-markdown strings": "非 Markdown 字符串",565 "Non-markdown strings": "非 Markdown 字符串",
@@ -1191,9 +1191,9 @@
1191 "welcome_message_part_8": "您可随时通过",1191 "welcome_message_part_8": "您可随时通过",
1192 "welcome_message_part_9": "图标来更改此设置。",1192 "welcome_message_part_9": "图标来更改此设置。",
1193 "Persona Name:": "用户角色名称:",1193 "Persona Name:": "用户角色名称:",
1194 "Temporarily disable automatic replies from this character": "暂时禁用此角色的自动回复",1194 "Temporarily disable automatic replies from this character": "临时禁言此角色",
1195 "Enable automatic replies from this character": "启用此角色的自动回复",1195 "Enable automatic replies from this character": "解除禁言此角色",
1196 "Trigger a message from this character": "从此角色触发消息",1196 "Trigger a message from this character": "强制触发该角色发言",
1197 "Move up": "向上移动",1197 "Move up": "向上移动",
1198 "Move down": "向下移动",1198 "Move down": "向下移动",
1199 "View character card": "查看角色卡片",1199 "View character card": "查看角色卡片",
@@ -1208,7 +1208,7 @@
1208 "View contents": "查看内容",1208 "View contents": "查看内容",
1209 "Remove the file": "删除文件",1209 "Remove the file": "删除文件",
1210 "Author's Note": "作者注释",1210 "Author's Note": "作者注释",
1211 "Unique to this chat": "此聊天独有",1211 "Unique to this chat": "仅对此聊天生效",
1212 "Checkpoints inherit the Note from their parent, and can be changed individually after that.": "检查点从其父级继承注释,之后可以单独更改。",1212 "Checkpoints inherit the Note from their parent, and can be changed individually after that.": "检查点从其父级继承注释,之后可以单独更改。",
1213 "Include in World Info Scanning": "纳入世界信息扫描",1213 "Include in World Info Scanning": "纳入世界信息扫描",
1214 "Before Main Prompt / Story String": "主提示词/故事线之前",1214 "Before Main Prompt / Story String": "主提示词/故事线之前",
@@ -1224,13 +1224,13 @@
1224 "Replace Author's Note": "替换作者注",1224 "Replace Author's Note": "替换作者注",
1225 "Default Author's Note": "默认作者注",1225 "Default Author's Note": "默认作者注",
1226 "Will be automatically added as the Author's Note for all new chats.": "将自动添加为所有新聊天的作者注释。",1226 "Will be automatically added as the Author's Note for all new chats.": "将自动添加为所有新聊天的作者注释。",
1227 "Chat CFG": "聊天CFG",1227 "Chat CFG": "本聊天的CFG缩放",
1228 "1 = disabled": "“1”为已禁用",1228 "1 = disabled": "“1”为禁用",
1229 "write short replies, write replies using past tense": "写简短的回复,用过去时写回复",1229 "write short replies, write replies using past tense": "写简短的回复,用过去时写回复",
1230 "Positive Prompt": "正面提示词",1230 "Positive Prompt": "正面提示词",
1231 "Use character CFG scales": "单独为各个角色设置CFG缩放",1231 "Use character CFG scales": "单独为各个角色设置CFG缩放",
1232 "Character CFG": "角色CFG配置",1232 "Character CFG": "角色CFG配置",
1233 "Will be automatically added as the CFG for this character.": "将自动添加为该角色的 CFG。",1233 "Will be automatically added as the CFG for this character.": "将自动添加到该角色的CFG设置中。",
1234 "Global CFG": "全局CFG",1234 "Global CFG": "全局CFG",
1235 "Will be used as the default CFG options for every chat unless overridden.": "除非被覆盖,否则将用作每次聊天的默认 CFG 选项。",1235 "Will be used as the default CFG options for every chat unless overridden.": "除非被覆盖,否则将用作每次聊天的默认 CFG 选项。",
1236 "CFG Prompt Cascading": "CFG 提示词级联",1236 "CFG Prompt Cascading": "CFG 提示词级联",
@@ -1486,7 +1486,7 @@
1486 "ext_regex_replace_string_placeholder": "使用 {{match}} 包含来自“查找正则表达式”或“$1”、“$2”等的匹配文本作为捕获组。",1486 "ext_regex_replace_string_placeholder": "使用 {{match}} 包含来自“查找正则表达式”或“$1”、“$2”等的匹配文本作为捕获组。",
1487 "Trim Out": "修剪掉",1487 "Trim Out": "修剪掉",
1488 "ext_regex_trim_placeholder": "在替换之前全局修剪正则表达式匹配中任何不需要的部分。用回车键分隔每个元素。",1488 "ext_regex_trim_placeholder": "在替换之前全局修剪正则表达式匹配中任何不需要的部分。用回车键分隔每个元素。",
1489 "ext_regex_affects": "影响",1489 "ext_regex_affects": "作用范围",
1490 "ext_regex_user_input_desc": "用户发送的消息",1490 "ext_regex_user_input_desc": "用户发送的消息",
1491 "ext_regex_user_input": "用户输入",1491 "ext_regex_user_input": "用户输入",
1492 "ext_regex_ai_input_desc": "从生成式API中获取的信息。",1492 "ext_regex_ai_input_desc": "从生成式API中获取的信息。",
@@ -1720,9 +1720,9 @@
1720 "Chat Lorebook for": "聊天知识书",1720 "Chat Lorebook for": "聊天知识书",
1721 "chat_world_template_txt": "选定的世界信息将绑定到此聊天。生成 AI 回复时,\n它将与全球和角色传说书中的条目相结合。",1721 "chat_world_template_txt": "选定的世界信息将绑定到此聊天。生成 AI 回复时,\n它将与全球和角色传说书中的条目相结合。",
1722 "chat_rename_1": "输入聊天的新名称:",1722 "chat_rename_1": "输入聊天的新名称:",
1723 "chat_rename_2": "注意!!使用已有文件名会导致错误!!",1723 "chat_rename_2": "注意!!与其他文件重名会导致错误!!",
1724 "chat_rename_3": "此举会将次聊天与标记为“检查点”的聊天解绑。",1724 "chat_rename_3": "此举会将此聊天与标记为“检查点”的聊天解绑。",
1725 "chat_rename_4": "不需要在结尾添加 '.JSONL'",1725 "chat_rename_4": "(不需要在结尾添加 '.JSONL' 后缀)",
1726 "Enter Checkpoint Name:": "输入检查点名称:",1726 "Enter Checkpoint Name:": "输入检查点名称:",
1727 "(Leave empty to auto-generate)": "(留空以自动生成)",1727 "(Leave empty to auto-generate)": "(留空以自动生成)",
1728 "The currently existing checkpoint will be unlinked and replaced with the new checkpoint, but can still be found in the Chat Management.": "当前检查点将会被解绑并替换为新的检查点,但仍可在聊天管理中找到。",1728 "The currently existing checkpoint will be unlinked and replaced with the new checkpoint, but can still be found in the Chat Management.": "当前检查点将会被解绑并替换为新的检查点,但仍可在聊天管理中找到。",
@@ -1829,7 +1829,7 @@
1829 "char_import_2": "Chub 知识书(直链或ID)",1829 "char_import_2": "Chub 知识书(直链或ID)",
1830 "char_import_3": "JanitorAI 角色(直链或UUID)",1830 "char_import_3": "JanitorAI 角色(直链或UUID)",
1831 "char_import_4": "Pygmalion.chat 角色(直链或UUID)",1831 "char_import_4": "Pygmalion.chat 角色(直链或UUID)",
1832 "char_import_5": "AICharacterCard.com 角色(直链或ID)",1832 "char_import_5": "AICharacterCards.com 角色(直链或ID)",
1833 "char_import_6": "被允许的PNG直链(请参阅",1833 "char_import_6": "被允许的PNG直链(请参阅",
1834 "char_import_7": ")",1834 "char_import_7": ")",
1835 "char_import_8": "RisuRealm 角色(直链)",1835 "char_import_8": "RisuRealm 角色(直链)",
@@ -1838,7 +1838,7 @@
1838 "Enter the Git URL of the extension to install": "输入扩展程序的 Git URL 以安装",1838 "Enter the Git URL of the extension to install": "输入扩展程序的 Git URL 以安装",
1839 "Disclaimer:": "免责声明:",1839 "Disclaimer:": "免责声明:",
1840 "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.": "使用外部的扩展程序可能存在意料外的副作用和安全隐患。在导入扩展程序前,请一定确认其来源可信。我们不为第三方扩展程序造成的任何损失负责。",1840 "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.": "使用外部的扩展程序可能存在意料外的副作用和安全隐患。在导入扩展程序前,请一定确认其来源可信。我们不为第三方扩展程序造成的任何损失负责。",
1841 "Prompt Itemization": "将提示词分条",1841 "Prompt Itemization": "提示词拆分",
1842 "Show Raw Prompt": "显示原始提示词",1842 "Show Raw Prompt": "显示原始提示词",
1843 "Copy Prompt": "复制提示词",1843 "Copy Prompt": "复制提示词",
1844 "Show Prompt Differences": "显示提示词差异",1844 "Show Prompt Differences": "显示提示词差异",
@@ -1975,7 +1975,7 @@
1975 "Enter your password below to confirm:": "输入您的密码以确认:",1975 "Enter your password below to confirm:": "输入您的密码以确认:",
1976 "Chat Scenario Override": "聊天场景覆盖",1976 "Chat Scenario Override": "聊天场景覆盖",
1977 "Remove": "移除",1977 "Remove": "移除",
1978 "Unique to this chat.": "Unique to this chat.",1978 "Unique to this chat.": "仅对此聊天生效。",
1979 "All group members will use the following scenario text instead of what is specified in their character cards.": "All group members will use the following scenario text instead of what is specified in their character cards.",1979 "All group members will use the following scenario text instead of what is specified in their character cards.": "All group members will use the following scenario text instead of what is specified in their character cards.",
1980 "The following scenario text will be used instead of the value set in the character card.": "The following scenario text will be used instead of the value set in the character card.",1980 "The following scenario text will be used instead of the value set in the character card.": "The following scenario text will be used instead of the value set in the character card.",
1981 "Checkpoints inherit the scenario override from their parent, and can be changed individually after that.": "Checkpoints inherit the scenario override from their parent, and can be changed individually after that.",1981 "Checkpoints inherit the scenario override from their parent, and can be changed individually after that.": "Checkpoints inherit the scenario override from their parent, and can be changed individually after that.",
@@ -2045,8 +2045,8 @@
2045 "Post a GitHub issue": "在 GitHub 发布问题",2045 "Post a GitHub issue": "在 GitHub 发布问题",
2046 "Contact the developers": "联系开发者",2046 "Contact the developers": "联系开发者",
2047 "If you're connected to an API, try asking me something!": "若您已经配置好API,尝试发送些什么吧!",2047 "If you're connected to an API, try asking me something!": "若您已经配置好API,尝试发送些什么吧!",
2048 "Title/Memo": "标题/备忘录",2048 "Title/Memo": "标题(备忘)",
2049 "Strategy": "Strategy",2049 "Strategy": "触发策略",
2050 "Position": "位置",2050 "Position": "插入位置",
2051 "Trigger %": "触发率 %"2051 "Trigger %": "触发概率%"
2052}2052}
public/locales/zh-tw.json+4 -4
@@ -483,7 +483,7 @@
483 "separate with commas w/o space between": "用逗號分隔,之間無空格",483 "separate with commas w/o space between": "用逗號分隔,之間無空格",
484 "Custom Stopping Strings": "自訂停止字串",484 "Custom Stopping Strings": "自訂停止字串",
485 "JSON serialized array of strings": "JSON 序列化字串數組",485 "JSON serialized array of strings": "JSON 序列化字串數組",
486 "Replace Macro in Custom Stopping Strings": "取代自訂停止字串中的巨集",486 "Replace Macro in Stop Strings": "取代自訂停止字串中的巨集",
487 "Auto-Continue": "自動繼續",487 "Auto-Continue": "自動繼續",
488 "Allow for Chat Completion APIs": "允許聊天補全 API",488 "Allow for Chat Completion APIs": "允許聊天補全 API",
489 "Target length (tokens)": "目標長度(符元)",489 "Target length (tokens)": "目標長度(符元)",
@@ -1381,7 +1381,7 @@
1381 "char_import_2": "Chub Lorebook(直接連結或 ID)",1381 "char_import_2": "Chub Lorebook(直接連結或 ID)",
1382 "char_import_3": "JanitorAI 角色(直接連結或 ID)",1382 "char_import_3": "JanitorAI 角色(直接連結或 ID)",
1383 "char_import_4": "Pygmalion.chat 角色(直接連結或 ID)",1383 "char_import_4": "Pygmalion.chat 角色(直接連結或 ID)",
1384 "char_import_5": "AICharacterCard.com 角色(直接連結或 ID)",1384 "char_import_5": "AICharacterCards.com 角色(直接連結或 ID)",
1385 "char_import_6": "直接 PNG 連結(請參閱",1385 "char_import_6": "直接 PNG 連結(請參閱",
1386 "char_import_7": "對於允許的主機)",1386 "char_import_7": "對於允許的主機)",
1387 "char_import_8": "RisuRealm角色(直接連結)",1387 "char_import_8": "RisuRealm角色(直接連結)",
@@ -2357,8 +2357,8 @@
2357 "Forbid": "禁止",2357 "Forbid": "禁止",
2358 "Aphrodite only. Determines the order of samplers. Skew is always applied post-softmax, so it's not included here.": "僅限 Aphrodite 使用。決定採樣器的順序。偏移總是在 softmax 後應用,因此不包括在此。",2358 "Aphrodite only. Determines the order of samplers. Skew is always applied post-softmax, so it's not included here.": "僅限 Aphrodite 使用。決定採樣器的順序。偏移總是在 softmax 後應用,因此不包括在此。",
2359 "Aphrodite only. Determines the order of samplers.": "僅限 Aphrodite 使用。決定採樣器的順序。",2359 "Aphrodite only. Determines the order of samplers.": "僅限 Aphrodite 使用。決定採樣器的順序。",
2360 "Show model thoughts": "顯示模型思維鏈",2360 "Request model reasoning": "請求模型思維鏈",
2361 "Display the model's internal thoughts in the response.": "在回應中顯示模型的思維鏈(內部思考過程)。",2361 "Allows the model to return its thinking process.": "讓模型回傳其思考過程。",
2362 "Generic (OpenAI-compatible) [LM Studio, LiteLLM, etc.]": "通用(兼容 OpenAI)[LM Studio, LiteLLM 等]",2362 "Generic (OpenAI-compatible) [LM Studio, LiteLLM, etc.]": "通用(兼容 OpenAI)[LM Studio, LiteLLM 等]",
2363 "Model ID (optional)": "模型 ID(可選)",2363 "Model ID (optional)": "模型 ID(可選)",
2364 "DeepSeek API Key": "DeepSeek API 金鑰",2364 "DeepSeek API Key": "DeepSeek API 金鑰",
public/script.js+298 -170
@@ -95,6 +95,7 @@ import {
95 resetMovableStyles,95 resetMovableStyles,
96 forceCharacterEditorTokenize,96 forceCharacterEditorTokenize,
97 applyPowerUserSettings,97 applyPowerUserSettings,
98 generatedTextFiltered,
98} from './scripts/power-user.js';99} from './scripts/power-user.js';
99100
100import {101import {
@@ -169,6 +170,7 @@ import {
169 toggleDrawer,170 toggleDrawer,
170 isElementInViewport,171 isElementInViewport,
171 copyText,172 copyText,
173 escapeHtml,
172} from './scripts/utils.js';174} from './scripts/utils.js';
173import { debounce_timeout } from './scripts/constants.js';175import { debounce_timeout } from './scripts/constants.js';
174176
@@ -267,6 +269,8 @@ import { initSettingsSearch } from './scripts/setting-search.js';
267import { initBulkEdit } from './scripts/bulk-edit.js';269import { initBulkEdit } from './scripts/bulk-edit.js';
268import { deriveTemplatesFromChatTemplate } from './scripts/chat-templates.js';270import { deriveTemplatesFromChatTemplate } from './scripts/chat-templates.js';
269import { getContext } from './scripts/st-context.js';271import { getContext } from './scripts/st-context.js';
272import { extractReasoningFromData, initReasoning, PromptReasoning, ReasoningHandler, removeReasoningFromString, updateReasoningUI } from './scripts/reasoning.js';
273import { accountStorage } from './scripts/util/AccountStorage.js';
270274
271// API OBJECT FOR EXTERNAL WIRING275// API OBJECT FOR EXTERNAL WIRING
272globalThis.SillyTavern = {276globalThis.SillyTavern = {
@@ -416,7 +420,7 @@ DOMPurify.addHook('uponSanitizeElement', (node, _, config) => {
416 const entityId = getCurrentEntityId();420 const entityId = getCurrentEntityId();
417 const warningShownKey = `mediaWarningShown:${entityId}`;421 const warningShownKey = `mediaWarningShown:${entityId}`;
418422
419 if (localStorage.getItem(warningShownKey) === null) {423 if (accountStorage.getItem(warningShownKey) === null) {
420 const warningToast = toastr.warning(424 const warningToast = toastr.warning(
421 t`Use the 'Ext. Media' button to allow it. Click on this message to dismiss.`,425 t`Use the 'Ext. Media' button to allow it. Click on this message to dismiss.`,
422 t`External media has been blocked`,426 t`External media has been blocked`,
@@ -427,7 +431,7 @@ DOMPurify.addHook('uponSanitizeElement', (node, _, config) => {
427 },431 },
428 );432 );
429433
430 localStorage.setItem(warningShownKey, 'true');434 accountStorage.setItem(warningShownKey, 'true');
431 }435 }
432 }436 }
433});437});
@@ -443,6 +447,7 @@ export const event_types = {
443 MESSAGE_DELETED: 'message_deleted',447 MESSAGE_DELETED: 'message_deleted',
444 MESSAGE_UPDATED: 'message_updated',448 MESSAGE_UPDATED: 'message_updated',
445 MESSAGE_FILE_EMBEDDED: 'message_file_embedded',449 MESSAGE_FILE_EMBEDDED: 'message_file_embedded',
450 MORE_MESSAGES_LOADED: 'more_messages_loaded',
446 IMPERSONATE_READY: 'impersonate_ready',451 IMPERSONATE_READY: 'impersonate_ready',
447 CHAT_CHANGED: 'chat_id_changed',452 CHAT_CHANGED: 'chat_id_changed',
448 GENERATION_AFTER_COMMANDS: 'GENERATION_AFTER_COMMANDS',453 GENERATION_AFTER_COMMANDS: 'GENERATION_AFTER_COMMANDS',
@@ -491,6 +496,7 @@ export const event_types = {
491 /** @deprecated The event is aliased to STREAM_TOKEN_RECEIVED. */496 /** @deprecated The event is aliased to STREAM_TOKEN_RECEIVED. */
492 SMOOTH_STREAM_TOKEN_RECEIVED: 'stream_token_received',497 SMOOTH_STREAM_TOKEN_RECEIVED: 'stream_token_received',
493 STREAM_TOKEN_RECEIVED: 'stream_token_received',498 STREAM_TOKEN_RECEIVED: 'stream_token_received',
499 STREAM_REASONING_DONE: 'stream_reasoning_done',
494 FILE_ATTACHMENT_DELETED: 'file_attachment_deleted',500 FILE_ATTACHMENT_DELETED: 'file_attachment_deleted',
495 WORLDINFO_FORCE_ACTIVATE: 'worldinfo_force_activate',501 WORLDINFO_FORCE_ACTIVATE: 'worldinfo_force_activate',
496 OPEN_CHARACTER_LIBRARY: 'open_character_library',502 OPEN_CHARACTER_LIBRARY: 'open_character_library',
@@ -723,6 +729,7 @@ async function getSystemMessages() {
723 is_user: false,729 is_user: false,
724 is_system: true,730 is_system: true,
725 mes: await renderTemplateAsync('assistantNote'),731 mes: await renderTemplateAsync('assistantNote'),
732 uses_system_ui: true,
726 extra: {733 extra: {
727 isSmallSys: true,734 isSmallSys: true,
728 },735 },
@@ -980,6 +987,7 @@ async function firstLoadInit() {
980 initServerHistory();987 initServerHistory();
981 initSettingsSearch();988 initSettingsSearch();
982 initBulkEdit();989 initBulkEdit();
990 initReasoning();
983 await initScrapers();991 await initScrapers();
984 doDailyExtensionUpdatesCheck();992 doDailyExtensionUpdatesCheck();
985 await hideLoader();993 await hideLoader();
@@ -1483,7 +1491,7 @@ export async function printCharacters(fullRefresh = false) {
14831491
1484 $('#rm_print_characters_pagination').pagination({1492 $('#rm_print_characters_pagination').pagination({
1485 dataSource: entities,1493 dataSource: entities,
1486 pageSize: Number(localStorage.getItem(storageKey)) || per_page_default,1494 pageSize: Number(accountStorage.getItem(storageKey)) || per_page_default,
1487 sizeChangerOptions: [10, 25, 50, 100, 250, 500, 1000],1495 sizeChangerOptions: [10, 25, 50, 100, 250, 500, 1000],
1488 pageRange: 1,1496 pageRange: 1,
1489 pageNumber: saveCharactersPage || 1,1497 pageNumber: saveCharactersPage || 1,
@@ -1527,7 +1535,7 @@ export async function printCharacters(fullRefresh = false) {
1527 eventSource.emit(event_types.CHARACTER_PAGE_LOADED);1535 eventSource.emit(event_types.CHARACTER_PAGE_LOADED);
1528 },1536 },
1529 afterSizeSelectorChange: function (e) {1537 afterSizeSelectorChange: function (e) {
1530 localStorage.setItem(storageKey, e.target.value);1538 accountStorage.setItem(storageKey, e.target.value);
1531 },1539 },
1532 afterPaging: function (e) {1540 afterPaging: function (e) {
1533 saveCharactersPage = e;1541 saveCharactersPage = e;
@@ -1829,7 +1837,7 @@ export async function replaceCurrentChat() {
1829 }1837 }
1830}1838}
18311839
1832export function showMoreMessages(messagesToLoad = null) {1840export async function showMoreMessages(messagesToLoad = null) {
1833 const firstDisplayedMesId = $('#chat').children('.mes').first().attr('mesid');1841 const firstDisplayedMesId = $('#chat').children('.mes').first().attr('mesid');
1834 let messageId = Number(firstDisplayedMesId);1842 let messageId = Number(firstDisplayedMesId);
1835 let count = messagesToLoad || power_user.chat_truncation || Number.MAX_SAFE_INTEGER;1843 let count = messagesToLoad || power_user.chat_truncation || Number.MAX_SAFE_INTEGER;
@@ -1859,6 +1867,8 @@ export function showMoreMessages(messagesToLoad = null) {
1859 const newHeight = $('#chat').prop('scrollHeight');1867 const newHeight = $('#chat').prop('scrollHeight');
1860 $('#chat').scrollTop(newHeight - prevHeight);1868 $('#chat').scrollTop(newHeight - prevHeight);
1861 }1869 }
1870
1871 await eventSource.emit(event_types.MORE_MESSAGES_LOADED);
1862}1872}
18631873
1864export async function printMessages() {1874export async function printMessages() {
@@ -1987,14 +1997,15 @@ export async function sendTextareaMessage() {
1987 * @param {boolean} isUser If the message was sent by the user1997 * @param {boolean} isUser If the message was sent by the user
1988 * @param {number} messageId Message index in chat array1998 * @param {number} messageId Message index in chat array
1989 * @param {object} [sanitizerOverrides] DOMPurify sanitizer option overrides1999 * @param {object} [sanitizerOverrides] DOMPurify sanitizer option overrides
2000 * @param {boolean} [isReasoning] If the message is reasoning output
1990 * @returns {string} HTML string2001 * @returns {string} HTML string
1991 */2002 */
1992export function messageFormatting(mes, ch_name, isSystem, isUser, messageId, sanitizerOverrides = {}) {2003export function messageFormatting(mes, ch_name, isSystem, isUser, messageId, sanitizerOverrides = {}, isReasoning = false) {
1993 if (!mes) {2004 if (!mes) {
1994 return '';2005 return '';
1995 }2006 }
19962007
1997 if (Number(messageId) === 0 && !isSystem && !isUser) {2008 if (Number(messageId) === 0 && !isSystem && !isUser && !isReasoning) {
1998 const mesBeforeReplace = mes;2009 const mesBeforeReplace = mes;
1999 const chatMessage = chat[messageId];2010 const chatMessage = chat[messageId];
2000 mes = substituteParams(mes, undefined, ch_name);2011 mes = substituteParams(mes, undefined, ch_name);
@@ -2023,6 +2034,9 @@ export function messageFormatting(mes, ch_name, isSystem, isUser, messageId, san
2023 if (!isSystem) {2034 if (!isSystem) {
2024 function getRegexPlacement() {2035 function getRegexPlacement() {
2025 try {2036 try {
2037 if (isReasoning) {
2038 return regex_placement.REASONING;
2039 }
2026 if (isUser) {2040 if (isUser) {
2027 return regex_placement.USER_INPUT;2041 return regex_placement.USER_INPUT;
2028 } else if (chat[messageId]?.extra?.type === 'narrator') {2042 } else if (chat[messageId]?.extra?.type === 'narrator') {
@@ -2056,6 +2070,17 @@ export function messageFormatting(mes, ch_name, isSystem, isUser, messageId, san
2056 mes = mes.replaceAll('<', '&lt;').replaceAll('>', '&gt;');2070 mes = mes.replaceAll('<', '&lt;').replaceAll('>', '&gt;');
2057 }2071 }
20582072
2073 // Make sure reasoning strings are always shown, even if they include "<" or ">"
2074 [power_user.reasoning.prefix, power_user.reasoning.suffix].forEach((reasoningString) => {
2075 if (!reasoningString || !reasoningString.trim().length) {
2076 return;
2077 }
2078 // Only replace the first occurrence of the reasoning string
2079 if (mes.includes(reasoningString)) {
2080 mes = mes.replace(reasoningString, escapeHtml(reasoningString));
2081 }
2082 });
2083
2059 if (!isSystem) {2084 if (!isSystem) {
2060 // Save double quotes in tags as a special character to prevent them from being encoded2085 // Save double quotes in tags as a special character to prevent them from being encoded
2061 if (!power_user.encode_tags) {2086 if (!power_user.encode_tags) {
@@ -2166,26 +2191,29 @@ function insertSVGIcon(mes, extra) {
2166 modelName = extra.api;2191 modelName = extra.api;
2167 }2192 }
21682193
2169 const image = new Image();2194 const insertOrReplaceSVG = (image, className, targetSelector, insertBefore) => {
2170 // Add classes for styling and identification
2171 image.classList.add('icon-svg', 'timestamp-icon');
2172 image.src = `/img/${modelName}.svg`;
2173 image.title = `${extra?.api ? extra.api + ' - ' : ''}${extra?.model ?? ''}`;
2174
2175 image.onload = async function () {2195 image.onload = async function () {
2176 // Check if an SVG already exists adjacent to the timestamp2196 let existingSVG = insertBefore ? mes.find(targetSelector).prev(`.${className}`) : mes.find(targetSelector).next(`.${className}`);
2177 let existingSVG = mes.find('.timestamp').next('.timestamp-icon');
2178
2179 if (existingSVG.length) {2197 if (existingSVG.length) {
2180 // Replace existing SVG
2181 existingSVG.replaceWith(image);2198 existingSVG.replaceWith(image);
2182 } else {2199 } else {
2183 // Append the new SVG if none exists2200 if (insertBefore) mes.find(targetSelector).before(image);
2184 mes.find('.timestamp').after(image);2201 else mes.find(targetSelector).after(image);
2185 }2202 }
2186
2187 await SVGInject(image);2203 await SVGInject(image);
2188 };2204 };
2205 };
2206
2207 const createModelImage = (className, targetSelector, insertBefore) => {
2208 const image = new Image();
2209 image.classList.add('icon-svg', className);
2210 image.src = `/img/${modelName}.svg`;
2211 image.title = `${extra?.api ? extra.api + ' - ' : ''}${extra?.model ?? ''}`;
2212 insertOrReplaceSVG(image, className, targetSelector, insertBefore);
2213 };
2214
2215 createModelImage('timestamp-icon', '.timestamp');
2216 createModelImage('thinking-icon', '.mes_reasoning_header_title', true);
2189}2217}
21902218
21912219
@@ -2227,6 +2255,8 @@ function getMessageFromTemplate({
2227 timerValue && mes.find('.mes_timer').attr('title', timerTitle).text(timerValue);2255 timerValue && mes.find('.mes_timer').attr('title', timerTitle).text(timerValue);
2228 bookmarkLink && updateBookmarkDisplay(mes);2256 bookmarkLink && updateBookmarkDisplay(mes);
22292257
2258 updateReasoningUI(mes);
2259
2230 if (power_user.timestamp_model_icon && extra?.api) {2260 if (power_user.timestamp_model_icon && extra?.api) {
2231 insertSVGIcon(mes, extra);2261 insertSVGIcon(mes, extra);
2232 }2262 }
@@ -2234,10 +2264,22 @@ function getMessageFromTemplate({
2234 return mes;2264 return mes;
2235}2265}
22362266
2237export function updateMessageBlock(messageId, message) {2267/**
2268 * Re-renders a message block with updated content.
2269 * @param {number} messageId Message ID
2270 * @param {object} message Message object
2271 * @param {object} [options={}] Optional arguments
2272 * @param {boolean} [options.rerenderMessage=true] Whether to re-render the message content (inside <c>.mes_text</c>)
2273 */
2274export function updateMessageBlock(messageId, message, { rerenderMessage = true } = {}) {
2238 const messageElement = $(`#chat [mesid="${messageId}"]`);2275 const messageElement = $(`#chat [mesid="${messageId}"]`);
2276 if (rerenderMessage) {
2239 const text = message?.extra?.display_text ?? message.mes;2277 const text = message?.extra?.display_text ?? message.mes;
2240 messageElement.find('.mes_text').html(messageFormatting(text, message.name, message.is_system, message.is_user, messageId));2278 messageElement.find('.mes_text').html(messageFormatting(text, message.name, message.is_system, message.is_user, messageId, {}, false));
2279 }
2280
2281 updateReasoningUI(messageElement);
2282
2241 addCopyToCodeBlocks(messageElement);2283 addCopyToCodeBlocks(messageElement);
2242 appendMediaToMessage(message, messageElement);2284 appendMediaToMessage(message, messageElement);
2243}2285}
@@ -2394,8 +2436,9 @@ export function addOneMessage(mes, { type = 'normal', insertAfter = null, scroll
2394 mes.is_user,2436 mes.is_user,
2395 chat.indexOf(mes),2437 chat.indexOf(mes),
2396 sanitizerOverrides,2438 sanitizerOverrides,
2439 false,
2397 );2440 );
2398 const bias = messageFormatting(mes.extra?.bias ?? '', '', false, false, -1);2441 const bias = messageFormatting(mes.extra?.bias ?? '', '', false, false, -1, {}, false);
2399 let bookmarkLink = mes?.extra?.bookmark_link ?? '';2442 let bookmarkLink = mes?.extra?.bookmark_link ?? '';
24002443
2401 let params = {2444 let params = {
@@ -2412,7 +2455,7 @@ export function addOneMessage(mes, { type = 'normal', insertAfter = null, scroll
2412 timestamp: timestamp,2455 timestamp: timestamp,
2413 extra: mes.extra,2456 extra: mes.extra,
2414 tokenCount: mes.extra?.token_count ?? 0,2457 tokenCount: mes.extra?.token_count ?? 0,
2415 ...formatGenerationTimer(mes.gen_started, mes.gen_finished, mes.extra?.token_count),2458 ...formatGenerationTimer(mes.gen_started, mes.gen_finished, mes.extra?.token_count, mes.extra?.reasoning_duration),
2416 };2459 };
24172460
2418 const renderedMessage = getMessageFromTemplate(params);2461 const renderedMessage = getMessageFromTemplate(params);
@@ -2465,6 +2508,7 @@ export function addOneMessage(mes, { type = 'normal', insertAfter = null, scroll
2465 swipeMessage.attr('swipeid', params.swipeId);2508 swipeMessage.attr('swipeid', params.swipeId);
2466 swipeMessage.find('.mes_text').html(messageText).attr('title', title);2509 swipeMessage.find('.mes_text').html(messageText).attr('title', title);
2467 swipeMessage.find('.timestamp').text(timestamp).attr('title', `${params.extra.api} - ${params.extra.model}`);2510 swipeMessage.find('.timestamp').text(timestamp).attr('title', `${params.extra.api} - ${params.extra.model}`);
2511 updateReasoningUI(swipeMessage);
2468 appendMediaToMessage(mes, swipeMessage);2512 appendMediaToMessage(mes, swipeMessage);
2469 if (power_user.timestamp_model_icon && params.extra?.api) {2513 if (power_user.timestamp_model_icon && params.extra?.api) {
2470 insertSVGIcon(swipeMessage, params.extra);2514 insertSVGIcon(swipeMessage, params.extra);
@@ -2531,13 +2575,14 @@ export function formatCharacterAvatar(characterAvatar) {
2531 * @param {Date} gen_started Date when generation was started2575 * @param {Date} gen_started Date when generation was started
2532 * @param {Date} gen_finished Date when generation was finished2576 * @param {Date} gen_finished Date when generation was finished
2533 * @param {number} tokenCount Number of tokens generated (0 if not available)2577 * @param {number} tokenCount Number of tokens generated (0 if not available)
2578 * @param {number?} [reasoningDuration=null] Reasoning duration (null if no reasoning was done)
2534 * @returns {Object} Object containing the formatted timer value and title2579 * @returns {Object} Object containing the formatted timer value and title
2535 * @example2580 * @example
2536 * const { timerValue, timerTitle } = formatGenerationTimer(gen_started, gen_finished, tokenCount);2581 * const { timerValue, timerTitle } = formatGenerationTimer(gen_started, gen_finished, tokenCount);
2537 * console.log(timerValue); // 1.2s2582 * console.log(timerValue); // 1.2s
2538 * 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/s2583 * 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
2539 */2584 */
2540function formatGenerationTimer(gen_started, gen_finished, tokenCount) {2585function formatGenerationTimer(gen_started, gen_finished, tokenCount, reasoningDuration = null) {
2541 if (!gen_started || !gen_finished) {2586 if (!gen_started || !gen_finished) {
2542 return {};2587 return {};
2543 }2588 }
@@ -2551,8 +2596,9 @@ function formatGenerationTimer(gen_started, gen_finished, tokenCount) {
2551 `Generation queued: ${start.format(dateFormat)}`,2596 `Generation queued: ${start.format(dateFormat)}`,
2552 `Reply received: ${finish.format(dateFormat)}`,2597 `Reply received: ${finish.format(dateFormat)}`,
2553 `Time to generate: ${seconds} seconds`,2598 `Time to generate: ${seconds} seconds`,
2599 reasoningDuration > 0 ? `Time to think: ${reasoningDuration / 1000} seconds` : '',
2554 tokenCount > 0 ? `Token rate: ${Number(tokenCount / seconds).toFixed(1)} t/s` : '',2600 tokenCount > 0 ? `Token rate: ${Number(tokenCount / seconds).toFixed(1)} t/s` : '',
2555 ].join('\n');2601 ].filter(x => x).join('\n').trim();
25562602
2557 if (isNaN(seconds) || seconds < 0) {2603 if (isNaN(seconds) || seconds < 0) {
2558 return { timerValue: '', timerTitle };2604 return { timerValue: '', timerTitle };
@@ -2740,7 +2786,8 @@ export async function generateQuietPrompt(quiet_prompt, quietToLoud, skipWIAN, q
2740 TempResponseLength.save(main_api, responseLength);2786 TempResponseLength.save(main_api, responseLength);
2741 eventHook = TempResponseLength.setupEventHook(main_api);2787 eventHook = TempResponseLength.setupEventHook(main_api);
2742 }2788 }
2743 return await Generate('quiet', options);2789 const result = await Generate('quiet', options);
2790 return removeReasoningFromString(result);
2744 } finally {2791 } finally {
2745 if (responseLengthCustomized && TempResponseLength.isCustomized()) {2792 if (responseLengthCustomized && TempResponseLength.isCustomized()) {
2746 TempResponseLength.restore(main_api);2793 TempResponseLength.restore(main_api);
@@ -3040,8 +3087,8 @@ export function isStreamingEnabled() {
3040 (main_api == 'openai' &&3087 (main_api == 'openai' &&
3041 oai_settings.stream_openai &&3088 oai_settings.stream_openai &&
3042 !noStreamSources.includes(oai_settings.chat_completion_source) &&3089 !noStreamSources.includes(oai_settings.chat_completion_source) &&
3043 !(oai_settings.chat_completion_source == chat_completion_sources.OPENAI && oai_settings.openai_model.startsWith('o1-')) &&3090 !(oai_settings.chat_completion_source == chat_completion_sources.OPENAI && ['o1-2024-12-17', 'o1'].includes(oai_settings.openai_model))
3044 !(oai_settings.chat_completion_source == chat_completion_sources.MAKERSUITE && oai_settings.google_model.includes('bison')))3091 )
3045 || (main_api == 'kobold' && kai_settings.streaming_kobold && kai_flags.can_use_streaming)3092 || (main_api == 'kobold' && kai_settings.streaming_kobold && kai_flags.can_use_streaming)
3046 || (main_api == 'novel' && nai_settings.streaming_novel)3093 || (main_api == 'novel' && nai_settings.streaming_novel)
3047 || (main_api == 'textgenerationwebui' && textgen_settings.streaming));3094 || (main_api == 'textgenerationwebui' && textgen_settings.streaming));
@@ -3070,9 +3117,13 @@ class StreamingProcessor {
3070 constructor(type, forceName2, timeStarted, continueMessage) {3117 constructor(type, forceName2, timeStarted, continueMessage) {
3071 this.result = '';3118 this.result = '';
3072 this.messageId = -1;3119 this.messageId = -1;
3120 /** @type {HTMLElement} */
3073 this.messageDom = null;3121 this.messageDom = null;
3122 /** @type {HTMLElement} */
3074 this.messageTextDom = null;3123 this.messageTextDom = null;
3124 /** @type {HTMLElement} */
3075 this.messageTimerDom = null;3125 this.messageTimerDom = null;
3126 /** @type {HTMLElement} */
3076 this.messageTokenCounterDom = null;3127 this.messageTokenCounterDom = null;
3077 /** @type {HTMLTextAreaElement} */3128 /** @type {HTMLTextAreaElement} */
3078 this.sendTextarea = document.querySelector('#send_textarea');3129 this.sendTextarea = document.querySelector('#send_textarea');
@@ -3089,6 +3140,8 @@ class StreamingProcessor {
3089 /** @type {import('./scripts/logprobs.js').TokenLogprobs[]} */3140 /** @type {import('./scripts/logprobs.js').TokenLogprobs[]} */
3090 this.messageLogprobs = [];3141 this.messageLogprobs = [];
3091 this.toolCalls = [];3142 this.toolCalls = [];
3143 // Initialize reasoning in its own handler
3144 this.reasoningHandler = new ReasoningHandler(timeStarted);
3092 }3145 }
30933146
3094 #checkDomElements(messageId) {3147 #checkDomElements(messageId) {
@@ -3098,6 +3151,7 @@ class StreamingProcessor {
3098 this.messageTimerDom = this.messageDom?.querySelector('.mes_timer');3151 this.messageTimerDom = this.messageDom?.querySelector('.mes_timer');
3099 this.messageTokenCounterDom = this.messageDom?.querySelector('.tokenCounterDisplay');3152 this.messageTokenCounterDom = this.messageDom?.querySelector('.tokenCounterDisplay');
3100 }3153 }
3154 this.reasoningHandler.updateDom(messageId);
3101 }3155 }
31023156
3103 #updateMessageBlockVisibility() {3157 #updateMessageBlockVisibility() {
@@ -3107,22 +3161,12 @@ class StreamingProcessor {
3107 }3161 }
3108 }3162 }
31093163
3110 showMessageButtons(messageId) {3164 markUIGenStarted() {
3111 if (messageId == -1) {3165 deactivateSendButtons();
3112 return;
3113 }
3114
3115 showStopButton();
3116 $(`#chat .mes[mesid="${messageId}"] .mes_buttons`).css({ 'display': 'none' });
3117 }
3118
3119 hideMessageButtons(messageId) {
3120 if (messageId == -1) {
3121 return;
3122 }3166 }
31233167
3124 hideStopButton();3168 markUIGenStopped() {
3125 $(`#chat .mes[mesid="${messageId}"] .mes_buttons`).css({ 'display': 'flex' });3169 activateSendButtons();
3126 }3170 }
31273171
3128 async onStartStreaming(text) {3172 async onStartStreaming(text) {
@@ -3131,20 +3175,18 @@ class StreamingProcessor {
3131 if (this.type == 'impersonate') {3175 if (this.type == 'impersonate') {
3132 this.sendTextarea.value = '';3176 this.sendTextarea.value = '';
3133 this.sendTextarea.dispatchEvent(new Event('input', { bubbles: true }));3177 this.sendTextarea.dispatchEvent(new Event('input', { bubbles: true }));
3134 }3178 } else {
3135 else {3179 await saveReply(this.type, text, true, '', [], '');
3136 await saveReply(this.type, text, true);
3137 messageId = chat.length - 1;3180 messageId = chat.length - 1;
3138 this.#checkDomElements(messageId);3181 this.#checkDomElements(messageId);
3139 this.showMessageButtons(messageId);3182 this.markUIGenStarted();
3140 }3183 }
3141
3142 hideSwipeButtons();3184 hideSwipeButtons();
3143 scrollChatToBottom();3185 scrollChatToBottom();
3144 return messageId;3186 return messageId;
3145 }3187 }
31463188
3147 onProgressStreaming(messageId, text, isFinal) {3189 async onProgressStreaming(messageId, text, isFinal) {
3148 const isImpersonate = this.type == 'impersonate';3190 const isImpersonate = this.type == 'impersonate';
3149 const isContinue = this.type == 'continue';3191 const isContinue = this.type == 'continue';
31503192
@@ -3156,11 +3198,9 @@ class StreamingProcessor {
31563198
3157 let processedText = cleanUpMessage(text, isImpersonate, isContinue, !isFinal, this.stoppingStrings);3199 let processedText = cleanUpMessage(text, isImpersonate, isContinue, !isFinal, this.stoppingStrings);
31583200
3159 // Predict unbalanced asterisks / quotes during streaming
3160 const charsToBalance = ['*', '"', '```'];3201 const charsToBalance = ['*', '"', '```'];
3161 for (const char of charsToBalance) {3202 for (const char of charsToBalance) {
3162 if (!isFinal && isOdd(countOccurrences(processedText, char))) {3203 if (!isFinal && isOdd(countOccurrences(processedText, char))) {
3163 // Add character at the end to balance it
3164 const separator = char.length > 1 ? '\n' : '';3204 const separator = char.length > 1 ? '\n' : '';
3165 processedText = processedText.trimEnd() + separator + char;3205 processedText = processedText.trimEnd() + separator + char;
3166 }3206 }
@@ -3169,23 +3209,25 @@ class StreamingProcessor {
3169 if (isImpersonate) {3209 if (isImpersonate) {
3170 this.sendTextarea.value = processedText;3210 this.sendTextarea.value = processedText;
3171 this.sendTextarea.dispatchEvent(new Event('input', { bubbles: true }));3211 this.sendTextarea.dispatchEvent(new Event('input', { bubbles: true }));
3172 }3212 } else {
3173 else {3213 const mesChanged = chat[messageId]['mes'] !== processedText;
3174 this.#checkDomElements(messageId);3214 this.#checkDomElements(messageId);
3175 this.#updateMessageBlockVisibility();3215 this.#updateMessageBlockVisibility();
3176 const currentTime = new Date();3216 const currentTime = new Date();
3177 // Don't waste time calculating token count for streaming
3178 const currentTokenCount = isFinal && power_user.message_token_count_enabled ? getTokenCount(processedText, 0) : 0;
3179 const timePassed = formatGenerationTimer(this.timeStarted, currentTime, currentTokenCount);
3180 chat[messageId]['mes'] = processedText;3217 chat[messageId]['mes'] = processedText;
3181 chat[messageId]['gen_started'] = this.timeStarted;3218 chat[messageId]['gen_started'] = this.timeStarted;
3182 chat[messageId]['gen_finished'] = currentTime;3219 chat[messageId]['gen_finished'] = currentTime;
3183
3184 if (currentTokenCount) {
3185 if (!chat[messageId]['extra']) {3220 if (!chat[messageId]['extra']) {
3186 chat[messageId]['extra'] = {};3221 chat[messageId]['extra'] = {};
3187 }3222 }
31883223
3224 // Update reasoning
3225 await this.reasoningHandler.process(messageId, mesChanged);
3226
3227 // Token count update.
3228 const tokenCountText = this.reasoningHandler.reasoning + processedText;
3229 const currentTokenCount = isFinal && power_user.message_token_count_enabled ? getTokenCount(tokenCountText, 0) : 0;
3230 if (currentTokenCount) {
3189 chat[messageId]['extra']['token_count'] = currentTokenCount;3231 chat[messageId]['extra']['token_count'] = currentTokenCount;
3190 if (this.messageTokenCounterDom instanceof HTMLElement) {3232 if (this.messageTokenCounterDom instanceof HTMLElement) {
3191 this.messageTokenCounterDom.textContent = `${currentTokenCount}t`;3233 this.messageTokenCounterDom.textContent = `${currentTokenCount}t`;
@@ -3203,14 +3245,19 @@ class StreamingProcessor {
3203 chat[messageId].is_system,3245 chat[messageId].is_system,
3204 chat[messageId].is_user,3246 chat[messageId].is_user,
3205 messageId,3247 messageId,
3248 {},
3249 false,
3206 );3250 );
3207 if (this.messageTextDom instanceof HTMLElement) {3251 if (this.messageTextDom instanceof HTMLElement) {
3208 this.messageTextDom.innerHTML = formattedText;3252 this.messageTextDom.innerHTML = formattedText;
3209 }3253 }
3254
3255 const timePassed = formatGenerationTimer(this.timeStarted, currentTime, currentTokenCount, this.reasoningHandler.getDuration());
3210 if (this.messageTimerDom instanceof HTMLElement) {3256 if (this.messageTimerDom instanceof HTMLElement) {
3211 this.messageTimerDom.textContent = timePassed.timerValue;3257 this.messageTimerDom.textContent = timePassed.timerValue;
3212 this.messageTimerDom.title = timePassed.timerTitle;3258 this.messageTimerDom.title = timePassed.timerTitle;
3213 }3259 }
3260
3214 this.setFirstSwipe(messageId);3261 this.setFirstSwipe(messageId);
3215 }3262 }
32163263
@@ -3220,10 +3267,12 @@ class StreamingProcessor {
3220 }3267 }
32213268
3222 async onFinishStreaming(messageId, text) {3269 async onFinishStreaming(messageId, text) {
3223 this.hideMessageButtons(this.messageId);3270 this.markUIGenStopped();
3224 this.onProgressStreaming(messageId, text, true);3271 await this.onProgressStreaming(messageId, text, true);
3225 addCopyToCodeBlocks($(`#chat .mes[mesid="${messageId}"]`));3272 addCopyToCodeBlocks($(`#chat .mes[mesid="${messageId}"]`));
32263273
3274 await this.reasoningHandler.finish(messageId);
3275
3227 if (Array.isArray(this.swipes) && this.swipes.length > 0) {3276 if (Array.isArray(this.swipes) && this.swipes.length > 0) {
3228 const message = chat[messageId];3277 const message = chat[messageId];
3229 const swipeInfo = {3278 const swipeInfo = {
@@ -3251,39 +3300,11 @@ class StreamingProcessor {
3251 unblockGeneration();3300 unblockGeneration();
3252 generatedPromptCache = '';3301 generatedPromptCache = '';
32533302
3254 //console.log("Generated text size:", text.length, text)
3255
3256 const isAborted = this.abortController.signal.aborted;3303 const isAborted = this.abortController.signal.aborted;
3257 if (power_user.auto_swipe && !isAborted) {3304 if (!isAborted && power_user.auto_swipe && generatedTextFiltered(text)) {
3258 function containsBlacklistedWords(str, blacklist, threshold) {3305 return swipe_right();
3259 const regex = new RegExp(`\\b(${blacklist.join('|')})\\b`, 'gi');
3260 const matches = str.match(regex) || [];
3261 return matches.length >= threshold;
3262 }3306 }
32633307
3264 const generatedTextFiltered = (text) => {
3265 if (text) {
3266 if (power_user.auto_swipe_minimum_length) {
3267 if (text.length < power_user.auto_swipe_minimum_length && text.length !== 0) {
3268 console.log('Generated text size too small');
3269 return true;
3270 }
3271 }
3272 if (power_user.auto_swipe_blacklist_threshold) {
3273 if (containsBlacklistedWords(text, power_user.auto_swipe_blacklist, power_user.auto_swipe_blacklist_threshold)) {
3274 console.log('Generated text has blacklisted words');
3275 return true;
3276 }
3277 }
3278 }
3279 return false;
3280 };
3281
3282 if (generatedTextFiltered(text)) {
3283 swipe_right();
3284 return;
3285 }
3286 }
3287 playMessageSound();3308 playMessageSound();
3288 }3309 }
32893310
@@ -3291,7 +3312,7 @@ class StreamingProcessor {
3291 this.abortController.abort();3312 this.abortController.abort();
3292 this.isStopped = true;3313 this.isStopped = true;
32933314
3294 this.hideMessageButtons(this.messageId);3315 this.markUIGenStopped();
3295 generatedPromptCache = '';3316 generatedPromptCache = '';
3296 unblockGeneration();3317 unblockGeneration();
32973318
@@ -3317,7 +3338,7 @@ class StreamingProcessor {
3317 }3338 }
33183339
3319 /**3340 /**
3320 * @returns {Generator<{ text: string, swipes: string[], logprobs: import('./scripts/logprobs.js').TokenLogprobs, toolCalls: any[] }, void, void>}3341 * @returns {Generator<{ text: string, swipes: string[], logprobs: import('./scripts/logprobs.js').TokenLogprobs, toolCalls: any[], state: any }, void, void>}
3321 */3342 */
3322 *nullStreamingGeneration() {3343 *nullStreamingGeneration() {
3323 throw new Error('Generation function for streaming is not hooked up');3344 throw new Error('Generation function for streaming is not hooked up');
@@ -3339,10 +3360,10 @@ class StreamingProcessor {
3339 try {3360 try {
3340 const sw = new Stopwatch(1000 / power_user.streaming_fps);3361 const sw = new Stopwatch(1000 / power_user.streaming_fps);
3341 const timestamps = [];3362 const timestamps = [];
3342 for await (const { text, swipes, logprobs, toolCalls } of this.generator()) {3363 for await (const { text, swipes, logprobs, toolCalls, state } of this.generator()) {
3343 timestamps.push(Date.now());3364 timestamps.push(Date.now());
3344 if (this.isStopped) {3365 if (this.isStopped || this.abortController.signal.aborted) {
3345 return;3366 return this.result;
3346 }3367 }
33473368
3348 this.toolCalls = toolCalls;3369 this.toolCalls = toolCalls;
@@ -3351,8 +3372,10 @@ class StreamingProcessor {
3351 if (logprobs) {3372 if (logprobs) {
3352 this.messageLogprobs.push(...(Array.isArray(logprobs) ? logprobs : [logprobs]));3373 this.messageLogprobs.push(...(Array.isArray(logprobs) ? logprobs : [logprobs]));
3353 }3374 }
3375 // Get the updated reasoning string into the handler
3376 this.reasoningHandler.updateReasoning(this.messageId, state?.reasoning ?? '');
3354 await eventSource.emit(event_types.STREAM_TOKEN_RECEIVED, text);3377 await eventSource.emit(event_types.STREAM_TOKEN_RECEIVED, text);
3355 await sw.tick(() => this.onProgressStreaming(this.messageId, this.continueMessage + text));3378 await sw.tick(async () => await this.onProgressStreaming(this.messageId, this.continueMessage + text));
3356 }3379 }
3357 const seconds = (timestamps[timestamps.length - 1] - timestamps[0]) / 1000;3380 const seconds = (timestamps[timestamps.length - 1] - timestamps[0]) / 1000;
3358 console.warn(`Stream stats: ${timestamps.length} tokens, ${seconds.toFixed(2)} seconds, rate: ${Number(timestamps.length / seconds).toFixed(2)} TPS`);3381 console.warn(`Stream stats: ${timestamps.length} tokens, ${seconds.toFixed(2)} seconds, rate: ${Number(timestamps.length / seconds).toFixed(2)} TPS`);
@@ -3428,7 +3451,7 @@ export async function generateRaw(prompt, api, instructOverride, quietToLoud, sy
3428 break;3451 break;
3429 }3452 }
3430 case 'textgenerationwebui':3453 case 'textgenerationwebui':
3431 generateData = getTextGenGenerationData(prompt, amount_gen, false, false, null, 'quiet');3454 generateData = await getTextGenGenerationData(prompt, amount_gen, false, false, null, 'quiet');
3432 TempResponseLength.restore(api);3455 TempResponseLength.restore(api);
3433 break;3456 break;
3434 case 'openai': {3457 case 'openai': {
@@ -3836,6 +3859,27 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
3836 };3859 };
3837 }));3860 }));
38383861
3862 const reasoning = new PromptReasoning();
3863 for (let i = coreChat.length - 1; i >= 0; i--) {
3864 const depth = coreChat.length - i - 1;
3865 const isPrefix = isContinue && i === coreChat.length - 1;
3866 coreChat[i] = {
3867 ...coreChat[i],
3868 mes: reasoning.addToMessage(
3869 coreChat[i].mes,
3870 getRegexedString(
3871 String(coreChat[i].extra?.reasoning ?? ''),
3872 regex_placement.REASONING,
3873 { isPrompt: true, depth: depth },
3874 ),
3875 isPrefix,
3876 ),
3877 };
3878 if (reasoning.isLimitReached()) {
3879 break;
3880 }
3881 }
3882
3839 // Determine token limit3883 // Determine token limit
3840 let this_max_context = getMaxContextSize();3884 let this_max_context = getMaxContextSize();
38413885
@@ -4394,7 +4438,7 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
4394 // For prompt bit itemization4438 // For prompt bit itemization
4395 let mesSendString = '';4439 let mesSendString = '';
43964440
4397 function getCombinedPrompt(isNegative) {4441 async function getCombinedPrompt(isNegative) {
4398 // Only return if the guidance scale doesn't exist or the value is 14442 // Only return if the guidance scale doesn't exist or the value is 1
4399 // Also don't return if constructing the neutral prompt4443 // Also don't return if constructing the neutral prompt
4400 if (isNegative && !useCfgPrompt) {4444 if (isNegative && !useCfgPrompt) {
@@ -4421,10 +4465,16 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
4421 // TODO: Make all extension prompts use an array/splice method4465 // TODO: Make all extension prompts use an array/splice method
4422 const lengthDiff = mesSend.length - cfgPrompt.depth;4466 const lengthDiff = mesSend.length - cfgPrompt.depth;
4423 const cfgDepth = lengthDiff >= 0 ? lengthDiff : 0;4467 const cfgDepth = lengthDiff >= 0 ? lengthDiff : 0;
4468 const cfgMessage = finalMesSend[cfgDepth];
4469 if (cfgMessage) {
4470 if (!Array.isArray(finalMesSend[cfgDepth].extensionPrompts)) {
4471 finalMesSend[cfgDepth].extensionPrompts = [];
4472 }
4424 finalMesSend[cfgDepth].extensionPrompts.push(`${cfgPrompt.value}\n`);4473 finalMesSend[cfgDepth].extensionPrompts.push(`${cfgPrompt.value}\n`);
4425 }4474 }
4426 }4475 }
4427 }4476 }
4477 }
44284478
4429 // Add prompt bias after everything else4479 // Add prompt bias after everything else
4430 // Always run with continue4480 // Always run with continue
@@ -4497,13 +4547,13 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
4497 };4547 };
44984548
4499 // Before returning the combined prompt, give available context related information to all subscribers.4549 // Before returning the combined prompt, give available context related information to all subscribers.
4500 eventSource.emitAndWait(event_types.GENERATE_BEFORE_COMBINE_PROMPTS, data);4550 await eventSource.emit(event_types.GENERATE_BEFORE_COMBINE_PROMPTS, data);
45014551
4502 // If one or multiple subscribers return a value, forfeit the responsibillity of flattening the context.4552 // If one or multiple subscribers return a value, forfeit the responsibillity of flattening the context.
4503 return !data.combinedPrompt ? combine() : data.combinedPrompt;4553 return !data.combinedPrompt ? combine() : data.combinedPrompt;
4504 }4554 }
45054555
4506 let finalPrompt = getCombinedPrompt(false);4556 let finalPrompt = await getCombinedPrompt(false);
45074557
4508 const eventData = { prompt: finalPrompt, dryRun: dryRun };4558 const eventData = { prompt: finalPrompt, dryRun: dryRun };
4509 await eventSource.emit(event_types.GENERATE_AFTER_COMBINE_PROMPTS, eventData);4559 await eventSource.emit(event_types.GENERATE_AFTER_COMBINE_PROMPTS, eventData);
@@ -4537,8 +4587,8 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
4537 }4587 }
4538 break;4588 break;
4539 case 'textgenerationwebui': {4589 case 'textgenerationwebui': {
4540 const cfgValues = useCfgPrompt ? { guidanceScale: cfgGuidanceScale, negativePrompt: getCombinedPrompt(true) } : null;4590 const cfgValues = useCfgPrompt ? { guidanceScale: cfgGuidanceScale, negativePrompt: await getCombinedPrompt(true) } : null;
4541 generate_data = getTextGenGenerationData(finalPrompt, maxLength, isImpersonate, isContinue, cfgValues, type);4591 generate_data = await getTextGenGenerationData(finalPrompt, maxLength, isImpersonate, isContinue, cfgValues, type);
4542 break;4592 break;
4543 }4593 }
4544 case 'novel': {4594 case 'novel': {
@@ -4738,11 +4788,17 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
4738 //const getData = await response.json();4788 //const getData = await response.json();
4739 let getMessage = extractMessageFromData(data);4789 let getMessage = extractMessageFromData(data);
4740 let title = extractTitleFromData(data);4790 let title = extractTitleFromData(data);
4791 let reasoning = extractReasoningFromData(data);
4741 kobold_horde_model = title;4792 kobold_horde_model = title;
47424793
4743 const swipes = extractMultiSwipes(data, type);4794 const swipes = extractMultiSwipes(data, type);
47444795
4745 messageChunk = cleanUpMessage(getMessage, isImpersonate, isContinue, false);4796 messageChunk = cleanUpMessage(getMessage, isImpersonate, isContinue, false);
4797 reasoning = getRegexedString(reasoning, regex_placement.REASONING);
4798
4799 if (power_user.trim_spaces) {
4800 reasoning = reasoning.trim();
4801 }
47464802
4747 if (isContinue) {4803 if (isContinue) {
4748 getMessage = continue_mag + getMessage;4804 getMessage = continue_mag + getMessage;
@@ -4764,10 +4820,10 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
4764 else {4820 else {
4765 // Without streaming we'll be having a full message on continuation. Treat it as a last chunk.4821 // Without streaming we'll be having a full message on continuation. Treat it as a last chunk.
4766 if (originalType !== 'continue') {4822 if (originalType !== 'continue') {
4767 ({ type, getMessage } = await saveReply(type, getMessage, false, title, swipes));4823 ({ type, getMessage } = await saveReply(type, getMessage, false, title, swipes, reasoning));
4768 }4824 }
4769 else {4825 else {
4770 ({ type, getMessage } = await saveReply('appendFinal', getMessage, false, title, swipes));4826 ({ type, getMessage } = await saveReply('appendFinal', getMessage, false, title, swipes, reasoning));
4771 }4827 }
47724828
4773 // This relies on `saveReply` having been called to add the message to the chat, so it must be last.4829 // This relies on `saveReply` having been called to add the message to the chat, so it must be last.
@@ -4801,32 +4857,9 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
4801 }4857 }
48024858
4803 const isAborted = abortController && abortController.signal.aborted;4859 const isAborted = abortController && abortController.signal.aborted;
4804 if (power_user.auto_swipe && !isAborted) {4860 if (!isAborted && power_user.auto_swipe && generatedTextFiltered(getMessage)) {
4805 console.debug('checking for autoswipeblacklist on non-streaming message');
4806 function containsBlacklistedWords(getMessage, blacklist, threshold) {
4807 console.debug('checking blacklisted words');
4808 const regex = new RegExp(`\\b(${blacklist.join('|')})\\b`, 'gi');
4809 const matches = getMessage.match(regex) || [];
4810 return matches.length >= threshold;
4811 }
4812
4813 const generatedTextFiltered = (getMessage) => {
4814 if (power_user.auto_swipe_blacklist_threshold) {
4815 if (containsBlacklistedWords(getMessage, power_user.auto_swipe_blacklist, power_user.auto_swipe_blacklist_threshold)) {
4816 console.debug('Generated text has blacklisted words');
4817 return true;
4818 }
4819 }
4820
4821 return false;
4822 };
4823 if (generatedTextFiltered(getMessage)) {
4824 console.debug('swiping right automatically');
4825 is_send_press = false;4861 is_send_press = false;
4826 swipe_right();4862 return swipe_right();
4827 // TODO: do we want to resolve after an auto-swipe?
4828 return;
4829 }
4830 }4863 }
48314864
4832 console.debug('/api/chats/save called by /Generate');4865 console.debug('/api/chats/save called by /Generate');
@@ -5481,7 +5514,7 @@ async function promptItemize(itemizedPrompts, requestedMesId) {
5481 toastr.info(t`Copied!`);5514 toastr.info(t`Copied!`);
5482 });5515 });
54835516
5484 popup.dlg.querySelector('#showRawPrompt').addEventListener('click', function () {5517 popup.dlg.querySelector('#showRawPrompt').addEventListener('click', async function () {
5485 //console.log(itemizedPrompts[PromptArrayItemForRawPromptDisplay].rawPrompt);5518 //console.log(itemizedPrompts[PromptArrayItemForRawPromptDisplay].rawPrompt);
5486 console.log(PromptArrayItemForRawPromptDisplay);5519 console.log(PromptArrayItemForRawPromptDisplay);
5487 console.log(itemizedPrompts);5520 console.log(itemizedPrompts);
@@ -5489,6 +5522,17 @@ async function promptItemize(itemizedPrompts, requestedMesId) {
54895522
5490 const rawPrompt = flatten(itemizedPrompts[PromptArrayItemForRawPromptDisplay].rawPrompt);5523 const rawPrompt = flatten(itemizedPrompts[PromptArrayItemForRawPromptDisplay].rawPrompt);
54915524
5525 // Mobile needs special handholding. The side-view on the popup wouldn't work,
5526 // so we just show an additional popup for this.
5527 if (isMobile()) {
5528 const content = document.createElement('div');
5529 content.classList.add('tokenItemizingMaintext');
5530 content.innerText = rawPrompt;
5531 const popup = new Popup(content, POPUP_TYPE.TEXT, null, { allowVerticalScrolling: true, leftAlign: true });
5532 await popup.show();
5533 return;
5534 }
5535
5492 //let DisplayStringifiedPrompt = JSON.stringify(itemizedPrompts[PromptArrayItemForRawPromptDisplay].rawPrompt).replace(/\n+/g, '<br>');5536 //let DisplayStringifiedPrompt = JSON.stringify(itemizedPrompts[PromptArrayItemForRawPromptDisplay].rawPrompt).replace(/\n+/g, '<br>');
5493 const rawPromptWrapper = document.getElementById('rawPromptWrapper');5537 const rawPromptWrapper = document.getElementById('rawPromptWrapper');
5494 rawPromptWrapper.innerText = rawPrompt;5538 rawPromptWrapper.innerText = rawPrompt;
@@ -5851,7 +5895,7 @@ export function cleanUpMessage(getMessage, isImpersonate, isContinue, displayInc
5851 return getMessage;5895 return getMessage;
5852}5896}
58535897
5854export async function saveReply(type, getMessage, fromStreaming, title, swipes) {5898export async function saveReply(type, getMessage, fromStreaming, title, swipes, reasoning) {
5855 if (type != 'append' && type != 'continue' && type != 'appendFinal' && chat.length && (chat[chat.length - 1]['swipe_id'] === undefined ||5899 if (type != 'append' && type != 'continue' && type != 'appendFinal' && chat.length && (chat[chat.length - 1]['swipe_id'] === undefined ||
5856 chat[chat.length - 1]['is_user'])) {5900 chat[chat.length - 1]['is_user'])) {
5857 type = 'normal';5901 type = 'normal';
@@ -5861,6 +5905,15 @@ export async function saveReply(type, getMessage, fromStreaming, title, swipes)
5861 chat[chat.length - 1]['extra'] = {};5905 chat[chat.length - 1]['extra'] = {};
5862 }5906 }
58635907
5908 // Coerce null/undefined to empty string
5909 if (chat.length && !chat[chat.length - 1]['extra']['reasoning']) {
5910 chat[chat.length - 1]['extra']['reasoning'] = '';
5911 }
5912
5913 if (!reasoning) {
5914 reasoning = '';
5915 }
5916
5864 let oldMessage = '';5917 let oldMessage = '';
5865 const generationFinished = new Date();5918 const generationFinished = new Date();
5866 const img = extractImageFromMessage(getMessage);5919 const img = extractImageFromMessage(getMessage);
@@ -5876,8 +5929,11 @@ export async function saveReply(type, getMessage, fromStreaming, title, swipes)
5876 chat[chat.length - 1]['send_date'] = getMessageTimeStamp();5929 chat[chat.length - 1]['send_date'] = getMessageTimeStamp();
5877 chat[chat.length - 1]['extra']['api'] = getGeneratingApi();5930 chat[chat.length - 1]['extra']['api'] = getGeneratingApi();
5878 chat[chat.length - 1]['extra']['model'] = getGeneratingModel();5931 chat[chat.length - 1]['extra']['model'] = getGeneratingModel();
5932 chat[chat.length - 1]['extra']['reasoning'] = reasoning;
5933 chat[chat.length - 1]['extra']['reasoning_duration'] = null;
5879 if (power_user.message_token_count_enabled) {5934 if (power_user.message_token_count_enabled) {
5880 chat[chat.length - 1]['extra']['token_count'] = await getTokenCountAsync(chat[chat.length - 1]['mes'], 0);5935 const tokenCountText = (reasoning || '') + chat[chat.length - 1]['mes'];
5936 chat[chat.length - 1]['extra']['token_count'] = await getTokenCountAsync(tokenCountText, 0);
5881 }5937 }
5882 const chat_id = (chat.length - 1);5938 const chat_id = (chat.length - 1);
5883 await eventSource.emit(event_types.MESSAGE_RECEIVED, chat_id);5939 await eventSource.emit(event_types.MESSAGE_RECEIVED, chat_id);
@@ -5896,8 +5952,11 @@ export async function saveReply(type, getMessage, fromStreaming, title, swipes)
5896 chat[chat.length - 1]['send_date'] = getMessageTimeStamp();5952 chat[chat.length - 1]['send_date'] = getMessageTimeStamp();
5897 chat[chat.length - 1]['extra']['api'] = getGeneratingApi();5953 chat[chat.length - 1]['extra']['api'] = getGeneratingApi();
5898 chat[chat.length - 1]['extra']['model'] = getGeneratingModel();5954 chat[chat.length - 1]['extra']['model'] = getGeneratingModel();
5955 chat[chat.length - 1]['extra']['reasoning'] = reasoning;
5956 chat[chat.length - 1]['extra']['reasoning_duration'] = null;
5899 if (power_user.message_token_count_enabled) {5957 if (power_user.message_token_count_enabled) {
5900 chat[chat.length - 1]['extra']['token_count'] = await getTokenCountAsync(chat[chat.length - 1]['mes'], 0);5958 const tokenCountText = (reasoning || '') + chat[chat.length - 1]['mes'];
5959 chat[chat.length - 1]['extra']['token_count'] = await getTokenCountAsync(tokenCountText, 0);
5901 }5960 }
5902 const chat_id = (chat.length - 1);5961 const chat_id = (chat.length - 1);
5903 await eventSource.emit(event_types.MESSAGE_RECEIVED, chat_id);5962 await eventSource.emit(event_types.MESSAGE_RECEIVED, chat_id);
@@ -5913,8 +5972,11 @@ export async function saveReply(type, getMessage, fromStreaming, title, swipes)
5913 chat[chat.length - 1]['send_date'] = getMessageTimeStamp();5972 chat[chat.length - 1]['send_date'] = getMessageTimeStamp();
5914 chat[chat.length - 1]['extra']['api'] = getGeneratingApi();5973 chat[chat.length - 1]['extra']['api'] = getGeneratingApi();
5915 chat[chat.length - 1]['extra']['model'] = getGeneratingModel();5974 chat[chat.length - 1]['extra']['model'] = getGeneratingModel();
5975 chat[chat.length - 1]['extra']['reasoning'] += reasoning;
5976 // We don't know if the reasoning duration extended, so we don't update it here on purpose.
5916 if (power_user.message_token_count_enabled) {5977 if (power_user.message_token_count_enabled) {
5917 chat[chat.length - 1]['extra']['token_count'] = await getTokenCountAsync(chat[chat.length - 1]['mes'], 0);5978 const tokenCountText = (reasoning || '') + chat[chat.length - 1]['mes'];
5979 chat[chat.length - 1]['extra']['token_count'] = await getTokenCountAsync(tokenCountText, 0);
5918 }5980 }
5919 const chat_id = (chat.length - 1);5981 const chat_id = (chat.length - 1);
5920 await eventSource.emit(event_types.MESSAGE_RECEIVED, chat_id);5982 await eventSource.emit(event_types.MESSAGE_RECEIVED, chat_id);
@@ -5930,6 +5992,8 @@ export async function saveReply(type, getMessage, fromStreaming, title, swipes)
5930 chat[chat.length - 1]['send_date'] = getMessageTimeStamp();5992 chat[chat.length - 1]['send_date'] = getMessageTimeStamp();
5931 chat[chat.length - 1]['extra']['api'] = getGeneratingApi();5993 chat[chat.length - 1]['extra']['api'] = getGeneratingApi();
5932 chat[chat.length - 1]['extra']['model'] = getGeneratingModel();5994 chat[chat.length - 1]['extra']['model'] = getGeneratingModel();
5995 chat[chat.length - 1]['extra']['reasoning'] = reasoning;
5996 chat[chat.length - 1]['extra']['reasoning_duration'] = null;
5933 if (power_user.trim_spaces) {5997 if (power_user.trim_spaces) {
5934 getMessage = getMessage.trim();5998 getMessage = getMessage.trim();
5935 }5999 }
@@ -5939,7 +6003,8 @@ export async function saveReply(type, getMessage, fromStreaming, title, swipes)
5939 chat[chat.length - 1]['gen_finished'] = generationFinished;6003 chat[chat.length - 1]['gen_finished'] = generationFinished;
59406004
5941 if (power_user.message_token_count_enabled) {6005 if (power_user.message_token_count_enabled) {
5942 chat[chat.length - 1]['extra']['token_count'] = await getTokenCountAsync(chat[chat.length - 1]['mes'], 0);6006 const tokenCountText = (reasoning || '') + chat[chat.length - 1]['mes'];
6007 chat[chat.length - 1]['extra']['token_count'] = await getTokenCountAsync(tokenCountText, 0);
5943 }6008 }
59446009
5945 if (selected_group) {6010 if (selected_group) {
@@ -6004,6 +6069,19 @@ export async function saveReply(type, getMessage, fromStreaming, title, swipes)
6004 return { type, getMessage };6069 return { type, getMessage };
6005}6070}
60066071
6072export function syncCurrentSwipeInfoExtras() {
6073 if (!chat.length) {
6074 return;
6075 }
6076 const currentMessage = chat[chat.length - 1];
6077 if (currentMessage && Array.isArray(currentMessage.swipe_info) && typeof currentMessage.swipe_id === 'number') {
6078 const swipeInfo = currentMessage.swipe_info[currentMessage.swipe_id];
6079 if (swipeInfo && typeof swipeInfo === 'object') {
6080 swipeInfo.extra = structuredClone(currentMessage.extra);
6081 }
6082 }
6083}
6084
6007function saveImageToMessage(img, mes) {6085function saveImageToMessage(img, mes) {
6008 if (mes && img.image) {6086 if (mes && img.image) {
6009 if (!mes.extra || typeof mes.extra !== 'object') {6087 if (!mes.extra || typeof mes.extra !== 'object') {
@@ -6056,20 +6134,21 @@ function extractImageFromMessage(getMessage) {
6056 return { getMessage, image, title };6134 return { getMessage, image, title };
6057}6135}
60586136
6137/**
6138 * A function mainly used to switch 'generating' state - setting it to false and activating the buttons again
6139 */
6059export function activateSendButtons() {6140export function activateSendButtons() {
6060 is_send_press = false;6141 is_send_press = false;
6061 $('#send_but').removeClass('displayNone');
6062 $('#mes_continue').removeClass('displayNone');
6063 $('#mes_impersonate').removeClass('displayNone');
6064 $('.mes_buttons:last').show();
6065 hideStopButton();6142 hideStopButton();
6143 delete document.body.dataset.generating;
6066}6144}
60676145
6146/**
6147 * A function mainly used to switch 'generating' state - setting it to true and deactivating the buttons
6148 */
6068export function deactivateSendButtons() {6149export function deactivateSendButtons() {
6069 $('#send_but').addClass('displayNone');
6070 $('#mes_continue').addClass('displayNone');
6071 $('#mes_impersonate').addClass('displayNone');
6072 showStopButton();6150 showStopButton();
6151 document.body.dataset.generating = 'true';
6073}6152}
60746153
6075export function resetChatState() {6154export function resetChatState() {
@@ -6765,10 +6844,11 @@ export async function getSettings() {
6765 $('#your_name').val(name1);6844 $('#your_name').val(name1);
6766 }6845 }
67676846
6847 accountStorage.init(settings?.accountStorage);
6768 await setUserControls(data.enable_accounts);6848 await setUserControls(data.enable_accounts);
67696849
6770 // Allow subscribers to mutate settings6850 // Allow subscribers to mutate settings
6771 eventSource.emit(event_types.SETTINGS_LOADED_BEFORE, settings);6851 await eventSource.emit(event_types.SETTINGS_LOADED_BEFORE, settings);
67726852
6773 //Load KoboldAI settings6853 //Load KoboldAI settings
6774 koboldai_setting_names = data.koboldai_setting_names;6854 koboldai_setting_names = data.koboldai_setting_names;
@@ -6865,7 +6945,7 @@ export async function getSettings() {
6865 loadProxyPresets(settings);6945 loadProxyPresets(settings);
68666946
6867 // Allow subscribers to mutate settings6947 // Allow subscribers to mutate settings
6868 eventSource.emit(event_types.SETTINGS_LOADED_AFTER, settings);6948 await eventSource.emit(event_types.SETTINGS_LOADED_AFTER, settings);
68696949
6870 // Set context size after loading power user (may override the max value)6950 // Set context size after loading power user (may override the max value)
6871 $('#max_context').val(max_context);6951 $('#max_context').val(max_context);
@@ -6925,7 +7005,7 @@ export async function getSettings() {
6925 }7005 }
6926 await validateDisabledSamplers();7006 await validateDisabledSamplers();
6927 settingsReady = true;7007 settingsReady = true;
6928 eventSource.emit(event_types.SETTINGS_LOADED);7008 await eventSource.emit(event_types.SETTINGS_LOADED);
6929}7009}
69307010
6931function selectKoboldGuiPreset() {7011function selectKoboldGuiPreset() {
@@ -6936,7 +7016,8 @@ function selectKoboldGuiPreset() {
69367016
6937export async function saveSettings(loopCounter = 0) {7017export async function saveSettings(loopCounter = 0) {
6938 if (!settingsReady) {7018 if (!settingsReady) {
6939 console.warn('Settings not ready, aborting save');7019 console.warn('Settings not ready, scheduling another save');
7020 saveSettingsDebounced();
6940 return;7021 return;
6941 }7022 }
69427023
@@ -6957,6 +7038,7 @@ export async function saveSettings(loopCounter = 0) {
6957 url: '/api/settings/save',7038 url: '/api/settings/save',
6958 data: JSON.stringify({7039 data: JSON.stringify({
6959 firstRun: firstRun,7040 firstRun: firstRun,
7041 accountStorage: accountStorage.getState(),
6960 currentVersion: currentVersion,7042 currentVersion: currentVersion,
6961 username: name1,7043 username: name1,
6962 active_character: active_character,7044 active_character: active_character,
@@ -7022,8 +7104,10 @@ export function setGenerationParamsFromPreset(preset) {
7022// Common code for message editor done and auto-save7104// Common code for message editor done and auto-save
7023function updateMessage(div) {7105function updateMessage(div) {
7024 const mesBlock = div.closest('.mes_block');7106 const mesBlock = div.closest('.mes_block');
7025 let text = mesBlock.find('.edit_textarea').val();7107 let text = mesBlock.find('.edit_textarea').val()
7026 const mes = chat[this_edit_mes_id];7108 ?? mesBlock.find('.mes_text').text();
7109 const mesElement = div.closest('.mes');
7110 const mes = chat[mesElement.attr('mesid')];
70277111
7028 let regexPlacement;7112 let regexPlacement;
7029 if (mes.is_user) {7113 if (mes.is_user) {
@@ -7107,9 +7191,11 @@ function messageEditAuto(div) {
7107 mes.is_system,7191 mes.is_system,
7108 mes.is_user,7192 mes.is_user,
7109 this_edit_mes_id,7193 this_edit_mes_id,
7194 {},
7195 false,
7110 ));7196 ));
7111 mesBlock.find('.mes_bias').empty();7197 mesBlock.find('.mes_bias').empty();
7112 mesBlock.find('.mes_bias').append(messageFormatting(bias, '', false, false, -1));7198 mesBlock.find('.mes_bias').append(messageFormatting(bias, '', false, false, -1, {}, false));
7113 saveChatDebounced();7199 saveChatDebounced();
7114}7200}
71157201
@@ -7131,13 +7217,20 @@ async function messageEditDone(div) {
7131 mes.is_system,7217 mes.is_system,
7132 mes.is_user,7218 mes.is_user,
7133 this_edit_mes_id,7219 this_edit_mes_id,
7220 {},
7221 false,
7134 ),7222 ),
7135 );7223 );
7136 mesBlock.find('.mes_bias').empty();7224 mesBlock.find('.mes_bias').empty();
7137 mesBlock.find('.mes_bias').append(messageFormatting(bias, '', false, false, -1));7225 mesBlock.find('.mes_bias').append(messageFormatting(bias, '', false, false, -1, {}, false));
7138 appendMediaToMessage(mes, div.closest('.mes'));7226 appendMediaToMessage(mes, div.closest('.mes'));
7139 addCopyToCodeBlocks(div.closest('.mes'));7227 addCopyToCodeBlocks(div.closest('.mes'));
71407228
7229 const reasoningEditDone = mesBlock.find('.mes_reasoning_edit_done:visible');
7230 if (reasoningEditDone.length > 0) {
7231 reasoningEditDone.trigger('click');
7232 }
7233
7141 await eventSource.emit(event_types.MESSAGE_UPDATED, this_edit_mes_id);7234 await eventSource.emit(event_types.MESSAGE_UPDATED, this_edit_mes_id);
7142 this_edit_mes_id = undefined;7235 this_edit_mes_id = undefined;
7143 await saveChatConditional();7236 await saveChatConditional();
@@ -7401,7 +7494,7 @@ export function select_rm_info(type, charId, previousCharId = null) {
7401 }7494 }
74027495
7403 try {7496 try {
7404 const perPage = Number(localStorage.getItem('Characters_PerPage')) || per_page_default;7497 const perPage = Number(accountStorage.getItem('Characters_PerPage')) || per_page_default;
7405 const page = Math.floor(charIndex / perPage) + 1;7498 const page = Math.floor(charIndex / perPage) + 1;
7406 const selector = `#rm_print_characters_block [title*="${avatarFileName}"]`;7499 const selector = `#rm_print_characters_block [title*="${avatarFileName}"]`;
7407 $('#rm_print_characters_pagination').pagination('go', page);7500 $('#rm_print_characters_pagination').pagination('go', page);
@@ -7433,7 +7526,7 @@ export function select_rm_info(type, charId, previousCharId = null) {
7433 return;7526 return;
7434 }7527 }
74357528
7436 const perPage = Number(localStorage.getItem('Characters_PerPage')) || per_page_default;7529 const perPage = Number(accountStorage.getItem('Characters_PerPage')) || per_page_default;
7437 const page = Math.floor(charIndex / perPage) + 1;7530 const page = Math.floor(charIndex / perPage) + 1;
7438 $('#rm_print_characters_pagination').pagination('go', page);7531 $('#rm_print_characters_pagination').pagination('go', page);
7439 const selector = `#rm_print_characters_block [grid="${charId}"]`;7532 const selector = `#rm_print_characters_block [grid="${charId}"]`;
@@ -7982,11 +8075,25 @@ function updateEditArrowClasses() {
7982 }8075 }
7983}8076}
79848077
7985function closeMessageEditor() {8078/**
8079 * Closes the message editor.
8080 * @param {'message'|'reasoning'|'all'} what What to close. Default is 'all'.
8081 */
8082export function closeMessageEditor(what = 'all') {
8083 if (what === 'message' || what === 'all') {
7986 if (this_edit_mes_id) {8084 if (this_edit_mes_id) {
7987 $(`#chat .mes[mesid="${this_edit_mes_id}"] .mes_edit_cancel`).click();8085 $(`#chat .mes[mesid="${this_edit_mes_id}"] .mes_edit_cancel`).click();
7988 }8086 }
7989 }8087 }
8088 if (what === 'reasoning' || what === 'all') {
8089 document.querySelectorAll('.reasoning_edit_textarea').forEach((el) => {
8090 const cancelButton = el.closest('.mes')?.querySelector('.mes_reasoning_edit_cancel');
8091 if (cancelButton instanceof HTMLElement) {
8092 cancelButton.click();
8093 }
8094 });
8095 }
8096}
79908097
7991export function setGenerationProgress(progress) {8098export function setGenerationProgress(progress) {
7992 if (!progress) {8099 if (!progress) {
@@ -8417,6 +8524,9 @@ function swipe_left() { // when we swipe left..but no generation.
8417 streamingProcessor.onStopStreaming();8524 streamingProcessor.onStopStreaming();
8418 }8525 }
84198526
8527 // Make sure ad-hoc changes to extras are saved before swiping away
8528 syncCurrentSwipeInfoExtras();
8529
8420 const swipe_duration = 120;8530 const swipe_duration = 120;
8421 const swipe_range = '700px';8531 const swipe_range = '700px';
8422 chat[chat.length - 1]['swipe_id']--;8532 chat[chat.length - 1]['swipe_id']--;
@@ -8468,7 +8578,8 @@ function swipe_left() { // when we swipe left..but no generation.
8468 }8578 }
84698579
8470 const swipeMessage = $('#chat').find(`[mesid="${chat.length - 1}"]`);8580 const swipeMessage = $('#chat').find(`[mesid="${chat.length - 1}"]`);
8471 const tokenCount = await getTokenCountAsync(chat[chat.length - 1].mes, 0);8581 const tokenCountText = (chat[chat.length - 1]?.extra?.reasoning || '') + chat[chat.length - 1].mes;
8582 const tokenCount = await getTokenCountAsync(tokenCountText, 0);
8472 chat[chat.length - 1]['extra']['token_count'] = tokenCount;8583 chat[chat.length - 1]['extra']['token_count'] = tokenCount;
8473 swipeMessage.find('.tokenCounterDisplay').text(`${tokenCount}t`);8584 swipeMessage.find('.tokenCounterDisplay').text(`${tokenCount}t`);
8474 }8585 }
@@ -8551,6 +8662,9 @@ const swipe_right = () => {
8551 return unblockGeneration();8662 return unblockGeneration();
8552 }8663 }
85538664
8665 // Make sure ad-hoc changes to extras are saved before swiping away
8666 syncCurrentSwipeInfoExtras();
8667
8554 const swipe_duration = 200;8668 const swipe_duration = 200;
8555 const swipe_range = 700;8669 const swipe_range = 700;
8556 //console.log(swipe_range);8670 //console.log(swipe_range);
@@ -8617,11 +8731,6 @@ const swipe_right = () => {
8617 easing: animation_easing,8731 easing: animation_easing,
8618 queue: false,8732 queue: false,
8619 complete: async function () {8733 complete: async function () {
8620 /*if (!selected_group) {
8621 var typingIndicator = $("#typing_indicator_template .typing_indicator").clone();
8622 typingIndicator.find(".typing_indicator_name").text(characters[this_chid].name);
8623 } */
8624 /* $("#chat").append(typingIndicator); */
8625 const is_animation_scroll = ($('#chat').scrollTop() >= ($('#chat').prop('scrollHeight') - $('#chat').outerHeight()) - 10);8734 const is_animation_scroll = ($('#chat').scrollTop() >= ($('#chat').prop('scrollHeight') - $('#chat').outerHeight()) - 10);
8626 //console.log(parseInt(chat[chat.length-1]['swipe_id']));8735 //console.log(parseInt(chat[chat.length-1]['swipe_id']));
8627 //console.log(chat[chat.length-1]['swipes'].length);8736 //console.log(chat[chat.length-1]['swipes'].length);
@@ -8632,6 +8741,7 @@ const swipe_right = () => {
8632 // resets the timer8741 // resets the timer
8633 swipeMessage.find('.mes_timer').html('');8742 swipeMessage.find('.mes_timer').html('');
8634 swipeMessage.find('.tokenCounterDisplay').text('');8743 swipeMessage.find('.tokenCounterDisplay').text('');
8744 updateReasoningUI(swipeMessage, { reset: true });
8635 } else {8745 } else {
8636 //console.log('showing previously generated swipe candidate, or "..."');8746 //console.log('showing previously generated swipe candidate, or "..."');
8637 //console.log('onclick right swipe calling addOneMessage');8747 //console.log('onclick right swipe calling addOneMessage');
@@ -8642,7 +8752,8 @@ const swipe_right = () => {
8642 chat[chat.length - 1].extra = {};8752 chat[chat.length - 1].extra = {};
8643 }8753 }
86448754
8645 const tokenCount = await getTokenCountAsync(chat[chat.length - 1].mes, 0);8755 const tokenCountText = (chat[chat.length - 1]?.extra?.reasoning || '') + chat[chat.length - 1].mes;
8756 const tokenCount = await getTokenCountAsync(tokenCountText, 0);
8646 chat[chat.length - 1]['extra']['token_count'] = tokenCount;8757 chat[chat.length - 1]['extra']['token_count'] = tokenCount;
8647 swipeMessage.find('.tokenCounterDisplay').text(`${tokenCount}t`);8758 swipeMessage.find('.tokenCounterDisplay').text(`${tokenCount}t`);
8648 }8759 }
@@ -8680,7 +8791,6 @@ const swipe_right = () => {
8680 if (run_generate && !is_send_press && parseInt(chat[chat.length - 1]['swipe_id']) === chat[chat.length - 1]['swipes'].length) {8791 if (run_generate && !is_send_press && parseInt(chat[chat.length - 1]['swipe_id']) === chat[chat.length - 1]['swipes'].length) {
8681 console.debug('caught here 2');8792 console.debug('caught here 2');
8682 is_send_press = true;8793 is_send_press = true;
8683 $('.mes_buttons:last').hide();
8684 await Generate('swipe');8794 await Generate('swipe');
8685 } else {8795 } else {
8686 if (parseInt(chat[chat.length - 1]['swipe_id']) !== chat[chat.length - 1]['swipes'].length) {8796 if (parseInt(chat[chat.length - 1]['swipe_id']) !== chat[chat.length - 1]['swipes'].length) {
@@ -9282,6 +9392,9 @@ export async function deleteCharacter(characterKey, { deleteChats = true } = {})
9282 continue;9392 continue;
9283 }9393 }
92849394
9395 accountStorage.removeItem(`AlertWI_${character.avatar}`);
9396 accountStorage.removeItem(`AlertRegex_${character.avatar}`);
9397 accountStorage.removeItem(`mediaWarningShown:${character.avatar}`);
9285 delete tag_map[character.avatar];9398 delete tag_map[character.avatar];
9286 select_rm_info('char_delete', character.name);9399 select_rm_info('char_delete', character.name);
92879400
@@ -9444,7 +9557,8 @@ function addDebugFunctions() {
9444 message.extra = {};9557 message.extra = {};
9445 }9558 }
94469559
9447 message.extra.token_count = await getTokenCountAsync(message.mes, 0);9560 const tokenCountText = (message?.extra?.reasoning || '') + message.mes;
9561 message.extra.token_count = await getTokenCountAsync(tokenCountText, 0);
9448 }9562 }
94499563
9450 await saveChatConditional();9564 await saveChatConditional();
@@ -9483,8 +9597,8 @@ function addDebugFunctions() {
9483 });9597 });
94849598
9485 registerDebugFunction('toggleRegenerateWarning', 'Toggle Ctrl+Enter regeneration confirmation', 'Toggle the warning when regenerating a message with a Ctrl+Enter hotkey.', () => {9599 registerDebugFunction('toggleRegenerateWarning', 'Toggle Ctrl+Enter regeneration confirmation', 'Toggle the warning when regenerating a message with a Ctrl+Enter hotkey.', () => {
9486 localStorage.setItem('RegenerateWithCtrlEnter', localStorage.getItem('RegenerateWithCtrlEnter') === 'true' ? 'false' : 'true');9600 accountStorage.setItem('RegenerateWithCtrlEnter', accountStorage.getItem('RegenerateWithCtrlEnter') === 'true' ? 'false' : 'true');
9487 toastr.info('Regenerate warning is now ' + (localStorage.getItem('RegenerateWithCtrlEnter') === 'true' ? 'disabled' : 'enabled'));9601 toastr.info('Regenerate warning is now ' + (accountStorage.getItem('RegenerateWithCtrlEnter') === 'true' ? 'disabled' : 'enabled'));
9488 });9602 });
94899603
9490 registerDebugFunction('copySetup', 'Copy ST setup to clipboard [WIP]', 'Useful data when reporting bugs', async () => {9604 registerDebugFunction('copySetup', 'Copy ST setup to clipboard [WIP]', 'Useful data when reporting bugs', async () => {
@@ -10626,6 +10740,12 @@ jQuery(async function () {
10626 var edit_mes_id = $(this).closest('.mes').attr('mesid');10740 var edit_mes_id = $(this).closest('.mes').attr('mesid');
10627 this_edit_mes_id = edit_mes_id;10741 this_edit_mes_id = edit_mes_id;
1062810742
10743 // Also edit reasoning, if it exists
10744 const reasoningEdit = $(this).closest('.mes_block').find('.mes_reasoning_edit:visible');
10745 if (reasoningEdit.length > 0) {
10746 reasoningEdit.trigger('click');
10747 }
10748
10629 var text = chat[edit_mes_id]['mes'];10749 var text = chat[edit_mes_id]['mes'];
10630 if (chat[edit_mes_id]['is_user']) {10750 if (chat[edit_mes_id]['is_user']) {
10631 this_edit_mes_chname = name1;10751 this_edit_mes_chname = name1;
@@ -10753,10 +10873,17 @@ jQuery(async function () {
10753 chat[this_edit_mes_id].is_system,10873 chat[this_edit_mes_id].is_system,
10754 chat[this_edit_mes_id].is_user,10874 chat[this_edit_mes_id].is_user,
10755 this_edit_mes_id,10875 this_edit_mes_id,
10876 {},
10877 false,
10756 ));10878 ));
10757 appendMediaToMessage(chat[this_edit_mes_id], $(this).closest('.mes'));10879 appendMediaToMessage(chat[this_edit_mes_id], $(this).closest('.mes'));
10758 addCopyToCodeBlocks($(this).closest('.mes'));10880 addCopyToCodeBlocks($(this).closest('.mes'));
1075910881
10882 const reasoningEditDone = $(this).closest('.mes_block').find('.mes_reasoning_edit_cancel:visible');
10883 if (reasoningEditDone.length > 0) {
10884 reasoningEditDone.trigger('click');
10885 }
10886
10760 await eventSource.emit(event_types.MESSAGE_UPDATED, this_edit_mes_id);10887 await eventSource.emit(event_types.MESSAGE_UPDATED, this_edit_mes_id);
10761 this_edit_mes_id = undefined;10888 this_edit_mes_id = undefined;
10762 });10889 });
@@ -11213,14 +11340,15 @@ jQuery(async function () {
1121311340
11214 $(document).keyup(function (e) {11341 $(document).keyup(function (e) {
11215 if (e.key === 'Escape') {11342 if (e.key === 'Escape') {
11216 const isEditVisible = $('#curEditTextarea').is(':visible');11343 const isEditVisible = $('#curEditTextarea').is(':visible') || $('.reasoning_edit_textarea').length > 0;
11217 if (isEditVisible && power_user.auto_save_msg_edits === false) {11344 if (isEditVisible && power_user.auto_save_msg_edits === false) {
11218 closeMessageEditor();11345 closeMessageEditor('all');
11219 $('#send_textarea').focus();11346 $('#send_textarea').focus();
11220 return;11347 return;
11221 }11348 }
11222 if (isEditVisible && power_user.auto_save_msg_edits === true) {11349 if (isEditVisible && power_user.auto_save_msg_edits === true) {
11223 $(`#chat .mes[mesid="${this_edit_mes_id}"] .mes_edit_done`).click();11350 $(`#chat .mes[mesid="${this_edit_mes_id}"] .mes_edit_done`).click();
11351 closeMessageEditor('reasoning');
11224 $('#send_textarea').focus();11352 $('#send_textarea').focus();
11225 return;11353 return;
11226 }11354 }
@@ -11298,7 +11426,7 @@ jQuery(async function () {
11298 );11426 );
11299 break;*/11427 break;*/
11300 default:11428 default:
11301 eventSource.emit('charManagementDropdown', target);11429 await eventSource.emit('charManagementDropdown', target);
11302 }11430 }
11303 $('#char-management-dropdown').prop('selectedIndex', 0);11431 $('#char-management-dropdown').prop('selectedIndex', 0);
11304 });11432 });
@@ -11454,13 +11582,13 @@ jQuery(async function () {
11454 $('#avatar-and-name-block').slideToggle();11582 $('#avatar-and-name-block').slideToggle();
11455 });11583 });
1145611584
11457 $(document).on('mouseup touchend', '#show_more_messages', () => {11585 $(document).on('mouseup touchend', '#show_more_messages', async function () {
11458 showMoreMessages();11586 await showMoreMessages();
11459 });11587 });
1146011588
11461 $(document).on('click', '.open_characters_library', async function () {11589 $(document).on('click', '.open_characters_library', async function () {
11462 await getCharacters();11590 await getCharacters();
11463 eventSource.emit(event_types.OPEN_CHARACTER_LIBRARY);11591 await eventSource.emit(event_types.OPEN_CHARACTER_LIBRARY);
11464 });11592 });
1146511593
11466 // Added here to prevent execution before script.js is loaded and get rid of quirky timeouts11594 // Added here to prevent execution before script.js is loaded and get rid of quirky timeouts
public/scripts/RossAscends-mods.js+41 -28
@@ -27,7 +27,6 @@ import {
27 send_on_enter_options,27 send_on_enter_options,
28} from './power-user.js';28} from './power-user.js';
2929
30import { LoadLocal, SaveLocal, LoadLocalBool } from './f-localStorage.js';
31import { selected_group, is_group_generating, openGroupById } from './group-chats.js';30import { selected_group, is_group_generating, openGroupById } from './group-chats.js';
32import { getTagKeyForEntity, applyTagsOnCharacterSelect } from './tags.js';31import { getTagKeyForEntity, applyTagsOnCharacterSelect } from './tags.js';
33import {32import {
@@ -41,6 +40,8 @@ import { textgen_types, textgenerationwebui_settings as textgen_settings, getTex
41import { debounce_timeout } from './constants.js';40import { debounce_timeout } from './constants.js';
4241
43import { Popup } from './popup.js';42import { Popup } from './popup.js';
43import { accountStorage } from './util/AccountStorage.js';
44import { getCurrentUserHandle } from './user.js';
4445
45var RPanelPin = document.getElementById('rm_button_panel_pin');46var RPanelPin = document.getElementById('rm_button_panel_pin');
46var LPanelPin = document.getElementById('lm_button_panel_pin');47var LPanelPin = document.getElementById('lm_button_panel_pin');
@@ -409,32 +410,34 @@ function RA_autoconnect(PrevApi) {
409function OpenNavPanels() {410function OpenNavPanels() {
410 if (!isMobile()) {411 if (!isMobile()) {
411 //auto-open R nav if locked and previously open412 //auto-open R nav if locked and previously open
412 if (LoadLocalBool('NavLockOn') == true && LoadLocalBool('NavOpened') == true) {413 if (accountStorage.getItem('NavLockOn') == 'true' && accountStorage.getItem('NavOpened') == 'true') {
413 //console.log("RA -- clicking right nav to open");414 //console.log("RA -- clicking right nav to open");
414 $('#rightNavDrawerIcon').click();415 $('#rightNavDrawerIcon').click();
415 }416 }
416417
417 //auto-open L nav if locked and previously open418 //auto-open L nav if locked and previously open
418 if (LoadLocalBool('LNavLockOn') == true && LoadLocalBool('LNavOpened') == true) {419 if (accountStorage.getItem('LNavLockOn') == 'true' && accountStorage.getItem('LNavOpened') == 'true') {
419 console.debug('RA -- clicking left nav to open');420 console.debug('RA -- clicking left nav to open');
420 $('#leftNavDrawerIcon').click();421 $('#leftNavDrawerIcon').click();
421 }422 }
422423
423 //auto-open WI if locked and previously open424 //auto-open WI if locked and previously open
424 if (LoadLocalBool('WINavLockOn') == true && LoadLocalBool('WINavOpened') == true) {425 if (accountStorage.getItem('WINavLockOn') == 'true' && accountStorage.getItem('WINavOpened') == 'true') {
425 console.debug('RA -- clicking WI to open');426 console.debug('RA -- clicking WI to open');
426 $('#WIDrawerIcon').click();427 $('#WIDrawerIcon').click();
427 }428 }
428 }429 }
429}430}
430431
432const getUserInputKey = () => getCurrentUserHandle() + '_userInput';
433
431function restoreUserInput() {434function restoreUserInput() {
432 if (!power_user.restore_user_input) {435 if (!power_user.restore_user_input) {
433 console.debug('restoreUserInput disabled');436 console.debug('restoreUserInput disabled');
434 return;437 return;
435 }438 }
436439
437 const userInput = LoadLocal('userInput');440 const userInput = localStorage.getItem(getUserInputKey());
438 if (userInput) {441 if (userInput) {
439 $('#send_textarea').val(userInput)[0].dispatchEvent(new Event('input', { bubbles: true }));442 $('#send_textarea').val(userInput)[0].dispatchEvent(new Event('input', { bubbles: true }));
440 }443 }
@@ -442,7 +445,8 @@ function restoreUserInput() {
442445
443function saveUserInput() {446function saveUserInput() {
444 const userInput = String($('#send_textarea').val());447 const userInput = String($('#send_textarea').val());
445 SaveLocal('userInput', userInput);448 localStorage.setItem(getUserInputKey(), userInput);
449 console.debug('User Input -- ', userInput);
446}450}
447const saveUserInputDebounced = debounce(saveUserInput);451const saveUserInputDebounced = debounce(saveUserInput);
448452
@@ -739,7 +743,7 @@ export function initRossMods() {
739743
740 //toggle pin class when lock toggle clicked744 //toggle pin class when lock toggle clicked
741 $(RPanelPin).on('click', function () {745 $(RPanelPin).on('click', function () {
742 SaveLocal('NavLockOn', $(RPanelPin).prop('checked'));746 accountStorage.setItem('NavLockOn', $(RPanelPin).prop('checked'));
743 if ($(RPanelPin).prop('checked') == true) {747 if ($(RPanelPin).prop('checked') == true) {
744 //console.log('adding pin class to right nav');748 //console.log('adding pin class to right nav');
745 $(RightNavPanel).addClass('pinnedOpen');749 $(RightNavPanel).addClass('pinnedOpen');
@@ -757,7 +761,7 @@ export function initRossMods() {
757 }761 }
758 });762 });
759 $(LPanelPin).on('click', function () {763 $(LPanelPin).on('click', function () {
760 SaveLocal('LNavLockOn', $(LPanelPin).prop('checked'));764 accountStorage.setItem('LNavLockOn', $(LPanelPin).prop('checked'));
761 if ($(LPanelPin).prop('checked') == true) {765 if ($(LPanelPin).prop('checked') == true) {
762 //console.log('adding pin class to Left nav');766 //console.log('adding pin class to Left nav');
763 $(LeftNavPanel).addClass('pinnedOpen');767 $(LeftNavPanel).addClass('pinnedOpen');
@@ -776,7 +780,7 @@ export function initRossMods() {
776 });780 });
777781
778 $(WIPanelPin).on('click', function () {782 $(WIPanelPin).on('click', function () {
779 SaveLocal('WINavLockOn', $(WIPanelPin).prop('checked'));783 accountStorage.setItem('WINavLockOn', $(WIPanelPin).prop('checked'));
780 if ($(WIPanelPin).prop('checked') == true) {784 if ($(WIPanelPin).prop('checked') == true) {
781 console.debug('adding pin class to WI');785 console.debug('adding pin class to WI');
782 $(WorldInfo).addClass('pinnedOpen');786 $(WorldInfo).addClass('pinnedOpen');
@@ -796,8 +800,8 @@ export function initRossMods() {
796 });800 });
797801
798 // read the state of right Nav Lock and apply to rightnav classlist802 // read the state of right Nav Lock and apply to rightnav classlist
799 $(RPanelPin).prop('checked', LoadLocalBool('NavLockOn'));803 $(RPanelPin).prop('checked', accountStorage.getItem('NavLockOn') == 'true');
800 if (LoadLocalBool('NavLockOn') == true) {804 if (accountStorage.getItem('NavLockOn') == 'true') {
801 //console.log('setting pin class via local var');805 //console.log('setting pin class via local var');
802 $(RightNavPanel).addClass('pinnedOpen');806 $(RightNavPanel).addClass('pinnedOpen');
803 $(RightNavDrawerIcon).addClass('drawerPinnedOpen');807 $(RightNavDrawerIcon).addClass('drawerPinnedOpen');
@@ -808,8 +812,8 @@ export function initRossMods() {
808 $(RightNavDrawerIcon).addClass('drawerPinnedOpen');812 $(RightNavDrawerIcon).addClass('drawerPinnedOpen');
809 }813 }
810 // read the state of left Nav Lock and apply to leftnav classlist814 // read the state of left Nav Lock and apply to leftnav classlist
811 $(LPanelPin).prop('checked', LoadLocalBool('LNavLockOn'));815 $(LPanelPin).prop('checked', accountStorage.getItem('LNavLockOn') === 'true');
812 if (LoadLocalBool('LNavLockOn') == true) {816 if (accountStorage.getItem('LNavLockOn') == 'true') {
813 //console.log('setting pin class via local var');817 //console.log('setting pin class via local var');
814 $(LeftNavPanel).addClass('pinnedOpen');818 $(LeftNavPanel).addClass('pinnedOpen');
815 $(LeftNavDrawerIcon).addClass('drawerPinnedOpen');819 $(LeftNavDrawerIcon).addClass('drawerPinnedOpen');
@@ -821,8 +825,8 @@ export function initRossMods() {
821 }825 }
822826
823 // read the state of left Nav Lock and apply to leftnav classlist827 // read the state of left Nav Lock and apply to leftnav classlist
824 $(WIPanelPin).prop('checked', LoadLocalBool('WINavLockOn'));828 $(WIPanelPin).prop('checked', accountStorage.getItem('WINavLockOn') === 'true');
825 if (LoadLocalBool('WINavLockOn') == true) {829 if (accountStorage.getItem('WINavLockOn') == 'true') {
826 //console.log('setting pin class via local var');830 //console.log('setting pin class via local var');
827 $(WorldInfo).addClass('pinnedOpen');831 $(WorldInfo).addClass('pinnedOpen');
828 $(WIDrawerIcon).addClass('drawerPinnedOpen');832 $(WIDrawerIcon).addClass('drawerPinnedOpen');
@@ -837,22 +841,22 @@ export function initRossMods() {
837 //save state of Right nav being open or closed841 //save state of Right nav being open or closed
838 $('#rightNavDrawerIcon').on('click', function () {842 $('#rightNavDrawerIcon').on('click', function () {
839 if (!$('#rightNavDrawerIcon').hasClass('openIcon')) {843 if (!$('#rightNavDrawerIcon').hasClass('openIcon')) {
840 SaveLocal('NavOpened', 'true');844 accountStorage.setItem('NavOpened', 'true');
841 } else { SaveLocal('NavOpened', 'false'); }845 } else { accountStorage.setItem('NavOpened', 'false'); }
842 });846 });
843847
844 //save state of Left nav being open or closed848 //save state of Left nav being open or closed
845 $('#leftNavDrawerIcon').on('click', function () {849 $('#leftNavDrawerIcon').on('click', function () {
846 if (!$('#leftNavDrawerIcon').hasClass('openIcon')) {850 if (!$('#leftNavDrawerIcon').hasClass('openIcon')) {
847 SaveLocal('LNavOpened', 'true');851 accountStorage.setItem('LNavOpened', 'true');
848 } else { SaveLocal('LNavOpened', 'false'); }852 } else { accountStorage.setItem('LNavOpened', 'false'); }
849 });853 });
850854
851 //save state of Left nav being open or closed855 //save state of Left nav being open or closed
852 $('#WorldInfo').on('click', function () {856 $('#WorldInfo').on('click', function () {
853 if (!$('#WorldInfo').hasClass('openIcon')) {857 if (!$('#WorldInfo').hasClass('openIcon')) {
854 SaveLocal('WINavOpened', 'true');858 accountStorage.setItem('WINavOpened', 'true');
855 } else { SaveLocal('WINavOpened', 'false'); }859 } else { accountStorage.setItem('WINavOpened', 'false'); }
856 });860 });
857861
858 var chatbarInFocus = false;862 var chatbarInFocus = false;
@@ -868,8 +872,8 @@ export function initRossMods() {
868 OpenNavPanels();872 OpenNavPanels();
869 }, 300);873 }, 300);
870874
871 $(SelectedCharacterTab).click(function () { SaveLocal('SelectedNavTab', 'rm_button_selected_ch'); });875 $(SelectedCharacterTab).click(function () { accountStorage.setItem('SelectedNavTab', 'rm_button_selected_ch'); });
872 $('#rm_button_characters').click(function () { SaveLocal('SelectedNavTab', 'rm_button_characters'); });876 $('#rm_button_characters').click(function () { accountStorage.setItem('SelectedNavTab', 'rm_button_characters'); });
873877
874 // when a char is selected from the list, save them as the auto-load character for next page load878 // when a char is selected from the list, save them as the auto-load character for next page load
875879
@@ -1063,14 +1067,21 @@ export function initRossMods() {
1063 // Ctrl+Enter for Regeneration Last Response. If editing, accept the edits instead1067 // Ctrl+Enter for Regeneration Last Response. If editing, accept the edits instead
1064 if (event.ctrlKey && event.key == 'Enter') {1068 if (event.ctrlKey && event.key == 'Enter') {
1065 const editMesDone = $('.mes_edit_done:visible');1069 const editMesDone = $('.mes_edit_done:visible');
1070 const reasoningMesDone = $('.mes_reasoning_edit_done:visible');
1066 if (editMesDone.length > 0) {1071 if (editMesDone.length > 0) {
1067 console.debug('Accepting edits with Ctrl+Enter');1072 console.debug('Accepting edits with Ctrl+Enter');
1068 $('#send_textarea').focus();1073 $('#send_textarea').trigger('focus');
1069 editMesDone.trigger('click');1074 editMesDone.trigger('click');
1070 return;1075 return;
1071 } else if (is_send_press == false) {1076 } else if (reasoningMesDone.length > 0) {
1077 console.debug('Accepting edits with Ctrl+Enter');
1078 $('#send_textarea').trigger('focus');
1079 reasoningMesDone.trigger('click');
1080 return;
1081 }
1082 else if (is_send_press == false) {
1072 const skipConfirmKey = 'RegenerateWithCtrlEnter';1083 const skipConfirmKey = 'RegenerateWithCtrlEnter';
1073 const skipConfirm = LoadLocalBool(skipConfirmKey);1084 const skipConfirm = accountStorage.getItem(skipConfirmKey) === 'true';
1074 function doRegenerate() {1085 function doRegenerate() {
1075 console.debug('Regenerating with Ctrl+Enter');1086 console.debug('Regenerating with Ctrl+Enter');
1076 $('#option_regenerate').trigger('click');1087 $('#option_regenerate').trigger('click');
@@ -1082,13 +1093,15 @@ export function initRossMods() {
1082 let regenerateWithCtrlEnter = false;1093 let regenerateWithCtrlEnter = false;
1083 const result = await Popup.show.confirm('Regenerate Message', 'Are you sure you want to regenerate the latest message?', {1094 const result = await Popup.show.confirm('Regenerate Message', 'Are you sure you want to regenerate the latest message?', {
1084 customInputs: [{ id: 'regenerateWithCtrlEnter', label: 'Don\'t ask again' }],1095 customInputs: [{ id: 'regenerateWithCtrlEnter', label: 'Don\'t ask again' }],
1085 onClose: (popup) => regenerateWithCtrlEnter = popup.inputResults.get('regenerateWithCtrlEnter') ?? false,1096 onClose: (popup) => {
1097 regenerateWithCtrlEnter = popup.inputResults.get('regenerateWithCtrlEnter') ?? false;
1098 },
1086 });1099 });
1087 if (!result) {1100 if (!result) {
1088 return;1101 return;
1089 }1102 }
10901103
1091 SaveLocal(skipConfirmKey, regenerateWithCtrlEnter);1104 accountStorage.setItem(skipConfirmKey, String(regenerateWithCtrlEnter));
1092 doRegenerate();1105 doRegenerate();
1093 }1106 }
1094 return;1107 return;
public/scripts/authors-note.js+1 -1
@@ -566,7 +566,7 @@ export function initAuthorsNote() {
566 namedArgumentList: [],566 namedArgumentList: [],
567 unnamedArgumentList: [567 unnamedArgumentList: [
568 new SlashCommandArgument(568 new SlashCommandArgument(
569 'position', [ARGUMENT_TYPE.STRING], false, false, null, ['system', 'user', 'assistant'],569 'role', [ARGUMENT_TYPE.STRING], false, false, null, ['system', 'user', 'assistant'],
570 ),570 ),
571 ],571 ],
572 helpString: `572 helpString: `
public/scripts/backgrounds.js+19 -9
@@ -96,8 +96,13 @@ function highlightLockedBackground() {
96 });96 });
97}97}
9898
99/**
100 * Locks the background for the current chat
101 * @param {Event} e Click event
102 * @returns {string} Empty string
103 */
99function onLockBackgroundClick(e) {104function onLockBackgroundClick(e) {
100 e.stopPropagation();105 e?.stopPropagation();
101106
102 const chatName = getCurrentChatId();107 const chatName = getCurrentChatId();
103108
@@ -106,7 +111,7 @@ function onLockBackgroundClick(e) {
106 return '';111 return '';
107 }112 }
108113
109 const relativeBgImage = getUrlParameter(this);114 const relativeBgImage = getUrlParameter(this) ?? background_settings.url;
110115
111 saveBackgroundMetadata(relativeBgImage);116 saveBackgroundMetadata(relativeBgImage);
112 setCustomBackground();117 setCustomBackground();
@@ -114,8 +119,13 @@ function onLockBackgroundClick(e) {
114 return '';119 return '';
115}120}
116121
122/**
123 * Locks the background for the current chat
124 * @param {Event} e Click event
125 * @returns {string} Empty string
126 */
117function onUnlockBackgroundClick(e) {127function onUnlockBackgroundClick(e) {
118 e.stopPropagation();128 e?.stopPropagation();
119 removeBackgroundMetadata();129 removeBackgroundMetadata();
120 unsetCustomBackground();130 unsetCustomBackground();
121 highlightLockedBackground();131 highlightLockedBackground();
@@ -482,10 +492,10 @@ function highlightNewBackground(bg) {
482 */492 */
483function setFittingClass(fitting) {493function setFittingClass(fitting) {
484 const backgrounds = $('#bg1, #bg_custom');494 const backgrounds = $('#bg1, #bg_custom');
485 backgrounds.toggleClass('cover', fitting === 'cover');495 for (const option of ['cover', 'contain', 'stretch', 'center']) {
486 backgrounds.toggleClass('contain', fitting === 'contain');496 backgrounds.toggleClass(option, option === fitting);
487 backgrounds.toggleClass('stretch', fitting === 'stretch');497 }
488 backgrounds.toggleClass('center', fitting === 'center');498 background_settings.fitting = fitting;
489}499}
490500
491function onBackgroundFilterInput() {501function onBackgroundFilterInput() {
@@ -513,12 +523,12 @@ export function initBackgrounds() {
513 $('#add_bg_button').on('change', onBackgroundUploadSelected);523 $('#add_bg_button').on('change', onBackgroundUploadSelected);
514 $('#bg-filter').on('input', onBackgroundFilterInput);524 $('#bg-filter').on('input', onBackgroundFilterInput);
515 SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'lockbg',525 SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'lockbg',
516 callback: onLockBackgroundClick,526 callback: () => onLockBackgroundClick(new CustomEvent('click')),
517 aliases: ['bglock'],527 aliases: ['bglock'],
518 helpString: 'Locks a background for the currently selected chat',528 helpString: 'Locks a background for the currently selected chat',
519 }));529 }));
520 SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'unlockbg',530 SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'unlockbg',
521 callback: onUnlockBackgroundClick,531 callback: () => onUnlockBackgroundClick(new CustomEvent('click')),
522 aliases: ['bgunlock'],532 aliases: ['bgunlock'],
523 helpString: 'Unlocks a background for the currently selected chat',533 helpString: 'Unlocks a background for the currently selected chat',
524 }));534 }));
public/scripts/chat-templates.js+12 -1
@@ -59,6 +59,17 @@ const hash_derivations = {
59 // Tulu-3-8B59 // Tulu-3-8B
60 // Tulu-3-70B60 // Tulu-3-70B
61 'Tulu'61 'Tulu'
62 ,
63
64 // DeepSeek V2.5
65 '54d400beedcd17f464e10063e0577f6f798fa896266a912d8a366f8a2fcc0bca':
66 'DeepSeek-V2.5'
67 ,
68
69 // DeepSeek R1
70 'b6835114b7303ddd78919a82e4d9f7d8c26ed0d7dfc36beeb12d524f6144eab1':
71 'DeepSeek-V2.5'
72 ,
62};73};
6374
64const substr_derivations = {75const substr_derivations = {
@@ -87,6 +98,6 @@ export async function deriveTemplatesFromChatTemplate(chat_template, hash) {
87 }98 }
88 }99 }
89100
90 console.log(`Unknown chat template hash: ${hash} for [${chat_template}]`);101 console.warn(`Unknown chat template hash: ${hash} for [${chat_template}]`);
91 return null;102 return null;
92}103}
public/scripts/chats.js+59 -6
@@ -11,6 +11,7 @@ import {
11 getCurrentChatId,11 getCurrentChatId,
12 getRequestHeaders,12 getRequestHeaders,
13 hideSwipeButtons,13 hideSwipeButtons,
14 name1,
14 name2,15 name2,
15 reloadCurrentChat,16 reloadCurrentChat,
16 saveChatDebounced,17 saveChatDebounced,
@@ -21,6 +22,7 @@ import {
21 chat_metadata,22 chat_metadata,
22 neutralCharacterName,23 neutralCharacterName,
23 updateChatMetadata,24 updateChatMetadata,
25 system_message_types,
24} from '../script.js';26} from '../script.js';
25import { selected_group } from './group-chats.js';27import { selected_group } from './group-chats.js';
26import { power_user } from './power-user.js';28import { power_user } from './power-user.js';
@@ -34,6 +36,7 @@ import {
34 humanFileSize,36 humanFileSize,
35 saveBase64AsFile,37 saveBase64AsFile,
36 extractTextFromOffice,38 extractTextFromOffice,
39 download,
37} from './utils.js';40} from './utils.js';
38import { extension_settings, renderExtensionTemplateAsync, saveMetadataDebounced } from './extensions.js';41import { extension_settings, renderExtensionTemplateAsync, saveMetadataDebounced } from './extensions.js';
39import { POPUP_RESULT, POPUP_TYPE, Popup, callGenericPopup } from './popup.js';42import { POPUP_RESULT, POPUP_TYPE, Popup, callGenericPopup } from './popup.js';
@@ -41,6 +44,8 @@ import { ScraperManager } from './scrapers.js';
41import { DragAndDropHandler } from './dragdrop.js';44import { DragAndDropHandler } from './dragdrop.js';
42import { renderTemplateAsync } from './templates.js';45import { renderTemplateAsync } from './templates.js';
43import { t } from './i18n.js';46import { t } from './i18n.js';
47import { humanizedDateTime } from './RossAscends-mods.js';
48import { accountStorage } from './util/AccountStorage.js';
4449
45/**50/**
46 * @typedef {Object} FileAttachment51 * @typedef {Object} FileAttachment
@@ -617,21 +622,56 @@ async function enlargeMessageImage() {
617}622}
618623
619async function deleteMessageImage() {624async function deleteMessageImage() {
620 const value = await callGenericPopup('<h3>Delete image from message?<br>This action can\'t be undone.</h3>', POPUP_TYPE.CONFIRM);625 const value = await callGenericPopup('<h3>Delete image from message?<br>This action can\'t be undone.</h3>', POPUP_TYPE.TEXT, '', {
626 okButton: t`Delete one`,
627 customButtons: [
628 {
629 text: t`Delete all`,
630 appendAtEnd: true,
631 result: POPUP_RESULT.CUSTOM1,
632 },
633 {
634 text: t`Cancel`,
635 appendAtEnd: true,
636 result: POPUP_RESULT.CANCELLED,
637 },
638 ],
639 });
621640
622 if (value !== POPUP_RESULT.AFFIRMATIVE) {641 if (!value) {
623 return;642 return;
624 }643 }
625644
626 const mesBlock = $(this).closest('.mes');645 const mesBlock = $(this).closest('.mes');
627 const mesId = mesBlock.attr('mesid');646 const mesId = mesBlock.attr('mesid');
628 const message = chat[mesId];647 const message = chat[mesId];
648
649 let isLastImage = true;
650
651 if (Array.isArray(message.extra.image_swipes)) {
652 const indexOf = message.extra.image_swipes.indexOf(message.extra.image);
653 if (indexOf > -1) {
654 message.extra.image_swipes.splice(indexOf, 1);
655 isLastImage = message.extra.image_swipes.length === 0;
656 if (!isLastImage) {
657 const newIndex = Math.min(indexOf, message.extra.image_swipes.length - 1);
658 message.extra.image = message.extra.image_swipes[newIndex];
659 }
660 }
661 }
662
663 if (isLastImage || value === POPUP_RESULT.CUSTOM1) {
629 delete message.extra.image;664 delete message.extra.image;
630 delete message.extra.inline_image;665 delete message.extra.inline_image;
631 delete message.extra.title;666 delete message.extra.title;
632 delete message.extra.append_title;667 delete message.extra.append_title;
668 delete message.extra.image_swipes;
633 mesBlock.find('.mes_img_container').removeClass('img_extra');669 mesBlock.find('.mes_img_container').removeClass('img_extra');
634 mesBlock.find('.mes_img').attr('src', '');670 mesBlock.find('.mes_img').attr('src', '');
671 } else {
672 appendMediaToMessage(message, mesBlock);
673 }
674
635 await saveChatConditional();675 await saveChatConditional();
636}676}
637677
@@ -1039,8 +1079,8 @@ async function openAttachmentManager() {
1039 renderAttachments();1079 renderAttachments();
1040 });1080 });
10411081
1042 let sortField = localStorage.getItem('DataBank_sortField') || 'created';1082 let sortField = accountStorage.getItem('DataBank_sortField') || 'created';
1043 let sortOrder = localStorage.getItem('DataBank_sortOrder') || 'desc';1083 let sortOrder = accountStorage.getItem('DataBank_sortOrder') || 'desc';
1044 let filterString = '';1084 let filterString = '';
10451085
1046 const template = $(await renderExtensionTemplateAsync('attachments', 'manager', {}));1086 const template = $(await renderExtensionTemplateAsync('attachments', 'manager', {}));
@@ -1056,8 +1096,8 @@ async function openAttachmentManager() {
10561096
1057 sortField = this.selectedOptions[0].dataset.sortField;1097 sortField = this.selectedOptions[0].dataset.sortField;
1058 sortOrder = this.selectedOptions[0].dataset.sortOrder;1098 sortOrder = this.selectedOptions[0].dataset.sortOrder;
1059 localStorage.setItem('DataBank_sortField', sortField);1099 accountStorage.setItem('DataBank_sortField', sortField);
1060 localStorage.setItem('DataBank_sortOrder', sortOrder);1100 accountStorage.setItem('DataBank_sortOrder', sortOrder);
1061 renderAttachments();1101 renderAttachments();
1062 });1102 });
1063 function handleBulkAction(action) {1103 function handleBulkAction(action) {
@@ -1437,6 +1477,19 @@ jQuery(function () {
1437 await viewMessageFile(messageId);1477 await viewMessageFile(messageId);
1438 });1478 });
14391479
1480 $(document).on('click', '.assistant_note_export', async function () {
1481 const chatToSave = [
1482 {
1483 user_name: name1,
1484 character_name: name2,
1485 chat_metadata: chat_metadata,
1486 },
1487 ...chat.filter(x => x?.extra?.type !== system_message_types.ASSISTANT_NOTE),
1488 ];
1489
1490 download(JSON.stringify(chatToSave, null, 4), `Assistant - ${humanizedDateTime()}.json`, 'application/json');
1491 });
1492
1440 // Do not change. #attachFile is added by extension.1493 // Do not change. #attachFile is added by extension.
1441 $(document).on('click', '#attachFile', function () {1494 $(document).on('click', '#attachFile', function () {
1442 $('#file_form_input').trigger('click');1495 $('#file_form_input').trigger('click');
public/scripts/extensions.js+5 -4
@@ -9,6 +9,7 @@ import { getContext } from './st-context.js';
9import { isAdmin } from './user.js';9import { isAdmin } from './user.js';
10import { t } from './i18n.js';10import { t } from './i18n.js';
11import { debounce_timeout } from './constants.js';11import { debounce_timeout } from './constants.js';
12import { accountStorage } from './util/AccountStorage.js';
1213
13export {14export {
14 getContext,15 getContext,
@@ -714,7 +715,7 @@ async function showExtensionsDetails() {
714 htmlExternal.append(htmlLoading);715 htmlExternal.append(htmlLoading);
715716
716 const sortOrderKey = 'extensions_sortByName';717 const sortOrderKey = 'extensions_sortByName';
717 const sortByName = localStorage.getItem(sortOrderKey) === 'true';718 const sortByName = accountStorage.getItem(sortOrderKey) === 'true';
718 const sortFn = sortByName ? sortManifestsByName : sortManifestsByOrder;719 const sortFn = sortByName ? sortManifestsByName : sortManifestsByOrder;
719 const extensions = Object.entries(manifests).sort((a, b) => sortFn(a[1], b[1])).map(getExtensionData);720 const extensions = Object.entries(manifests).sort((a, b) => sortFn(a[1], b[1])).map(getExtensionData);
720721
@@ -745,7 +746,7 @@ async function showExtensionsDetails() {
745 text: sortByName ? t`Sort: Display Name` : t`Sort: Loading Order`,746 text: sortByName ? t`Sort: Display Name` : t`Sort: Loading Order`,
746 action: async () => {747 action: async () => {
747 abortController.abort();748 abortController.abort();
748 localStorage.setItem(sortOrderKey, sortByName ? 'false' : 'true');749 accountStorage.setItem(sortOrderKey, sortByName ? 'false' : 'true');
749 await showExtensionsDetails();750 await showExtensionsDetails();
750 },751 },
751 };752 };
@@ -1153,11 +1154,11 @@ async function checkForExtensionUpdates(force) {
1153 const currentDate = new Date().toDateString();1154 const currentDate = new Date().toDateString();
11541155
1155 // Don't nag more than once a day1156 // Don't nag more than once a day
1156 if (localStorage.getItem(STORAGE_NAG_KEY) === currentDate) {1157 if (accountStorage.getItem(STORAGE_NAG_KEY) === currentDate) {
1157 return;1158 return;
1158 }1159 }
11591160
1160 localStorage.setItem(STORAGE_NAG_KEY, currentDate);1161 accountStorage.setItem(STORAGE_NAG_KEY, currentDate);
1161 }1162 }
11621163
1163 const isCurrentUserAdmin = isAdmin();1164 const isCurrentUserAdmin = isAdmin();
public/scripts/extensions/assets/index.js+3 -2
@@ -8,6 +8,7 @@ import { getRequestHeaders, processDroppedFiles, eventSource, event_types } from
8import { deleteExtension, extensionNames, getContext, installExtension, renderExtensionTemplateAsync } from '../../extensions.js';8import { deleteExtension, extensionNames, getContext, installExtension, renderExtensionTemplateAsync } from '../../extensions.js';
9import { POPUP_TYPE, Popup, callGenericPopup } from '../../popup.js';9import { POPUP_TYPE, Popup, callGenericPopup } from '../../popup.js';
10import { executeSlashCommands } from '../../slash-commands.js';10import { executeSlashCommands } from '../../slash-commands.js';
11import { accountStorage } from '../../util/AccountStorage.js';
11import { flashHighlight, getStringHash, isValidUrl } from '../../utils.js';12import { flashHighlight, getStringHash, isValidUrl } from '../../utils.js';
12export { MODULE_NAME };13export { MODULE_NAME };
1314
@@ -432,14 +433,14 @@ jQuery(async () => {
432 connectButton.on('click', async function () {433 connectButton.on('click', async function () {
433 const url = DOMPurify.sanitize(String(assetsJsonUrl.val()));434 const url = DOMPurify.sanitize(String(assetsJsonUrl.val()));
434 const rememberKey = `Assets_SkipConfirm_${getStringHash(url)}`;435 const rememberKey = `Assets_SkipConfirm_${getStringHash(url)}`;
435 const skipConfirm = localStorage.getItem(rememberKey) === 'true';436 const skipConfirm = accountStorage.getItem(rememberKey) === 'true';
436437
437 const confirmation = skipConfirm || await Popup.show.confirm('Loading Asset List', `<span>Are you sure you want to connect to the following url?</span><var>${url}</var>`, {438 const confirmation = skipConfirm || await Popup.show.confirm('Loading Asset List', `<span>Are you sure you want to connect to the following url?</span><var>${url}</var>`, {
438 customInputs: [{ id: 'assets-remember', label: 'Don\'t ask again for this URL' }],439 customInputs: [{ id: 'assets-remember', label: 'Don\'t ask again for this URL' }],
439 onClose: popup => {440 onClose: popup => {
440 if (popup.result) {441 if (popup.result) {
441 const rememberValue = popup.inputResults.get('assets-remember');442 const rememberValue = popup.inputResults.get('assets-remember');
442 localStorage.setItem(rememberKey, String(rememberValue));443 accountStorage.setItem(rememberKey, String(rememberValue));
443 }444 }
444 },445 },
445 });446 });
public/scripts/extensions/caption/settings.html+9 -1
@@ -10,7 +10,7 @@
10 <select id="caption_source" class="text_pole">10 <select id="caption_source" class="text_pole">
11 <option value="local" data-i18n="Local">Local</option>11 <option value="local" data-i18n="Local">Local</option>
12 <option value="multimodal" data-i18n="Multimodal (OpenAI / Anthropic / llama / Google)">Multimodal (OpenAI / Anthropic / llama / Google)</option>12 <option value="multimodal" data-i18n="Multimodal (OpenAI / Anthropic / llama / Google)">Multimodal (OpenAI / Anthropic / llama / Google)</option>
13 <option value="extras" data-i18n="Extras">Extras</option>13 <option value="extras" data-i18n="Extras">Extras (deprecated)</option>
14 <option value="horde" data-i18n="Horde">Horde</option>14 <option value="horde" data-i18n="Horde">Horde</option>
15 </select>15 </select>
16 <div id="caption_multimodal_block" class="flex-container wide100p">16 <div id="caption_multimodal_block" class="flex-container wide100p">
@@ -53,7 +53,15 @@
53 <option data-type="anthropic" value="claude-3-opus-20240229">claude-3-opus-20240229</option>53 <option data-type="anthropic" value="claude-3-opus-20240229">claude-3-opus-20240229</option>
54 <option data-type="anthropic" value="claude-3-sonnet-20240229">claude-3-sonnet-20240229</option>54 <option data-type="anthropic" value="claude-3-sonnet-20240229">claude-3-sonnet-20240229</option>
55 <option data-type="anthropic" value="claude-3-haiku-20240307">claude-3-haiku-20240307</option>55 <option data-type="anthropic" value="claude-3-haiku-20240307">claude-3-haiku-20240307</option>
56 <option data-type="google" value="gemini-2.0-pro-exp">gemini-2.0-pro-exp</option>
57 <option data-type="google" value="gemini-2.0-pro-exp-02-05">gemini-2.0-pro-exp-02-05</option>
58 <option data-type="google" value="gemini-2.0-flash-lite-preview">gemini-2.0-flash-lite-preview</option>
59 <option data-type="google" value="gemini-2.0-flash-lite-preview-02-05">gemini-2.0-flash-lite-preview-02-05</option>
60 <option data-type="google" value="gemini-2.0-flash">gemini-2.0-flash</option>
61 <option data-type="google" value="gemini-2.0-flash-001">gemini-2.0-flash-001</option>
56 <option data-type="google" value="gemini-2.0-flash-exp">gemini-2.0-flash-exp</option>62 <option data-type="google" value="gemini-2.0-flash-exp">gemini-2.0-flash-exp</option>
63 <option data-type="google" value="gemini-2.0-flash-thinking-exp">gemini-2.0-flash-thinking-exp</option>
64 <option data-type="google" value="gemini-2.0-flash-thinking-exp-01-21">gemini-2.0-flash-thinking-exp-01-21</option>
57 <option data-type="google" value="gemini-2.0-flash-thinking-exp-1219">gemini-2.0-flash-thinking-exp-1219</option>65 <option data-type="google" value="gemini-2.0-flash-thinking-exp-1219">gemini-2.0-flash-thinking-exp-1219</option>
58 <option data-type="google" value="gemini-1.5-flash">gemini-1.5-flash</option>66 <option data-type="google" value="gemini-1.5-flash">gemini-1.5-flash</option>
59 <option data-type="google" value="gemini-1.5-flash-latest">gemini-1.5-flash-latest</option>67 <option data-type="google" value="gemini-1.5-flash-latest">gemini-1.5-flash-latest</option>
public/scripts/extensions/connection-manager/index.js+4 -0
@@ -30,6 +30,7 @@ const CC_COMMANDS = [
30 'api-url',30 'api-url',
31 'model',31 'model',
32 'proxy',32 'proxy',
33 'stop-strings',
33];34];
3435
35const TC_COMMANDS = [36const TC_COMMANDS = [
@@ -43,6 +44,7 @@ const TC_COMMANDS = [
43 'context',44 'context',
44 'instruct-state',45 'instruct-state',
45 'tokenizer',46 'tokenizer',
47 'stop-strings',
46];48];
4749
48const FANCY_NAMES = {50const FANCY_NAMES = {
@@ -57,6 +59,7 @@ const FANCY_NAMES = {
57 'instruct': 'Instruct Template',59 'instruct': 'Instruct Template',
58 'context': 'Context Template',60 'context': 'Context Template',
59 'tokenizer': 'Tokenizer',61 'tokenizer': 'Tokenizer',
62 'stop-strings': 'Custom Stopping Strings',
60};63};
6164
62/**65/**
@@ -138,6 +141,7 @@ const profilesProvider = () => [
138 * @property {string} [context] Context Template141 * @property {string} [context] Context Template
139 * @property {string} [instruct-state] Instruct Mode142 * @property {string} [instruct-state] Instruct Mode
140 * @property {string} [tokenizer] Tokenizer143 * @property {string} [tokenizer] Tokenizer
144 * @property {string} [stop-strings] Custom Stopping Strings
141 * @property {string[]} [exclude] Commands to exclude145 * @property {string[]} [exclude] Commands to exclude
142 */146 */
143147
public/scripts/extensions/expressions/index.js+0 -1
@@ -2178,7 +2178,6 @@ function migrateSettings() {
2178 typeList: [ARGUMENT_TYPE.STRING],2178 typeList: [ARGUMENT_TYPE.STRING],
2179 isRequired: true,2179 isRequired: true,
2180 enumProvider: commonEnumProviders.characters('character'),2180 enumProvider: commonEnumProviders.characters('character'),
2181 forceEnum: true,
2182 }),2181 }),
2183 ],2182 ],
2184 helpString: 'Returns the last set sprite / expression for the named character.',2183 helpString: 'Returns the last set sprite / expression for the named character.',
public/scripts/extensions/expressions/settings.html+1 -1
@@ -23,7 +23,7 @@
23 <small data-i18n="Select the API for classifying expressions.">Select the API for classifying expressions.</small>23 <small data-i18n="Select the API for classifying expressions.">Select the API for classifying expressions.</small>
24 <select id="expression_api" class="flex1 margin0">24 <select id="expression_api" class="flex1 margin0">
25 <option value="0" data-i18n="Local">Local</option>25 <option value="0" data-i18n="Local">Local</option>
26 <option value="1" data-i18n="Extras">Extras</option>26 <option value="1" data-i18n="Extras">Extras (deprecated)</option>
27 <option value="2" data-i18n="Main API">Main API</option>27 <option value="2" data-i18n="Main API">Main API</option>
28 <option value="3" data-i18n="WebLLM Extension">WebLLM Extension</option>28 <option value="3" data-i18n="WebLLM Extension">WebLLM Extension</option>
29 </select>29 </select>
public/scripts/extensions/gallery/index.js+0 -1
@@ -441,7 +441,6 @@ SlashCommandParser.addCommandObject(SlashCommand.fromProps({
441 description: 'character name',441 description: 'character name',
442 typeList: [ARGUMENT_TYPE.STRING],442 typeList: [ARGUMENT_TYPE.STRING],
443 enumProvider: commonEnumProviders.characters('character'),443 enumProvider: commonEnumProviders.characters('character'),
444 forceEnum: true,
445 }),444 }),
446 SlashCommandNamedArgument.fromProps({445 SlashCommandNamedArgument.fromProps({
447 name: 'group',446 name: 'group',
public/scripts/extensions/memory/settings.html+1 -1
@@ -12,7 +12,7 @@
12 <label for="summary_source" data-i18n="ext_sum_with">Summarize with:</label>12 <label for="summary_source" data-i18n="ext_sum_with">Summarize with:</label>
13 <select id="summary_source">13 <select id="summary_source">
14 <option value="main" data-i18n="ext_sum_main_api">Main API</option>14 <option value="main" data-i18n="ext_sum_main_api">Main API</option>
15 <option value="extras">Extras API</option>15 <option value="extras">Extras API (deprecated)</option>
16 <option value="webllm" data-i18n="ext_sum_webllm">WebLLM Extension</option>16 <option value="webllm" data-i18n="ext_sum_webllm">WebLLM Extension</option>
17 </select><br>17 </select><br>
1818
public/scripts/extensions/quick-reply/src/QuickReply.js+9 -8
@@ -10,6 +10,7 @@ import { SlashCommandExecutor } from '../../../slash-commands/SlashCommandExecut
10import { SlashCommandParser } from '../../../slash-commands/SlashCommandParser.js';10import { SlashCommandParser } from '../../../slash-commands/SlashCommandParser.js';
11import { SlashCommandParserError } from '../../../slash-commands/SlashCommandParserError.js';11import { SlashCommandParserError } from '../../../slash-commands/SlashCommandParserError.js';
12import { SlashCommandScope } from '../../../slash-commands/SlashCommandScope.js';12import { SlashCommandScope } from '../../../slash-commands/SlashCommandScope.js';
13import { accountStorage } from '../../../util/AccountStorage.js';
13import { debounce, delay, getSortableDelay, showFontAwesomePicker } from '../../../utils.js';14import { debounce, delay, getSortableDelay, showFontAwesomePicker } from '../../../utils.js';
14import { log, quickReplyApi, warn } from '../index.js';15import { log, quickReplyApi, warn } from '../index.js';
15import { QuickReplyContextLink } from './QuickReplyContextLink.js';16import { QuickReplyContextLink } from './QuickReplyContextLink.js';
@@ -544,9 +545,9 @@ export class QuickReply {
544 this.editorSyntax = messageSyntaxInner;545 this.editorSyntax = messageSyntaxInner;
545 /**@type {HTMLInputElement}*/546 /**@type {HTMLInputElement}*/
546 const wrap = dom.querySelector('#qr--modal-wrap');547 const wrap = dom.querySelector('#qr--modal-wrap');
547 wrap.checked = JSON.parse(localStorage.getItem('qr--wrap') ?? 'false');548 wrap.checked = JSON.parse(accountStorage.getItem('qr--wrap') ?? 'false');
548 wrap.addEventListener('click', () => {549 wrap.addEventListener('click', () => {
549 localStorage.setItem('qr--wrap', JSON.stringify(wrap.checked));550 accountStorage.setItem('qr--wrap', JSON.stringify(wrap.checked));
550 updateWrap();551 updateWrap();
551 });552 });
552 const updateWrap = () => {553 const updateWrap = () => {
@@ -594,27 +595,27 @@ export class QuickReply {
594 };595 };
595 /**@type {HTMLInputElement}*/596 /**@type {HTMLInputElement}*/
596 const tabSize = dom.querySelector('#qr--modal-tabSize');597 const tabSize = dom.querySelector('#qr--modal-tabSize');
597 tabSize.value = JSON.parse(localStorage.getItem('qr--tabSize') ?? '4');598 tabSize.value = JSON.parse(accountStorage.getItem('qr--tabSize') ?? '4');
598 const updateTabSize = () => {599 const updateTabSize = () => {
599 message.style.tabSize = tabSize.value;600 message.style.tabSize = tabSize.value;
600 messageSyntaxInner.style.tabSize = tabSize.value;601 messageSyntaxInner.style.tabSize = tabSize.value;
601 updateScrollDebounced();602 updateScrollDebounced();
602 };603 };
603 tabSize.addEventListener('change', () => {604 tabSize.addEventListener('change', () => {
604 localStorage.setItem('qr--tabSize', JSON.stringify(Number(tabSize.value)));605 accountStorage.setItem('qr--tabSize', JSON.stringify(Number(tabSize.value)));
605 updateTabSize();606 updateTabSize();
606 });607 });
607 /**@type {HTMLInputElement}*/608 /**@type {HTMLInputElement}*/
608 const executeShortcut = dom.querySelector('#qr--modal-executeShortcut');609 const executeShortcut = dom.querySelector('#qr--modal-executeShortcut');
609 executeShortcut.checked = JSON.parse(localStorage.getItem('qr--executeShortcut') ?? 'true');610 executeShortcut.checked = JSON.parse(accountStorage.getItem('qr--executeShortcut') ?? 'true');
610 executeShortcut.addEventListener('click', () => {611 executeShortcut.addEventListener('click', () => {
611 localStorage.setItem('qr--executeShortcut', JSON.stringify(executeShortcut.checked));612 accountStorage.setItem('qr--executeShortcut', JSON.stringify(executeShortcut.checked));
612 });613 });
613 /**@type {HTMLInputElement}*/614 /**@type {HTMLInputElement}*/
614 const syntax = dom.querySelector('#qr--modal-syntax');615 const syntax = dom.querySelector('#qr--modal-syntax');
615 syntax.checked = JSON.parse(localStorage.getItem('qr--syntax') ?? 'true');616 syntax.checked = JSON.parse(accountStorage.getItem('qr--syntax') ?? 'true');
616 syntax.addEventListener('click', () => {617 syntax.addEventListener('click', () => {
617 localStorage.setItem('qr--syntax', JSON.stringify(syntax.checked));618 accountStorage.setItem('qr--syntax', JSON.stringify(syntax.checked));
618 updateSyntaxEnabled();619 updateSyntaxEnabled();
619 });620 });
620 if (navigator.keyboard) {621 if (navigator.keyboard) {
public/scripts/extensions/quick-reply/src/QuickReplySet.js+6 -28
@@ -1,15 +1,14 @@
1import { getRequestHeaders, substituteParams } from '../../../../script.js';1import { getRequestHeaders, substituteParams } from '../../../../script.js';
2import { Popup, POPUP_RESULT, POPUP_TYPE } from '../../../popup.js';2import { Popup, POPUP_RESULT, POPUP_TYPE } from '../../../popup.js';
3import { executeSlashCommands, executeSlashCommandsOnChatInput, executeSlashCommandsWithOptions } from '../../../slash-commands.js';3import { executeSlashCommandsOnChatInput, executeSlashCommandsWithOptions } from '../../../slash-commands.js';
4import { SlashCommandParser } from '../../../slash-commands/SlashCommandParser.js';
5import { SlashCommandScope } from '../../../slash-commands/SlashCommandScope.js';4import { SlashCommandScope } from '../../../slash-commands/SlashCommandScope.js';
6import { debounceAsync, log, warn } from '../index.js';5import { SlashCommandParser } from '../../../slash-commands/SlashCommandParser.js';
6import { debounceAsync, warn } from '../index.js';
7import { QuickReply } from './QuickReply.js';7import { QuickReply } from './QuickReply.js';
88
9export class QuickReplySet {9export class QuickReplySet {
10 /**@type {QuickReplySet[]}*/ static list = [];10 /**@type {QuickReplySet[]}*/ static list = [];
1111
12
13 static from(props) {12 static from(props) {
14 props.qrList = []; //props.qrList?.map(it=>QuickReply.from(it));13 props.qrList = []; //props.qrList?.map(it=>QuickReply.from(it));
15 const instance = Object.assign(new this(), props);14 const instance = Object.assign(new this(), props);
@@ -24,9 +23,6 @@ export class QuickReplySet {
24 return this.list.find(it=>it.name == name);23 return this.list.find(it=>it.name == name);
25 }24 }
2625
27
28
29
30 /**@type {string}*/ name;26 /**@type {string}*/ name;
31 /**@type {boolean}*/ disableSend = false;27 /**@type {boolean}*/ disableSend = false;
32 /**@type {boolean}*/ placeBeforeInput = false;28 /**@type {boolean}*/ placeBeforeInput = false;
@@ -34,19 +30,12 @@ export class QuickReplySet {
34 /**@type {string}*/ color = 'transparent';30 /**@type {string}*/ color = 'transparent';
35 /**@type {boolean}*/ onlyBorderColor = false;31 /**@type {boolean}*/ onlyBorderColor = false;
36 /**@type {QuickReply[]}*/ qrList = [];32 /**@type {QuickReply[]}*/ qrList = [];
37
38 /**@type {number}*/ idIndex = 0;33 /**@type {number}*/ idIndex = 0;
39
40 /**@type {boolean}*/ isDeleted = false;34 /**@type {boolean}*/ isDeleted = false;
41
42 /**@type {function}*/ save;35 /**@type {function}*/ save;
43
44 /**@type {HTMLElement}*/ dom;36 /**@type {HTMLElement}*/ dom;
45 /**@type {HTMLElement}*/ settingsDom;37 /**@type {HTMLElement}*/ settingsDom;
4638
47
48
49
50 constructor() {39 constructor() {
51 this.save = debounceAsync(()=>this.performSave(), 200);40 this.save = debounceAsync(()=>this.performSave(), 200);
52 }41 }
@@ -55,9 +44,6 @@ export class QuickReplySet {
55 this.qrList.forEach(qr=>this.hookQuickReply(qr));44 this.qrList.forEach(qr=>this.hookQuickReply(qr));
56 }45 }
5746
58
59
60
61 unrender() {47 unrender() {
62 this.dom?.remove();48 this.dom?.remove();
63 this.dom = null;49 this.dom = null;
@@ -100,9 +86,6 @@ export class QuickReplySet {
100 }86 }
101 }87 }
10288
103
104
105
106 renderSettings() {89 renderSettings() {
107 if (!this.settingsDom) {90 if (!this.settingsDom) {
108 this.settingsDom = document.createElement('div'); {91 this.settingsDom = document.createElement('div'); {
@@ -123,9 +106,6 @@ export class QuickReplySet {
123 this.settingsDom.append(qr.renderSettings(idx));106 this.settingsDom.append(qr.renderSettings(idx));
124 }107 }
125108
126
127
128
129 /**109 /**
130 *110 *
131 * @param {QuickReply} qr111 * @param {QuickReply} qr
@@ -138,6 +118,7 @@ export class QuickReplySet {
138 closure.scope.setMacro('arg::*', '');118 closure.scope.setMacro('arg::*', '');
139 return (await closure.execute())?.pipe;119 return (await closure.execute())?.pipe;
140 }120 }
121
141 /**122 /**
142 *123 *
143 * @param {QuickReply} qr The QR to execute.124 * @param {QuickReply} qr The QR to execute.
@@ -207,6 +188,7 @@ export class QuickReplySet {
207 document.querySelector('#send_but').click();188 document.querySelector('#send_but').click();
208 }189 }
209 }190 }
191
210 /**192 /**
211 * @param {QuickReply} qr193 * @param {QuickReply} qr
212 * @param {string} [message] - optional altered message to be used194 * @param {string} [message] - optional altered message to be used
@@ -220,9 +202,6 @@ export class QuickReplySet {
220 });202 });
221 }203 }
222204
223
224
225
226 addQuickReply(data = {}) {205 addQuickReply(data = {}) {
227 const id = Math.max(this.idIndex, this.qrList.reduce((max,qr)=>Math.max(max,qr.id),0)) + 1;206 const id = Math.max(this.idIndex, this.qrList.reduce((max,qr)=>Math.max(max,qr.id),0)) + 1;
228 data.id =207 data.id =
@@ -239,6 +218,7 @@ export class QuickReplySet {
239 this.save();218 this.save();
240 return qr;219 return qr;
241 }220 }
221
242 addQuickReplyFromText(qrJson) {222 addQuickReplyFromText(qrJson) {
243 let data;223 let data;
244 if (qrJson) {224 if (qrJson) {
@@ -371,7 +351,6 @@ export class QuickReplySet {
371 this.save();351 this.save();
372 }352 }
373353
374
375 toJSON() {354 toJSON() {
376 return {355 return {
377 version: 2,356 version: 2,
@@ -386,7 +365,6 @@ export class QuickReplySet {
386 };365 };
387 }366 }
388367
389
390 async performSave() {368 async performSave() {
391 const response = await fetch('/api/quick-replies/save', {369 const response = await fetch('/api/quick-replies/save', {
392 method: 'POST',370 method: 'POST',
public/scripts/extensions/quick-reply/src/SlashCommandHandler.js+4 -0
@@ -883,6 +883,10 @@ export class SlashCommandHandler {
883 }883 }
884 }884 }
885 getQuickReply(args) {885 getQuickReply(args) {
886 if (!args.id && !args.label) {
887 toastr.error('Please provide a valid id or label.');
888 return '';
889 }
886 try {890 try {
887 return JSON.stringify(this.api.getQrByLabel(args.set, args.id !== undefined ? Number(args.id) : args.label));891 return JSON.stringify(this.api.getQrByLabel(args.set, args.id !== undefined ? Number(args.id) : args.label));
888 } catch (ex) {892 } catch (ex) {
public/scripts/extensions/quick-reply/src/ui/SettingsUi.js+1 -1
@@ -346,7 +346,7 @@ export class SettingsUi {
346 }346 }
347347
348 async addQrSet() {348 async addQrSet() {
349 const name = await Popup.show.input('Create a new World Info', 'Enter a name for the new Quick Reply Set:');349 const name = await Popup.show.input('Create a new Quick Reply Set', 'Enter a name for the new Quick Reply Set:');
350 if (name && name.length > 0) {350 if (name && name.length > 0) {
351 const oldQrs = QuickReplySet.get(name);351 const oldQrs = QuickReplySet.get(name);
352 if (oldQrs) {352 if (oldQrs) {
public/scripts/extensions/regex/editor.html+6 -0
@@ -94,6 +94,12 @@
94 <span data-i18n="World Info">World Info</span>94 <span data-i18n="World Info">World Info</span>
95 </label>95 </label>
96 </div>96 </div>
97 <div data-i18n="[title]ext_regex_reasoning_desc" title="Reasoning block contents. When 'Only Format Prompt' is checked, it will also affect the reasoning contents added to the prompt.">
98 <label class="checkbox flex-container">
99 <input type="checkbox" name="replace_position" value="6">
100 <span data-i18n="Reasoning">Reasoning</span>
101 </label>
102 </div>
97 <div class="flex-container wide100p marginTop5">103 <div class="flex-container wide100p marginTop5">
98 <div class="flex1 flex-container flexNoGap">104 <div class="flex1 flex-container flexNoGap">
99 <small data-i18n="[title]ext_regex_min_depth_desc" title="When applied to prompts or display, only affect messages that are at least N levels deep. 0 = last message, 1 = penultimate message, etc. Only counts WI entries @Depth and usable messages, i.e. not hidden or system.">105 <small data-i18n="[title]ext_regex_min_depth_desc" title="When applied to prompts or display, only affect messages that are at least N levels deep. 0 = last message, 1 = penultimate message, etc. Only counts WI entries @Depth and usable messages, i.e. not hidden or system.">
public/scripts/extensions/regex/engine.js+2 -1
@@ -20,6 +20,7 @@ const regex_placement = {
20 SLASH_COMMAND: 3,20 SLASH_COMMAND: 3,
21 // 4 - sendAs (legacy)21 // 4 - sendAs (legacy)
22 WORLD_INFO: 5,22 WORLD_INFO: 5,
23 REASONING: 6,
23};24};
2425
25export const substitute_find_regex = {26export const substitute_find_regex = {
@@ -94,7 +95,7 @@ function getRegexedString(rawString, placement, { characterOverride, isMarkdown,
94 // Script applies to Generate and input is Generate95 // Script applies to Generate and input is Generate
95 (script.promptOnly && isPrompt) ||96 (script.promptOnly && isPrompt) ||
96 // Script applies to all cases when neither "only"s are true, but there's no need to do it when `isMarkdown`, the as source (chat history) should already be changed beforehand97 // Script applies to all cases when neither "only"s are true, but there's no need to do it when `isMarkdown`, the as source (chat history) should already be changed beforehand
97 (!script.markdownOnly && !script.promptOnly && !isMarkdown)98 (!script.markdownOnly && !script.promptOnly && !isMarkdown && !isPrompt)
98 ) {99 ) {
99 if (isEdit && !script.runOnEdit) {100 if (isEdit && !script.runOnEdit) {
100 console.debug(`getRegexedString: Skipping script ${script.scriptName} because it does not run on edit`);101 console.debug(`getRegexedString: Skipping script ${script.scriptName} because it does not run on edit`);
public/scripts/extensions/regex/index.js+4 -3
@@ -10,6 +10,7 @@ import { SlashCommandParser } from '../../slash-commands/SlashCommandParser.js';
10import { download, getFileText, getSortableDelay, uuidv4 } from '../../utils.js';10import { download, getFileText, getSortableDelay, uuidv4 } from '../../utils.js';
11import { regex_placement, runRegexScript, substitute_find_regex } from './engine.js';11import { regex_placement, runRegexScript, substitute_find_regex } from './engine.js';
12import { t } from '../../i18n.js';12import { t } from '../../i18n.js';
13import { accountStorage } from '../../util/AccountStorage.js';
1314
14/**15/**
15 * @typedef {object} RegexScript16 * @typedef {object} RegexScript
@@ -18,7 +19,7 @@ import { t } from '../../i18n.js';
18 * @property {string} replaceString - The replace string19 * @property {string} replaceString - The replace string
19 * @property {string[]} trimStrings - The trim strings20 * @property {string[]} trimStrings - The trim strings
20 * @property {string?} findRegex - The find regex21 * @property {string?} findRegex - The find regex
21 * @property {string?} substituteRegex - The substitute regex22 * @property {number?} substituteRegex - The substitute regex
22 */23 */
2324
24/**25/**
@@ -440,8 +441,8 @@ async function checkEmbeddedRegexScripts() {
440 if (avatar && !extension_settings.character_allowed_regex.includes(avatar)) {441 if (avatar && !extension_settings.character_allowed_regex.includes(avatar)) {
441 const checkKey = `AlertRegex_${characters[chid].avatar}`;442 const checkKey = `AlertRegex_${characters[chid].avatar}`;
442443
443 if (!localStorage.getItem(checkKey)) {444 if (!accountStorage.getItem(checkKey)) {
444 localStorage.setItem(checkKey, 'true');445 accountStorage.setItem(checkKey, 'true');
445 const template = await renderExtensionTemplateAsync('regex', 'embeddedScripts', {});446 const template = await renderExtensionTemplateAsync('regex', 'embeddedScripts', {});
446 const result = await callGenericPopup(template, POPUP_TYPE.CONFIRM, '', { okButton: 'Yes' });447 const result = await callGenericPopup(template, POPUP_TYPE.CONFIRM, '', { okButton: 'Yes' });
447448
public/scripts/extensions/stable-diffusion/index.js+67 -0
@@ -81,6 +81,7 @@ const sources = {
81 huggingface: 'huggingface',81 huggingface: 'huggingface',
82 nanogpt: 'nanogpt',82 nanogpt: 'nanogpt',
83 bfl: 'bfl',83 bfl: 'bfl',
84 falai: 'falai',
84};85};
8586
86const initiators = {87const initiators = {
@@ -1169,6 +1170,10 @@ async function onBflKeyClick() {
1169 return onApiKeyClick('BFL API Key:', SECRET_KEYS.BFL);1170 return onApiKeyClick('BFL API Key:', SECRET_KEYS.BFL);
1170}1171}
11711172
1173async function onFalaiKeyClick() {
1174 return onApiKeyClick('FALAI API Key:', SECRET_KEYS.FALAI);
1175}
1176
1172function onBflUpsamplingInput() {1177function onBflUpsamplingInput() {
1173 extension_settings.sd.bfl_upsampling = !!$('#sd_bfl_upsampling').prop('checked');1178 extension_settings.sd.bfl_upsampling = !!$('#sd_bfl_upsampling').prop('checked');
1174 saveSettingsDebounced();1179 saveSettingsDebounced();
@@ -1299,6 +1304,7 @@ async function onModelChange() {
1299 sources.huggingface,1304 sources.huggingface,
1300 sources.nanogpt,1305 sources.nanogpt,
1301 sources.bfl,1306 sources.bfl,
1307 sources.falai,
1302 ];1308 ];
13031309
1304 if (cloudSources.includes(extension_settings.sd.source)) {1310 if (cloudSources.includes(extension_settings.sd.source)) {
@@ -1707,6 +1713,9 @@ async function loadModels() {
1707 case sources.bfl:1713 case sources.bfl:
1708 models = await loadBflModels();1714 models = await loadBflModels();
1709 break;1715 break;
1716 case sources.falai:
1717 models = await loadFalaiModels();
1718 break;
1710 }1719 }
17111720
1712 for (const model of models) {1721 for (const model of models) {
@@ -1744,6 +1753,21 @@ async function loadBflModels() {
1744 ];1753 ];
1745}1754}
17461755
1756async function loadFalaiModels() {
1757 $('#sd_falai_key').toggleClass('success', !!secret_state[SECRET_KEYS.FALAI]);
1758
1759 const result = await fetch('/api/sd/falai/models', {
1760 method: 'POST',
1761 headers: getRequestHeaders(),
1762 });
1763
1764 if (result.ok) {
1765 return await result.json();
1766 }
1767
1768 return [];
1769}
1770
1747async function loadPollinationsModels() {1771async function loadPollinationsModels() {
1748 const result = await fetch('/api/sd/pollinations/models', {1772 const result = await fetch('/api/sd/pollinations/models', {
1749 method: 'POST',1773 method: 'POST',
@@ -2081,6 +2105,9 @@ async function loadSchedulers() {
2081 case sources.bfl:2105 case sources.bfl:
2082 schedulers = ['N/A'];2106 schedulers = ['N/A'];
2083 break;2107 break;
2108 case sources.falai:
2109 schedulers = ['N/A'];
2110 break;
2084 }2111 }
20852112
2086 for (const scheduler of schedulers) {2113 for (const scheduler of schedulers) {
@@ -2735,6 +2762,9 @@ async function sendGenerationRequest(generationType, prompt, additionalNegativeP
2735 case sources.bfl:2762 case sources.bfl:
2736 result = await generateBflImage(prefixedPrompt, signal);2763 result = await generateBflImage(prefixedPrompt, signal);
2737 break;2764 break;
2765 case sources.falai:
2766 result = await generateFalaiImage(prefixedPrompt, negativePrompt, signal);
2767 break;
2738 }2768 }
27392769
2740 if (!result.data) {2770 if (!result.data) {
@@ -3496,6 +3526,40 @@ async function generateBflImage(prompt, signal) {
3496 }3526 }
3497}3527}
34983528
3529/**
3530 * Generates an image using the FAL.AI API.
3531 * @param {string} prompt - The main instruction used to guide the image generation.
3532 * @param {string} negativePrompt - The negative prompt used to guide the image generation.
3533 * @param {AbortSignal} signal - An AbortSignal object that can be used to cancel the request.
3534 * @returns {Promise<{format: string, data: string}>} - A promise that resolves when the image generation and processing are complete.
3535 */
3536async function generateFalaiImage(prompt, negativePrompt, signal) {
3537 const result = await fetch('/api/sd/falai/generate', {
3538 method: 'POST',
3539 headers: getRequestHeaders(),
3540 signal: signal,
3541 body: JSON.stringify({
3542 prompt: prompt,
3543 negative_prompt: negativePrompt,
3544 model: extension_settings.sd.model,
3545 steps: clamp(extension_settings.sd.steps, 1, 50),
3546 guidance: clamp(extension_settings.sd.scale, 1.5, 5),
3547 width: clamp(extension_settings.sd.width, 256, 1440),
3548 height: clamp(extension_settings.sd.height, 256, 1440),
3549 seed: extension_settings.sd.seed >= 0 ? extension_settings.sd.seed : undefined,
3550 }),
3551 });
3552
3553 if (result.ok) {
3554 const data = await result.json();
3555 return { format: 'jpg', data: data.image };
3556 } else {
3557 const text = await result.text();
3558 console.log(text);
3559 throw new Error(text);
3560 }
3561}
3562
3499async function onComfyOpenWorkflowEditorClick() {3563async function onComfyOpenWorkflowEditorClick() {
3500 let workflow = await (await fetch('/api/sd/comfy/workflow', {3564 let workflow = await (await fetch('/api/sd/comfy/workflow', {
3501 method: 'POST',3565 method: 'POST',
@@ -3782,6 +3846,8 @@ function isValidState() {
3782 return secret_state[SECRET_KEYS.NANOGPT];3846 return secret_state[SECRET_KEYS.NANOGPT];
3783 case sources.bfl:3847 case sources.bfl:
3784 return secret_state[SECRET_KEYS.BFL];3848 return secret_state[SECRET_KEYS.BFL];
3849 case sources.falai:
3850 return secret_state[SECRET_KEYS.FALAI];
3785 }3851 }
3786}3852}
37873853
@@ -4443,6 +4509,7 @@ jQuery(async () => {
4443 $('#sd_function_tool').on('input', onFunctionToolInput);4509 $('#sd_function_tool').on('input', onFunctionToolInput);
4444 $('#sd_bfl_key').on('click', onBflKeyClick);4510 $('#sd_bfl_key').on('click', onBflKeyClick);
4445 $('#sd_bfl_upsampling').on('input', onBflUpsamplingInput);4511 $('#sd_bfl_upsampling').on('input', onBflUpsamplingInput);
4512 $('#sd_falai_key').on('click', onFalaiKeyClick);
44464513
4447 if (!CSS.supports('field-sizing', 'content')) {4514 if (!CSS.supports('field-sizing', 'content')) {
4448 $('.sd_settings .inline-drawer-toggle').on('click', function () {4515 $('.sd_settings .inline-drawer-toggle').on('click', function () {
public/scripts/extensions/stable-diffusion/settings.html+16 -1
@@ -41,7 +41,8 @@
41 <option value="blockentropy">Block Entropy</option>41 <option value="blockentropy">Block Entropy</option>
42 <option value="comfy">ComfyUI</option>42 <option value="comfy">ComfyUI</option>
43 <option value="drawthings">DrawThings HTTP API</option>43 <option value="drawthings">DrawThings HTTP API</option>
44 <option value="extras">Extras API (local / remote)</option>44 <option value="extras">Extras API (deprecated)</option>
45 <option value="falai">FAL.AI</option>
45 <option value="huggingface">HuggingFace Inference API (serverless)</option>46 <option value="huggingface">HuggingFace Inference API (serverless)</option>
46 <option value="nanogpt">NanoGPT</option>47 <option value="nanogpt">NanoGPT</option>
47 <option value="novel">NovelAI Diffusion</option>48 <option value="novel">NovelAI Diffusion</option>
@@ -256,6 +257,20 @@
256 </label>257 </label>
257 </div>258 </div>
258259
260 <div data-sd-source="falai">
261 <div class="flex-container flexnowrap alignItemsBaseline marginBot5">
262 <a href="https://fal.ai/dashboard" target="_blank" rel="noopener noreferrer">
263 <strong data-i18n="API Key">API Key</strong>
264 <i class="fa-solid fa-share-from-square"></i>
265 </a>
266 <span class="expander"></span>
267 <div id="sd_falai_key" class="menu_button menu_button_icon">
268 <i class="fa-fw fa-solid fa-key"></i>
269 <span data-i18n="Click to set">Click to set</span>
270 </div>
271 </div>
272 </div>
273
259 <div class="flex-container">274 <div class="flex-container">
260 <div class="flex1">275 <div class="flex1">
261 <label for="sd_model" data-i18n="Model">Model</label>276 <label for="sd_model" data-i18n="Model">Model</label>
public/scripts/extensions/tts/index.js+48 -5
@@ -30,6 +30,7 @@ import { GoogleTranslateTtsProvider } from './google-translate.js';
30export { talkingAnimation };30export { talkingAnimation };
3131
32const UPDATE_INTERVAL = 1000;32const UPDATE_INTERVAL = 1000;
33const wrapper = new ModuleWorkerWrapper(moduleWorker);
3334
34let voiceMapEntries = [];35let voiceMapEntries = [];
35let voiceMap = {}; // {charName:voiceid, charName2:voiceid2}36let voiceMap = {}; // {charName:voiceid, charName2:voiceid2}
@@ -120,7 +121,7 @@ async function onNarrateOneMessage() {
120 }121 }
121122
122 resetTtsPlayback();123 resetTtsPlayback();
123 ttsJobQueue.push(message);124 processAndQueueTtsMessage(message);
124 moduleWorker();125 moduleWorker();
125}126}
126127
@@ -147,7 +148,7 @@ async function onNarrateText(args, text) {
147 }148 }
148149
149 resetTtsPlayback();150 resetTtsPlayback();
150 ttsJobQueue.push({ mes: text, name: name });151 processAndQueueTtsMessage({ mes: text, name: name });
151 await moduleWorker();152 await moduleWorker();
152153
153 // Return back to the chat voices154 // Return back to the chat voices
@@ -220,6 +221,36 @@ function isTtsProcessing() {
220 return processing;221 return processing;
221}222}
222223
224/**
225 * Splits a message into lines and adds each non-empty line to the TTS job queue.
226 * @param {Object} message - The message object to be processed.
227 * @param {string} message.mes - The text of the message to be split into lines.
228 * @param {string} message.name - The name associated with the message.
229 * @returns {void}
230 */
231function processAndQueueTtsMessage(message) {
232 if (!extension_settings.tts.narrate_by_paragraphs) {
233 ttsJobQueue.push(message);
234 return;
235 }
236
237 const lines = message.mes.split('\n');
238
239 for (let i = 0; i < lines.length; i++) {
240 const line = lines[i];
241
242 if (line.length === 0) {
243 continue;
244 }
245
246 ttsJobQueue.push(
247 Object.assign({}, message, {
248 mes: line,
249 }),
250 );
251 }
252}
253
223function debugTtsPlayback() {254function debugTtsPlayback() {
224 console.log(JSON.stringify(255 console.log(JSON.stringify(
225 {256 {
@@ -350,7 +381,7 @@ function onAudioControlClicked() {
350 talkingAnimation(false);381 talkingAnimation(false);
351 } else {382 } else {
352 // Default play behavior if not processing or playing is to play the last message.383 // Default play behavior if not processing or playing is to play the last message.
353 ttsJobQueue.push(context.chat[context.chat.length - 1]);384 processAndQueueTtsMessage(context.chat[context.chat.length - 1]);
354 }385 }
355 updateUiAudioPlayState();386 updateUiAudioPlayState();
356}387}
@@ -376,6 +407,7 @@ function completeCurrentAudioJob() {
376 currentAudioJob = null;407 currentAudioJob = null;
377 talkingAnimation(false); //stop lip animation408 talkingAnimation(false); //stop lip animation
378 // updateUiPlayState();409 // updateUiPlayState();
410 wrapper.update();
379}411}
380412
381/**413/**
@@ -466,7 +498,7 @@ async function processTtsQueue() {
466 }498 }
467499
468 if (extension_settings.tts.skip_tags) {500 if (extension_settings.tts.skip_tags) {
469 text = text.replace(/<.*?>.*?<\/.*?>/g, '').trim();501 text = text.replace(/<.*?>[\s\S]*?<\/.*?>/g, '').trim();
470 }502 }
471503
472 if (!extension_settings.tts.pass_asterisks) {504 if (!extension_settings.tts.pass_asterisks) {
@@ -569,6 +601,7 @@ function loadSettings() {
569 $('#tts_narrate_quoted').prop('checked', extension_settings.tts.narrate_quoted_only);601 $('#tts_narrate_quoted').prop('checked', extension_settings.tts.narrate_quoted_only);
570 $('#tts_auto_generation').prop('checked', extension_settings.tts.auto_generation);602 $('#tts_auto_generation').prop('checked', extension_settings.tts.auto_generation);
571 $('#tts_periodic_auto_generation').prop('checked', extension_settings.tts.periodic_auto_generation);603 $('#tts_periodic_auto_generation').prop('checked', extension_settings.tts.periodic_auto_generation);
604 $('#tts_narrate_by_paragraphs').prop('checked', extension_settings.tts.narrate_by_paragraphs);
572 $('#tts_narrate_translated_only').prop('checked', extension_settings.tts.narrate_translated_only);605 $('#tts_narrate_translated_only').prop('checked', extension_settings.tts.narrate_translated_only);
573 $('#tts_narrate_user').prop('checked', extension_settings.tts.narrate_user);606 $('#tts_narrate_user').prop('checked', extension_settings.tts.narrate_user);
574 $('#tts_pass_asterisks').prop('checked', extension_settings.tts.pass_asterisks);607 $('#tts_pass_asterisks').prop('checked', extension_settings.tts.pass_asterisks);
@@ -638,6 +671,11 @@ function onPeriodicAutoGenerationClick() {
638 saveSettingsDebounced();671 saveSettingsDebounced();
639}672}
640673
674function onNarrateByParagraphsClick() {
675 extension_settings.tts.narrate_by_paragraphs = !!$('#tts_narrate_by_paragraphs').prop('checked');
676 saveSettingsDebounced();
677}
678
641679
642function onNarrateDialoguesClick() {680function onNarrateDialoguesClick() {
643 extension_settings.tts.narrate_dialogues_only = !!$('#tts_narrate_dialogues').prop('checked');681 extension_settings.tts.narrate_dialogues_only = !!$('#tts_narrate_dialogues').prop('checked');
@@ -816,7 +854,12 @@ async function onMessageEvent(messageId, lastCharIndex) {
816 lastChatId = context.chatId;854 lastChatId = context.chatId;
817855
818 console.debug(`Adding message from ${message.name} for TTS processing: "${message.mes}"`);856 console.debug(`Adding message from ${message.name} for TTS processing: "${message.mes}"`);
857
858 if (extension_settings.tts.periodic_auto_generation) {
819 ttsJobQueue.push(message);859 ttsJobQueue.push(message);
860 } else {
861 processAndQueueTtsMessage(message);
862 }
820}863}
821864
822async function onMessageDeleted() {865async function onMessageDeleted() {
@@ -1156,6 +1199,7 @@ jQuery(async function () {
1156 $('#tts_pass_asterisks').on('click', onPassAsterisksClick);1199 $('#tts_pass_asterisks').on('click', onPassAsterisksClick);
1157 $('#tts_auto_generation').on('click', onAutoGenerationClick);1200 $('#tts_auto_generation').on('click', onAutoGenerationClick);
1158 $('#tts_periodic_auto_generation').on('click', onPeriodicAutoGenerationClick);1201 $('#tts_periodic_auto_generation').on('click', onPeriodicAutoGenerationClick);
1202 $('#tts_narrate_by_paragraphs').on('click', onNarrateByParagraphsClick);
1159 $('#tts_narrate_user').on('click', onNarrateUserClick);1203 $('#tts_narrate_user').on('click', onNarrateUserClick);
11601204
1161 $('#playback_rate').on('input', function () {1205 $('#playback_rate').on('input', function () {
@@ -1177,7 +1221,6 @@ jQuery(async function () {
1177 loadSettings(); // Depends on Extension Controls and loadTtsProvider1221 loadSettings(); // Depends on Extension Controls and loadTtsProvider
1178 loadTtsProvider(extension_settings.tts.currentProvider); // No dependencies1222 loadTtsProvider(extension_settings.tts.currentProvider); // No dependencies
1179 addAudioControl(); // Depends on Extension Controls1223 addAudioControl(); // Depends on Extension Controls
1180 const wrapper = new ModuleWorkerWrapper(moduleWorker);
1181 setInterval(wrapper.update.bind(wrapper), UPDATE_INTERVAL); // Init depends on all the things1224 setInterval(wrapper.update.bind(wrapper), UPDATE_INTERVAL); // Init depends on all the things
1182 eventSource.on(event_types.MESSAGE_SWIPED, resetTtsPlayback);1225 eventSource.on(event_types.MESSAGE_SWIPED, resetTtsPlayback);
1183 eventSource.on(event_types.CHAT_CHANGED, onChatChanged);1226 eventSource.on(event_types.CHAT_CHANGED, onChatChanged);
public/scripts/extensions/tts/settings.html+4 -0
@@ -30,6 +30,10 @@
30 <input type="checkbox" id="tts_periodic_auto_generation">30 <input type="checkbox" id="tts_periodic_auto_generation">
31 <small data-i18n="Narrate by paragraphs (when streaming)">Narrate by paragraphs (when streaming)</small>31 <small data-i18n="Narrate by paragraphs (when streaming)">Narrate by paragraphs (when streaming)</small>
32 </label>32 </label>
33 <label class="checkbox_label" for="tts_narrate_by_paragraphs">
34 <input type="checkbox" id="tts_narrate_by_paragraphs">
35 <small data-i18n="Narrate by paragraphs (when not streaming)">Narrate by paragraphs (when not streaming)</small>
36 </label>
33 <label class="checkbox_label" for="tts_narrate_quoted">37 <label class="checkbox_label" for="tts_narrate_quoted">
34 <input type="checkbox" id="tts_narrate_quoted">38 <input type="checkbox" id="tts_narrate_quoted">
35 <small data-i18n="Only narrate quotes">Only narrate "quotes"</small>39 <small data-i18n="Only narrate quotes">Only narrate "quotes"</small>
public/scripts/extensions/vectors/index.js+2 -4
@@ -1638,14 +1638,12 @@ jQuery(async () => {
1638 }1638 }
1639 return textResult;1639 return textResult;
1640 };1640 };
1641
1642 if (args.return === 'chunks') {1641 if (args.return === 'chunks') {
1643 return getChunksText();1642 return getChunksText();
1644 }1643 }
16451644
1646 // @ts-ignore1645 // @ts-ignore
1647 return slashCommandReturnHelper.doReturn(args.return ?? 'object', urls, { objectToStringFunc: list => list.join('\n') });1646 return slashCommandReturnHelper.doReturn(args.return ?? 'object', urls, { objectToStringFunc: list => list.join('\n') });
1648
1649 },1647 },
1650 aliases: ['databank-search', 'data-bank-search'],1648 aliases: ['databank-search', 'data-bank-search'],
1651 helpString: 'Search the Data Bank for a specific query using vector similarity. Returns a list of file URLs with the most relevant content.',1649 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 +1658,10 @@ jQuery(async () => {
1660 defaultValue: 'object',1658 defaultValue: 'object',
1661 enumList: [1659 enumList: [
1662 new SlashCommandEnumValue('chunks', 'Return the actual content chunks', enumTypes.enum, '{}'),1660 new SlashCommandEnumValue('chunks', 'Return the actual content chunks', enumTypes.enum, '{}'),
1663 ...slashCommandReturnHelper.enumList({ allowObject: true })1661 ...slashCommandReturnHelper.enumList({ allowObject: true }),
1664 ],1662 ],
1665 forceEnum: true,1663 forceEnum: true,
1666 })1664 }),
1667 ],1665 ],
1668 unnamedArgumentList: [1666 unnamedArgumentList: [
1669 new SlashCommandArgument('Query to search by.', ARGUMENT_TYPE.STRING, true, false),1667 new SlashCommandArgument('Query to search by.', ARGUMENT_TYPE.STRING, true, false),
public/scripts/extensions/vectors/settings.html+1 -1
@@ -11,7 +11,7 @@
11 </label>11 </label>
12 <select id="vectors_source" class="text_pole">12 <select id="vectors_source" class="text_pole">
13 <option value="cohere">Cohere</option>13 <option value="cohere">Cohere</option>
14 <option value="extras">Extras</option>14 <option value="extras">Extras (deprecated)</option>
15 <option value="palm">Google AI Studio</option>15 <option value="palm">Google AI Studio</option>
16 <option value="llamacpp">llama.cpp</option>16 <option value="llamacpp">llama.cpp</option>
17 <option value="transformers" data-i18n="Local (Transformers)">Local (Transformers)</option>17 <option value="transformers" data-i18n="Local (Transformers)">Local (Transformers)</option>
public/scripts/f-localStorage.js+15 -0
@@ -1,18 +1,30 @@
1////////////////// LOCAL STORAGE HANDLING /////////////////////1////////////////// LOCAL STORAGE HANDLING /////////////////////
22
3/**
4 * @deprecated THIS FUNCTION IS OBSOLETE. DO NOT USE
5 */
3export function SaveLocal(target, val) {6export function SaveLocal(target, val) {
4 localStorage.setItem(target, val);7 localStorage.setItem(target, val);
5 console.debug('SaveLocal -- ' + target + ' : ' + val);8 console.debug('SaveLocal -- ' + target + ' : ' + val);
6}9}
10/**
11 * @deprecated THIS FUNCTION IS OBSOLETE. DO NOT USE
12 */
7export function LoadLocal(target) {13export function LoadLocal(target) {
8 console.debug('LoadLocal -- ' + target);14 console.debug('LoadLocal -- ' + target);
9 return localStorage.getItem(target);15 return localStorage.getItem(target);
1016
11}17}
18/**
19 * @deprecated THIS FUNCTION IS OBSOLETE. DO NOT USE
20 */
12export function LoadLocalBool(target) {21export function LoadLocalBool(target) {
13 let result = localStorage.getItem(target) === 'true';22 let result = localStorage.getItem(target) === 'true';
14 return result;23 return result;
15}24}
25/**
26 * @deprecated THIS FUNCTION IS OBSOLETE. DO NOT USE
27 */
16export function CheckLocal() {28export function CheckLocal() {
17 console.log('----------local storage---------');29 console.log('----------local storage---------');
18 var i;30 var i;
@@ -22,6 +34,9 @@ export function CheckLocal() {
22 console.log('------------------------------');34 console.log('------------------------------');
23}35}
2436
37/**
38 * @deprecated THIS FUNCTION IS OBSOLETE. DO NOT USE
39 */
25export function ClearLocal() { localStorage.clear(); console.log('Removed All Local Storage'); }40export function ClearLocal() { localStorage.clear(); console.log('Removed All Local Storage'); }
2641
27/////////////////////////////////////////////////////////////////////////42/////////////////////////////////////////////////////////////////////////
public/scripts/group-chats.js+42 -37
@@ -78,9 +78,11 @@ import { FILTER_TYPES, FilterHelper } from './filters.js';
78import { isExternalMediaAllowed } from './chats.js';78import { isExternalMediaAllowed } from './chats.js';
79import { POPUP_TYPE, Popup, callGenericPopup } from './popup.js';79import { POPUP_TYPE, Popup, callGenericPopup } from './popup.js';
80import { t } from './i18n.js';80import { t } from './i18n.js';
81import { accountStorage } from './util/AccountStorage.js';
8182
82export {83export {
83 selected_group,84 selected_group,
85 openGroupId,
84 is_group_automode_enabled,86 is_group_automode_enabled,
85 hideMutedSprites,87 hideMutedSprites,
86 is_group_generating,88 is_group_generating,
@@ -291,10 +293,11 @@ export function getGroupNames() {
291293
292/**294/**
293 * Finds the character ID for a group member.295 * Finds the character ID for a group member.
294 * @param {string} arg 0-based member index or character name296 * @param {number|string} arg 0-based member index or character name
295 * @returns {number} 0-based character ID297 * @param {Boolean} full Whether to return a key-value object containing extra data
298 * @returns {number|Object} 0-based character ID or key-value object if full is true
296 */299 */
297export function findGroupMemberId(arg) {300export function findGroupMemberId(arg, full = false) {
298 arg = arg?.trim();301 arg = arg?.trim();
299302
300 if (!arg) {303 if (!arg) {
@@ -310,15 +313,19 @@ export function findGroupMemberId(arg) {
310 }313 }
311314
312 const index = parseInt(arg);315 const index = parseInt(arg);
313 const searchByName = isNaN(index);316 const searchByString = isNaN(index);
314317
315 if (searchByName) {318 if (searchByString) {
316 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 => ({
317 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'] });
318 const result = fuse.search(arg);325 const result = fuse.search(arg);
319326
320 if (!result.length) {327 if (!result.length) {
321 console.warn(`WARN: No group member found with name ${arg}`);328 console.warn(`WARN: No group member found using string ${arg}`);
322 return;329 return;
323 }330 }
324331
@@ -329,9 +336,11 @@ export function findGroupMemberId(arg) {
329 return;336 return;
330 }337 }
331338
332 console.log(`Triggering group member ${chid} (${arg}) from search result`, result[0]);339 console.log(`Targeting group member ${chid} (${arg}) from search result`, result[0]);
333 return chid;340
334 } else {341 return !full ? chid : { ...{ id: chid }, ...result[0].item };
342 }
343 else {
335 const memberAvatar = group.members[index];344 const memberAvatar = group.members[index];
336345
337 if (memberAvatar === undefined) {346 if (memberAvatar === undefined) {
@@ -346,8 +355,14 @@ export function findGroupMemberId(arg) {
346 return;355 return;
347 }356 }
348357
349 console.log(`Triggering group member ${memberAvatar} at index ${index}`);358 console.log(`Targeting group member ${memberAvatar} at index ${index}`);
350 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 };
351 }366 }
352}367}
353368
@@ -804,7 +819,6 @@ async function generateGroupWrapper(by_auto_mode, type = null, params = {}) {
804819
805 /** @type {any} Caution: JS war crimes ahead */820 /** @type {any} Caution: JS war crimes ahead */
806 let textResult = '';821 let textResult = '';
807 let typingIndicator = $('#chat .typing_indicator');
808 const group = groups.find((x) => x.id === selected_group);822 const group = groups.find((x) => x.id === selected_group);
809823
810 if (!group || !Array.isArray(group.members) || !group.members.length) {824 if (!group || !Array.isArray(group.members) || !group.members.length) {
@@ -820,14 +834,6 @@ async function generateGroupWrapper(by_auto_mode, type = null, params = {}) {
820 setCharacterId(undefined);834 setCharacterId(undefined);
821 const userInput = String($('#send_textarea').val());835 const userInput = String($('#send_textarea').val());
822836
823 if (typingIndicator.length === 0 && !isStreamingEnabled()) {
824 typingIndicator = $(
825 '#typing_indicator_template .typing_indicator',
826 ).clone();
827 typingIndicator.hide();
828 $('#chat').append(typingIndicator);
829 }
830
831 // id of this specific batch for regeneration purposes837 // id of this specific batch for regeneration purposes
832 group_generation_id = Date.now();838 group_generation_id = Date.now();
833 const lastMessage = chat[chat.length - 1];839 const lastMessage = chat[chat.length - 1];
@@ -905,14 +911,6 @@ async function generateGroupWrapper(by_auto_mode, type = null, params = {}) {
905 }911 }
906 await eventSource.emit(event_types.GROUP_MEMBER_DRAFTED, chId);912 await eventSource.emit(event_types.GROUP_MEMBER_DRAFTED, chId);
907913
908 if (type !== 'swipe' && type !== 'impersonate' && !isStreamingEnabled()) {
909 // update indicator and scroll down
910 typingIndicator
911 .find('.typing_indicator_name')
912 .text(characters[chId].name);
913 typingIndicator.show();
914 }
915
916 // Wait for generation to finish914 // Wait for generation to finish
917 textResult = await Generate(generateType, { automatic_trigger: by_auto_mode, ...(params || {}) });915 textResult = await Generate(generateType, { automatic_trigger: by_auto_mode, ...(params || {}) });
918 let messageChunk = textResult?.messageChunk;916 let messageChunk = textResult?.messageChunk;
@@ -929,8 +927,6 @@ async function generateGroupWrapper(by_auto_mode, type = null, params = {}) {
929 }927 }
930 }928 }
931 } finally {929 } finally {
932 typingIndicator.hide();
933
934 is_group_generating = false;930 is_group_generating = false;
935 setSendButtonState(false);931 setSendButtonState(false);
936 setCharacterId(undefined);932 setCharacterId(undefined);
@@ -1314,10 +1310,10 @@ function printGroupCandidates() {
1314 formatNavigator: PAGINATION_TEMPLATE,1310 formatNavigator: PAGINATION_TEMPLATE,
1315 showNavigator: true,1311 showNavigator: true,
1316 showSizeChanger: true,1312 showSizeChanger: true,
1317 pageSize: Number(localStorage.getItem(storageKey)) || 5,1313 pageSize: Number(accountStorage.getItem(storageKey)) || 5,
1318 sizeChangerOptions: [5, 10, 25, 50, 100, 200, 500, 1000],1314 sizeChangerOptions: [5, 10, 25, 50, 100, 200, 500, 1000],
1319 afterSizeSelectorChange: function (e) {1315 afterSizeSelectorChange: function (e) {
1320 localStorage.setItem(storageKey, e.target.value);1316 accountStorage.setItem(storageKey, e.target.value);
1321 },1317 },
1322 callback: function (data) {1318 callback: function (data) {
1323 $('#rm_group_add_members').empty();1319 $('#rm_group_add_members').empty();
@@ -1341,10 +1337,10 @@ function printGroupMembers() {
1341 formatNavigator: PAGINATION_TEMPLATE,1337 formatNavigator: PAGINATION_TEMPLATE,
1342 showNavigator: true,1338 showNavigator: true,
1343 showSizeChanger: true,1339 showSizeChanger: true,
1344 pageSize: Number(localStorage.getItem(storageKey)) || 5,1340 pageSize: Number(accountStorage.getItem(storageKey)) || 5,
1345 sizeChangerOptions: [5, 10, 25, 50, 100, 200, 500, 1000],1341 sizeChangerOptions: [5, 10, 25, 50, 100, 200, 500, 1000],
1346 afterSizeSelectorChange: function (e) {1342 afterSizeSelectorChange: function (e) {
1347 localStorage.setItem(storageKey, e.target.value);1343 accountStorage.setItem(storageKey, e.target.value);
1348 },1344 },
1349 callback: function (data) {1345 callback: function (data) {
1350 $('.rm_group_members').empty();1346 $('.rm_group_members').empty();
@@ -1367,6 +1363,15 @@ function getGroupCharacterBlock(character) {
1367 template.find('.ch_fav').val(isFav);1363 template.find('.ch_fav').val(isFav);
1368 template.toggleClass('is_fav', isFav);1364 template.toggleClass('is_fav', isFav);
13691365
1366 const auxFieldName = power_user.aux_field || 'character_version';
1367 const auxFieldValue = (character.data && character.data[auxFieldName]) || '';
1368 if (auxFieldValue) {
1369 template.find('.character_version').text(auxFieldValue);
1370 }
1371 else {
1372 template.find('.character_version').hide();
1373 }
1374
1370 let queuePosition = groupChatQueueOrder.get(character.avatar);1375 let queuePosition = groupChatQueueOrder.get(character.avatar);
1371 if (queuePosition) {1376 if (queuePosition) {
1372 template.find('.queue_position').text(queuePosition);1377 template.find('.queue_position').text(queuePosition);
public/scripts/kai-settings.js+1 -1
@@ -188,7 +188,7 @@ export async function generateKoboldWithStreaming(generate_data, signal) {
188 if (data?.token) {188 if (data?.token) {
189 text += data.token;189 text += data.token;
190 }190 }
191 yield { text, swipes: [], toolCalls: [] };191 yield { text, swipes: [], toolCalls: [], state: {} };
192 }192 }
193 };193 };
194}194}
public/scripts/loader.js+27 -6
@@ -27,21 +27,42 @@ export async function hideLoader() {
27 }27 }
2828
29 return new Promise((resolve) => {29 return new Promise((resolve) => {
30 // Spinner blurs/fades out30 const spinner = $('#load-spinner');
31 $('#load-spinner').on('transitionend webkitTransitionEnd oTransitionEnd MSTransitionEnd', function () {31 if (!spinner.length) {
32 console.warn('Spinner element not found, skipping animation');
33 cleanup();
34 return;
35 }
36
37 // Check if transitions are enabled
38 const transitionDuration = spinner[0] ? getComputedStyle(spinner[0]).transitionDuration : '0s';
39 const hasTransitions = parseFloat(transitionDuration) > 0;
40
41 if (hasTransitions) {
42 Promise.race([
43 new Promise((r) => setTimeout(r, 500)), // Fallback timeout
44 new Promise((r) => spinner.one('transitionend webkitTransitionEnd oTransitionEnd MSTransitionEnd', r)),
45 ]).finally(cleanup);
46 } else {
47 cleanup();
48 }
49
50 function cleanup() {
32 $('#loader').remove();51 $('#loader').remove();
33 // Yoink preloader entirely; it only exists to cover up unstyled content while loading JS52 // Yoink preloader entirely; it only exists to cover up unstyled content while loading JS
34 // If it's present, we remove it once and then it's gone.53 // If it's present, we remove it once and then it's gone.
35 yoinkPreloader();54 yoinkPreloader();
3655
37 loaderPopup.complete(POPUP_RESULT.AFFIRMATIVE).then(() => {56 loaderPopup.complete(POPUP_RESULT.AFFIRMATIVE)
57 .catch((err) => console.error('Error completing loaderPopup:', err))
58 .finally(() => {
38 loaderPopup = null;59 loaderPopup = null;
39 resolve();60 resolve();
40 });61 });
41 });62 }
4263
43 $('#load-spinner')64 // Apply the styles
44 .css({65 spinner.css({
45 'filter': 'blur(15px)',66 'filter': 'blur(15px)',
46 'opacity': '0',67 'opacity': '0',
47 });68 });
public/scripts/nai-settings.js+1 -1
@@ -746,7 +746,7 @@ export async function generateNovelWithStreaming(generate_data, signal) {
746 text += data.token;746 text += data.token;
747 }747 }
748748
749 yield { text, swipes: [], logprobs: parseNovelAILogprobs(data.logprobs), toolCalls: [] };749 yield { text, swipes: [], logprobs: parseNovelAILogprobs(data.logprobs), toolCalls: [], state: {} };
750 }750 }
751 };751 };
752}752}
public/scripts/openai.js+113 -64
@@ -73,6 +73,7 @@ import { SlashCommandEnumValue } from './slash-commands/SlashCommandEnumValue.js
73import { Popup, POPUP_RESULT } from './popup.js';73import { Popup, POPUP_RESULT } from './popup.js';
74import { t } from './i18n.js';74import { t } from './i18n.js';
75import { ToolManager } from './tool-calling.js';75import { ToolManager } from './tool-calling.js';
76import { accountStorage } from './util/AccountStorage.js';
7677
77export {78export {
78 openai_messages_count,79 openai_messages_count,
@@ -82,7 +83,6 @@ export {
82 setOpenAIMessageExamples,83 setOpenAIMessageExamples,
83 setupChatCompletionPromptManager,84 setupChatCompletionPromptManager,
84 sendOpenAIRequest,85 sendOpenAIRequest,
85 getChatCompletionModel,
86 TokenHandler,86 TokenHandler,
87 IdentifierNotFoundError,87 IdentifierNotFoundError,
88 Message,88 Message,
@@ -258,8 +258,8 @@ const default_settings = {
258 ai21_model: 'jamba-1.5-large',258 ai21_model: 'jamba-1.5-large',
259 mistralai_model: 'mistral-large-latest',259 mistralai_model: 'mistral-large-latest',
260 cohere_model: 'command-r-plus',260 cohere_model: 'command-r-plus',
261 perplexity_model: 'llama-3.1-70b-instruct',261 perplexity_model: 'sonar-pro',
262 groq_model: 'llama-3.1-70b-versatile',262 groq_model: 'llama-3.3-70b-versatile',
263 nanogpt_model: 'gpt-4o-mini',263 nanogpt_model: 'gpt-4o-mini',
264 zerooneai_model: 'yi-large',264 zerooneai_model: 'yi-large',
265 blockentropy_model: 'be-70b-base-llama3.1',265 blockentropy_model: 'be-70b-base-llama3.1',
@@ -298,7 +298,8 @@ const default_settings = {
298 names_behavior: character_names_behavior.DEFAULT,298 names_behavior: character_names_behavior.DEFAULT,
299 continue_postfix: continue_postfix_types.SPACE,299 continue_postfix: continue_postfix_types.SPACE,
300 custom_prompt_post_processing: custom_prompt_post_processing_types.NONE,300 custom_prompt_post_processing: custom_prompt_post_processing_types.NONE,
301 show_thoughts: false,301 show_thoughts: true,
302 reasoning_effort: 'medium',
302 seed: -1,303 seed: -1,
303 n: 1,304 n: 1,
304};305};
@@ -337,7 +338,7 @@ const oai_settings = {
337 ai21_model: 'jamba-1.5-large',338 ai21_model: 'jamba-1.5-large',
338 mistralai_model: 'mistral-large-latest',339 mistralai_model: 'mistral-large-latest',
339 cohere_model: 'command-r-plus',340 cohere_model: 'command-r-plus',
340 perplexity_model: 'llama-3.1-70b-instruct',341 perplexity_model: 'sonar-pro',
341 groq_model: 'llama-3.1-70b-versatile',342 groq_model: 'llama-3.1-70b-versatile',
342 nanogpt_model: 'gpt-4o-mini',343 nanogpt_model: 'gpt-4o-mini',
343 zerooneai_model: 'yi-large',344 zerooneai_model: 'yi-large',
@@ -377,7 +378,8 @@ const oai_settings = {
377 names_behavior: character_names_behavior.DEFAULT,378 names_behavior: character_names_behavior.DEFAULT,
378 continue_postfix: continue_postfix_types.SPACE,379 continue_postfix: continue_postfix_types.SPACE,
379 custom_prompt_post_processing: custom_prompt_post_processing_types.NONE,380 custom_prompt_post_processing: custom_prompt_post_processing_types.NONE,
380 show_thoughts: false,381 show_thoughts: true,
382 reasoning_effort: 'medium',
381 seed: -1,383 seed: -1,
382 n: 1,384 n: 1,
383};385};
@@ -412,7 +414,7 @@ async function validateReverseProxy() {
412 throw err;414 throw err;
413 }415 }
414 const rememberKey = `Proxy_SkipConfirm_${getStringHash(oai_settings.reverse_proxy)}`;416 const rememberKey = `Proxy_SkipConfirm_${getStringHash(oai_settings.reverse_proxy)}`;
415 const skipConfirm = localStorage.getItem(rememberKey) === 'true';417 const skipConfirm = accountStorage.getItem(rememberKey) === 'true';
416418
417 const confirmation = skipConfirm || await Popup.show.confirm(t`Connecting To Proxy`, await renderTemplateAsync('proxyConnectionWarning', { proxyURL: DOMPurify.sanitize(oai_settings.reverse_proxy) }));419 const confirmation = skipConfirm || await Popup.show.confirm(t`Connecting To Proxy`, await renderTemplateAsync('proxyConnectionWarning', { proxyURL: DOMPurify.sanitize(oai_settings.reverse_proxy) }));
418420
@@ -423,7 +425,7 @@ async function validateReverseProxy() {
423 throw new Error('Proxy connection denied.');425 throw new Error('Proxy connection denied.');
424 }426 }
425427
426 localStorage.setItem(rememberKey, String(true));428 accountStorage.setItem(rememberKey, String(true));
427}429}
428430
429/**431/**
@@ -1096,8 +1098,8 @@ async function preparePromptsForChatCompletion({ Scenario, charPersonality, name
1096 // Unordered prompts without marker1098 // Unordered prompts without marker
1097 { role: 'system', content: impersonationPrompt, identifier: 'impersonate' },1099 { role: 'system', content: impersonationPrompt, identifier: 'impersonate' },
1098 { role: 'system', content: quietPrompt, identifier: 'quietPrompt' },1100 { role: 'system', content: quietPrompt, identifier: 'quietPrompt' },
1099 { role: 'system', content: bias, identifier: 'bias' },
1100 { role: 'system', content: groupNudge, identifier: 'groupNudge' },1101 { role: 'system', content: groupNudge, identifier: 'groupNudge' },
1102 { role: 'assistant', content: bias, identifier: 'bias' },
1101 ];1103 ];
11021104
1103 // Tavern Extras - Summary1105 // Tavern Extras - Summary
@@ -1443,9 +1445,7 @@ async function sendWindowAIRequest(messages, signal, stream) {
1443 }1445 }
14441446
1445 const onStreamResult = (res, err) => {1447 const onStreamResult = (res, err) => {
1446 if (err) {1448 if (err) return;
1447 return;
1448 }
14491449
1450 const thisContent = res?.message?.content;1450 const thisContent = res?.message?.content;
14511451
@@ -1497,7 +1497,7 @@ async function sendWindowAIRequest(messages, signal, stream) {
1497 }1497 }
1498}1498}
14991499
1500function getChatCompletionModel() {1500export function getChatCompletionModel() {
1501 switch (oai_settings.chat_completion_source) {1501 switch (oai_settings.chat_completion_source) {
1502 case chat_completion_sources.CLAUDE:1502 case chat_completion_sources.CLAUDE:
1503 return oai_settings.claude_model;1503 return oai_settings.claude_model;
@@ -1869,7 +1869,7 @@ async function sendOpenAIRequest(type, messages, signal) {
1869 const isQuiet = type === 'quiet';1869 const isQuiet = type === 'quiet';
1870 const isImpersonate = type === 'impersonate';1870 const isImpersonate = type === 'impersonate';
1871 const isContinue = type === 'continue';1871 const isContinue = type === 'continue';
1872 const stream = oai_settings.stream_openai && !isQuiet && !isScale && !(isGoogle && oai_settings.google_model.includes('bison')) && !(isOAI && oai_settings.openai_model.startsWith('o1-'));1872 const stream = oai_settings.stream_openai && !isQuiet && !isScale && !(isOAI && ['o1-2024-12-17', 'o1'].includes(oai_settings.openai_model));
1873 const useLogprobs = !!power_user.request_token_probabilities;1873 const useLogprobs = !!power_user.request_token_probabilities;
1874 const canMultiSwipe = oai_settings.n > 1 && !isContinue && !isImpersonate && !isQuiet && (isOAI || isCustom);1874 const canMultiSwipe = oai_settings.n > 1 && !isContinue && !isImpersonate && !isQuiet && (isOAI || isCustom);
18751875
@@ -1913,16 +1913,21 @@ async function sendOpenAIRequest(type, messages, signal) {
1913 'user_name': name1,1913 'user_name': name1,
1914 'char_name': name2,1914 'char_name': name2,
1915 'group_names': getGroupNames(),1915 'group_names': getGroupNames(),
1916 'show_thoughts': Boolean(oai_settings.show_thoughts),1916 'include_reasoning': Boolean(oai_settings.show_thoughts),
1917 'reasoning_effort': String(oai_settings.reasoning_effort),
1917 };1918 };
19181919
1920 if (!canMultiSwipe && ToolManager.canPerformToolCalls(type)) {
1921 await ToolManager.registerFunctionToolsOpenAI(generate_data);
1922 }
1923
1919 // Empty array will produce a validation error1924 // Empty array will produce a validation error
1920 if (!Array.isArray(generate_data.stop) || !generate_data.stop.length) {1925 if (!Array.isArray(generate_data.stop) || !generate_data.stop.length) {
1921 delete generate_data.stop;1926 delete generate_data.stop;
1922 }1927 }
19231928
1924 // Proxy is only supported for Claude, OpenAI, Mistral, and Google MakerSuite1929 // Proxy is only supported for Claude, OpenAI, Mistral, and Google MakerSuite
1925 if (oai_settings.reverse_proxy && [chat_completion_sources.CLAUDE, chat_completion_sources.OPENAI, chat_completion_sources.MISTRALAI, chat_completion_sources.MAKERSUITE].includes(oai_settings.chat_completion_source)) {1930 if (oai_settings.reverse_proxy && [chat_completion_sources.CLAUDE, chat_completion_sources.OPENAI, chat_completion_sources.MISTRALAI, chat_completion_sources.MAKERSUITE, chat_completion_sources.DEEPSEEK].includes(oai_settings.chat_completion_source)) {
1926 await validateReverseProxy();1931 await validateReverseProxy();
1927 generate_data['reverse_proxy'] = oai_settings.reverse_proxy;1932 generate_data['reverse_proxy'] = oai_settings.reverse_proxy;
1928 generate_data['proxy_password'] = oai_settings.proxy_password;1933 generate_data['proxy_password'] = oai_settings.proxy_password;
@@ -2030,17 +2035,25 @@ async function sendOpenAIRequest(type, messages, signal) {
2030 // https://api-docs.deepseek.com/api/create-chat-completion2035 // https://api-docs.deepseek.com/api/create-chat-completion
2031 if (isDeepSeek) {2036 if (isDeepSeek) {
2032 generate_data.top_p = generate_data.top_p || Number.EPSILON;2037 generate_data.top_p = generate_data.top_p || Number.EPSILON;
2038
2039 if (generate_data.model.endsWith('-reasoner')) {
2040 delete generate_data.top_p;
2041 delete generate_data.temperature;
2042 delete generate_data.frequency_penalty;
2043 delete generate_data.presence_penalty;
2044 delete generate_data.top_logprobs;
2045 delete generate_data.logprobs;
2046 delete generate_data.logit_bias;
2047 delete generate_data.tools;
2048 delete generate_data.tool_choice;
2049 }
2033 }2050 }
20342051
2035 if ((isOAI || isOpenRouter || isMistral || isCustom || isCohere || isNano) && oai_settings.seed >= 0) {2052 if ((isOAI || isOpenRouter || isMistral || isCustom || isCohere || isNano) && oai_settings.seed >= 0) {
2036 generate_data['seed'] = oai_settings.seed;2053 generate_data['seed'] = oai_settings.seed;
2037 }2054 }
20382055
2039 if (!canMultiSwipe && ToolManager.canPerformToolCalls(type)) {2056 if (isOAI && (oai_settings.openai_model.startsWith('o1') || oai_settings.openai_model.startsWith('o3'))) {
2040 await ToolManager.registerFunctionToolsOpenAI(generate_data);
2041 }
2042
2043 if (isOAI && oai_settings.openai_model.startsWith('o1-')) {
2044 generate_data.messages.forEach((msg) => {2057 generate_data.messages.forEach((msg) => {
2045 if (msg.role === 'system') {2058 if (msg.role === 'system') {
2046 msg.role = 'user';2059 msg.role = 'user';
@@ -2048,7 +2061,6 @@ async function sendOpenAIRequest(type, messages, signal) {
2048 });2061 });
2049 generate_data.max_completion_tokens = generate_data.max_tokens;2062 generate_data.max_completion_tokens = generate_data.max_tokens;
2050 delete generate_data.max_tokens;2063 delete generate_data.max_tokens;
2051 delete generate_data.stream;
2052 delete generate_data.logprobs;2064 delete generate_data.logprobs;
2053 delete generate_data.top_logprobs;2065 delete generate_data.top_logprobs;
2054 delete generate_data.n;2066 delete generate_data.n;
@@ -2059,8 +2071,7 @@ async function sendOpenAIRequest(type, messages, signal) {
2059 delete generate_data.tools;2071 delete generate_data.tools;
2060 delete generate_data.tool_choice;2072 delete generate_data.tool_choice;
2061 delete generate_data.stop;2073 delete generate_data.stop;
2062 // It does support logit_bias, but the tokenizer used and its effect is yet unknown.2074 delete generate_data.logit_bias;
2063 // delete generate_data.logit_bias;
2064 }2075 }
20652076
2066 await eventSource.emit(event_types.CHAT_COMPLETION_SETTINGS_READY, generate_data);2077 await eventSource.emit(event_types.CHAT_COMPLETION_SETTINGS_READY, generate_data);
@@ -2085,6 +2096,7 @@ async function sendOpenAIRequest(type, messages, signal) {
2085 let text = '';2096 let text = '';
2086 const swipes = [];2097 const swipes = [];
2087 const toolCalls = [];2098 const toolCalls = [];
2099 const state = { reasoning: '' };
2088 while (true) {2100 while (true) {
2089 const { done, value } = await reader.read();2101 const { done, value } = await reader.read();
2090 if (done) return;2102 if (done) return;
@@ -2095,14 +2107,14 @@ async function sendOpenAIRequest(type, messages, signal) {
20952107
2096 if (Array.isArray(parsed?.choices) && parsed?.choices?.[0]?.index > 0) {2108 if (Array.isArray(parsed?.choices) && parsed?.choices?.[0]?.index > 0) {
2097 const swipeIndex = parsed.choices[0].index - 1;2109 const swipeIndex = parsed.choices[0].index - 1;
2098 swipes[swipeIndex] = (swipes[swipeIndex] || '') + getStreamingReply(parsed);2110 swipes[swipeIndex] = (swipes[swipeIndex] || '') + getStreamingReply(parsed, state);
2099 } else {2111 } else {
2100 text += getStreamingReply(parsed);2112 text += getStreamingReply(parsed, state);
2101 }2113 }
21022114
2103 ToolManager.parseToolCalls(toolCalls, parsed);2115 ToolManager.parseToolCalls(toolCalls, parsed);
21042116
2105 yield { text, swipes: swipes, logprobs: parseChatCompletionLogprobs(parsed), toolCalls: toolCalls };2117 yield { text, swipes: swipes, logprobs: parseChatCompletionLogprobs(parsed), toolCalls: toolCalls, state: state };
2106 }2118 }
2107 };2119 };
2108 }2120 }
@@ -2129,13 +2141,32 @@ async function sendOpenAIRequest(type, messages, signal) {
2129 }2141 }
2130}2142}
21312143
2132function getStreamingReply(data) {2144/**
2145 * Extracts the reply from the response data from a chat completions-like source
2146 * @param {object} data Response data from the chat completions-like source
2147 * @param {object} state Additional state to keep track of
2148 * @returns {string} The reply extracted from the response data
2149 */
2150function getStreamingReply(data, state) {
2133 if (oai_settings.chat_completion_source === chat_completion_sources.CLAUDE) {2151 if (oai_settings.chat_completion_source === chat_completion_sources.CLAUDE) {
2134 return data?.delta?.text || '';2152 return data?.delta?.text || '';
2135 } else if (oai_settings.chat_completion_source === chat_completion_sources.MAKERSUITE) {2153 } else if (oai_settings.chat_completion_source === chat_completion_sources.MAKERSUITE) {
2136 return data?.candidates?.[0]?.content?.parts?.filter(x => oai_settings.show_thoughts || !x.thought)?.map(x => x.text)?.filter(x => x)?.join('\n\n') || '';2154 if (oai_settings.show_thoughts) {
2155 state.reasoning += (data?.candidates?.[0]?.content?.parts?.filter(x => x.thought)?.map(x => x.text)?.[0] || '');
2156 }
2157 return data?.candidates?.[0]?.content?.parts?.filter(x => !x.thought)?.map(x => x.text)?.[0] || '';
2137 } else if (oai_settings.chat_completion_source === chat_completion_sources.COHERE) {2158 } else if (oai_settings.chat_completion_source === chat_completion_sources.COHERE) {
2138 return data?.delta?.message?.content?.text || data?.delta?.message?.tool_plan || '';2159 return data?.delta?.message?.content?.text || data?.delta?.message?.tool_plan || '';
2160 } else if (oai_settings.chat_completion_source === chat_completion_sources.DEEPSEEK) {
2161 if (oai_settings.show_thoughts) {
2162 state.reasoning += (data.choices?.filter(x => x?.delta?.reasoning_content)?.[0]?.delta?.reasoning_content || '');
2163 }
2164 return data.choices?.[0]?.delta?.content || '';
2165 } else if (oai_settings.chat_completion_source === chat_completion_sources.OPENROUTER) {
2166 if (oai_settings.show_thoughts) {
2167 state.reasoning += (data.choices?.filter(x => x?.delta?.reasoning)?.[0]?.delta?.reasoning || '');
2168 }
2169 return data.choices?.[0]?.delta?.content ?? data.choices?.[0]?.message?.content ?? data.choices?.[0]?.text ?? '';
2139 } else {2170 } else {
2140 return data.choices?.[0]?.delta?.content ?? data.choices?.[0]?.message?.content ?? data.choices?.[0]?.text ?? '';2171 return data.choices?.[0]?.delta?.content ?? data.choices?.[0]?.message?.content ?? data.choices?.[0]?.text ?? '';
2141 }2172 }
@@ -3094,6 +3125,7 @@ function loadOpenAISettings(data, settings) {
3094 oai_settings.inline_image_quality = settings.inline_image_quality ?? default_settings.inline_image_quality;3125 oai_settings.inline_image_quality = settings.inline_image_quality ?? default_settings.inline_image_quality;
3095 oai_settings.bypass_status_check = settings.bypass_status_check ?? default_settings.bypass_status_check;3126 oai_settings.bypass_status_check = settings.bypass_status_check ?? default_settings.bypass_status_check;
3096 oai_settings.show_thoughts = settings.show_thoughts ?? default_settings.show_thoughts;3127 oai_settings.show_thoughts = settings.show_thoughts ?? default_settings.show_thoughts;
3128 oai_settings.reasoning_effort = settings.reasoning_effort ?? default_settings.reasoning_effort;
3097 oai_settings.seed = settings.seed ?? default_settings.seed;3129 oai_settings.seed = settings.seed ?? default_settings.seed;
3098 oai_settings.n = settings.n ?? default_settings.n;3130 oai_settings.n = settings.n ?? default_settings.n;
30993131
@@ -3223,6 +3255,9 @@ function loadOpenAISettings(data, settings) {
3223 $('#n_openai').val(oai_settings.n);3255 $('#n_openai').val(oai_settings.n);
3224 $('#openai_show_thoughts').prop('checked', oai_settings.show_thoughts);3256 $('#openai_show_thoughts').prop('checked', oai_settings.show_thoughts);
32253257
3258 $('#openai_reasoning_effort').val(oai_settings.reasoning_effort);
3259 $(`#openai_reasoning_effort option[value="${oai_settings.reasoning_effort}"]`).prop('selected', true);
3260
3226 if (settings.reverse_proxy !== undefined) oai_settings.reverse_proxy = settings.reverse_proxy;3261 if (settings.reverse_proxy !== undefined) oai_settings.reverse_proxy = settings.reverse_proxy;
3227 $('#openai_reverse_proxy').val(oai_settings.reverse_proxy);3262 $('#openai_reverse_proxy').val(oai_settings.reverse_proxy);
32283263
@@ -3346,7 +3381,7 @@ async function getStatusOpen() {
3346 chat_completion_source: oai_settings.chat_completion_source,3381 chat_completion_source: oai_settings.chat_completion_source,
3347 };3382 };
33483383
3349 if (oai_settings.reverse_proxy && [chat_completion_sources.CLAUDE, chat_completion_sources.OPENAI, chat_completion_sources.MISTRALAI, chat_completion_sources.MAKERSUITE].includes(oai_settings.chat_completion_source)) {3384 if (oai_settings.reverse_proxy && [chat_completion_sources.CLAUDE, chat_completion_sources.OPENAI, chat_completion_sources.MISTRALAI, chat_completion_sources.MAKERSUITE, chat_completion_sources.DEEPSEEK].includes(oai_settings.chat_completion_source)) {
3350 await validateReverseProxy();3385 await validateReverseProxy();
3351 }3386 }
33523387
@@ -3483,6 +3518,7 @@ async function saveOpenAIPreset(name, settings, triggerUi = true) {
3483 continue_postfix: settings.continue_postfix,3518 continue_postfix: settings.continue_postfix,
3484 function_calling: settings.function_calling,3519 function_calling: settings.function_calling,
3485 show_thoughts: settings.show_thoughts,3520 show_thoughts: settings.show_thoughts,
3521 reasoning_effort: settings.reasoning_effort,
3486 seed: settings.seed,3522 seed: settings.seed,
3487 n: settings.n,3523 n: settings.n,
3488 };3524 };
@@ -3941,6 +3977,7 @@ function onSettingsPresetChange() {
3941 continue_postfix: ['#continue_postfix', 'continue_postfix', false],3977 continue_postfix: ['#continue_postfix', 'continue_postfix', false],
3942 function_calling: ['#openai_function_calling', 'function_calling', true],3978 function_calling: ['#openai_function_calling', 'function_calling', true],
3943 show_thoughts: ['#openai_show_thoughts', 'show_thoughts', true],3979 show_thoughts: ['#openai_show_thoughts', 'show_thoughts', true],
3980 reasoning_effort: ['#openai_reasoning_effort', 'reasoning_effort', false],
3944 seed: ['#seed_openai', 'seed', false],3981 seed: ['#seed_openai', 'seed', false],
3945 n: ['#n_openai', 'n', false],3982 n: ['#n_openai', 'n', false],
3946 };3983 };
@@ -3997,7 +4034,7 @@ function getMaxContextOpenAI(value) {
3997 if (oai_settings.max_context_unlocked) {4034 if (oai_settings.max_context_unlocked) {
3998 return unlocked_max;4035 return unlocked_max;
3999 }4036 }
4000 else if (value.startsWith('o1-')) {4037 else if (value.startsWith('o1') || value.startsWith('o3')) {
4001 return max_128k;4038 return max_128k;
4002 }4039 }
4003 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')) {4040 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')) {
@@ -4202,9 +4239,9 @@ async function onModelChange() {
4202 $('#openai_max_context').attr('max', max_2mil);4239 $('#openai_max_context').attr('max', max_2mil);
4203 } else if (value.includes('gemini-exp-1114') || value.includes('gemini-exp-1121') || value.includes('gemini-2.0-flash-thinking-exp-1219')) {4240 } else if (value.includes('gemini-exp-1114') || value.includes('gemini-exp-1121') || value.includes('gemini-2.0-flash-thinking-exp-1219')) {
4204 $('#openai_max_context').attr('max', max_32k);4241 $('#openai_max_context').attr('max', max_32k);
4205 } else if (value.includes('gemini-1.5-pro') || value.includes('gemini-exp-1206')) {4242 } else if (value.includes('gemini-1.5-pro') || value.includes('gemini-exp-1206') || value.includes('gemini-2.0-pro')) {
4206 $('#openai_max_context').attr('max', max_2mil);4243 $('#openai_max_context').attr('max', max_2mil);
4207 } else if (value.includes('gemini-1.5-flash') || value.includes('gemini-2.0-flash-exp')) {4244 } else if (value.includes('gemini-1.5-flash') || value.includes('gemini-2.0-flash')) {
4208 $('#openai_max_context').attr('max', max_1mil);4245 $('#openai_max_context').attr('max', max_1mil);
4209 } else if (value.includes('gemini-1.0-pro') || value === 'gemini-pro') {4246 } else if (value.includes('gemini-1.0-pro') || value === 'gemini-pro') {
4210 $('#openai_max_context').attr('max', max_32k);4247 $('#openai_max_context').attr('max', max_32k);
@@ -4350,28 +4387,19 @@ async function onModelChange() {
4350 if (oai_settings.max_context_unlocked) {4387 if (oai_settings.max_context_unlocked) {
4351 $('#openai_max_context').attr('max', unlocked_max);4388 $('#openai_max_context').attr('max', unlocked_max);
4352 }4389 }
4390 else if (['sonar', 'sonar-reasoning'].includes(oai_settings.perplexity_model)) {
4391 $('#openai_max_context').attr('max', 127000);
4392 }
4393 else if (['sonar-pro'].includes(oai_settings.perplexity_model)) {
4394 $('#openai_max_context').attr('max', 200000);
4395 }
4353 else if (oai_settings.perplexity_model.includes('llama-3.1')) {4396 else if (oai_settings.perplexity_model.includes('llama-3.1')) {
4354 const isOnline = oai_settings.perplexity_model.includes('online');4397 const isOnline = oai_settings.perplexity_model.includes('online');
4355 const contextSize = isOnline ? 128 * 1024 - 4000 : 128 * 1024;4398 const contextSize = isOnline ? 128 * 1024 - 4000 : 128 * 1024;
4356 $('#openai_max_context').attr('max', contextSize);4399 $('#openai_max_context').attr('max', contextSize);
4357 }4400 }
4358 else if (['llama-3-sonar-small-32k-chat', 'llama-3-sonar-large-32k-chat'].includes(oai_settings.perplexity_model)) {
4359 $('#openai_max_context').attr('max', max_32k);
4360 }
4361 else if (['llama-3-sonar-small-32k-online', 'llama-3-sonar-large-32k-online'].includes(oai_settings.perplexity_model)) {
4362 $('#openai_max_context').attr('max', 28000);
4363 }
4364 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)) {
4365 $('#openai_max_context').attr('max', max_16k);
4366 }
4367 else if (['llama-3-8b-instruct', 'llama-3-70b-instruct'].includes(oai_settings.perplexity_model)) {
4368 $('#openai_max_context').attr('max', max_8k);
4369 }
4370 else if (['sonar-small-online', 'sonar-medium-online'].includes(oai_settings.perplexity_model)) {
4371 $('#openai_max_context').attr('max', 12000);
4372 }
4373 else {4401 else {
4374 $('#openai_max_context').attr('max', max_4k);4402 $('#openai_max_context').attr('max', max_128k);
4375 }4403 }
4376 oai_settings.openai_max_context = Math.min(Number($('#openai_max_context').attr('max')), oai_settings.openai_max_context);4404 oai_settings.openai_max_context = Math.min(Number($('#openai_max_context').attr('max')), oai_settings.openai_max_context);
4377 $('#openai_max_context').val(oai_settings.openai_max_context).trigger('input');4405 $('#openai_max_context').val(oai_settings.openai_max_context).trigger('input');
@@ -4382,24 +4410,30 @@ async function onModelChange() {
4382 if (oai_settings.chat_completion_source == chat_completion_sources.GROQ) {4410 if (oai_settings.chat_completion_source == chat_completion_sources.GROQ) {
4383 if (oai_settings.max_context_unlocked) {4411 if (oai_settings.max_context_unlocked) {
4384 $('#openai_max_context').attr('max', unlocked_max);4412 $('#openai_max_context').attr('max', unlocked_max);
4385 }4413 } else if (oai_settings.groq_model.includes('gemma2-9b-it')) {
4386 else if (oai_settings.groq_model.includes('llama-3.2') && oai_settings.groq_model.includes('-preview')) {
4387 $('#openai_max_context').attr('max', max_8k);4414 $('#openai_max_context').attr('max', max_8k);
4388 }4415 } else if (oai_settings.groq_model.includes('llama-3.3-70b-versatile')) {
4389 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')) {
4390 $('#openai_max_context').attr('max', max_128k);4416 $('#openai_max_context').attr('max', max_128k);
4391 }4417 } else if (oai_settings.groq_model.includes('llama-3.1-8b-instant')) {
4392 else if (oai_settings.groq_model.includes('llama3-groq')) {4418 $('#openai_max_context').attr('max', max_128k);
4419 } else if (oai_settings.groq_model.includes('llama3-70b-8192')) {
4393 $('#openai_max_context').attr('max', max_8k);4420 $('#openai_max_context').attr('max', max_8k);
4394 }4421 } else if (oai_settings.groq_model.includes('llama3-8b-8192')) {
4395 else if (['llama3-8b-8192', 'llama3-70b-8192', 'gemma-7b-it', 'gemma2-9b-it'].includes(oai_settings.groq_model)) {
4396 $('#openai_max_context').attr('max', max_8k);4422 $('#openai_max_context').attr('max', max_8k);
4397 }4423 } else if (oai_settings.groq_model.includes('mixtral-8x7b-32768')) {
4398 else if (['mixtral-8x7b-32768'].includes(oai_settings.groq_model)) {
4399 $('#openai_max_context').attr('max', max_32k);4424 $('#openai_max_context').attr('max', max_32k);
4400 }4425 } else if (oai_settings.groq_model.includes('deepseek-r1-distill-llama-70b')) {
4401 else {4426 $('#openai_max_context').attr('max', max_128k);
4402 $('#openai_max_context').attr('max', max_4k);4427 } else if (oai_settings.groq_model.includes('llama-3.3-70b-specdec')) {
4428 $('#openai_max_context').attr('max', max_8k);
4429 } else if (oai_settings.groq_model.includes('llama-3.2-1b-preview')) {
4430 $('#openai_max_context').attr('max', max_128k);
4431 } else if (oai_settings.groq_model.includes('llama-3.2-3b-preview')) {
4432 $('#openai_max_context').attr('max', max_128k);
4433 } else if (oai_settings.groq_model.includes('llama-3.2-11b-vision-preview')) {
4434 $('#openai_max_context').attr('max', max_128k);
4435 } else if (oai_settings.groq_model.includes('llama-3.2-90b-vision-preview')) {
4436 $('#openai_max_context').attr('max', max_128k);
4403 }4437 }
4404 oai_settings.openai_max_context = Math.min(Number($('#openai_max_context').attr('max')), oai_settings.openai_max_context);4438 oai_settings.openai_max_context = Math.min(Number($('#openai_max_context').attr('max')), oai_settings.openai_max_context);
4405 $('#openai_max_context').val(oai_settings.openai_max_context).trigger('input');4439 $('#openai_max_context').val(oai_settings.openai_max_context).trigger('input');
@@ -4488,7 +4522,7 @@ async function onModelChange() {
4488 if (oai_settings.chat_completion_source === chat_completion_sources.DEEPSEEK) {4522 if (oai_settings.chat_completion_source === chat_completion_sources.DEEPSEEK) {
4489 if (oai_settings.max_context_unlocked) {4523 if (oai_settings.max_context_unlocked) {
4490 $('#openai_max_context').attr('max', unlocked_max);4524 $('#openai_max_context').attr('max', unlocked_max);
4491 } else if (oai_settings.deepseek_model == 'deepseek-chat') {4525 } else if (['deepseek-reasoner', 'deepseek-chat'].includes(oai_settings.deepseek_model)) {
4492 $('#openai_max_context').attr('max', max_64k);4526 $('#openai_max_context').attr('max', max_64k);
4493 } else if (oai_settings.deepseek_model == 'deepseek-coder') {4527 } else if (oai_settings.deepseek_model == 'deepseek-coder') {
4494 $('#openai_max_context').attr('max', max_16k);4528 $('#openai_max_context').attr('max', max_16k);
@@ -4725,7 +4759,7 @@ async function onConnectButtonClick(e) {
4725 await writeSecret(SECRET_KEYS.DEEPSEEK, api_key_deepseek);4759 await writeSecret(SECRET_KEYS.DEEPSEEK, api_key_deepseek);
4726 }4760 }
47274761
4728 if (!secret_state[SECRET_KEYS.DEEPSEEK]) {4762 if (!secret_state[SECRET_KEYS.DEEPSEEK] && !oai_settings.reverse_proxy) {
4729 console.log('No secret key saved for DeepSeek');4763 console.log('No secret key saved for DeepSeek');
4730 return;4764 return;
4731 }4765 }
@@ -4900,7 +4934,15 @@ export function isImageInliningSupported() {
4900 // gultra just isn't being offered as multimodal, thanks google.4934 // gultra just isn't being offered as multimodal, thanks google.
4901 const visionSupportedModels = [4935 const visionSupportedModels = [
4902 'gpt-4-vision',4936 'gpt-4-vision',
4937 'gemini-2.0-pro-exp',
4938 'gemini-2.0-pro-exp-02-05',
4939 'gemini-2.0-flash-lite-preview',
4940 'gemini-2.0-flash-lite-preview-02-05',
4941 'gemini-2.0-flash',
4942 'gemini-2.0-flash-001',
4903 'gemini-2.0-flash-thinking-exp-1219',4943 'gemini-2.0-flash-thinking-exp-1219',
4944 'gemini-2.0-flash-thinking-exp-01-21',
4945 'gemini-2.0-flash-thinking-exp',
4904 'gemini-2.0-flash-exp',4946 'gemini-2.0-flash-exp',
4905 'gemini-1.5-flash',4947 'gemini-1.5-flash',
4906 'gemini-1.5-flash-latest',4948 'gemini-1.5-flash-latest',
@@ -4925,6 +4967,8 @@ export function isImageInliningSupported() {
4925 'gpt-4-turbo',4967 'gpt-4-turbo',
4926 'gpt-4o',4968 'gpt-4o',
4927 'gpt-4o-mini',4969 'gpt-4o-mini',
4970 'o1',
4971 'o1-2024-12-17',
4928 'chatgpt-4o-latest',4972 'chatgpt-4o-latest',
4929 'yi-vision',4973 'yi-vision',
4930 'pixtral-latest',4974 'pixtral-latest',
@@ -5483,6 +5527,11 @@ export function initOpenAI() {
5483 saveSettingsDebounced();5527 saveSettingsDebounced();
5484 });5528 });
54855529
5530 $('#openai_reasoning_effort').on('input', function () {
5531 oai_settings.reasoning_effort = String($(this).val());
5532 saveSettingsDebounced();
5533 });
5534
5486 if (!CSS.supports('field-sizing', 'content')) {5535 if (!CSS.supports('field-sizing', 'content')) {
5487 $(document).on('input', '#openai_settings .autoSetHeight', function () {5536 $(document).on('input', '#openai_settings .autoSetHeight', function () {
5488 resetScrollHeight($(this));5537 resetScrollHeight($(this));
public/scripts/personas.js+6 -5
@@ -25,6 +25,7 @@ import { POPUP_RESULT, POPUP_TYPE, Popup, callGenericPopup } from './popup.js';
25import { t } from './i18n.js';25import { t } from './i18n.js';
26import { openWorldInfoEditor, world_names } from './world-info.js';26import { openWorldInfoEditor, world_names } from './world-info.js';
27import { renderTemplateAsync } from './templates.js';27import { renderTemplateAsync } from './templates.js';
28import { accountStorage } from './util/AccountStorage.js';
2829
29let savePersonasPage = 0;30let savePersonasPage = 0;
30const GRID_STORAGE_KEY = 'Personas_GridView';31const GRID_STORAGE_KEY = 'Personas_GridView';
@@ -34,7 +35,7 @@ export let user_avatar = '';
34export const personasFilter = new FilterHelper(debounce(getUserAvatars, debounce_timeout.quick));35export const personasFilter = new FilterHelper(debounce(getUserAvatars, debounce_timeout.quick));
3536
36function switchPersonaGridView() {37function switchPersonaGridView() {
37 const state = localStorage.getItem(GRID_STORAGE_KEY) === 'true';38 const state = accountStorage.getItem(GRID_STORAGE_KEY) === 'true';
38 $('#user_avatar_block').toggleClass('gridView', state);39 $('#user_avatar_block').toggleClass('gridView', state);
39}40}
4041
@@ -182,7 +183,7 @@ export async function getUserAvatars(doRender = true, openPageAt = '') {
182183
183 const storageKey = 'Personas_PerPage';184 const storageKey = 'Personas_PerPage';
184 const listId = '#user_avatar_block';185 const listId = '#user_avatar_block';
185 const perPage = Number(localStorage.getItem(storageKey)) || 5;186 const perPage = Number(accountStorage.getItem(storageKey)) || 5;
186187
187 $('#persona_pagination_container').pagination({188 $('#persona_pagination_container').pagination({
188 dataSource: entities,189 dataSource: entities,
@@ -205,7 +206,7 @@ export async function getUserAvatars(doRender = true, openPageAt = '') {
205 highlightSelectedAvatar();206 highlightSelectedAvatar();
206 },207 },
207 afterSizeSelectorChange: function (e) {208 afterSizeSelectorChange: function (e) {
208 localStorage.setItem(storageKey, e.target.value);209 accountStorage.setItem(storageKey, e.target.value);
209 },210 },
210 afterPaging: function (e) {211 afterPaging: function (e) {
211 savePersonasPage = e;212 savePersonasPage = e;
@@ -1132,8 +1133,8 @@ export function initPersonas() {
1132 saveSettingsDebounced();1133 saveSettingsDebounced();
1133 });1134 });
1134 $('#persona_grid_toggle').on('click', () => {1135 $('#persona_grid_toggle').on('click', () => {
1135 const state = localStorage.getItem(GRID_STORAGE_KEY) === 'true';1136 const state = accountStorage.getItem(GRID_STORAGE_KEY) === 'true';
1136 localStorage.setItem(GRID_STORAGE_KEY, String(!state));1137 accountStorage.setItem(GRID_STORAGE_KEY, String(!state));
1137 switchPersonaGridView();1138 switchPersonaGridView();
1138 });1139 });
11391140
public/scripts/popup.js+12 -1
@@ -24,6 +24,15 @@ export const POPUP_RESULT = {
24 AFFIRMATIVE: 1,24 AFFIRMATIVE: 1,
25 NEGATIVE: 0,25 NEGATIVE: 0,
26 CANCELLED: null,26 CANCELLED: null,
27 CUSTOM1: 1001,
28 CUSTOM2: 1002,
29 CUSTOM3: 1003,
30 CUSTOM4: 1004,
31 CUSTOM5: 1005,
32 CUSTOM6: 1006,
33 CUSTOM7: 1007,
34 CUSTOM8: 1008,
35 CUSTOM9: 1009,
27};36};
2837
29/**38/**
@@ -37,6 +46,7 @@ export const POPUP_RESULT = {
37 * @property {boolean?} [transparent=false] - Whether to display the popup in transparent mode (no background, border, shadow or anything, only its content)46 * @property {boolean?} [transparent=false] - Whether to display the popup in transparent mode (no background, border, shadow or anything, only its content)
38 * @property {boolean?} [allowHorizontalScrolling=false] - Whether to allow horizontal scrolling in the popup47 * @property {boolean?} [allowHorizontalScrolling=false] - Whether to allow horizontal scrolling in the popup
39 * @property {boolean?} [allowVerticalScrolling=false] - Whether to allow vertical scrolling in the popup48 * @property {boolean?} [allowVerticalScrolling=false] - Whether to allow vertical scrolling in the popup
49 * @property {boolean?} [leftAlign=false] - Whether the popup content should be left-aligned by default
40 * @property {'slow'|'fast'|'none'?} [animation='slow'] - Animation speed for the popup (opening, closing, ...)50 * @property {'slow'|'fast'|'none'?} [animation='slow'] - Animation speed for the popup (opening, closing, ...)
41 * @property {POPUP_RESULT|number?} [defaultResult=POPUP_RESULT.AFFIRMATIVE] - The default result of this popup when Enter is pressed. Can be changed from `POPUP_RESULT.AFFIRMATIVE`.51 * @property {POPUP_RESULT|number?} [defaultResult=POPUP_RESULT.AFFIRMATIVE] - The default result of this popup when Enter is pressed. Can be changed from `POPUP_RESULT.AFFIRMATIVE`.
42 * @property {CustomPopupButton[]|string[]?} [customButtons=null] - Custom buttons to add to the popup. If only strings are provided, the buttons will be added with default options, and their result will be in order from `2` onward.52 * @property {CustomPopupButton[]|string[]?} [customButtons=null] - Custom buttons to add to the popup. If only strings are provided, the buttons will be added with default options, and their result will be in order from `2` onward.
@@ -164,7 +174,7 @@ export class Popup {
164 * @param {string} [inputValue=''] - The initial value of the input field174 * @param {string} [inputValue=''] - The initial value of the input field
165 * @param {PopupOptions} [options={}] - Additional options for the popup175 * @param {PopupOptions} [options={}] - Additional options for the popup
166 */176 */
167 constructor(content, type, inputValue = '', { okButton = null, cancelButton = null, rows = 1, wide = false, wider = false, large = false, transparent = false, allowHorizontalScrolling = false, allowVerticalScrolling = false, animation = 'fast', defaultResult = POPUP_RESULT.AFFIRMATIVE, customButtons = null, customInputs = null, onClosing = null, onClose = null, cropAspect = null, cropImage = null } = {}) {177 constructor(content, type, inputValue = '', { okButton = null, cancelButton = null, rows = 1, wide = false, wider = false, large = false, transparent = false, allowHorizontalScrolling = false, allowVerticalScrolling = false, leftAlign = false, animation = 'fast', defaultResult = POPUP_RESULT.AFFIRMATIVE, customButtons = null, customInputs = null, onClosing = null, onClose = null, cropAspect = null, cropImage = null } = {}) {
168 Popup.util.popups.push(this);178 Popup.util.popups.push(this);
169179
170 // Make this popup uniquely identifiable180 // Make this popup uniquely identifiable
@@ -209,6 +219,7 @@ export class Popup {
209 if (transparent) this.dlg.classList.add('transparent_dialogue_popup');219 if (transparent) this.dlg.classList.add('transparent_dialogue_popup');
210 if (allowHorizontalScrolling) this.dlg.classList.add('horizontal_scrolling_dialogue_popup');220 if (allowHorizontalScrolling) this.dlg.classList.add('horizontal_scrolling_dialogue_popup');
211 if (allowVerticalScrolling) this.dlg.classList.add('vertical_scrolling_dialogue_popup');221 if (allowVerticalScrolling) this.dlg.classList.add('vertical_scrolling_dialogue_popup');
222 if (leftAlign) this.dlg.classList.add('left_aligned_dialogue_popup');
212 if (animation) this.dlg.classList.add('popup--animation-' + animation);223 if (animation) this.dlg.classList.add('popup--animation-' + animation);
213224
214 // If custom button captions are provided, we set them beforehand225 // If custom button captions are provided, we set them beforehand
public/scripts/power-user.js+99 -6
@@ -54,6 +54,7 @@ import { commonEnumProviders, enumIcons } from './slash-commands/SlashCommandCom
54import { POPUP_TYPE, callGenericPopup } from './popup.js';54import { POPUP_TYPE, callGenericPopup } from './popup.js';
55import { loadSystemPrompts } from './sysprompt.js';55import { loadSystemPrompts } from './sysprompt.js';
56import { fuzzySearchCategories } from './filters.js';56import { fuzzySearchCategories } from './filters.js';
57import { accountStorage } from './util/AccountStorage.js';
5758
58export {59export {
59 loadPowerUserSettings,60 loadPowerUserSettings,
@@ -253,6 +254,17 @@ let power_user = {
253 content: 'Write {{char}}\'s next reply in a fictional chat between {{char}} and {{user}}.',254 content: 'Write {{char}}\'s next reply in a fictional chat between {{char}} and {{user}}.',
254 },255 },
255256
257 reasoning: {
258 auto_parse: false,
259 add_to_prompts: false,
260 auto_expand: false,
261 show_hidden: false,
262 prefix: '<think>\n',
263 suffix: '\n</think>',
264 separator: '\n\n',
265 max_additions: 1,
266 },
267
256 personas: {},268 personas: {},
257 default_persona: null,269 default_persona: null,
258 persona_descriptions: {},270 persona_descriptions: {},
@@ -2009,7 +2021,7 @@ export function renderStoryString(params) {
2009 */2021 */
2010function validateStoryString(storyString, params) {2022function validateStoryString(storyString, params) {
2011 /** @type {{hashCache: {[hash: string]: {fieldsWarned: {[key: string]: boolean}}}}} */2023 /** @type {{hashCache: {[hash: string]: {fieldsWarned: {[key: string]: boolean}}}}} */
2012 const cache = JSON.parse(localStorage.getItem(storage_keys.storyStringValidationCache)) ?? { hashCache: {} };2024 const cache = JSON.parse(accountStorage.getItem(storage_keys.storyStringValidationCache)) ?? { hashCache: {} };
20132025
2014 const hash = getStringHash(storyString);2026 const hash = getStringHash(storyString);
20152027
@@ -2046,7 +2058,7 @@ function validateStoryString(storyString, params) {
2046 toastr.warning(`The story string does not contain the following fields, but they would contain content: ${fieldsList}`, 'Story String Validation');2058 toastr.warning(`The story string does not contain the following fields, but they would contain content: ${fieldsList}`, 'Story String Validation');
2047 }2059 }
20482060
2049 localStorage.setItem(storage_keys.storyStringValidationCache, JSON.stringify(cache));2061 accountStorage.setItem(storage_keys.storyStringValidationCache, JSON.stringify(cache));
2050}2062}
20512063
20522064
@@ -2441,7 +2453,7 @@ async function resetMovablePanels(type) {
2441 }2453 }
24422454
2443 saveSettingsDebounced();2455 saveSettingsDebounced();
2444 eventSource.emit(event_types.MOVABLE_PANELS_RESET);2456 await eventSource.emit(event_types.MOVABLE_PANELS_RESET);
24452457
2446 eventSource.once(event_types.SETTINGS_UPDATED, () => {2458 eventSource.once(event_types.SETTINGS_UPDATED, () => {
2447 $('.resizing').removeClass('resizing');2459 $('.resizing').removeClass('resizing');
@@ -2534,7 +2546,7 @@ async function loadUntilMesId(mesId) {
2534 let target;2546 let target;
25352547
2536 while (getFirstDisplayedMessageId() > mesId && getFirstDisplayedMessageId() !== 0) {2548 while (getFirstDisplayedMessageId() > mesId && getFirstDisplayedMessageId() !== 0) {
2537 showMoreMessages();2549 await showMoreMessages();
2538 await delay(1);2550 await delay(1);
2539 target = $('#chat').find(`.mes[mesid=${mesId}]`);2551 target = $('#chat').find(`.mes[mesid=${mesId}]`);
25402552
@@ -2909,6 +2921,46 @@ export function flushEphemeralStoppingStrings() {
2909}2921}
29102922
2911/**2923/**
2924 * Checks if the generated text should be filtered based on the auto-swipe settings.
2925 * @param {string} text The text to check
2926 * @returns {boolean} If the generated text should be filtered
2927 */
2928export function generatedTextFiltered(text) {
2929 /**
2930 * Checks if the given text contains any of the blacklisted words.
2931 * @param {string} text The text to check
2932 * @param {string[]} blacklist The list of blacklisted words
2933 * @param {number} threshold The number of blacklisted words that need to be present to trigger the check
2934 * @returns {boolean} Whether the text contains blacklisted words
2935 */
2936 function containsBlacklistedWords(text, blacklist, threshold) {
2937 const regex = new RegExp(`\\b(${blacklist.join('|')})\\b`, 'gi');
2938 const matches = text.match(regex) || [];
2939 return matches.length >= threshold;
2940 }
2941
2942 // Make sure a generated text is non-empty
2943 // Otherwise we might get in a loop with a broken API
2944 text = text.trim();
2945 if (text.length > 0) {
2946 if (power_user.auto_swipe_minimum_length) {
2947 if (text.length < power_user.auto_swipe_minimum_length) {
2948 console.log('Generated text size too small');
2949 return true;
2950 }
2951 }
2952 if (power_user.auto_swipe_blacklist.length && power_user.auto_swipe_blacklist_threshold) {
2953 if (containsBlacklistedWords(text, power_user.auto_swipe_blacklist, power_user.auto_swipe_blacklist_threshold)) {
2954 console.log('Generated text has blacklisted words');
2955 return true;
2956 }
2957 }
2958 }
2959
2960 return false;
2961}
2962
2963/**
2912 * Gets the custom stopping strings from the power user settings.2964 * Gets the custom stopping strings from the power user settings.
2913 * @param {number | undefined} limit Number of strings to return. If 0 or undefined, returns all strings.2965 * @param {number | undefined} limit Number of strings to return. If 0 or undefined, returns all strings.
2914 * @returns {string[]} An array of custom stopping strings2966 * @returns {string[]} An array of custom stopping strings
@@ -3879,9 +3931,9 @@ $(document).ready(() => {
3879 helpString: 'Start a new chat with a random character. If an argument is provided, only considers characters that have the specified tag.',3931 helpString: 'Start a new chat with a random character. If an argument is provided, only considers characters that have the specified tag.',
3880 }));3932 }));
3881 SlashCommandParser.addCommandObject(SlashCommand.fromProps({3933 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
3882 name: 'delmode',3934 name: 'del',
3883 callback: doDelMode,3935 callback: doDelMode,
3884 aliases: ['del'],3936 aliases: ['delete', 'delmode'],
3885 unnamedArgumentList: [3937 unnamedArgumentList: [
3886 new SlashCommandArgument(3938 new SlashCommandArgument(
3887 'optional number', [ARGUMENT_TYPE.NUMBER], false,3939 'optional number', [ARGUMENT_TYPE.NUMBER], false,
@@ -4064,4 +4116,45 @@ $(document).ready(() => {
4064 ],4116 ],
4065 helpString: 'activates a movingUI preset by name',4117 helpString: 'activates a movingUI preset by name',
4066 }));4118 }));
4119 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
4120 name: 'stop-strings',
4121 aliases: ['stopping-strings', 'custom-stopping-strings', 'custom-stop-strings'],
4122 helpString: `
4123 <div>
4124 Sets a list of custom stopping strings. Gets the list if no value is provided.
4125 </div>
4126 <div>
4127 <strong>Examples:</strong>
4128 </div>
4129 <ul>
4130 <li>Value must be a JSON-serialized array: <pre><code class="language-stscript">/stop-strings ["goodbye", "farewell"]</code></pre></li>
4131 <li>Pipe characters must be escaped with a backslash: <pre><code class="language-stscript">/stop-strings ["left\\|right"]</code></pre></li>
4132 </ul>
4133 `,
4134 returns: ARGUMENT_TYPE.LIST,
4135 unnamedArgumentList: [
4136 SlashCommandArgument.fromProps({
4137 description: 'list of strings',
4138 typeList: [ARGUMENT_TYPE.LIST],
4139 acceptsMultiple: false,
4140 isRequired: false,
4141 }),
4142 ],
4143 callback: (_, value) => {
4144 if (String(value ?? '').trim()) {
4145 const parsedValue = ((x) => { try { return JSON.parse(x.toString()); } catch { return null; } })(value);
4146 if (!parsedValue || !Array.isArray(parsedValue)) {
4147 throw new Error('Invalid list format. The value must be a JSON-serialized array of strings.');
4148 }
4149 parsedValue.forEach((item, index) => {
4150 parsedValue[index] = String(item);
4151 });
4152 power_user.custom_stopping_strings = JSON.stringify(parsedValue);
4153 $('#custom_stopping_strings').val(power_user.custom_stopping_strings);
4154 saveSettingsDebounced();
4155 }
4156
4157 return power_user.custom_stopping_strings;
4158 },
4159 }));
4067});4160});
public/scripts/preset-manager.js+3 -0
@@ -586,6 +586,9 @@ class PresetManager {
586 'tabby_model',586 'tabby_model',
587 'derived',587 'derived',
588 'generic_model',588 'generic_model',
589 'include_reasoning',
590 'global_banned_tokens',
591 'send_banned_tokens',
589 ];592 ];
590 const settings = Object.assign({}, getSettingsByApiId(this.apiId));593 const settings = Object.assign({}, getSettingsByApiId(this.apiId));
591594
public/scripts/reasoning.js+913 -0
@@ -0,0 +1,913 @@
1import {
2 moment,
3} from '../lib.js';
4import { chat, closeMessageEditor, event_types, eventSource, main_api, messageFormatting, saveChatConditional, saveSettingsDebounced, substituteParams, updateMessageBlock } from '../script.js';
5import { getRegexedString, regex_placement } from './extensions/regex/engine.js';
6import { getCurrentLocale, t } from './i18n.js';
7import { MacrosParser } from './macros.js';
8import { chat_completion_sources, getChatCompletionModel, oai_settings } from './openai.js';
9import { Popup } from './popup.js';
10import { power_user } from './power-user.js';
11import { SlashCommand } from './slash-commands/SlashCommand.js';
12import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from './slash-commands/SlashCommandArgument.js';
13import { commonEnumProviders } from './slash-commands/SlashCommandCommonEnumsProvider.js';
14import { SlashCommandParser } from './slash-commands/SlashCommandParser.js';
15import { textgen_types, textgenerationwebui_settings } from './textgen-settings.js';
16import { copyText, escapeRegex, isFalseBoolean, setDatasetProperty } from './utils.js';
17
18/**
19 * Gets a message from a jQuery element.
20 * @param {Element} element
21 * @returns {{messageId: number, message: object, messageBlock: JQuery<HTMLElement>}}
22 */
23function getMessageFromJquery(element) {
24 const messageBlock = $(element).closest('.mes');
25 const messageId = Number(messageBlock.attr('mesid'));
26 const message = chat[messageId];
27 return { messageId: messageId, message, messageBlock };
28}
29
30/**
31 * Toggles the auto-expand state of reasoning blocks.
32 */
33function toggleReasoningAutoExpand() {
34 const reasoningBlocks = document.querySelectorAll('details.mes_reasoning_details');
35 reasoningBlocks.forEach((block) => {
36 if (block instanceof HTMLDetailsElement) {
37 block.open = power_user.reasoning.auto_expand;
38 }
39 });
40}
41
42/**
43 * Extracts the reasoning from the response data.
44 * @param {object} data Response data
45 * @returns {string} Extracted reasoning
46 */
47export function extractReasoningFromData(data) {
48 switch (main_api) {
49 case 'textgenerationwebui':
50 switch (textgenerationwebui_settings.type) {
51 case textgen_types.OPENROUTER:
52 return data?.choices?.[0]?.reasoning ?? '';
53 }
54 break;
55
56 case 'openai':
57 if (!oai_settings.show_thoughts) break;
58
59 switch (oai_settings.chat_completion_source) {
60 case chat_completion_sources.DEEPSEEK:
61 return data?.choices?.[0]?.message?.reasoning_content ?? '';
62 case chat_completion_sources.OPENROUTER:
63 return data?.choices?.[0]?.message?.reasoning ?? '';
64 case chat_completion_sources.MAKERSUITE:
65 return data?.responseContent?.parts?.filter(part => part.thought)?.map(part => part.text)?.join('\n\n') ?? '';
66 }
67 break;
68 }
69
70 return '';
71}
72
73/**
74 * Check if the model supports reasoning, but does not send back the reasoning
75 * @returns {boolean} True if the model supports reasoning
76 */
77export function isHiddenReasoningModel() {
78 if (main_api !== 'openai') {
79 return false;
80 }
81
82 /** @typedef {{ (currentModel: string, supportedModel: string): boolean }} MatchingFunc */
83 /** @type {Record.<string, MatchingFunc>} */
84 const FUNCS = {
85 equals: (currentModel, supportedModel) => currentModel === supportedModel,
86 startsWith: (currentModel, supportedModel) => currentModel.startsWith(supportedModel),
87 };
88
89 /** @type {{ name: string; func: MatchingFunc; }[]} */
90 const hiddenReasoningModels = [
91 { name: 'o1', func: FUNCS.startsWith },
92 { name: 'o3', func: FUNCS.startsWith },
93 { name: 'gemini-2.0-flash-thinking-exp', func: FUNCS.startsWith },
94 { name: 'gemini-2.0-pro-exp', func: FUNCS.startsWith },
95 ];
96
97 const model = getChatCompletionModel();
98
99 const isHidden = hiddenReasoningModels.some(({ name, func }) => func(model, name));
100 return isHidden;
101}
102
103/**
104 * Updates the Reasoning UI for a specific message
105 * @param {number|JQuery<HTMLElement>|HTMLElement} messageIdOrElement The message ID or the message element
106 * @param {Object} [options={}] - Optional arguments
107 * @param {boolean} [options.reset=false] - Whether to reset state, and not take the current mess properties (for example when swiping)
108 */
109export function updateReasoningUI(messageIdOrElement, { reset = false } = {}) {
110 const handler = new ReasoningHandler();
111 handler.initHandleMessage(messageIdOrElement, { reset });
112}
113
114
115/**
116 * Enum for representing the state of reasoning
117 * @enum {string}
118 * @readonly
119 */
120export const ReasoningState = {
121 None: 'none',
122 Thinking: 'thinking',
123 Done: 'done',
124 Hidden: 'hidden',
125};
126
127/**
128 * Handles reasoning-specific logic and DOM updates for messages.
129 * This class is used inside the {@link StreamingProcessor} to manage reasoning states and UI updates.
130 */
131export class ReasoningHandler {
132 #isHiddenReasoningModel;
133
134 /**
135 * @param {Date?} [timeStarted=null] - When the generation started
136 */
137 constructor(timeStarted = null) {
138 /** @type {ReasoningState} The current state of the reasoning process */
139 this.state = ReasoningState.None;
140 /** @type {string} The reasoning output */
141 this.reasoning = '';
142 /** @type {Date} When the reasoning started */
143 this.startTime = null;
144 /** @type {Date} When the reasoning ended */
145 this.endTime = null;
146
147 /** @type {Date} Initial starting time of the generation */
148 this.initialTime = timeStarted ?? new Date();
149
150 /** @type {boolean} True if the model supports reasoning, but hides the reasoning output */
151 this.#isHiddenReasoningModel = isHiddenReasoningModel();
152
153 // Cached DOM elements for reasoning
154 /** @type {HTMLElement} Main message DOM element `.mes` */
155 this.messageDom = null;
156 /** @type {HTMLDetailsElement} Reasoning details DOM element `.mes_reasoning_details` */
157 this.messageReasoningDetailsDom = null;
158 /** @type {HTMLElement} Reasoning content DOM element `.mes_reasoning` */
159 this.messageReasoningContentDom = null;
160 /** @type {HTMLElement} Reasoning header DOM element `.mes_reasoning_header_title` */
161 this.messageReasoningHeaderDom = null;
162 }
163
164 /**
165 * Initializes the reasoning handler for a specific message.
166 *
167 * Can be used to update the DOM elements or read other reasoning states.
168 * It will internally take the message-saved data and write the states back into the handler, as if during streaming of the message.
169 * The state will always be either done/hidden or none.
170 *
171 * @param {number|JQuery<HTMLElement>|HTMLElement} messageIdOrElement - The message ID or the message element
172 * @param {Object} [options={}] - Optional arguments
173 * @param {boolean} [options.reset=false] - Whether to reset state of the handler, and not take the current mess properties (for example when swiping)
174 */
175 initHandleMessage(messageIdOrElement, { reset = false } = {}) {
176 /** @type {HTMLElement} */
177 const messageElement = typeof messageIdOrElement === 'number'
178 ? document.querySelector(`#chat [mesid="${messageIdOrElement}"]`)
179 : messageIdOrElement instanceof HTMLElement
180 ? messageIdOrElement
181 : $(messageIdOrElement)[0];
182 const messageId = Number(messageElement.getAttribute('mesid'));
183
184 if (isNaN(messageId) || !chat[messageId]) return;
185
186 if (!chat[messageId].extra) {
187 chat[messageId].extra = {};
188 }
189 const extra = chat[messageId].extra;
190
191 if (extra.reasoning) {
192 this.state = ReasoningState.Done;
193 } else if (extra.reasoning_duration) {
194 this.state = ReasoningState.Hidden;
195 }
196
197 this.reasoning = extra?.reasoning ?? '';
198
199 if (this.state !== ReasoningState.None) {
200 this.initialTime = new Date(chat[messageId].gen_started);
201 this.startTime = this.initialTime;
202 this.endTime = new Date(this.startTime.getTime() + (extra?.reasoning_duration ?? 0));
203 }
204
205 // Prefill main dom element, as message might not have been rendered yet
206 this.messageDom = messageElement;
207
208 // Make sure reset correctly clears all relevant states
209 if (reset) {
210 this.state = this.#isHiddenReasoningModel ? ReasoningState.Thinking : ReasoningState.None;
211 this.reasoning = '';
212 this.initialTime = new Date();
213 this.startTime = null;
214 this.endTime = null;
215 }
216
217 this.updateDom(messageId);
218
219 if (power_user.reasoning.auto_expand && this.state !== ReasoningState.Hidden) {
220 this.messageReasoningDetailsDom.open = true;
221 }
222 }
223
224 /**
225 * Gets the duration of the reasoning in milliseconds.
226 *
227 * @returns {number?} The duration in milliseconds, or null if the start or end time is not set
228 */
229 getDuration() {
230 if (this.startTime && this.endTime) {
231 return this.endTime.getTime() - this.startTime.getTime();
232 }
233 return null;
234 }
235
236 /**
237 * Updates the reasoning text/string for a message.
238 *
239 * @param {number} messageId - The ID of the message to update
240 * @param {string?} [reasoning=null] - The reasoning text to update - If null, uses the current reasoning
241 * @param {Object} [options={}] - Optional arguments
242 * @param {boolean} [options.persist=false] - Whether to persist the reasoning to the message object
243 * @returns {boolean} - Returns true if the reasoning was changed, otherwise false
244 */
245 updateReasoning(messageId, reasoning = null, { persist = false } = {}) {
246 if (messageId == -1 || !chat[messageId]) {
247 return false;
248 }
249
250 reasoning = reasoning ?? this.reasoning;
251 reasoning = power_user.trim_spaces ? reasoning.trim() : reasoning;
252
253 // Ensure the chat extra exists
254 if (!chat[messageId].extra) {
255 chat[messageId].extra = {};
256 }
257 const extra = chat[messageId].extra;
258
259 const reasoningChanged = extra.reasoning !== reasoning;
260 this.reasoning = getRegexedString(reasoning ?? '', regex_placement.REASONING);
261
262 if (persist) {
263 // Build and save the reasoning data to message extras
264 extra.reasoning = this.reasoning;
265 extra.reasoning_duration = this.getDuration();
266 }
267
268 return reasoningChanged;
269 }
270
271
272 /**
273 * Handles processing of reasoning for a message.
274 *
275 * This is usually called by the message processor when a message is changed.
276 *
277 * @param {number} messageId - The ID of the message to process
278 * @param {boolean} mesChanged - Whether the message has changed
279 * @returns {Promise<void>}
280 */
281 async process(messageId, mesChanged) {
282 if (!this.reasoning && !this.#isHiddenReasoningModel) return;
283
284 // Ensure reasoning string is updated and regexes are applied correctly
285 const reasoningChanged = this.updateReasoning(messageId, null, { persist: true });
286
287 if ((this.#isHiddenReasoningModel || reasoningChanged) && this.state === ReasoningState.None) {
288 this.state = ReasoningState.Thinking;
289 this.startTime = this.initialTime;
290 }
291 if ((this.#isHiddenReasoningModel || !reasoningChanged) && mesChanged && this.state === ReasoningState.Thinking) {
292 this.endTime = new Date();
293 await this.finish(messageId);
294 }
295 }
296
297 /**
298 * Completes the reasoning process for a message.
299 *
300 * Records the finish time if it was not set during streaming and updates the reasoning state.
301 * Emits an event to signal the completion of reasoning and updates the DOM elements accordingly.
302 *
303 * @param {number} messageId - The ID of the message to complete reasoning for
304 * @returns {Promise<void>}
305 */
306 async finish(messageId) {
307 if (this.state === ReasoningState.None) return;
308
309 // Make sure the finish time is recorded if a reasoning was in process and it wasn't ended correctly during streaming
310 if (this.startTime !== null && this.endTime === null) {
311 this.endTime = new Date();
312 }
313
314 if (this.state === ReasoningState.Thinking) {
315 this.state = this.#isHiddenReasoningModel ? ReasoningState.Hidden : ReasoningState.Done;
316 this.updateReasoning(messageId, null, { persist: true });
317 await eventSource.emit(event_types.STREAM_REASONING_DONE, this.reasoning, this.getDuration(), messageId, this.state);
318 }
319
320 this.updateDom(messageId);
321 }
322
323 /**
324 * Updates the reasoning UI elements for a message.
325 *
326 * Toggles the CSS class, updates states, reasoning message, and duration.
327 *
328 * @param {number} messageId - The ID of the message to update
329 */
330 updateDom(messageId) {
331 this.#checkDomElements(messageId);
332
333 // Main CSS class to show this message includes reasoning
334 this.messageDom.classList.toggle('reasoning', this.state !== ReasoningState.None);
335
336 // Update states to the relevant DOM elements
337 setDatasetProperty(this.messageDom, 'reasoningState', this.state !== ReasoningState.None ? this.state : null);
338 setDatasetProperty(this.messageReasoningDetailsDom, 'state', this.state);
339
340 // Update the reasoning message
341 const reasoning = power_user.trim_spaces ? this.reasoning.trim() : this.reasoning;
342 const displayReasoning = messageFormatting(reasoning, '', false, false, messageId, {}, true);
343 this.messageReasoningContentDom.innerHTML = displayReasoning;
344
345 // Update tooltip for hidden reasoning edit
346 /** @type {HTMLElement} */
347 const button = this.messageDom.querySelector('.mes_edit_add_reasoning');
348 button.title = this.state === ReasoningState.Hidden ? t`Hidden reasoning - Add reasoning block` : t`Add reasoning block`;
349
350 // Make sure that hidden reasoning headers are collapsed by default, to not show a useless edit button
351 if (this.state === ReasoningState.Hidden) {
352 this.messageReasoningDetailsDom.open = false;
353 }
354
355 // Update the reasoning duration in the UI
356 this.#updateReasoningTimeUI();
357 }
358
359 /**
360 * Finds and caches reasoning-related DOM elements for the given message.
361 *
362 * @param {number} messageId - The ID of the message to cache the DOM elements for
363 */
364 #checkDomElements(messageId) {
365 // Make sure we reset dom elements if we are checking for a different message (shouldn't happen, but be sure)
366 if (this.messageDom !== null && this.messageDom.getAttribute('mesid') !== messageId.toString()) {
367 this.messageDom = null;
368 }
369
370 // Cache the DOM elements once
371 if (this.messageDom === null) {
372 this.messageDom = document.querySelector(`#chat .mes[mesid="${messageId}"]`);
373 if (this.messageDom === null) throw new Error('message dom does not exist');
374 }
375 if (this.messageReasoningDetailsDom === null) {
376 this.messageReasoningDetailsDom = this.messageDom.querySelector('.mes_reasoning_details');
377 }
378 if (this.messageReasoningContentDom === null) {
379 this.messageReasoningContentDom = this.messageDom.querySelector('.mes_reasoning');
380 }
381 if (this.messageReasoningHeaderDom === null) {
382 this.messageReasoningHeaderDom = this.messageDom.querySelector('.mes_reasoning_header_title');
383 }
384 }
385
386 /**
387 * Updates the reasoning time display in the UI.
388 *
389 * Shows the duration in a human-readable format with a tooltip for exact seconds.
390 * Displays "Thinking..." if still processing, or a generic message otherwise.
391 */
392 #updateReasoningTimeUI() {
393 const element = this.messageReasoningHeaderDom;
394 const duration = this.getDuration();
395 let data = null;
396 if (duration) {
397 const durationStr = moment.duration(duration).locale(getCurrentLocale()).humanize({ s: 50, ss: 3 });
398 const secondsStr = moment.duration(duration).asSeconds();
399
400 const span = document.createElement('span');
401 span.title = t`${secondsStr} seconds`;
402 span.textContent = durationStr;
403
404 element.textContent = t`Thought for `;
405 element.appendChild(span);
406 data = String(secondsStr);
407 } else if ([ReasoningState.Done, ReasoningState.Hidden].includes(this.state)) {
408 element.textContent = t`Thought for some time`;
409 data = 'unknown';
410 } else {
411 element.textContent = t`Thinking...`;
412 data = null;
413 }
414
415 setDatasetProperty(this.messageReasoningDetailsDom, 'duration', data);
416 setDatasetProperty(element, 'duration', data);
417 }
418}
419
420/**
421 * Helper class for adding reasoning to messages.
422 * Keeps track of the number of reasoning additions.
423 */
424export class PromptReasoning {
425 static REASONING_PLACEHOLDER = '\u200B';
426
427 constructor() {
428 this.counter = 0;
429 }
430
431 /**
432 * Checks if the limit of reasoning additions has been reached.
433 * @returns {boolean} True if the limit of reasoning additions has been reached, false otherwise.
434 */
435 isLimitReached() {
436 if (!power_user.reasoning.add_to_prompts) {
437 return true;
438 }
439
440 return this.counter >= power_user.reasoning.max_additions;
441 }
442
443 /**
444 * Add reasoning to a message according to the power user settings.
445 * @param {string} content Message content
446 * @param {string} reasoning Message reasoning
447 * @param {boolean} isPrefix Whether this is the last message prefix
448 * @returns {string} Message content with reasoning
449 */
450 addToMessage(content, reasoning, isPrefix) {
451 // Disabled or reached limit of additions
452 if (!isPrefix && (!power_user.reasoning.add_to_prompts || this.counter >= power_user.reasoning.max_additions)) {
453 return content;
454 }
455
456 // No reasoning provided or a legacy placeholder
457 if (!reasoning || reasoning === PromptReasoning.REASONING_PLACEHOLDER) {
458 return content;
459 }
460
461 // Increment the counter
462 this.counter++;
463
464 // Substitute macros in variable parts
465 const prefix = substituteParams(power_user.reasoning.prefix || '');
466 const separator = substituteParams(power_user.reasoning.separator || '');
467 const suffix = substituteParams(power_user.reasoning.suffix || '');
468
469 // Combine parts with reasoning only
470 if (isPrefix && !content) {
471 return `${prefix}${reasoning}`;
472 }
473
474 // Combine parts with reasoning and content
475 return `${prefix}${reasoning}${suffix}${separator}${content}`;
476 }
477}
478
479function loadReasoningSettings() {
480 $('#reasoning_add_to_prompts').prop('checked', power_user.reasoning.add_to_prompts);
481 $('#reasoning_add_to_prompts').on('change', function () {
482 power_user.reasoning.add_to_prompts = !!$(this).prop('checked');
483 saveSettingsDebounced();
484 });
485
486 $('#reasoning_prefix').val(power_user.reasoning.prefix);
487 $('#reasoning_prefix').on('input', function () {
488 power_user.reasoning.prefix = String($(this).val());
489 saveSettingsDebounced();
490 });
491
492 $('#reasoning_suffix').val(power_user.reasoning.suffix);
493 $('#reasoning_suffix').on('input', function () {
494 power_user.reasoning.suffix = String($(this).val());
495 saveSettingsDebounced();
496 });
497
498 $('#reasoning_separator').val(power_user.reasoning.separator);
499 $('#reasoning_separator').on('input', function () {
500 power_user.reasoning.separator = String($(this).val());
501 saveSettingsDebounced();
502 });
503
504 $('#reasoning_max_additions').val(power_user.reasoning.max_additions);
505 $('#reasoning_max_additions').on('input', function () {
506 power_user.reasoning.max_additions = Number($(this).val());
507 saveSettingsDebounced();
508 });
509
510 $('#reasoning_auto_parse').prop('checked', power_user.reasoning.auto_parse);
511 $('#reasoning_auto_parse').on('change', function () {
512 power_user.reasoning.auto_parse = !!$(this).prop('checked');
513 saveSettingsDebounced();
514 });
515
516 $('#reasoning_auto_expand').prop('checked', power_user.reasoning.auto_expand);
517 $('#reasoning_auto_expand').on('change', function () {
518 power_user.reasoning.auto_expand = !!$(this).prop('checked');
519 toggleReasoningAutoExpand();
520 saveSettingsDebounced();
521 });
522 toggleReasoningAutoExpand();
523
524 $('#reasoning_show_hidden').prop('checked', power_user.reasoning.show_hidden);
525 $('#reasoning_show_hidden').on('change', function () {
526 power_user.reasoning.show_hidden = !!$(this).prop('checked');
527 $('#chat').attr('data-show-hidden-reasoning', power_user.reasoning.show_hidden ? 'true' : null);
528 saveSettingsDebounced();
529 });
530 $('#chat').attr('data-show-hidden-reasoning', power_user.reasoning.show_hidden ? 'true' : null);
531}
532
533function registerReasoningSlashCommands() {
534 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
535 name: 'reasoning-get',
536 aliases: ['get-reasoning'],
537 returns: ARGUMENT_TYPE.STRING,
538 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.`,
539 unnamedArgumentList: [
540 SlashCommandArgument.fromProps({
541 description: 'Message ID. If not provided, the message ID of the last message is used.',
542 typeList: ARGUMENT_TYPE.NUMBER,
543 enumProvider: commonEnumProviders.messages(),
544 }),
545 ],
546 callback: (_args, value) => {
547 const messageId = !isNaN(parseInt(value.toString())) ? parseInt(value.toString()) : chat.length - 1;
548 const message = chat[messageId];
549 const reasoning = String(message?.extra?.reasoning ?? '');
550 return reasoning;
551 },
552 }));
553
554 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
555 name: 'reasoning-set',
556 aliases: ['set-reasoning'],
557 returns: ARGUMENT_TYPE.STRING,
558 helpString: t`Set the reasoning block of a message. Returns the reasoning block content.`,
559 namedArgumentList: [
560 SlashCommandNamedArgument.fromProps({
561 name: 'at',
562 description: 'Message ID. If not provided, the message ID of the last message is used.',
563 typeList: ARGUMENT_TYPE.NUMBER,
564 enumProvider: commonEnumProviders.messages(),
565 }),
566 ],
567 unnamedArgumentList: [
568 SlashCommandArgument.fromProps({
569 description: 'Reasoning block content.',
570 typeList: ARGUMENT_TYPE.STRING,
571 }),
572 ],
573 callback: async (args, value) => {
574 const messageId = !isNaN(Number(args.at)) ? Number(args.at) : chat.length - 1;
575 const message = chat[messageId];
576 if (!message?.extra) {
577 return '';
578 }
579
580 message.extra.reasoning = String(value ?? '');
581 await saveChatConditional();
582
583 closeMessageEditor('reasoning');
584 updateMessageBlock(messageId, message);
585 return message.extra.reasoning;
586 },
587 }));
588
589 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
590 name: 'reasoning-parse',
591 aliases: ['parse-reasoning'],
592 returns: 'reasoning string',
593 helpString: t`Extracts the reasoning block from a string using the Reasoning Formatting settings.`,
594 namedArgumentList: [
595 SlashCommandNamedArgument.fromProps({
596 name: 'regex',
597 description: 'Whether to apply regex scripts to the reasoning content.',
598 typeList: [ARGUMENT_TYPE.BOOLEAN],
599 defaultValue: 'true',
600 isRequired: false,
601 enumProvider: commonEnumProviders.boolean('trueFalse'),
602 }),
603 ],
604 unnamedArgumentList: [
605 SlashCommandArgument.fromProps({
606 description: 'input string',
607 typeList: [ARGUMENT_TYPE.STRING],
608 }),
609 ],
610 callback: (args, value) => {
611 if (!value) {
612 return '';
613 }
614
615 if (!power_user.reasoning.prefix || !power_user.reasoning.suffix) {
616 toastr.warning(t`Both prefix and suffix must be set in the Reasoning Formatting settings.`);
617 return String(value);
618 }
619
620 const parsedReasoning = parseReasoningFromString(String(value));
621
622 if (!parsedReasoning) {
623 return '';
624 }
625
626 const applyRegex = !isFalseBoolean(String(args.regex ?? ''));
627 return applyRegex
628 ? getRegexedString(parsedReasoning.reasoning, regex_placement.REASONING)
629 : parsedReasoning.reasoning;
630 },
631 }));
632}
633
634function registerReasoningMacros() {
635 MacrosParser.registerMacro('reasoningPrefix', () => power_user.reasoning.prefix, t`Reasoning Prefix`);
636 MacrosParser.registerMacro('reasoningSuffix', () => power_user.reasoning.suffix, t`Reasoning Suffix`);
637 MacrosParser.registerMacro('reasoningSeparator', () => power_user.reasoning.separator, t`Reasoning Separator`);
638}
639
640function setReasoningEventHandlers() {
641 $(document).on('click', '.mes_reasoning_details', function (e) {
642 if (!e.target.closest('.mes_reasoning_actions') && !e.target.closest('.mes_reasoning_header')) {
643 e.preventDefault();
644 }
645 });
646
647 $(document).on('click', '.mes_reasoning_header', function (e) {
648 const details = $(this).closest('.mes_reasoning_details');
649 // Along with the CSS rules to mark blocks not toggle-able when they are empty, prevent them from actually being toggled, or being edited
650 if (details.find('.mes_reasoning').is(':empty')) {
651 e.preventDefault();
652 return;
653 }
654
655 // If we are in message edit mode and reasoning area is closed, a click opens and edits it
656 const mes = $(this).closest('.mes');
657 const mesEditArea = mes.find('#curEditTextarea');
658 if (mesEditArea.length) {
659 const summary = $(mes).find('.mes_reasoning_summary');
660 if (!summary.attr('open')) {
661 summary.find('.mes_reasoning_edit').trigger('click');
662 }
663 }
664 });
665
666 $(document).on('click', '.mes_reasoning_copy', (e) => {
667 e.stopPropagation();
668 e.preventDefault();
669 });
670
671 $(document).on('click', '.mes_reasoning_edit', function (e) {
672 e.stopPropagation();
673 e.preventDefault();
674 const { message, messageBlock } = getMessageFromJquery(this);
675 if (!message?.extra) {
676 return;
677 }
678
679 const reasoning = String(message?.extra?.reasoning ?? '');
680 const chatElement = document.getElementById('chat');
681 const textarea = document.createElement('textarea');
682 const reasoningBlock = messageBlock.find('.mes_reasoning');
683 textarea.classList.add('reasoning_edit_textarea');
684 textarea.value = reasoning;
685 $(textarea).insertBefore(reasoningBlock);
686
687 if (!CSS.supports('field-sizing', 'content')) {
688 const resetHeight = function () {
689 const scrollTop = chatElement.scrollTop;
690 textarea.style.height = '0px';
691 textarea.style.height = `${textarea.scrollHeight}px`;
692 chatElement.scrollTop = scrollTop;
693 };
694
695 textarea.addEventListener('input', resetHeight);
696 resetHeight();
697 }
698
699 textarea.focus();
700 textarea.setSelectionRange(textarea.value.length, textarea.value.length);
701
702 const textareaRect = textarea.getBoundingClientRect();
703 const chatRect = chatElement.getBoundingClientRect();
704
705 // Scroll if textarea bottom is below visible area
706 if (textareaRect.bottom > chatRect.bottom) {
707 const scrollOffset = textareaRect.bottom - chatRect.bottom;
708 chatElement.scrollTop += scrollOffset;
709 }
710 });
711
712 $(document).on('click', '.mes_reasoning_edit_done', async function (e) {
713 e.stopPropagation();
714 e.preventDefault();
715 const { message, messageId, messageBlock } = getMessageFromJquery(this);
716 if (!message?.extra) {
717 return;
718 }
719
720 const textarea = messageBlock.find('.reasoning_edit_textarea');
721 const reasoning = getRegexedString(String(textarea.val()), regex_placement.REASONING, { isEdit: true });
722 message.extra.reasoning = reasoning;
723 await saveChatConditional();
724 updateMessageBlock(messageId, message);
725 textarea.remove();
726
727 messageBlock.find('.mes_edit_done:visible').trigger('click');
728 });
729
730 $(document).on('click', '.mes_reasoning_edit_cancel', function (e) {
731 e.stopPropagation();
732 e.preventDefault();
733
734 const { messageBlock } = getMessageFromJquery(this);
735 const textarea = messageBlock.find('.reasoning_edit_textarea');
736 textarea.remove();
737
738 messageBlock.find('.mes_reasoning_edit_cancel:visible').trigger('click');
739
740 updateReasoningUI(messageBlock);
741 });
742
743 $(document).on('click', '.mes_edit_add_reasoning', async function () {
744 const { message, messageBlock } = getMessageFromJquery(this);
745 if (!message?.extra) {
746 return;
747 }
748
749 if (message.extra.reasoning) {
750 toastr.info(t`Reasoning already exists.`, t`Edit Message`);
751 return;
752 }
753
754 messageBlock.addClass('reasoning');
755
756 // To make hidden reasoning blocks editable, we just set them to "Done" here already.
757 // They will be done on save anyway - and on cancel the reasoning block gets rerendered too.
758 if (messageBlock.attr('data-reasoning-state') === ReasoningState.Hidden) {
759 messageBlock.attr('data-reasoning-state', ReasoningState.Done);
760 }
761
762 // Open the reasoning area so we can actually edit it
763 messageBlock.find('.mes_reasoning_details').attr('open', '');
764 messageBlock.find('.mes_reasoning_edit').trigger('click');
765 await saveChatConditional();
766 });
767
768 $(document).on('click', '.mes_reasoning_delete', async function (e) {
769 e.stopPropagation();
770 e.preventDefault();
771
772 const confirm = await Popup.show.confirm(t`Remove Reasoning`, t`Are you sure you want to clear the reasoning?<br />Visible message contents will stay intact.`);
773
774 if (!confirm) {
775 return;
776 }
777
778 const { message, messageId, messageBlock } = getMessageFromJquery(this);
779 if (!message?.extra) {
780 return;
781 }
782 message.extra.reasoning = '';
783 await saveChatConditional();
784 updateMessageBlock(messageId, message);
785 const textarea = messageBlock.find('.reasoning_edit_textarea');
786 textarea.remove();
787 });
788
789 $(document).on('pointerup', '.mes_reasoning_copy', async function () {
790 const { message } = getMessageFromJquery(this);
791 const reasoning = String(message?.extra?.reasoning ?? '');
792
793 if (!reasoning) {
794 return;
795 }
796
797 await copyText(reasoning);
798 toastr.info(t`Copied!`, '', { timeOut: 2000 });
799 });
800}
801
802/**
803 * Removes reasoning from a string if auto-parsing is enabled.
804 * @param {string} str Input string
805 * @returns {string} Output string
806 */
807export function removeReasoningFromString(str) {
808 if (!power_user.reasoning.auto_parse) {
809 return str;
810 }
811
812 const parsedReasoning = parseReasoningFromString(str);
813 return parsedReasoning?.content ?? str;
814}
815
816/**
817 * Parses reasoning from a string using the power user reasoning settings.
818 * @typedef {Object} ParsedReasoning
819 * @property {string} reasoning Reasoning block
820 * @property {string} content Message content
821 * @param {string} str Content of the message
822 * @returns {ParsedReasoning|null} Parsed reasoning block and message content
823 */
824function parseReasoningFromString(str) {
825 // Both prefix and suffix must be defined
826 if (!power_user.reasoning.prefix || !power_user.reasoning.suffix) {
827 return null;
828 }
829
830 try {
831 const regex = new RegExp(`${escapeRegex(power_user.reasoning.prefix)}(.*?)${escapeRegex(power_user.reasoning.suffix)}`, 's');
832
833 let didReplace = false;
834 let reasoning = '';
835 let content = String(str).replace(regex, (_match, captureGroup) => {
836 didReplace = true;
837 reasoning = captureGroup;
838 return '';
839 });
840
841 if (didReplace && power_user.trim_spaces) {
842 reasoning = reasoning.trim();
843 content = content.trim();
844 }
845
846 return { reasoning, content };
847 } catch (error) {
848 console.error('[Reasoning] Error parsing reasoning block', error);
849 return null;
850 }
851}
852
853function registerReasoningAppEvents() {
854 eventSource.makeFirst(event_types.MESSAGE_RECEIVED, (/** @type {number} */ idx) => {
855 if (!power_user.reasoning.auto_parse) {
856 return;
857 }
858
859 console.debug('[Reasoning] Auto-parsing reasoning block for message', idx);
860 const message = chat[idx];
861
862 if (!message) {
863 console.warn('[Reasoning] Message not found', idx);
864 return null;
865 }
866
867 if (!message.mes || message.mes === '...') {
868 console.debug('[Reasoning] Message content is empty or a placeholder', idx);
869 return null;
870 }
871
872 const parsedReasoning = parseReasoningFromString(message.mes);
873
874 // No reasoning block found
875 if (!parsedReasoning) {
876 return;
877 }
878
879 // Make sure the message has an extra object
880 if (!message.extra || typeof message.extra !== 'object') {
881 message.extra = {};
882 }
883
884 const contentUpdated = !!parsedReasoning.reasoning || parsedReasoning.content !== message.mes;
885
886 // If reasoning was found, add it to the message
887 if (parsedReasoning.reasoning) {
888 message.extra.reasoning = getRegexedString(parsedReasoning.reasoning, regex_placement.REASONING);
889 }
890
891 // Update the message text if it was changed
892 if (parsedReasoning.content !== message.mes) {
893 message.mes = parsedReasoning.content;
894 }
895
896 // Find if a message already exists in DOM and must be updated
897 if (contentUpdated) {
898 const messageRendered = document.querySelector(`.mes[mesid="${idx}"]`) !== null;
899 if (messageRendered) {
900 console.debug('[Reasoning] Updating message block', idx);
901 updateMessageBlock(idx, message);
902 }
903 }
904 });
905}
906
907export function initReasoning() {
908 loadReasoningSettings();
909 setReasoningEventHandlers();
910 registerReasoningSlashCommands();
911 registerReasoningMacros();
912 registerReasoningAppEvents();
913}
public/scripts/secrets.js+2 -0
@@ -40,6 +40,8 @@ export const SECRET_KEYS = {
40 BFL: 'api_key_bfl',40 BFL: 'api_key_bfl',
41 GENERIC: 'api_key_generic',41 GENERIC: 'api_key_generic',
42 DEEPSEEK: 'api_key_deepseek',42 DEEPSEEK: 'api_key_deepseek',
43 SERPER: 'api_key_serper',
44 FALAI: 'api_key_falai',
43};45};
4446
45const INPUT_MAP = {47const INPUT_MAP = {
public/scripts/slash-commands.js+133 -49
@@ -42,6 +42,7 @@ import {
42 showMoreMessages,42 showMoreMessages,
43 stopGeneration,43 stopGeneration,
44 substituteParams,44 substituteParams,
45 syncCurrentSwipeInfoExtras,
45 system_avatar,46 system_avatar,
46 system_message_types,47 system_message_types,
47 this_chid,48 this_chid,
@@ -58,7 +59,7 @@ import { autoSelectPersona, retriggerFirstMessageOnEmptyChat, setPersonaLockStat
58import { addEphemeralStoppingString, chat_styles, flushEphemeralStoppingStrings, power_user } from './power-user.js';59import { addEphemeralStoppingString, chat_styles, flushEphemeralStoppingStrings, power_user } from './power-user.js';
59import { SERVER_INPUTS, textgen_types, textgenerationwebui_settings } from './textgen-settings.js';60import { SERVER_INPUTS, textgen_types, textgenerationwebui_settings } from './textgen-settings.js';
60import { decodeTextTokens, getAvailableTokenizers, getFriendlyTokenizerName, getTextTokens, getTokenCountAsync, selectTokenizer } from './tokenizers.js';61import { decodeTextTokens, getAvailableTokenizers, getFriendlyTokenizerName, getTextTokens, getTokenCountAsync, selectTokenizer } from './tokenizers.js';
61import { debounce, delay, equalsIgnoreCaseAndAccents, findChar, getCharIndex, isFalseBoolean, isTrueBoolean, onlyUnique, showFontAwesomePicker, stringToRange, trimToEndSentence, trimToStartSentence, waitUntilCondition } from './utils.js';62import { debounce, delay, equalsIgnoreCaseAndAccents, findChar, getCharIndex, isFalseBoolean, isTrueBoolean, onlyUnique, regexFromString, showFontAwesomePicker, stringToRange, trimToEndSentence, trimToStartSentence, waitUntilCondition } from './utils.js';
62import { registerVariableCommands, resolveVariable } from './variables.js';63import { registerVariableCommands, resolveVariable } from './variables.js';
63import { background_settings } from './backgrounds.js';64import { background_settings } from './backgrounds.js';
64import { SlashCommandClosure } from './slash-commands/SlashCommandClosure.js';65import { SlashCommandClosure } from './slash-commands/SlashCommandClosure.js';
@@ -74,6 +75,7 @@ import { commonEnumProviders, enumIcons } from './slash-commands/SlashCommandCom
74import { SlashCommandBreakController } from './slash-commands/SlashCommandBreakController.js';75import { SlashCommandBreakController } from './slash-commands/SlashCommandBreakController.js';
75import { SlashCommandExecutionError } from './slash-commands/SlashCommandExecutionError.js';76import { SlashCommandExecutionError } from './slash-commands/SlashCommandExecutionError.js';
76import { slashCommandReturnHelper } from './slash-commands/SlashCommandReturnHelper.js';77import { slashCommandReturnHelper } from './slash-commands/SlashCommandReturnHelper.js';
78import { accountStorage } from './util/AccountStorage.js';
77export {79export {
78 executeSlashCommands, executeSlashCommandsWithOptions, getSlashCommandsHelp, registerSlashCommand,80 executeSlashCommands, executeSlashCommandsWithOptions, getSlashCommandsHelp, registerSlashCommand,
79};81};
@@ -234,7 +236,6 @@ export function initDefaultSlashCommands() {
234 description: 'Character name - or unique character identifier (avatar key)',236 description: 'Character name - or unique character identifier (avatar key)',
235 typeList: [ARGUMENT_TYPE.STRING],237 typeList: [ARGUMENT_TYPE.STRING],
236 enumProvider: commonEnumProviders.characters('character'),238 enumProvider: commonEnumProviders.characters('character'),
237 forceEnum: false,
238 }),239 }),
239 ],240 ],
240 helpString: `241 helpString: `
@@ -273,7 +274,6 @@ export function initDefaultSlashCommands() {
273 typeList: [ARGUMENT_TYPE.STRING],274 typeList: [ARGUMENT_TYPE.STRING],
274 isRequired: true,275 isRequired: true,
275 enumProvider: commonEnumProviders.characters('character'),276 enumProvider: commonEnumProviders.characters('character'),
276 forceEnum: false,
277 }),277 }),
278 SlashCommandNamedArgument.fromProps({278 SlashCommandNamedArgument.fromProps({
279 name: 'avatar',279 name: 'avatar',
@@ -517,7 +517,6 @@ export function initDefaultSlashCommands() {
517 typeList: [ARGUMENT_TYPE.STRING],517 typeList: [ARGUMENT_TYPE.STRING],
518 isRequired: true,518 isRequired: true,
519 enumProvider: commonEnumProviders.characters('all'),519 enumProvider: commonEnumProviders.characters('all'),
520 forceEnum: true,
521 }),520 }),
522 ],521 ],
523 helpString: 'Opens up a chat with the character or group by its name',522 helpString: 'Opens up a chat with the character or group by its name',
@@ -733,6 +732,57 @@ export function initDefaultSlashCommands() {
733 helpString: 'Unhides a message from the prompt.',732 helpString: 'Unhides a message from the prompt.',
734 }));733 }));
735 SlashCommandParser.addCommandObject(SlashCommand.fromProps({734 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
735 name: 'member-get',
736 aliases: ['getmember', 'memberget'],
737 callback: (async ({ field = 'name' }, arg) => {
738 if (!selected_group) {
739 toastr.warning('Cannot run /member-get command outside of a group chat.');
740 return '';
741 }
742 if (field === '') {
743 toastr.warning('\'/member-get field=\' argument required!');
744 return '';
745 }
746 field = field.toString();
747 arg = arg.toString();
748 if (!['name', 'index', 'id', 'avatar'].includes(field)) {
749 toastr.warning('\'/member-get field=\' argument required!');
750 return '';
751 }
752 const isId = !isNaN(parseInt(arg));
753 const groupMember = findGroupMemberId(arg, true);
754 if (!groupMember) {
755 toastr.warn(`No group member found using ${isId ? 'id' : 'string'} ${arg}`);
756 return '';
757 }
758 return groupMember[field];
759 }),
760 namedArgumentList: [
761 SlashCommandNamedArgument.fromProps({
762 name: 'field',
763 description: 'Whether to retrieve the name, index, id, or avatar.',
764 typeList: [ARGUMENT_TYPE.STRING],
765 isRequired: true,
766 defaultValue: 'name',
767 enumList: [
768 new SlashCommandEnumValue('name', 'Character name'),
769 new SlashCommandEnumValue('index', 'Group member index'),
770 new SlashCommandEnumValue('avatar', 'Character avatar'),
771 new SlashCommandEnumValue('id', 'Character index'),
772 ],
773 }),
774 ],
775 unnamedArgumentList: [
776 SlashCommandArgument.fromProps({
777 description: 'member index (starts with 0), name, or avatar',
778 typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.STRING],
779 isRequired: true,
780 enumProvider: commonEnumProviders.groupMembers(),
781 }),
782 ],
783 helpString: 'Retrieves a group member\'s name, index, id, or avatar.',
784 }));
785 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
736 name: 'member-disable',786 name: 'member-disable',
737 callback: disableGroupMemberCallback,787 callback: disableGroupMemberCallback,
738 aliases: ['disable', 'disablemember', 'memberdisable'],788 aliases: ['disable', 'disablemember', 'memberdisable'],
@@ -842,7 +892,8 @@ export function initDefaultSlashCommands() {
842 helpString: 'Moves a group member down in the group chat list.',892 helpString: 'Moves a group member down in the group chat list.',
843 }));893 }));
844 SlashCommandParser.addCommandObject(SlashCommand.fromProps({894 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
845 name: 'peek',895 name: 'member-peek',
896 aliases: ['peek', 'memberpeek', 'peekmember'],
846 callback: peekCallback,897 callback: peekCallback,
847 unnamedArgumentList: [898 unnamedArgumentList: [
848 SlashCommandArgument.fromProps({899 SlashCommandArgument.fromProps({
@@ -1008,7 +1059,6 @@ export function initDefaultSlashCommands() {
1008 typeList: [ARGUMENT_TYPE.STRING],1059 typeList: [ARGUMENT_TYPE.STRING],
1009 defaultValue: 'System',1060 defaultValue: 'System',
1010 enumProvider: () => [...commonEnumProviders.characters('character')(), new SlashCommandEnumValue('System', null, enumTypes.enum, enumIcons.assistant)],1061 enumProvider: () => [...commonEnumProviders.characters('character')(), new SlashCommandEnumValue('System', null, enumTypes.enum, enumIcons.assistant)],
1011 forceEnum: false,
1012 }),1062 }),
1013 new SlashCommandNamedArgument(1063 new SlashCommandNamedArgument(
1014 'length', 'API response length in tokens', [ARGUMENT_TYPE.NUMBER], false,1064 'length', 'API response length in tokens', [ARGUMENT_TYPE.NUMBER], false,
@@ -1902,7 +1952,7 @@ export function initDefaultSlashCommands() {
1902 returns: 'uppercase string',1952 returns: 'uppercase string',
1903 unnamedArgumentList: [1953 unnamedArgumentList: [
1904 new SlashCommandArgument(1954 new SlashCommandArgument(
1905 'string', [ARGUMENT_TYPE.STRING], true, false,1955 'text to affect', [ARGUMENT_TYPE.STRING], true, false,
1906 ),1956 ),
1907 ],1957 ],
1908 helpString: 'Converts the provided string to uppercase.',1958 helpString: 'Converts the provided string to uppercase.',
@@ -1914,7 +1964,7 @@ export function initDefaultSlashCommands() {
1914 returns: 'lowercase string',1964 returns: 'lowercase string',
1915 unnamedArgumentList: [1965 unnamedArgumentList: [
1916 new SlashCommandArgument(1966 new SlashCommandArgument(
1917 'string', [ARGUMENT_TYPE.STRING], true, false,1967 'text to affect', [ARGUMENT_TYPE.STRING], true, false,
1918 ),1968 ),
1919 ],1969 ],
1920 helpString: 'Converts the provided string to lowercase.',1970 helpString: 'Converts the provided string to lowercase.',
@@ -1934,7 +1984,7 @@ export function initDefaultSlashCommands() {
1934 ],1984 ],
1935 unnamedArgumentList: [1985 unnamedArgumentList: [
1936 new SlashCommandArgument(1986 new SlashCommandArgument(
1937 'string', [ARGUMENT_TYPE.STRING], true, false,1987 'text to affect', [ARGUMENT_TYPE.STRING], true, false,
1938 ),1988 ),
1939 ],1989 ],
1940 helpString: `1990 helpString: `
@@ -1968,8 +2018,8 @@ export function initDefaultSlashCommands() {
1968 SlashCommandParser.addCommandObject(SlashCommand.fromProps({2018 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
1969 name: 'chat-render',2019 name: 'chat-render',
1970 helpString: 'Renders a specified number of messages into the chat window. Displays all messages if no argument is provided.',2020 helpString: 'Renders a specified number of messages into the chat window. Displays all messages if no argument is provided.',
1971 callback: (args, number) => {2021 callback: async (args, number) => {
1972 showMoreMessages(number && !isNaN(Number(number)) ? Number(number) : Number.MAX_SAFE_INTEGER);2022 await showMoreMessages(number && !isNaN(Number(number)) ? Number(number) : Number.MAX_SAFE_INTEGER);
1973 if (isTrueBoolean(String(args?.scroll ?? ''))) {2023 if (isTrueBoolean(String(args?.scroll ?? ''))) {
1974 $('#chat').scrollTop(0);2024 $('#chat').scrollTop(0);
1975 }2025 }
@@ -1998,6 +2048,62 @@ export function initDefaultSlashCommands() {
1998 return '';2048 return '';
1999 },2049 },
2000 }));2050 }));
2051 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
2052 name: 'replace',
2053 aliases: ['re'],
2054 callback: (async ({ mode = 'literal', pattern, replacer = '' }, text) => {
2055 if (pattern === '')
2056 throw new Error('Argument of \'pattern=\' cannot be empty');
2057 switch (mode) {
2058 case 'literal':
2059 return text.replaceAll(pattern, replacer);
2060 case 'regex':
2061 return text.replace(regexFromString(pattern), replacer);
2062 default:
2063 throw new Error('Invalid \'/replace mode=\' argument specified!');
2064 }
2065 }),
2066 returns: 'replaced text',
2067 namedArgumentList: [
2068 SlashCommandNamedArgument.fromProps({
2069 name: 'mode',
2070 description: 'Replaces occurrence(s) of a pattern',
2071 typeList: [ARGUMENT_TYPE.STRING],
2072 defaultValue: 'literal',
2073 enumList: ['literal', 'regex'],
2074 }),
2075 new SlashCommandNamedArgument(
2076 'pattern', 'pattern to search with', [ARGUMENT_TYPE.STRING], true, false,
2077 ),
2078 new SlashCommandNamedArgument(
2079 'replacer', 'replacement text for matches', [ARGUMENT_TYPE.STRING], false, false, '',
2080 ),
2081 ],
2082 unnamedArgumentList: [
2083 new SlashCommandArgument(
2084 'text to affect', [ARGUMENT_TYPE.STRING], true, false,
2085 ),
2086 ],
2087 helpString: `
2088 <div>
2089 Replaces text within the provided string based on the pattern.
2090 </div>
2091 <div>
2092 If <code>mode</code> is <code>literal</code> (or omitted), <code>pattern</code> is a literal search string (case-sensitive).<br />
2093 If <code>mode</code> is <code>regex</code>, <code>pattern</code> is parsed as an ECMAScript Regular Expression.<br />
2094 The <code>replacer</code> replaces based on the <code>pattern</code> in the input text.<br />
2095 If <code>replacer</code> is omitted, the replacement(s) will be an empty string.<br />
2096 </div>
2097 <div>
2098 <strong>Example:</strong>
2099 <pre>/let x Blue house and blue car || </pre>
2100 <pre>/replace pattern="blue" {{var::x}} | /echo |/# Blue house and car ||</pre>
2101 <pre>/replace pattern="blue" replacer="red" {{var::x}} | /echo |/# Blue house and red car ||</pre>
2102 <pre>/replace mode=regex pattern="/blue/i" replacer="red" {{var::x}} | /echo |/# red house and blue car ||</pre>
2103 <pre>/replace mode=regex pattern="/blue/gi" replacer="red" {{var::x}} | /echo |/# red house and red car ||</pre>
2104 </div>
2105 `,
2106 }));
20012107
2002 registerVariableCommands();2108 registerVariableCommands();
2003}2109}
@@ -2814,8 +2920,11 @@ async function addSwipeCallback(args, value) {
2814 const newSwipeId = lastMessage.swipes.length - 1;2920 const newSwipeId = lastMessage.swipes.length - 1;
28152921
2816 if (isTrueBoolean(args.switch)) {2922 if (isTrueBoolean(args.switch)) {
2923 // Make sure ad-hoc changes to extras are saved before swiping away
2924 syncCurrentSwipeInfoExtras();
2817 lastMessage.swipe_id = newSwipeId;2925 lastMessage.swipe_id = newSwipeId;
2818 lastMessage.mes = lastMessage.swipes[newSwipeId];2926 lastMessage.mes = lastMessage.swipes[newSwipeId];
2927 lastMessage.extra = structuredClone(lastMessage.swipe_info?.[newSwipeId]?.extra ?? lastMessage.extra ?? {});
2819 }2928 }
28202929
2821 await saveChatConditional();2930 await saveChatConditional();
@@ -2987,7 +3096,7 @@ function performGroupMemberAction(chid, action) {
29873096
2988async function disableGroupMemberCallback(_, arg) {3097async function disableGroupMemberCallback(_, arg) {
2989 if (!selected_group) {3098 if (!selected_group) {
2990 toastr.warning('Cannot run /disable command outside of a group chat.');3099 toastr.warning('Cannot run /member-disable command outside of a group chat.');
2991 return '';3100 return '';
2992 }3101 }
29933102
@@ -3004,7 +3113,7 @@ async function disableGroupMemberCallback(_, arg) {
30043113
3005async function enableGroupMemberCallback(_, arg) {3114async function enableGroupMemberCallback(_, arg) {
3006 if (!selected_group) {3115 if (!selected_group) {
3007 toastr.warning('Cannot run /enable command outside of a group chat.');3116 toastr.warning('Cannot run /member-enable command outside of a group chat.');
3008 return '';3117 return '';
3009 }3118 }
30103119
@@ -3021,7 +3130,7 @@ async function enableGroupMemberCallback(_, arg) {
30213130
3022async function moveGroupMemberUpCallback(_, arg) {3131async function moveGroupMemberUpCallback(_, arg) {
3023 if (!selected_group) {3132 if (!selected_group) {
3024 toastr.warning('Cannot run /memberup command outside of a group chat.');3133 toastr.warning('Cannot run /member-up command outside of a group chat.');
3025 return '';3134 return '';
3026 }3135 }
30273136
@@ -3038,7 +3147,7 @@ async function moveGroupMemberUpCallback(_, arg) {
30383147
3039async function moveGroupMemberDownCallback(_, arg) {3148async function moveGroupMemberDownCallback(_, arg) {
3040 if (!selected_group) {3149 if (!selected_group) {
3041 toastr.warning('Cannot run /memberdown command outside of a group chat.');3150 toastr.warning('Cannot run /member-down command outside of a group chat.');
3042 return '';3151 return '';
3043 }3152 }
30443153
@@ -3055,12 +3164,12 @@ async function moveGroupMemberDownCallback(_, arg) {
30553164
3056async function peekCallback(_, arg) {3165async function peekCallback(_, arg) {
3057 if (!selected_group) {3166 if (!selected_group) {
3058 toastr.warning('Cannot run /peek command outside of a group chat.');3167 toastr.warning('Cannot run /member-peek command outside of a group chat.');
3059 return '';3168 return '';
3060 }3169 }
30613170
3062 if (is_group_generating) {3171 if (is_group_generating) {
3063 toastr.warning('Cannot run /peek command while the group reply is generating.');3172 toastr.warning('Cannot run /member-peek command while the group reply is generating.');
3064 return '';3173 return '';
3065 }3174 }
30663175
@@ -3077,12 +3186,7 @@ async function peekCallback(_, arg) {
30773186
3078async function removeGroupMemberCallback(_, arg) {3187async function removeGroupMemberCallback(_, arg) {
3079 if (!selected_group) {3188 if (!selected_group) {
3080 toastr.warning('Cannot run /memberremove command outside of a group chat.');3189 toastr.warning('Cannot run /member-remove command outside of a group chat.');
3081 return '';
3082 }
3083
3084 if (is_group_generating) {
3085 toastr.warning('Cannot run /memberremove command while the group reply is generating.');
3086 return '';3190 return '';
3087 }3191 }
30883192
@@ -3190,12 +3294,7 @@ function findPersonaByName(name) {
3190}3294}
31913295
3192async function sendUserMessageCallback(args, text) {3296async function sendUserMessageCallback(args, text) {
3193 if (!text) {3297 text = String(text ?? '').trim();
3194 toastr.warning('You must specify text to send');
3195 return;
3196 }
3197
3198 text = text.trim();
3199 const compact = isTrueBoolean(args?.compact);3298 const compact = isTrueBoolean(args?.compact);
3200 const bias = extractMessageBias(text);3299 const bias = extractMessageBias(text);
32013300
@@ -3504,24 +3603,18 @@ export function getNameAndAvatarForMessage(character, name = null) {
3504}3603}
35053604
3506export async function sendMessageAs(args, text) {3605export async function sendMessageAs(args, text) {
3507 if (!text) {
3508 toastr.warning('You must specify text to send as');
3509 return '';
3510 }
3511
3512 let name = args.name?.trim();3606 let name = args.name?.trim();
3513 let mesText;
35143607
3515 if (!name) {3608 if (!name) {
3516 const namelessWarningKey = 'sendAsNamelessWarningShown';3609 const namelessWarningKey = 'sendAsNamelessWarningShown';
3517 if (localStorage.getItem(namelessWarningKey) !== 'true') {3610 if (accountStorage.getItem(namelessWarningKey) !== 'true') {
3518 toastr.warning('To avoid confusion, please use /sendas name="Character Name"', 'Name defaulted to {{char}}', { timeOut: 10000 });3611 toastr.warning('To avoid confusion, please use /sendas name="Character Name"', 'Name defaulted to {{char}}', { timeOut: 10000 });
3519 localStorage.setItem(namelessWarningKey, 'true');3612 accountStorage.setItem(namelessWarningKey, 'true');
3520 }3613 }
3521 name = name2;3614 name = name2;
3522 }3615 }
35233616
3524 mesText = text.trim();3617 let mesText = String(text ?? '').trim();
35253618
3526 // Requires a regex check after the slash command is pushed to output3619 // Requires a regex check after the slash command is pushed to output
3527 mesText = getRegexedString(mesText, regex_placement.SLASH_COMMAND, { characterOverride: name });3620 mesText = getRegexedString(mesText, regex_placement.SLASH_COMMAND, { characterOverride: name });
@@ -3599,11 +3692,7 @@ export async function sendMessageAs(args, text) {
3599}3692}
36003693
3601export async function sendNarratorMessage(args, text) {3694export async function sendNarratorMessage(args, text) {
3602 if (!text) {3695 text = String(text ?? '');
3603 toastr.warning('You must specify text to send');
3604 return '';
3605 }
3606
3607 const name = chat_metadata[NARRATOR_NAME_KEY] || NARRATOR_NAME_DEFAULT;3696 const name = chat_metadata[NARRATOR_NAME_KEY] || NARRATOR_NAME_DEFAULT;
3608 // Messages that do nothing but set bias will be hidden from the context3697 // Messages that do nothing but set bias will be hidden from the context
3609 const bias = extractMessageBias(text);3698 const bias = extractMessageBias(text);
@@ -3694,18 +3783,13 @@ export async function promptQuietForLoudResponse(who, text) {
3694}3783}
36953784
3696async function sendCommentMessage(args, text) {3785async function sendCommentMessage(args, text) {
3697 if (!text) {
3698 toastr.warning('You must specify text to send');
3699 return '';
3700 }
3701
3702 const compact = isTrueBoolean(args?.compact);3786 const compact = isTrueBoolean(args?.compact);
3703 const message = {3787 const message = {
3704 name: COMMENT_NAME_DEFAULT,3788 name: COMMENT_NAME_DEFAULT,
3705 is_user: false,3789 is_user: false,
3706 is_system: true,3790 is_system: true,
3707 send_date: getMessageTimeStamp(),3791 send_date: getMessageTimeStamp(),
3708 mes: substituteParams(text.trim()),3792 mes: substituteParams(String(text ?? '').trim()),
3709 force_avatar: comment_avatar,3793 force_avatar: comment_avatar,
3710 extra: {3794 extra: {
3711 type: system_message_types.COMMENT,3795 type: system_message_types.COMMENT,
public/scripts/sse-stream.js+30 -0
@@ -220,6 +220,36 @@ async function* parseStreamData(json) {
220 }220 }
221 return;221 return;
222 }222 }
223 else if (typeof json.choices[0].delta.reasoning_content === 'string' && json.choices[0].delta.reasoning_content.length > 0) {
224 for (let j = 0; j < json.choices[0].delta.reasoning_content.length; j++) {
225 const str = json.choices[0].delta.reasoning_content[j];
226 const isLastSymbol = j === json.choices[0].delta.reasoning_content.length - 1;
227 const choiceClone = structuredClone(json.choices[0]);
228 choiceClone.delta.reasoning_content = str;
229 choiceClone.delta.content = isLastSymbol ? choiceClone.delta.content : '';
230 const choices = [choiceClone];
231 yield {
232 data: { ...json, choices },
233 chunk: str,
234 };
235 }
236 return;
237 }
238 else if (typeof json.choices[0].delta.reasoning === 'string' && json.choices[0].delta.reasoning.length > 0) {
239 for (let j = 0; j < json.choices[0].delta.reasoning.length; j++) {
240 const str = json.choices[0].delta.reasoning[j];
241 const isLastSymbol = j === json.choices[0].delta.reasoning.length - 1;
242 const choiceClone = structuredClone(json.choices[0]);
243 choiceClone.delta.reasoning = str;
244 choiceClone.delta.content = isLastSymbol ? choiceClone.delta.content : '';
245 const choices = [choiceClone];
246 yield {
247 data: { ...json, choices },
248 chunk: str,
249 };
250 }
251 return;
252 }
223 else if (typeof json.choices[0].delta.content === 'string' && json.choices[0].delta.content.length > 0) {253 else if (typeof json.choices[0].delta.content === 'string' && json.choices[0].delta.content.length > 0) {
224 for (let j = 0; j < json.choices[0].delta.content.length; j++) {254 for (let j = 0; j < json.choices[0].delta.content.length; j++) {
225 const str = json.choices[0].delta.content[j];255 const str = json.choices[0].delta.content[j];
public/scripts/st-context.js+28 -2
@@ -1,6 +1,7 @@
1import {1import {
2 activateSendButtons,2 activateSendButtons,
3 addOneMessage,3 addOneMessage,
4 appendMediaToMessage,
4 callPopup,5 callPopup,
5 characters,6 characters,
6 chat,7 chat,
@@ -12,6 +13,7 @@ import {
12 extension_prompts,13 extension_prompts,
13 Generate,14 Generate,
14 generateQuietPrompt,15 generateQuietPrompt,
16 getCharacters,
15 getCurrentChatId,17 getCurrentChatId,
16 getRequestHeaders,18 getRequestHeaders,
17 getThumbnailUrl,19 getThumbnailUrl,
@@ -40,6 +42,7 @@ import {
40 substituteParamsExtended,42 substituteParamsExtended,
41 this_chid,43 this_chid,
42 updateChatMetadata,44 updateChatMetadata,
45 updateMessageBlock,
43} from '../script.js';46} from '../script.js';
44import {47import {
45 extension_settings,48 extension_settings,
@@ -55,7 +58,7 @@ import { MacrosParser } from './macros.js';
55import { oai_settings } from './openai.js';58import { oai_settings } from './openai.js';
56import { callGenericPopup, Popup, POPUP_RESULT, POPUP_TYPE } from './popup.js';59import { callGenericPopup, Popup, POPUP_RESULT, POPUP_TYPE } from './popup.js';
57import { power_user, registerDebugFunction } from './power-user.js';60import { power_user, registerDebugFunction } from './power-user.js';
58import { isMobile, shouldSendOnEnter } from './RossAscends-mods.js';61import { humanizedDateTime, isMobile, shouldSendOnEnter } from './RossAscends-mods.js';
59import { ScraperManager } from './scrapers.js';62import { ScraperManager } from './scrapers.js';
60import { executeSlashCommands, executeSlashCommandsWithOptions, registerSlashCommand } from './slash-commands.js';63import { executeSlashCommands, executeSlashCommandsWithOptions, registerSlashCommand } from './slash-commands.js';
61import { SlashCommand } from './slash-commands/SlashCommand.js';64import { SlashCommand } from './slash-commands/SlashCommand.js';
@@ -65,10 +68,14 @@ import { tag_map, tags } from './tags.js';
65import { textgenerationwebui_settings } from './textgen-settings.js';68import { textgenerationwebui_settings } from './textgen-settings.js';
66import { tokenizers, getTextTokens, getTokenCount, getTokenCountAsync, getTokenizerModel } from './tokenizers.js';69import { tokenizers, getTextTokens, getTokenCount, getTokenCountAsync, getTokenizerModel } from './tokenizers.js';
67import { ToolManager } from './tool-calling.js';70import { ToolManager } from './tool-calling.js';
68import { timestampToMoment } from './utils.js';71import { accountStorage } from './util/AccountStorage.js';
72import { timestampToMoment, uuidv4 } from './utils.js';
73import { getGlobalVariable, getLocalVariable, setGlobalVariable, setLocalVariable } from './variables.js';
74import { convertCharacterBook, loadWorldInfo, saveWorldInfo, updateWorldInfoList } from './world-info.js';
6975
70export function getContext() {76export function getContext() {
71 return {77 return {
78 accountStorage,
72 chat,79 chat,
73 characters,80 characters,
74 groups,81 groups,
@@ -167,6 +174,25 @@ export function getContext() {
167 chatCompletionSettings: oai_settings,174 chatCompletionSettings: oai_settings,
168 textCompletionSettings: textgenerationwebui_settings,175 textCompletionSettings: textgenerationwebui_settings,
169 powerUserSettings: power_user,176 powerUserSettings: power_user,
177 getCharacters,
178 uuidv4,
179 humanizedDateTime,
180 updateMessageBlock,
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,
170 };196 };
171}197}
172198
public/scripts/templates/assistantNote.html+6 -0
@@ -1,3 +1,9 @@
1<div data-type="assistant_note">
1 <div>2 <div>
2 <b data-i18n="Note:">Note:</b> <span data-i18n="this chat is temporary and will be deleted as soon as you leave it.">this chat is temporary and will be deleted as soon as you leave it.</span>3 <b data-i18n="Note:">Note:</b> <span data-i18n="this chat is temporary and will be deleted as soon as you leave it.">this chat is temporary and will be deleted as soon as you leave it.</span>
4 <span>Click the button to save it as a file.</span>
5 </div>
6 <div class="assistant_note_export menu_button menu_button_icon" title="Export as JSONL">
7 <i class="fa-solid fa-file-export"></i>
8 </div>
3</div>9</div>
public/scripts/templates/importCharacters.html+1 -1
@@ -7,7 +7,7 @@
7 <li><span data-i18n="char_import_2">Chub Lorebook (Direct Link or ID)</span><br><span data-i18n="char_import_example">Example:</span> <tt>lorebooks/bartleby/example-lorebook</tt></li>7 <li><span data-i18n="char_import_2">Chub Lorebook (Direct Link or ID)</span><br><span data-i18n="char_import_example">Example:</span> <tt>lorebooks/bartleby/example-lorebook</tt></li>
8 <li><span data-i18n="char_import_3">JanitorAI Character (Direct Link or UUID)</span><br><span data-i18n="char_import_example">Example:</span> <tt>ddd1498a-a370-4136-b138-a8cd9461fdfe_character-aqua-the-useless-goddess</tt></li>8 <li><span data-i18n="char_import_3">JanitorAI Character (Direct Link or UUID)</span><br><span data-i18n="char_import_example">Example:</span> <tt>ddd1498a-a370-4136-b138-a8cd9461fdfe_character-aqua-the-useless-goddess</tt></li>
9 <li><span data-i18n="char_import_4">Pygmalion.chat Character (Direct Link or UUID)</span><br><span data-i18n="char_import_example">Example:</span> <tt>a7ca95a1-0c88-4e23-91b3-149db1e78ab9</tt></li>9 <li><span data-i18n="char_import_4">Pygmalion.chat Character (Direct Link or UUID)</span><br><span data-i18n="char_import_example">Example:</span> <tt>a7ca95a1-0c88-4e23-91b3-149db1e78ab9</tt></li>
10 <li><span data-i18n="char_import_5">AICharacterCard.com Character (Direct Link or ID)</span><br><span data-i18n="char_import_example">Example:</span> <tt>AICC/aicharcards/the-game-master</tt></li>10 <li><span data-i18n="char_import_5">AICharacterCards.com Character (Direct Link or ID)</span><br><span data-i18n="char_import_example">Example:</span> <tt>AICC/aicharcards/the-game-master</tt></li>
11 <li><span data-i18n="char_import_6">Direct PNG Link (refer to</span> <code>config.yaml</code><span data-i18n="char_import_7"> for allowed hosts)</span><br><span data-i18n="char_import_example">Example:</span> <tt>https://files.catbox.moe/notarealfile.png</tt></li>11 <li><span data-i18n="char_import_6">Direct PNG Link (refer to</span> <code>config.yaml</code><span data-i18n="char_import_7"> for allowed hosts)</span><br><span data-i18n="char_import_example">Example:</span> <tt>https://files.catbox.moe/notarealfile.png</tt></li>
12 <li><span data-i18n="char_import_8">RisuRealm Character (Direct Link)</span><br><span data-i18n="char_import_example">Example:</span> <tt>https://realm.risuai.net/character/3ca54c71-6efe-46a2-b9d0-4f62df23d712</tt></li>12 <li><span data-i18n="char_import_8">RisuRealm Character (Direct Link)</span><br><span data-i18n="char_import_example">Example:</span> <tt>https://realm.risuai.net/character/3ca54c71-6efe-46a2-b9d0-4f62df23d712</tt></li>
13 </ul>13 </ul>
public/scripts/templates/itemizationChat.html+1 -1
@@ -146,5 +146,5 @@
146</div>146</div>
147<hr>147<hr>
148<div id="rawPromptPopup" class="list-group">148<div id="rawPromptPopup" class="list-group">
149 <div id="rawPromptWrapper" class="tokenItemizingSubclass"></div>149 <div id="rawPromptWrapper" class="tokenItemizingMaintext"></div>
150</div>150</div>
public/scripts/textgen-models.js+15 -3
@@ -6,6 +6,7 @@ import { tokenizers } from './tokenizers.js';
6import { renderTemplateAsync } from './templates.js';6import { renderTemplateAsync } from './templates.js';
7import { POPUP_TYPE, callGenericPopup } from './popup.js';7import { POPUP_TYPE, callGenericPopup } from './popup.js';
8import { t } from './i18n.js';8import { t } from './i18n.js';
9import { accountStorage } from './util/AccountStorage.js';
910
10let mancerModels = [];11let mancerModels = [];
11let togetherModels = [];12let togetherModels = [];
@@ -54,6 +55,17 @@ const OPENROUTER_PROVIDERS = [
54 'xAI',55 'xAI',
55 'Cloudflare',56 'Cloudflare',
56 'SF Compute',57 'SF Compute',
58 'Minimax',
59 'Nineteen',
60 'Liquid',
61 'InferenceNet',
62 'Friendli',
63 'AionLabs',
64 'Alibaba',
65 'Nebius',
66 'Chutes',
67 'Kluster',
68 'Targon',
57 '01.AI',69 '01.AI',
58 'HuggingFace',70 'HuggingFace',
59 'Mancer',71 'Mancer',
@@ -330,7 +342,7 @@ export async function loadFeatherlessModels(data) {
330 populateClassSelection(data);342 populateClassSelection(data);
331343
332 // Retrieve the stored number of items per page or default to 10344 // Retrieve the stored number of items per page or default to 10
333 const perPage = Number(localStorage.getItem(storageKey)) || 10;345 const perPage = Number(accountStorage.getItem(storageKey)) || 10;
334346
335 // Initialize pagination347 // Initialize pagination
336 applyFiltersAndSort();348 applyFiltersAndSort();
@@ -406,7 +418,7 @@ export async function loadFeatherlessModels(data) {
406 },418 },
407 afterSizeSelectorChange: function (e) {419 afterSizeSelectorChange: function (e) {
408 const newPerPage = e.target.value;420 const newPerPage = e.target.value;
409 localStorage.setItem('Models_PerPage', newPerPage);421 accountStorage.setItem(storageKey, newPerPage);
410 setupPagination(models, Number(newPerPage), featherlessCurrentPage); // Use the stored current page number422 setupPagination(models, Number(newPerPage), featherlessCurrentPage); // Use the stored current page number
411 },423 },
412 });424 });
@@ -507,7 +519,7 @@ export async function loadFeatherlessModels(data) {
507 const currentModelIndex = filteredModels.findIndex(x => x.id === textgen_settings.featherless_model);519 const currentModelIndex = filteredModels.findIndex(x => x.id === textgen_settings.featherless_model);
508 featherlessCurrentPage = currentModelIndex >= 0 ? (currentModelIndex / perPage) + 1 : 1;520 featherlessCurrentPage = currentModelIndex >= 0 ? (currentModelIndex / perPage) + 1 : 1;
509521
510 setupPagination(filteredModels, Number(localStorage.getItem(storageKey)) || perPage, featherlessCurrentPage);522 setupPagination(filteredModels, Number(accountStorage.getItem(storageKey)) || perPage, featherlessCurrentPage);
511 }523 }
512524
513 // Required to keep the /model command function525 // Required to keep the /model command function
public/scripts/textgen-settings.js+45 -9
@@ -10,6 +10,7 @@ import {
10 setOnlineStatus,10 setOnlineStatus,
11 substituteParams,11 substituteParams,
12} from '../script.js';12} from '../script.js';
13import { t } from './i18n.js';
13import { BIAS_CACHE, createNewLogitBiasEntry, displayLogitBias, getLogitBiasListResult } from './logit-bias.js';14import { BIAS_CACHE, createNewLogitBiasEntry, displayLogitBias, getLogitBiasListResult } from './logit-bias.js';
1415
15import { power_user, registerDebugFunction } from './power-user.js';16import { power_user, registerDebugFunction } from './power-user.js';
@@ -172,6 +173,7 @@ const settings = {
172 //truncation_length: 2048,173 //truncation_length: 2048,
173 ban_eos_token: false,174 ban_eos_token: false,
174 skip_special_tokens: true,175 skip_special_tokens: true,
176 include_reasoning: true,
175 streaming: false,177 streaming: false,
176 mirostat_mode: 0,178 mirostat_mode: 0,
177 mirostat_tau: 5,179 mirostat_tau: 5,
@@ -181,6 +183,8 @@ const settings = {
181 grammar_string: '',183 grammar_string: '',
182 json_schema: {},184 json_schema: {},
183 banned_tokens: '',185 banned_tokens: '',
186 global_banned_tokens: '',
187 send_banned_tokens: true,
184 sampler_priority: OOBA_DEFAULT_ORDER,188 sampler_priority: OOBA_DEFAULT_ORDER,
185 samplers: LLAMACPP_DEFAULT_ORDER,189 samplers: LLAMACPP_DEFAULT_ORDER,
186 samplers_priorities: APHRODITE_DEFAULT_ORDER,190 samplers_priorities: APHRODITE_DEFAULT_ORDER,
@@ -263,6 +267,7 @@ export const setting_names = [
263 'add_bos_token',267 'add_bos_token',
264 'ban_eos_token',268 'ban_eos_token',
265 'skip_special_tokens',269 'skip_special_tokens',
270 'include_reasoning',
266 'streaming',271 'streaming',
267 'mirostat_mode',272 'mirostat_mode',
268 'mirostat_tau',273 'mirostat_tau',
@@ -272,6 +277,8 @@ export const setting_names = [
272 'grammar_string',277 'grammar_string',
273 'json_schema',278 'json_schema',
274 'banned_tokens',279 'banned_tokens',
280 'global_banned_tokens',
281 'send_banned_tokens',
275 'ignore_eos_token',282 'ignore_eos_token',
276 'spaces_between_special_tokens',283 'spaces_between_special_tokens',
277 'speculative_ngram',284 'speculative_ngram',
@@ -392,7 +399,7 @@ function getTokenizerForTokenIds() {
392 * @returns {TokenBanResult} String with comma-separated banned token IDs399 * @returns {TokenBanResult} String with comma-separated banned token IDs
393 */400 */
394function getCustomTokenBans() {401function getCustomTokenBans() {
395 if (!settings.banned_tokens && !textgenerationwebui_banned_in_macros.length) {402 if (!settings.send_banned_tokens || (!settings.banned_tokens && !settings.global_banned_tokens && !textgenerationwebui_banned_in_macros.length)) {
396 return {403 return {
397 banned_tokens: '',404 banned_tokens: '',
398 banned_strings: [],405 banned_strings: [],
@@ -402,8 +409,9 @@ function getCustomTokenBans() {
402 const tokenizer = getTokenizerForTokenIds();409 const tokenizer = getTokenizerForTokenIds();
403 const banned_tokens = [];410 const banned_tokens = [];
404 const banned_strings = [];411 const banned_strings = [];
405 const sequences = settings.banned_tokens412 const sequences = []
406 .split('\n')413 .concat(settings.banned_tokens.split('\n'))
414 .concat(settings.global_banned_tokens.split('\n'))
407 .concat(textgenerationwebui_banned_in_macros)415 .concat(textgenerationwebui_banned_in_macros)
408 .filter(x => x.length > 0)416 .filter(x => x.length > 0)
409 .filter(onlyUnique);417 .filter(onlyUnique);
@@ -451,6 +459,18 @@ function getCustomTokenBans() {
451}459}
452460
453/**461/**
462 * Sets the banned strings kill switch toggle.
463 * @param {boolean} isEnabled Kill switch state
464 * @param {string} title Label title
465 */
466function toggleBannedStringsKillSwitch(isEnabled, title) {
467 $('#send_banned_tokens_textgenerationwebui').prop('checked', isEnabled);
468 $('#send_banned_tokens_label').find('.menu_button').toggleClass('toggleEnabled', isEnabled).prop('title', title);
469 settings.send_banned_tokens = isEnabled;
470 saveSettingsDebounced();
471}
472
473/**
454 * Calculates logit bias object from the logit bias list.474 * Calculates logit bias object from the logit bias list.
455 * @returns {object} Logit bias object475 * @returns {object} Logit bias object
456 */476 */
@@ -501,7 +521,7 @@ export function loadTextGenSettings(data, loadedSettings) {
501 for (const [type, selector] of Object.entries(SERVER_INPUTS)) {521 for (const [type, selector] of Object.entries(SERVER_INPUTS)) {
502 const control = $(selector);522 const control = $(selector);
503 control.val(settings.server_urls[type] ?? '').on('input', function () {523 control.val(settings.server_urls[type] ?? '').on('input', function () {
504 settings.server_urls[type] = String($(this).val());524 settings.server_urls[type] = String($(this).val()).trim();
505 saveSettingsDebounced();525 saveSettingsDebounced();
506 });526 });
507 }527 }
@@ -592,6 +612,14 @@ function sortAphroditeItemsByOrder(orderArray) {
592}612}
593613
594jQuery(function () {614jQuery(function () {
615 $('#send_banned_tokens_textgenerationwebui').on('change', function () {
616 const checked = !!$(this).prop('checked');
617 toggleBannedStringsKillSwitch(checked,
618 checked
619 ? t`Banned tokens/strings are being sent in the request.`
620 : t`Banned tokens/strings are NOT being sent in the request.`);
621 });
622
595 $('#koboldcpp_order').sortable({623 $('#koboldcpp_order').sortable({
596 delay: getSortableDelay(),624 delay: getSortableDelay(),
597 stop: function () {625 stop: function () {
@@ -740,6 +768,7 @@ jQuery(function () {
740 'add_bos_token_textgenerationwebui': true,768 'add_bos_token_textgenerationwebui': true,
741 'temperature_last_textgenerationwebui': true,769 'temperature_last_textgenerationwebui': true,
742 'skip_special_tokens_textgenerationwebui': true,770 'skip_special_tokens_textgenerationwebui': true,
771 'include_reasoning_textgenerationwebui': true,
743 'top_a_textgenerationwebui': 0,772 'top_a_textgenerationwebui': 0,
744 'top_a_counter_textgenerationwebui': 0,773 'top_a_counter_textgenerationwebui': 0,
745 'mirostat_mode_textgenerationwebui': 0,774 'mirostat_mode_textgenerationwebui': 0,
@@ -929,6 +958,10 @@ function setSettingByName(setting, value, trigger) {
929 if (isCheckbox) {958 if (isCheckbox) {
930 const val = Boolean(value);959 const val = Boolean(value);
931 $(`#${setting}_textgenerationwebui`).prop('checked', val);960 $(`#${setting}_textgenerationwebui`).prop('checked', val);
961
962 if ('send_banned_tokens' === setting) {
963 $(`#${setting}_textgenerationwebui`).trigger('change');
964 }
932 }965 }
933 else if (isText) {966 else if (isText) {
934 $(`#${setting}_textgenerationwebui`).val(value);967 $(`#${setting}_textgenerationwebui`).val(value);
@@ -986,6 +1019,7 @@ export async function generateTextGenWithStreaming(generate_data, signal) {
986 let logprobs = null;1019 let logprobs = null;
987 const swipes = [];1020 const swipes = [];
988 const toolCalls = [];1021 const toolCalls = [];
1022 const state = { reasoning: '' };
989 while (true) {1023 while (true) {
990 const { done, value } = await reader.read();1024 const { done, value } = await reader.read();
991 if (done) return;1025 if (done) return;
@@ -1002,9 +1036,10 @@ export async function generateTextGenWithStreaming(generate_data, signal) {
1002 const newText = data?.choices?.[0]?.text || data?.content || '';1036 const newText = data?.choices?.[0]?.text || data?.content || '';
1003 text += newText;1037 text += newText;
1004 logprobs = parseTextgenLogprobs(newText, data.choices?.[0]?.logprobs || data?.completion_probabilities);1038 logprobs = parseTextgenLogprobs(newText, data.choices?.[0]?.logprobs || data?.completion_probabilities);
1039 state.reasoning += data?.choices?.[0]?.reasoning ?? '';
1005 }1040 }
10061041
1007 yield { text, swipes, logprobs, toolCalls };1042 yield { text, swipes, logprobs, toolCalls, state };
1008 }1043 }
1009 };1044 };
1010}1045}
@@ -1216,7 +1251,7 @@ function replaceMacrosInList(str) {
1216 }1251 }
1217}1252}
12181253
1219export function getTextGenGenerationData(finalPrompt, maxTokens, isImpersonate, isContinue, cfgValues, type) {1254export async function getTextGenGenerationData(finalPrompt, maxTokens, isImpersonate, isContinue, cfgValues, type) {
1220 const canMultiSwipe = !isContinue && !isImpersonate && type !== 'quiet';1255 const canMultiSwipe = !isContinue && !isImpersonate && type !== 'quiet';
1221 const dynatemp = isDynamicTemperatureSupported();1256 const dynatemp = isDynamicTemperatureSupported();
1222 const { banned_tokens, banned_strings } = getCustomTokenBans();1257 const { banned_tokens, banned_strings } = getCustomTokenBans();
@@ -1231,7 +1266,7 @@ export function getTextGenGenerationData(finalPrompt, maxTokens, isImpersonate,
1231 'top_p': settings.top_p,1266 'top_p': settings.top_p,
1232 'typical_p': settings.typical_p,1267 'typical_p': settings.typical_p,
1233 'typical': settings.typical_p,1268 'typical': settings.typical_p,
1234 'sampler_seed': settings.seed,1269 'sampler_seed': settings.seed >= 0 ? settings.seed : undefined,
1235 'min_p': settings.min_p,1270 'min_p': settings.min_p,
1236 'repetition_penalty': settings.rep_pen,1271 'repetition_penalty': settings.rep_pen,
1237 'frequency_penalty': settings.freq_pen,1272 'frequency_penalty': settings.freq_pen,
@@ -1265,6 +1300,7 @@ export function getTextGenGenerationData(finalPrompt, maxTokens, isImpersonate,
1265 'truncation_length': max_context,1300 'truncation_length': max_context,
1266 'ban_eos_token': settings.ban_eos_token,1301 'ban_eos_token': settings.ban_eos_token,
1267 'skip_special_tokens': settings.skip_special_tokens,1302 'skip_special_tokens': settings.skip_special_tokens,
1303 'include_reasoning': settings.include_reasoning,
1268 'top_a': settings.top_a,1304 'top_a': settings.top_a,
1269 'tfs': settings.tfs,1305 'tfs': settings.tfs,
1270 'epsilon_cutoff': [OOBA, MANCER].includes(settings.type) ? settings.epsilon_cutoff : undefined,1306 'epsilon_cutoff': [OOBA, MANCER].includes(settings.type) ? settings.epsilon_cutoff : undefined,
@@ -1294,7 +1330,7 @@ export function getTextGenGenerationData(finalPrompt, maxTokens, isImpersonate,
1294 'temperature_last': (settings.type === OOBA || settings.type === APHRODITE || settings.type == TABBY) ? settings.temperature_last : undefined,1330 'temperature_last': (settings.type === OOBA || settings.type === APHRODITE || settings.type == TABBY) ? settings.temperature_last : undefined,
1295 'speculative_ngram': settings.type === TABBY ? settings.speculative_ngram : undefined,1331 'speculative_ngram': settings.type === TABBY ? settings.speculative_ngram : undefined,
1296 'do_sample': settings.type === OOBA ? settings.do_sample : undefined,1332 'do_sample': settings.type === OOBA ? settings.do_sample : undefined,
1297 'seed': settings.seed,1333 'seed': settings.seed >= 0 ? settings.seed : undefined,
1298 'guidance_scale': cfgValues?.guidanceScale?.value ?? settings.guidance_scale ?? 1,1334 'guidance_scale': cfgValues?.guidanceScale?.value ?? settings.guidance_scale ?? 1,
1299 'negative_prompt': cfgValues?.negativePrompt ?? substituteParams(settings.negative_prompt) ?? '',1335 'negative_prompt': cfgValues?.negativePrompt ?? substituteParams(settings.negative_prompt) ?? '',
1300 'grammar_string': settings.grammar_string,1336 'grammar_string': settings.grammar_string,
@@ -1443,7 +1479,7 @@ export function getTextGenGenerationData(finalPrompt, maxTokens, isImpersonate,
1443 }1479 }
1444 }1480 }
14451481
1446 eventSource.emitAndWait(event_types.TEXT_COMPLETION_SETTINGS_READY, params);1482 await eventSource.emit(event_types.TEXT_COMPLETION_SETTINGS_READY, params);
14471483
1448 // Grammar conflicts with with json_schema1484 // Grammar conflicts with with json_schema
1449 if (settings.type === LLAMACPP) {1485 if (settings.type === LLAMACPP) {
public/scripts/tokenizers.js+3 -0
@@ -679,6 +679,9 @@ export function getTokenizerModel() {
679 }679 }
680680
681 if (oai_settings.chat_completion_source === chat_completion_sources.PERPLEXITY) {681 if (oai_settings.chat_completion_source === chat_completion_sources.PERPLEXITY) {
682 if (oai_settings.perplexity_model.includes('sonar-reasoning')) {
683 return deepseekTokenizer;
684 }
682 if (oai_settings.perplexity_model.includes('llama-3') || oai_settings.perplexity_model.includes('llama3')) {685 if (oai_settings.perplexity_model.includes('llama-3') || oai_settings.perplexity_model.includes('llama3')) {
683 return llama3Tokenizer;686 return llama3Tokenizer;
684 }687 }
public/scripts/tool-calling.js+1 -0
@@ -563,6 +563,7 @@ export class ToolManager {
563 chat_completion_sources.OPENROUTER,563 chat_completion_sources.OPENROUTER,
564 chat_completion_sources.GROQ,564 chat_completion_sources.GROQ,
565 chat_completion_sources.COHERE,565 chat_completion_sources.COHERE,
566 chat_completion_sources.DEEPSEEK,
566 ];567 ];
567 return supportedSources.includes(oai_settings.chat_completion_source);568 return supportedSources.includes(oai_settings.chat_completion_source);
568 }569 }
public/scripts/user.js+8 -0
@@ -44,6 +44,14 @@ export function isAdmin() {
44}44}
4545
46/**46/**
47 * Gets the handle string of the current user.
48 * @returns {string} User handle
49 */
50export function getCurrentUserHandle() {
51 return currentUser?.handle || 'default-user';
52}
53
54/**
47 * Get the current user.55 * Get the current user.
48 * @returns {Promise<void>}56 * @returns {Promise<void>}
49 */57 */
public/scripts/util/AccountStorage.js+139 -0
@@ -0,0 +1,139 @@
1import { saveSettingsDebounced } from '../../script.js';
2
3const MIGRATED_MARKER = '__migrated';
4const MIGRATABLE_KEYS = [
5 /^AlertRegex_/,
6 /^AlertWI_/,
7 /^Assets_SkipConfirm_/,
8 /^Characters_PerPage$/,
9 /^DataBank_sortField$/,
10 /^DataBank_sortOrder$/,
11 /^extension_update_nag$/,
12 /^extensions_sortByName$/,
13 /^FeatherlessModels_PerPage$/,
14 /^GroupMembers_PerPage$/,
15 /^GroupCandidates_PerPage$/,
16 /^LNavLockOn$/,
17 /^LNavOpened$/,
18 /^mediaWarningShown:/,
19 /^NavLockOn$/,
20 /^NavOpened$/,
21 /^Personas_PerPage$/,
22 /^Personas_GridView$/,
23 /^Proxy_SkipConfirm_/,
24 /^qr--executeShortcut$/,
25 /^qr--syntax$/,
26 /^qr--tabSize$/,
27 /^qr--wrap$/,
28 /^RegenerateWithCtrlEnter$/,
29 /^SelectedNavTab$/,
30 /^sendAsNamelessWarningShown$/,
31 /^StoryStringValidationCache$/,
32 /^WINavOpened$/,
33 /^WI_PerPage$/,
34 /^world_info_sort_order$/,
35];
36
37/**
38 * Provides access to account storage of arbitrary key-value pairs.
39 */
40class AccountStorage {
41 /**
42 * @type {Record<string, string>} Storage state
43 */
44 #state = {};
45
46 /**
47 * @type {boolean} If the storage was initialized
48 */
49 #ready = false;
50
51 #migrateLocalStorage() {
52 const localStorageKeys = [];
53 for (let i = 0; i < globalThis.localStorage.length; i++) {
54 localStorageKeys.push(globalThis.localStorage.key(i));
55 }
56 for (const key of localStorageKeys) {
57 if (MIGRATABLE_KEYS.some(k => k.test(key))) {
58 const value = globalThis.localStorage.getItem(key);
59 this.#state[key] = value;
60 globalThis.localStorage.removeItem(key);
61 }
62 }
63 }
64
65 /**
66 * Initialize the account storage.
67 * @param {Object} state Initial state
68 */
69 init(state) {
70 if (state && typeof state === 'object') {
71 this.#state = Object.assign(this.#state, state);
72 }
73
74 if (!Object.hasOwn(this.#state, MIGRATED_MARKER)) {
75 this.#migrateLocalStorage();
76 this.#state[MIGRATED_MARKER] = '1';
77 saveSettingsDebounced();
78 }
79
80 this.#ready = true;
81 }
82
83 /**
84 * Get the value of a key in account storage.
85 * @param {string} key Key to get
86 * @returns {string|null} Value of the key
87 */
88 getItem(key) {
89 if (!this.#ready) {
90 console.warn(`AccountStorage not ready (trying to read from ${key})`);
91 }
92
93 return Object.hasOwn(this.#state, key) ? String(this.#state[key]) : null;
94 }
95
96 /**
97 * Set a key in account storage.
98 * @param {string} key Key to set
99 * @param {string} value Value to set
100 */
101 setItem(key, value) {
102 if (!this.#ready) {
103 console.warn(`AccountStorage not ready (trying to write to ${key})`);
104 }
105
106 this.#state[key] = String(value);
107 saveSettingsDebounced();
108 }
109
110 /**
111 * Remove a key from account storage.
112 * @param {string} key Key to remove
113 */
114 removeItem(key) {
115 if (!this.#ready) {
116 console.warn(`AccountStorage not ready (trying to remove ${key})`);
117 }
118
119 if (!Object.hasOwn(this.#state, key)) {
120 return;
121 }
122
123 delete this.#state[key];
124 saveSettingsDebounced();
125 }
126
127 /**
128 * Gets a snapshot of the storage state.
129 * @returns {Record<string, string>} A deep clone of the storage state
130 */
131 getState() {
132 return structuredClone(this.#state);
133 }
134}
135
136/**
137 * Account storage instance.
138 */
139export const accountStorage = new AccountStorage();
public/scripts/utils.js+24 -7
@@ -1733,17 +1733,17 @@ export function hasAnimation(control) {
17331733
1734/**1734/**
1735 * Run an action once an animation on a control ends. If the control has no animation, the action will be executed immediately.1735 * Run an action once an animation on a control ends. If the control has no animation, the action will be executed immediately.
1736 *1736 * The action will be executed after the animation ends or after the timeout, whichever comes first.
1737 * @param {HTMLElement} control - The control element to listen for animation end event1737 * @param {HTMLElement} control - The control element to listen for animation end event
1738 * @param {(control:*?) => void} callback - The callback function to be executed when the animation ends1738 * @param {(control:*?) => void} callback - The callback function to be executed when the animation ends
1739 * @param {number} [timeout=500] - The timeout in milliseconds to wait for the animation to end before executing the callback
1739 */1740 */
1740export function runAfterAnimation(control, callback) {1741export function runAfterAnimation(control, callback, timeout = 500) {
1741 if (hasAnimation(control)) {1742 if (hasAnimation(control)) {
1742 const onAnimationEnd = () => {1743 Promise.race([
1743 control.removeEventListener('animationend', onAnimationEnd);1744 new Promise((r) => setTimeout(r, timeout)), // Fallback timeout
1744 callback(control);1745 new Promise((r) => control.addEventListener('animationend', r, { once: true })),
1745 };1746 ]).finally(() => callback(control));
1746 control.addEventListener('animationend', onAnimationEnd);
1747 } else {1747 } else {
1748 callback(control);1748 callback(control);
1749 }1749 }
@@ -2059,6 +2059,23 @@ export function toggleDrawer(drawer, expand = true) {
2059 }2059 }
2060}2060}
20612061
2062/**
2063 * Sets or removes a dataset property on an HTMLElement
2064 *
2065 * Utility function to make it easier to reset dataset properties on null, without them being "null" as value.
2066 *
2067 * @param {HTMLElement} element - The element to modify
2068 * @param {string} name - The name of the dataset property
2069 * @param {string|null} value - The value to set - If null, the dataset property will be removed
2070 */
2071export function setDatasetProperty(element, name, value) {
2072 if (value === null) {
2073 delete element.dataset[name];
2074 } else {
2075 element.dataset[name] = value;
2076 }
2077}
2078
2062export async function fetchFaFile(name) {2079export async function fetchFaFile(name) {
2063 const style = document.createElement('style');2080 const style = document.createElement('style');
2064 style.innerHTML = await (await fetch(`/css/${name}`)).text();2081 style.innerHTML = await (await fetch(`/css/${name}`)).text();
public/scripts/variables.js+4 -4
@@ -19,7 +19,7 @@ import { isFalseBoolean, convertValueType, isTrueBoolean } from './utils.js';
1919
20const MAX_LOOPS = 100;20const MAX_LOOPS = 100;
2121
22function getLocalVariable(name, args = {}) {22export function getLocalVariable(name, args = {}) {
23 if (!chat_metadata.variables) {23 if (!chat_metadata.variables) {
24 chat_metadata.variables = {};24 chat_metadata.variables = {};
25 }25 }
@@ -45,7 +45,7 @@ function getLocalVariable(name, args = {}) {
45 return (localVariable?.trim?.() === '' || isNaN(Number(localVariable))) ? (localVariable || '') : Number(localVariable);45 return (localVariable?.trim?.() === '' || isNaN(Number(localVariable))) ? (localVariable || '') : Number(localVariable);
46}46}
4747
48function setLocalVariable(name, value, args = {}) {48export function setLocalVariable(name, value, args = {}) {
49 if (!name) {49 if (!name) {
50 throw new Error('Variable name cannot be empty or undefined.');50 throw new Error('Variable name cannot be empty or undefined.');
51 }51 }
@@ -80,7 +80,7 @@ function setLocalVariable(name, value, args = {}) {
80 return value;80 return value;
81}81}
8282
83function getGlobalVariable(name, args = {}) {83export function getGlobalVariable(name, args = {}) {
84 let globalVariable = extension_settings.variables.global[args.key ?? name];84 let globalVariable = extension_settings.variables.global[args.key ?? name];
85 if (args.index !== undefined) {85 if (args.index !== undefined) {
86 try {86 try {
@@ -102,7 +102,7 @@ function getGlobalVariable(name, args = {}) {
102 return (globalVariable?.trim?.() === '' || isNaN(Number(globalVariable))) ? (globalVariable || '') : Number(globalVariable);102 return (globalVariable?.trim?.() === '' || isNaN(Number(globalVariable))) ? (globalVariable || '') : Number(globalVariable);
103}103}
104104
105function setGlobalVariable(name, value, args = {}) {105export function setGlobalVariable(name, value, args = {}) {
106 if (!name) {106 if (!name) {
107 throw new Error('Variable name cannot be empty or undefined.');107 throw new Error('Variable name cannot be empty or undefined.');
108 }108 }
public/scripts/world-info.js+36 -18
@@ -21,6 +21,7 @@ import { callGenericPopup, Popup, POPUP_TYPE } from './popup.js';
21import { StructuredCloneMap } from './util/StructuredCloneMap.js';21import { StructuredCloneMap } from './util/StructuredCloneMap.js';
22import { renderTemplateAsync } from './templates.js';22import { renderTemplateAsync } from './templates.js';
23import { t } from './i18n.js';23import { t } from './i18n.js';
24import { accountStorage } from './util/AccountStorage.js';
2425
25export const world_info_insertion_strategy = {26export const world_info_insertion_strategy = {
26 evenly: 0,27 evenly: 0,
@@ -400,6 +401,12 @@ class WorldInfoTimedEffects {
400 #entries = [];401 #entries = [];
401402
402 /**403 /**
404 * Is this a dry run?
405 * @type {boolean}
406 */
407 #isDryRun = false;
408
409 /**
403 * Buffer for active timed effects.410 * Buffer for active timed effects.
404 * @type {Record<TimedEffectType, WIScanEntry[]>}411 * @type {Record<TimedEffectType, WIScanEntry[]>}
405 */412 */
@@ -448,10 +455,12 @@ class WorldInfoTimedEffects {
448 * Initialize the timed effects with the given messages.455 * Initialize the timed effects with the given messages.
449 * @param {string[]} chat Array of chat messages456 * @param {string[]} chat Array of chat messages
450 * @param {WIScanEntry[]} entries Array of entries457 * @param {WIScanEntry[]} entries Array of entries
458 * @param {boolean} isDryRun Whether the operation is a dry run
451 */459 */
452 constructor(chat, entries) {460 constructor(chat, entries, isDryRun = false) {
453 this.#chat = chat;461 this.#chat = chat;
454 this.#entries = entries;462 this.#entries = entries;
463 this.#isDryRun = isDryRun;
455 this.#ensureChatMetadata();464 this.#ensureChatMetadata();
456 }465 }
457466
@@ -583,8 +592,10 @@ class WorldInfoTimedEffects {
583 * Checks for timed effects on chat messages.592 * Checks for timed effects on chat messages.
584 */593 */
585 checkTimedEffects() {594 checkTimedEffects() {
595 if (!this.#isDryRun) {
586 this.#checkTimedEffectOfType('sticky', this.#buffer.sticky, this.#onEnded.sticky.bind(this));596 this.#checkTimedEffectOfType('sticky', this.#buffer.sticky, this.#onEnded.sticky.bind(this));
587 this.#checkTimedEffectOfType('cooldown', this.#buffer.cooldown, this.#onEnded.cooldown.bind(this));597 this.#checkTimedEffectOfType('cooldown', this.#buffer.cooldown, this.#onEnded.cooldown.bind(this));
598 }
588 this.#checkDelayEffect(this.#buffer.delay);599 this.#checkDelayEffect(this.#buffer.delay);
589 }600 }
590601
@@ -629,6 +640,7 @@ class WorldInfoTimedEffects {
629 * @param {WIScanEntry[]} activatedEntries Entries that were activated640 * @param {WIScanEntry[]} activatedEntries Entries that were activated
630 */641 */
631 setTimedEffects(activatedEntries) {642 setTimedEffects(activatedEntries) {
643 if (this.#isDryRun) return;
632 for (const entry of activatedEntries) {644 for (const entry of activatedEntries) {
633 this.#setTimedEffectOfType('sticky', entry);645 this.#setTimedEffectOfType('sticky', entry);
634 this.#setTimedEffectOfType('cooldown', entry);646 this.#setTimedEffectOfType('cooldown', entry);
@@ -645,6 +657,9 @@ class WorldInfoTimedEffects {
645 if (!this.isValidEffectType(type)) {657 if (!this.isValidEffectType(type)) {
646 return;658 return;
647 }659 }
660 if (this.#isDryRun && type !== 'delay') {
661 return;
662 }
648663
649 const key = this.#getEntryKey(entry);664 const key = this.#getEntryKey(entry);
650 delete chat_metadata.timedWorldInfo[type][key];665 delete chat_metadata.timedWorldInfo[type][key];
@@ -858,7 +873,7 @@ export function setWorldInfoSettings(settings, data) {
858 $('#world_editor_select').append(`<option value='${i}'>${item}</option>`);873 $('#world_editor_select').append(`<option value='${i}'>${item}</option>`);
859 });874 });
860875
861 $('#world_info_sort_order').val(localStorage.getItem(SORT_ORDER_KEY) || '0');876 $('#world_info_sort_order').val(accountStorage.getItem(SORT_ORDER_KEY) || '0');
862 $('#world_info').trigger('change');877 $('#world_info').trigger('change');
863 $('#world_editor_select').trigger('change');878 $('#world_editor_select').trigger('change');
864879
@@ -1708,7 +1723,7 @@ export async function loadWorldInfo(name) {
1708 return null;1723 return null;
1709}1724}
17101725
1711async function updateWorldInfoList() {1726export async function updateWorldInfoList() {
1712 const result = await fetch('/api/settings/get', {1727 const result = await fetch('/api/settings/get', {
1713 method: 'POST',1728 method: 'POST',
1714 headers: getRequestHeaders(),1729 headers: getRequestHeaders(),
@@ -1933,13 +1948,13 @@ function displayWorldEntries(name, data, navigation = navigation_option.none, fl
1933 if (typeof navigation === 'number' && Number(navigation) >= 0) {1948 if (typeof navigation === 'number' && Number(navigation) >= 0) {
1934 const data = getDataArray();1949 const data = getDataArray();
1935 const uidIndex = data.findIndex(x => x.uid === navigation);1950 const uidIndex = data.findIndex(x => x.uid === navigation);
1936 const perPage = Number(localStorage.getItem(storageKey)) || perPageDefault;1951 const perPage = Number(accountStorage.getItem(storageKey)) || perPageDefault;
1937 startPage = Math.floor(uidIndex / perPage) + 1;1952 startPage = Math.floor(uidIndex / perPage) + 1;
1938 }1953 }
19391954
1940 $('#world_info_pagination').pagination({1955 $('#world_info_pagination').pagination({
1941 dataSource: getDataArray,1956 dataSource: getDataArray,
1942 pageSize: Number(localStorage.getItem(storageKey)) || perPageDefault,1957 pageSize: Number(accountStorage.getItem(storageKey)) || perPageDefault,
1943 sizeChangerOptions: [10, 25, 50, 100, 500, 1000],1958 sizeChangerOptions: [10, 25, 50, 100, 500, 1000],
1944 showSizeChanger: true,1959 showSizeChanger: true,
1945 pageRange: 1,1960 pageRange: 1,
@@ -1969,7 +1984,7 @@ function displayWorldEntries(name, data, navigation = navigation_option.none, fl
1969 worldEntriesList.append(blocks);1984 worldEntriesList.append(blocks);
1970 },1985 },
1971 afterSizeSelectorChange: function (e) {1986 afterSizeSelectorChange: function (e) {
1972 localStorage.setItem(storageKey, e.target.value);1987 accountStorage.setItem(storageKey, e.target.value);
1973 },1988 },
1974 afterPaging: function () {1989 afterPaging: function () {
1975 $('#world_popup_entries_list textarea[name="comment"]').each(function () {1990 $('#world_popup_entries_list textarea[name="comment"]').each(function () {
@@ -2174,7 +2189,7 @@ function verifyWorldInfoSearchSortRule() {
2174 // If search got cleared, we make sure to hide the option and go back to the one before2189 // If search got cleared, we make sure to hide the option and go back to the one before
2175 if (!searchTerm && !isHidden) {2190 if (!searchTerm && !isHidden) {
2176 searchOption.attr('hidden', '');2191 searchOption.attr('hidden', '');
2177 selector.val(localStorage.getItem(SORT_ORDER_KEY) || '0');2192 selector.val(accountStorage.getItem(SORT_ORDER_KEY) || '0');
2178 }2193 }
2179}2194}
21802195
@@ -2423,7 +2438,9 @@ export async function getWorldEntry(name, data, entry) {
2423 setWIOriginalDataValue(data, uid, originalDataValueName, data.entries[uid][entryPropName]);2438 setWIOriginalDataValue(data, uid, originalDataValueName, data.entries[uid][entryPropName]);
2424 await saveWorldInfo(name, data);2439 await saveWorldInfo(name, data);
2425 }2440 }
2441 $(this).toggleClass('empty', !data.entries[uid][entryPropName].length);
2426 });2442 });
2443 input.toggleClass('empty', !entry[entryPropName].length);
2427 input.on('select2:select', /** @type {function(*):void} */ event => updateWorldEntryKeyOptionsCache([event.params.data]));2444 input.on('select2:select', /** @type {function(*):void} */ event => updateWorldEntryKeyOptionsCache([event.params.data]));
2428 input.on('select2:unselect', /** @type {function(*):void} */ event => updateWorldEntryKeyOptionsCache([event.params.data], { remove: true }));2445 input.on('select2:unselect', /** @type {function(*):void} */ event => updateWorldEntryKeyOptionsCache([event.params.data], { remove: true }));
24292446
@@ -2458,6 +2475,7 @@ export async function getWorldEntry(name, data, entry) {
2458 data.entries[uid][entryPropName] = splitKeywordsAndRegexes(value);2475 data.entries[uid][entryPropName] = splitKeywordsAndRegexes(value);
2459 setWIOriginalDataValue(data, uid, originalDataValueName, data.entries[uid][entryPropName]);2476 setWIOriginalDataValue(data, uid, originalDataValueName, data.entries[uid][entryPropName]);
2460 await saveWorldInfo(name, data);2477 await saveWorldInfo(name, data);
2478 $(this).toggleClass('empty', !data.entries[uid][entryPropName].length);
2461 }2479 }
2462 });2480 });
2463 input.val(entry[entryPropName].join(', ')).trigger('input', { skipReset: true });2481 input.val(entry[entryPropName].join(', ')).trigger('input', { skipReset: true });
@@ -3435,7 +3453,7 @@ async function _save(name, data) {
3435 headers: getRequestHeaders(),3453 headers: getRequestHeaders(),
3436 body: JSON.stringify({ name: name, data: data }),3454 body: JSON.stringify({ name: name, data: data }),
3437 });3455 });
3438 eventSource.emit(event_types.WORLDINFO_UPDATED, name, data);3456 await eventSource.emit(event_types.WORLDINFO_UPDATED, name, data);
3439}3457}
34403458
34413459
@@ -3847,7 +3865,7 @@ export async function checkWorldInfo(chat, maxContext, isDryRun) {
3847 const context = getContext();3865 const context = getContext();
3848 const buffer = new WorldInfoBuffer(chat);3866 const buffer = new WorldInfoBuffer(chat);
38493867
3850 console.debug(`[WI] --- START WI SCAN (on ${chat.length} messages) ---`);3868 console.debug(`[WI] --- START WI SCAN (on ${chat.length} messages)${isDryRun ? ' (DRY RUN)' : ''} ---`);
38513869
3852 // Combine the chat3870 // Combine the chat
38533871
@@ -3879,9 +3897,9 @@ export async function checkWorldInfo(chat, maxContext, isDryRun) {
38793897
3880 console.debug(`[WI] Context size: ${maxContext}; WI budget: ${budget} (max% = ${world_info_budget}%, cap = ${world_info_budget_cap})`);3898 console.debug(`[WI] Context size: ${maxContext}; WI budget: ${budget} (max% = ${world_info_budget}%, cap = ${world_info_budget_cap})`);
3881 const sortedEntries = await getSortedEntries();3899 const sortedEntries = await getSortedEntries();
3882 const timedEffects = new WorldInfoTimedEffects(chat, sortedEntries);3900 const timedEffects = new WorldInfoTimedEffects(chat, sortedEntries, isDryRun);
38833901
3884 !isDryRun && timedEffects.checkTimedEffects();3902 timedEffects.checkTimedEffects();
38853903
3886 if (sortedEntries.length === 0) {3904 if (sortedEntries.length === 0) {
3887 return { worldInfoBefore: '', worldInfoAfter: '', WIDepthEntries: [], EMEntries: [], allActivatedEntries: new Set() };3905 return { worldInfoBefore: '', worldInfoAfter: '', WIDepthEntries: [], EMEntries: [], allActivatedEntries: new Set() };
@@ -4324,12 +4342,12 @@ export async function checkWorldInfo(chat, maxContext, isDryRun) {
4324 context.setExtensionPrompt(NOTE_MODULE_NAME, ANWithWI, chat_metadata[metadata_keys.position], chat_metadata[metadata_keys.depth], extension_settings.note.allowWIScan, chat_metadata[metadata_keys.role]);4342 context.setExtensionPrompt(NOTE_MODULE_NAME, ANWithWI, chat_metadata[metadata_keys.position], chat_metadata[metadata_keys.depth], extension_settings.note.allowWIScan, chat_metadata[metadata_keys.role]);
4325 }4343 }
43264344
4327 !isDryRun && timedEffects.setTimedEffects(Array.from(allActivatedEntries.values()));4345 timedEffects.setTimedEffects(Array.from(allActivatedEntries.values()));
4328 buffer.resetExternalEffects();4346 buffer.resetExternalEffects();
4329 timedEffects.cleanUp();4347 timedEffects.cleanUp();
43304348
4331 console.log(`[WI] Adding ${allActivatedEntries.size} entries to prompt`, Array.from(allActivatedEntries.values()));4349 console.log(`[WI] ${isDryRun ? 'Hypothetically adding' : 'Adding'} ${allActivatedEntries.size} entries to prompt`, Array.from(allActivatedEntries.values()));
4332 console.debug('[WI] --- DONE ---');4350 console.debug(`[WI] --- DONE${isDryRun ? ' (DRY RUN)' : ''} ---`);
43334351
4334 return { worldInfoBefore, worldInfoAfter, EMEntries, WIDepthEntries, allActivatedEntries: new Set(allActivatedEntries.values()) };4352 return { worldInfoBefore, worldInfoAfter, EMEntries, WIDepthEntries, allActivatedEntries: new Set(allActivatedEntries.values()) };
4335}4353}
@@ -4658,7 +4676,7 @@ function convertNovelLorebook(inputObj) {
4658 return outputObj;4676 return outputObj;
4659}4677}
46604678
4661function convertCharacterBook(characterBook) {4679export function convertCharacterBook(characterBook) {
4662 const result = { entries: {}, originalData: characterBook };4680 const result = { entries: {}, originalData: characterBook };
46634681
4664 characterBook.entries.forEach((entry, index) => {4682 characterBook.entries.forEach((entry, index) => {
@@ -4736,8 +4754,8 @@ export function checkEmbeddedWorld(chid) {
4736 // Only show the alert once per character4754 // Only show the alert once per character
4737 const checkKey = `AlertWI_${characters[chid].avatar}`;4755 const checkKey = `AlertWI_${characters[chid].avatar}`;
4738 const worldName = characters[chid]?.data?.extensions?.world;4756 const worldName = characters[chid]?.data?.extensions?.world;
4739 if (!localStorage.getItem(checkKey) && (!worldName || !world_names.includes(worldName))) {4757 if (!accountStorage.getItem(checkKey) && (!worldName || !world_names.includes(worldName))) {
4740 localStorage.setItem(checkKey, 'true');4758 accountStorage.setItem(checkKey, 'true');
47414759
4742 if (power_user.world_import_dialog) {4760 if (power_user.world_import_dialog) {
4743 const html = `<h3>This character has an embedded World/Lorebook.</h3>4761 const html = `<h3>This character has an embedded World/Lorebook.</h3>
@@ -5181,7 +5199,7 @@ jQuery(() => {
5181 $('#world_info_sort_order').on('change', function () {5199 $('#world_info_sort_order').on('change', function () {
5182 const value = String($(this).find(':selected').val());5200 const value = String($(this).find(':selected').val());
5183 // Save sort order, but do not save search sorting, as this is a temporary sorting option5201 // Save sort order, but do not save search sorting, as this is a temporary sorting option
5184 if (value !== 'search') localStorage.setItem(SORT_ORDER_KEY, value);5202 if (value !== 'search') accountStorage.setItem(SORT_ORDER_KEY, value);
5185 updateEditor(navigation_option.none);5203 updateEditor(navigation_option.none);
5186 });5204 });
51875205
public/style.css+240 -47
@@ -106,6 +106,8 @@
106 --tool-cool-color-picker-btn-bg: transparent;106 --tool-cool-color-picker-btn-bg: transparent;
107 --tool-cool-color-picker-btn-border-color: transparent;107 --tool-cool-color-picker-btn-border-color: transparent;
108108
109 --mes-right-spacing: 30px;
110
109 --avatar-base-height: 50px;111 --avatar-base-height: 50px;
110 --avatar-base-width: 50px;112 --avatar-base-width: 50px;
111 --avatar-base-border-radius: 2px;113 --avatar-base-border-radius: 2px;
@@ -260,6 +262,10 @@ input[type='checkbox']:focus-visible {
260 color: var(--SmartThemeEmColor);262 color: var(--SmartThemeEmColor);
261}263}
262264
265.tokenItemizingMaintext {
266 font-size: calc(var(--mainFontSize) * 0.8);
267}
268
263.tokenGraph {269.tokenGraph {
264 border-radius: 10px;270 border-radius: 10px;
265 border: 1px solid var(--SmartThemeBorderColor);271 border: 1px solid var(--SmartThemeBorderColor);
@@ -292,36 +298,44 @@ input[type='checkbox']:focus-visible {
292 filter: grayscale(25%);298 filter: grayscale(25%);
293}299}
294300
295.mes_text table {301.mes_text table,
302.mes_reasoning table {
296 border-spacing: 0;303 border-spacing: 0;
297 border-collapse: collapse;304 border-collapse: collapse;
298 margin-bottom: 10px;305 margin-bottom: 10px;
299}306}
300307
301.mes_text td,308.mes_text td,
302.mes_text th {309.mes_text th,
310.mes_reasoning td,
311.mes_reasoning th {
303 border: 1px solid;312 border: 1px solid;
304 border-collapse: collapse;313 border-collapse: collapse;
305 padding: 0.25em;314 padding: 0.25em;
306}315}
307316
308.mes_text p {317.mes_text p,
318.mes_reasoning p {
309 margin-top: 0;319 margin-top: 0;
310 margin-bottom: 10px;320 margin-bottom: 10px;
311}321}
312322
313.mes_text li tt {323.mes_text li tt,
324.mes_reasoning li tt {
314 display: inline-block;325 display: inline-block;
315}326}
316327
317.mes_text ol,328.mes_text ol,
318.mes_text ul {329.mes_text ul,
330.mes_reasoning ol,
331.mes_reasoning ul {
319 margin-top: 5px;332 margin-top: 5px;
320 margin-bottom: 5px;333 margin-bottom: 5px;
321}334}
322335
323.mes_text br,336.mes_text br,
324.mes_bias br {337.mes_bias br,
338.mes_reasoning br {
325 content: ' ';339 content: ' ';
326}340}
327341
@@ -332,25 +346,150 @@ input[type='checkbox']:focus-visible {
332 color: var(--SmartThemeQuoteColor);346 color: var(--SmartThemeQuoteColor);
333}347}
334348
349.mes_reasoning {
350 display: block;
351 border-left: 2px solid var(--SmartThemeEmColor);
352 border-radius: 2px;
353 padding: 5px;
354 padding-left: 14px;
355 margin-bottom: 0.5em;
356 overflow-y: auto;
357 color: var(--SmartThemeEmColor);
358}
359
360.mes_reasoning_details {
361 margin-right: var(--mes-right-spacing);
362}
363
364.mes_reasoning_details .mes_reasoning_summary {
365 list-style: none;
366 margin-right: calc(var(--mes-right-spacing) * -1);
367}
368
369.mes_reasoning_details summary::-webkit-details-marker {
370 display: none;
371}
372
373.mes_reasoning *:last-child {
374 margin-bottom: 0;
375}
376
377.mes_reasoning em,
378.mes_reasoning i,
379.mes_reasoning u,
380.mes_reasoning q,
381.mes_reasoning blockquote {
382 filter: saturate(0.5);
383}
384
385.mes_reasoning_details .mes_reasoning em {
386 color: color-mix(in srgb, var(--SmartThemeEmColor) 67%, var(--SmartThemeBlurTintColor) 33%);
387}
388
389.mes_reasoning_header_block {
390 flex-grow: 1;
391}
392
393.mes_reasoning_header {
394 cursor: pointer;
395 position: relative;
396 user-select: none;
397 margin: 0.5em 2px;
398 padding: 7px 14px;
399 padding-right: calc(0.7em + 14px);
400 border-radius: 5px;
401 background-color: var(--grey30);
402 font-size: calc(var(--mainFontSize) * 0.9);
403 align-items: baseline;
404}
405
406.mes:has(.mes_reasoning:empty) .mes_reasoning_header {
407 cursor: default;
408}
409
410/* TWIMC: Remove with custom CSS to show the icon */
411.mes_reasoning_header>.icon-svg {
412 display: none;
413}
414
415@supports not selector(:has(*)) {
416 .mes_reasoning_details {
417 display: none !important;
418 }
419}
420
421.mes_bias:empty,
422.mes:not(.reasoning) .mes_reasoning_details,
423.mes_reasoning_details:not([open]) .mes_reasoning_actions,
424.mes_reasoning_details:has(.reasoning_edit_textarea) .mes_reasoning,
425.mes_reasoning_details:has(.reasoning_edit_textarea) .mes_reasoning_header,
426.mes_reasoning_details:has(.reasoning_edit_textarea) .mes_reasoning_actions .mes_button:not(.edit_button),
427.mes_reasoning_details:not(:has(.reasoning_edit_textarea)) .mes_reasoning_actions .edit_button,
428.mes_block:has(.edit_textarea):has(.reasoning_edit_textarea) .mes_reasoning_actions,
429.mes.reasoning:not([data-reasoning-state="hidden"]) .mes_edit_add_reasoning,
430.mes:has(.mes_reasoning:empty) .mes_reasoning_arrow,
431.mes:has(.mes_reasoning:empty) .mes_reasoning,
432.mes:has(.mes_reasoning:empty) .mes_reasoning_copy {
433 display: none;
434}
435
436.mes[data-reasoning-state="hidden"] .mes_edit_add_reasoning {
437 background-color: color-mix(in srgb, var(--SmartThemeQuoteColor) 33%, var(--SmartThemeBlurTintColor) 66%);
438}
439
440/** If hidden reasoning should not be shown, we hide all blocks that don't have content */
441#chat:not([data-show-hidden-reasoning="true"]) .mes:has(.mes_reasoning:empty) .mes_reasoning_details {
442 display: none;
443}
444
445.mes_reasoning_details .mes_reasoning_arrow {
446 position: absolute;
447 top: 50%;
448 right: 7px;
449 transform: translateY(-50%);
450 font-size: calc(var(--mainFontSize) * 0.7);
451 width: calc(var(--mainFontSize) * 0.7);
452 height: calc(var(--mainFontSize) * 0.7);
453}
454
455.mes_reasoning_details:not([open]) .mes_reasoning_arrow {
456 transform: translateY(-50%) rotate(180deg);
457}
458
459.mes_reasoning_summary>span {
460 margin-left: 0.5em;
461}
462
335.mes_text i,463.mes_text i,
336.mes_text em {464.mes_text em,
465.mes_reasoning i,
466.mes_reasoning em {
337 color: var(--SmartThemeEmColor);467 color: var(--SmartThemeEmColor);
338}468}
339469
340.mes_text u {470.mes_text q i,
471.mes_text q em {
472 color: inherit;
473}
474
475.mes_text u,
476.mes_reasoning u {
341 color: var(--SmartThemeUnderlineColor);477 color: var(--SmartThemeUnderlineColor);
342}478}
343479
344.mes_text q {480.mes_text q,
481.mes_reasoning q {
345 color: var(--SmartThemeQuoteColor);482 color: var(--SmartThemeQuoteColor);
346}483}
347484
348.mes_text font[color] em,485.mes_text font[color] em,
349.mes_text font[color] i {486.mes_text font[color] i,
350 color: inherit;487.mes_text font[color] u,
351}488.mes_text font[color] q,
352489.mes_reasoning font[color] em,
353.mes_text font[color] q {490.mes_reasoning font[color] i,
491.mes_reasoning font[color] u,
492.mes_reasoning font[color] q {
354 color: inherit;493 color: inherit;
355}494}
356495
@@ -358,7 +497,8 @@ input[type='checkbox']:focus-visible {
358 display: block;497 display: block;
359}498}
360499
361.mes_text blockquote {500.mes_text blockquote,
501.mes_reasoning blockquote {
362 border-left: 3px solid var(--SmartThemeQuoteColor);502 border-left: 3px solid var(--SmartThemeQuoteColor);
363 padding-left: 10px;503 padding-left: 10px;
364 background-color: var(--black30a);504 background-color: var(--black30a);
@@ -368,18 +508,24 @@ input[type='checkbox']:focus-visible {
368.mes_text strong em,508.mes_text strong em,
369.mes_text strong,509.mes_text strong,
370.mes_text h2,510.mes_text h2,
371.mes_text h1 {511.mes_text h1,
512.mes_reasoning strong em,
513.mes_reasoning strong,
514.mes_reasoning h2,
515.mes_reasoning h1 {
372 font-weight: bold;516 font-weight: bold;
373}517}
374518
375.mes_text pre code {519.mes_text pre code,
520.mes_reasoning pre code {
376 position: relative;521 position: relative;
377 display: block;522 display: block;
378 overflow-x: auto;523 overflow-x: auto;
379 padding: 1em;524 padding: 1em;
380}525}
381526
382.mes_text img:not(.mes_img) {527.mes_text img:not(.mes_img),
528.mes_reasoning img:not(.mes_img) {
383 max-width: 100%;529 max-width: 100%;
384 max-height: var(--doc-height);530 max-height: var(--doc-height);
385}531}
@@ -1022,8 +1168,8 @@ body .panelControlBar {
1022 /*only affects bubblechat to make it sit nicely at the bottom*/1168 /*only affects bubblechat to make it sit nicely at the bottom*/
1023}1169}
10241170
1025.last_mes .mes_text {1171.last_mes:has(.mes_text:empty):has(.mes_reasoning_details) .mes_reasoning:not(:empty) {
1026 padding-right: 30px;1172 margin-bottom: var(--mes-right-spacing);
1027}1173}
10281174
1029/* SWIPE RELATED STYLES*/1175/* SWIPE RELATED STYLES*/
@@ -1235,14 +1381,19 @@ body.swipeAllMessages .mes:not(.last_mes) .swipes-counter {
1235 overflow-y: clip;1381 overflow-y: clip;
1236}1382}
12371383
1238.mes_text {1384.mes_text,
1385.mes_reasoning {
1239 font-weight: 500;1386 font-weight: 500;
1240 line-height: calc(var(--mainFontSize) + .5rem);1387 line-height: calc(var(--mainFontSize) + .5rem);
1388 max-width: 100%;
1389 overflow-wrap: anywhere;
1390}
1391
1392.mes_text {
1241 padding-left: 0;1393 padding-left: 0;
1242 padding-top: 5px;1394 padding-top: 5px;
1243 padding-bottom: 5px;1395 padding-bottom: 5px;
1244 max-width: 100%;1396 padding-right: var(--mes-right-spacing);
1245 overflow-wrap: anywhere;
1246}1397}
12471398
1248br {1399br {
@@ -2728,9 +2879,8 @@ select option:not(:checked) {
2728 color: var(--active) !important;2879 color: var(--active) !important;
2729}2880}
27302881
2731#instruct_enabled_label .menu_button:not(.toggleEnabled),2882.menu_button.togglable:not(.toggleEnabled) {
2732#sysprompt_enabled_label .menu_button:not(.toggleEnabled) {2883 color: red;
2733 color: Red;
2734}2884}
27352885
2736.displayBlock {2886.displayBlock {
@@ -2913,6 +3063,7 @@ input[type=search]:focus::-webkit-search-cancel-button {
2913.mes_block .ch_name {3063.mes_block .ch_name {
2914 max-width: 100%;3064 max-width: 100%;
2915 min-height: 22px;3065 min-height: 22px;
3066 align-items: flex-start;
2916}3067}
29173068
2918/*applies to both groups and solos chars in the char list*/3069/*applies to both groups and solos chars in the char list*/
@@ -2921,7 +3072,7 @@ input[type=search]:focus::-webkit-search-cancel-button {
2921 position: relative;3072 position: relative;
2922}3073}
29233074
2924#rm_print_characters_block .ch_name,3075.character_name_block .ch_name,
2925.avatar-container .ch_name {3076.avatar-container .ch_name {
2926 flex: 1 1 auto;3077 flex: 1 1 auto;
2927 white-space: nowrap;3078 white-space: nowrap;
@@ -2931,6 +3082,13 @@ input[type=search]:focus::-webkit-search-cancel-button {
2931 display: block;3082 display: block;
2932}3083}
29333084
3085.character_name_block .character_version {
3086 text-overflow: ellipsis;
3087 overflow: hidden;
3088 text-wrap: nowrap;
3089 max-width: 50%;
3090}
3091
2934#rm_print_characters_block .character_name_block> :last-child {3092#rm_print_characters_block .character_name_block> :last-child {
2935 flex: 0 100000 auto;3093 flex: 0 100000 auto;
2936 /* Force shrinking first */3094 /* Force shrinking first */
@@ -4130,7 +4288,13 @@ input[type="range"]::-webkit-slider-thumb {
4130 transition: 0.3s ease-in-out;4288 transition: 0.3s ease-in-out;
4131}4289}
41324290
4133.mes_edit_buttons .menu_button {4291.mes_reasoning_actions {
4292 margin: 0;
4293 margin-top: 0.5em;
4294}
4295
4296.mes_edit_buttons .menu_button,
4297.mes_reasoning_actions .edit_button {
4134 opacity: 0.5;4298 opacity: 0.5;
4135 padding: 0px;4299 padding: 0px;
4136 font-size: 1rem;4300 font-size: 1rem;
@@ -4143,10 +4307,18 @@ input[type="range"]::-webkit-slider-thumb {
4143 align-items: center;4307 align-items: center;
4144}4308}
41454309
4310.mes_reasoning_actions .edit_button {
4311 margin-bottom: 0.5em;
4312 opacity: 1;
4313 filter: brightness(0.7);
4314}
4315
4316.mes_reasoning_edit_cancel,
4146.mes_edit_cancel.menu_button {4317.mes_edit_cancel.menu_button {
4147 background-color: var(--crimson70a);4318 background-color: var(--crimson70a);
4148}4319}
41494320
4321.mes_reasoning_edit_done,
4150.mes_edit_done.menu_button {4322.mes_edit_done.menu_button {
4151 background-color: var(--okGreen70a);4323 background-color: var(--okGreen70a);
4152}4324}
@@ -4155,6 +4327,7 @@ input[type="range"]::-webkit-slider-thumb {
4155 opacity: 1;4327 opacity: 1;
4156}4328}
41574329
4330.reasoning_edit_textarea,
4158.edit_textarea {4331.edit_textarea {
4159 padding: 5px;4332 padding: 5px;
4160 margin: 0;4333 margin: 0;
@@ -4166,6 +4339,14 @@ input[type="range"]::-webkit-slider-thumb {
4166 field-sizing: content;4339 field-sizing: content;
4167}4340}
41684341
4342body[data-generating="true"] #send_but,
4343body[data-generating="true"] #mes_continue,
4344body[data-generating="true"] #mes_impersonate,
4345body[data-generating="true"] #chat .last_mes .mes_buttons,
4346body[data-generating="true"] #chat .last_mes .mes_reasoning_actions {
4347 display: none;
4348}
4349
4169#anchor_order {4350#anchor_order {
4170 margin-bottom: 15px;4351 margin-bottom: 15px;
4171}4352}
@@ -4505,23 +4686,6 @@ body .ui-widget-content li:hover {
4505 opacity: 1;4686 opacity: 1;
4506}4687}
45074688
4508.typing_indicator {
4509 position: sticky;
4510 bottom: 10px;
4511 margin: 10px;
4512 opacity: 0.85;
4513 text-shadow: 0px 0px calc(var(--shadowWidth) * 1px) var(--SmartThemeShadowColor);
4514 order: 9999;
4515}
4516
4517.typing_indicator:after {
4518 display: inline-block;
4519 vertical-align: bottom;
4520 animation: ellipsis steps(4, end) 1500ms infinite;
4521 content: "";
4522 width: 0px;
4523}
4524
4525#group_avatar_preview .missing-avatar {4689#group_avatar_preview .missing-avatar {
4526 display: inline;4690 display: inline;
4527 vertical-align: middle;4691 vertical-align: middle;
@@ -5610,11 +5774,13 @@ body:not(.movingUI) .drawer-content.maximized {
5610 overflow-wrap: anywhere;5774 overflow-wrap: anywhere;
5611}5775}
56125776
5777#SystemPromptColumn summary,
5613#InstructSequencesColumn summary {5778#InstructSequencesColumn summary {
5614 font-size: 0.95em;5779 font-size: 0.95em;
5615 cursor: pointer;5780 cursor: pointer;
5616}5781}
56175782
5783#SystemPromptColumn details,
5618#InstructSequencesColumn details:not(:last-of-type) {5784#InstructSequencesColumn details:not(:last-of-type) {
5619 margin-bottom: 5px;5785 margin-bottom: 5px;
5620}5786}
@@ -5643,6 +5809,7 @@ body:not(.movingUI) .drawer-content.maximized {
56435809
5644.model-card .details-container {5810.model-card .details-container {
5645 text-align: right;5811 text-align: right;
5812 line-height: 0.9;
5646}5813}
56475814
5648.model-card:hover {5815.model-card:hover {
@@ -5665,7 +5832,7 @@ body:not(.movingUI) .drawer-content.maximized {
5665}5832}
56665833
5667.model-title {5834.model-title {
5668 font-size: 13px;5835 font-size: calc(var(--mainFontSize) * 0.95);
5669 font-weight: bold;5836 font-weight: bold;
5670 overflow: hidden;5837 overflow: hidden;
5671}5838}
@@ -5681,7 +5848,7 @@ body:not(.movingUI) .drawer-content.maximized {
5681.model-class,5848.model-class,
5682.model-context-length,5849.model-context-length,
5683.model-date-added {5850.model-date-added {
5684 font-size: 10px;5851 font-size: calc(var(--mainFontSize) * 0.75);
5685}5852}
56865853
5687.model-class,5854.model-class,
@@ -5763,3 +5930,29 @@ body:not(.movingUI) .drawer-content.maximized {
5763.alternate_greetings_list {5930.alternate_greetings_list {
5764 overflow-y: scroll;5931 overflow-y: scroll;
5765}5932}
5933
5934.mes_text div[data-type="assistant_note"]:has(.assistant_note_export) {
5935 display: flex;
5936 flex-direction: row;
5937 flex-wrap: nowrap;
5938 justify-content: space-between;
5939 align-items: center;
5940 gap: 10px;
5941 padding: 0 2px;
5942}
5943
5944.mes_text div[data-type="assistant_note"]:has(.assistant_note_export)>div:not(.assistant_note_export) {
5945 flex: 1;
5946}
5947
5948.oneline-dropdown label {
5949 margin-top: 3px;
5950 margin-bottom: 5px;
5951 flex-grow: 1;
5952 text-align: left;
5953}
5954
5955.oneline-dropdown select {
5956 min-width: fit-content;
5957 width: 40%;
5958}
server.js+269 -60
@@ -4,6 +4,7 @@
4import fs from 'node:fs';4import fs from 'node:fs';
5import http from 'node:http';5import http from 'node:http';
6import https from 'node:https';6import https from 'node:https';
7import os from 'os';
7import path from 'node:path';8import path from 'node:path';
8import util from 'node:util';9import util from 'node:util';
9import net from 'node:net';10import net from 'node:net';
@@ -18,10 +19,9 @@ import { hideBin } from 'yargs/helpers';
1819
19// express/server related library imports20// express/server related library imports
20import cors from 'cors';21import cors from 'cors';
21import { doubleCsrf } from 'csrf-csrf';22import { csrfSync } from 'csrf-sync';
22import express from 'express';23import express from 'express';
23import compression from 'compression';24import compression from 'compression';
24import cookieParser from 'cookie-parser';
25import cookieSession from 'cookie-session';25import cookieSession from 'cookie-session';
26import multer from 'multer';26import multer from 'multer';
27import responseTime from 'response-time';27import responseTime from 'response-time';
@@ -30,6 +30,7 @@ import bodyParser from 'body-parser';
3030
31// net related library imports31// net related library imports
32import fetch from 'node-fetch';32import fetch from 'node-fetch';
33import ipRegex from 'ip-regex';
3334
34// Unrestrict console logs display limit35// Unrestrict console logs display limit
35util.inspect.defaultOptions.maxArrayLength = null;36util.inspect.defaultOptions.maxArrayLength = null;
@@ -40,7 +41,6 @@ util.inspect.defaultOptions.depth = 4;
40import { loadPlugins } from './src/plugin-loader.js';41import { loadPlugins } from './src/plugin-loader.js';
41import {42import {
42 initUserStorage,43 initUserStorage,
43 getCsrfSecret,
44 getCookieSecret,44 getCookieSecret,
45 getCookieSessionName,45 getCookieSessionName,
46 getAllEnabledUsers,46 getAllEnabledUsers,
@@ -60,6 +60,7 @@ import basicAuthMiddleware from './src/middleware/basicAuth.js';
60import whitelistMiddleware from './src/middleware/whitelist.js';60import whitelistMiddleware from './src/middleware/whitelist.js';
61import multerMonkeyPatch from './src/middleware/multerMonkeyPatch.js';61import multerMonkeyPatch from './src/middleware/multerMonkeyPatch.js';
62import initRequestProxy from './src/request-proxy.js';62import initRequestProxy from './src/request-proxy.js';
63import getCacheBusterMiddleware from './src/middleware/cacheBuster.js';
63import {64import {
64 getVersion,65 getVersion,
65 getConfigValue,66 getConfigValue,
@@ -67,6 +68,11 @@ import {
67 forwardFetchResponse,68 forwardFetchResponse,
68 removeColorFormatting,69 removeColorFormatting,
69 getSeparator,70 getSeparator,
71 stringToBool,
72 urlHostnameToIPv6,
73 canResolve,
74 safeReadFileSync,
75 setupLogLevel,
70} from './src/util.js';76} from './src/util.js';
71import { UPLOADS_DIRECTORY } from './src/constants.js';77import { UPLOADS_DIRECTORY } from './src/constants.js';
72import { ensureThumbnailCache } from './src/endpoints/thumbnails.js';78import { ensureThumbnailCache } from './src/endpoints/thumbnails.js';
@@ -126,6 +132,8 @@ if (process.versions && process.versions.node && process.versions.node.match(/20
126const DEFAULT_PORT = 8000;132const DEFAULT_PORT = 8000;
127const DEFAULT_AUTORUN = false;133const DEFAULT_AUTORUN = false;
128const DEFAULT_LISTEN = false;134const DEFAULT_LISTEN = false;
135const DEFAULT_LISTEN_ADDRESS_IPV6 = '[::]';
136const DEFAULT_LISTEN_ADDRESS_IPV4 = '0.0.0.0';
129const DEFAULT_CORS_PROXY = false;137const DEFAULT_CORS_PROXY = false;
130const DEFAULT_WHITELIST = true;138const DEFAULT_WHITELIST = true;
131const DEFAULT_ACCOUNTS = false;139const DEFAULT_ACCOUNTS = false;
@@ -150,11 +158,11 @@ const DEFAULT_PROXY_BYPASS = [];
150const cliArguments = yargs(hideBin(process.argv))158const cliArguments = yargs(hideBin(process.argv))
151 .usage('Usage: <your-start-script> <command> [options]')159 .usage('Usage: <your-start-script> <command> [options]')
152 .option('enableIPv6', {160 .option('enableIPv6', {
153 type: 'boolean',161 type: 'string',
154 default: null,162 default: null,
155 describe: `Enables IPv6.\n[config default: ${DEFAULT_ENABLE_IPV6}]`,163 describe: `Enables IPv6.\n[config default: ${DEFAULT_ENABLE_IPV6}]`,
156 }).option('enableIPv4', {164 }).option('enableIPv4', {
157 type: 'boolean',165 type: 'string',
158 default: null,166 default: null,
159 describe: `Enables IPv4.\n[config default: ${DEFAULT_ENABLE_IPV4}]`,167 describe: `Enables IPv4.\n[config default: ${DEFAULT_ENABLE_IPV4}]`,
160 }).option('port', {168 }).option('port', {
@@ -181,6 +189,14 @@ const cliArguments = yargs(hideBin(process.argv))
181 type: 'boolean',189 type: 'boolean',
182 default: null,190 default: null,
183 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}]`,191 describe: `SillyTavern is listening on all network interfaces (Wi-Fi, LAN, localhost). If false, will limit it only to internal localhost (127.0.0.1).\nIf not provided falls back to yaml config 'listen'.\n[config default: ${DEFAULT_LISTEN}]`,
192 }).option('listenAddressIPv6', {
193 type: 'string',
194 default: null,
195 describe: 'Set SillyTavern to listen to a specific IPv6 address. If not set, it will fallback to listen to all.\n[config default: [::] ]',
196 }).option('listenAddressIPv4', {
197 type: 'string',
198 default: null,
199 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 ]',
184 }).option('corsProxy', {200 }).option('corsProxy', {
185 type: 'boolean',201 type: 'boolean',
186 default: null,202 default: null,
@@ -243,27 +259,46 @@ app.use(helmet({
243app.use(compression());259app.use(compression());
244app.use(responseTime());260app.use(responseTime());
245261
262
263/** @type {number} */
246const server_port = cliArguments.port ?? process.env.SILLY_TAVERN_PORT ?? getConfigValue('port', DEFAULT_PORT);264const server_port = cliArguments.port ?? process.env.SILLY_TAVERN_PORT ?? getConfigValue('port', DEFAULT_PORT);
265/** @type {boolean} */
247const autorun = (cliArguments.autorun ?? getConfigValue('autorun', DEFAULT_AUTORUN)) && !cliArguments.ssl;266const autorun = (cliArguments.autorun ?? getConfigValue('autorun', DEFAULT_AUTORUN)) && !cliArguments.ssl;
267/** @type {boolean} */
248const listen = cliArguments.listen ?? getConfigValue('listen', DEFAULT_LISTEN);268const listen = cliArguments.listen ?? getConfigValue('listen', DEFAULT_LISTEN);
269/** @type {string} */
270const listenAddressIPv6 = cliArguments.listenAddressIPv6 ?? getConfigValue('listenAddress.ipv6', DEFAULT_LISTEN_ADDRESS_IPV6);
271/** @type {string} */
272const listenAddressIPv4 = cliArguments.listenAddressIPv4 ?? getConfigValue('listenAddress.ipv4', DEFAULT_LISTEN_ADDRESS_IPV4);
273/** @type {boolean} */
249const enableCorsProxy = cliArguments.corsProxy ?? getConfigValue('enableCorsProxy', DEFAULT_CORS_PROXY);274const enableCorsProxy = cliArguments.corsProxy ?? getConfigValue('enableCorsProxy', DEFAULT_CORS_PROXY);
250const enableWhitelist = cliArguments.whitelist ?? getConfigValue('whitelistMode', DEFAULT_WHITELIST);275const enableWhitelist = cliArguments.whitelist ?? getConfigValue('whitelistMode', DEFAULT_WHITELIST);
276/** @type {string} */
251const dataRoot = cliArguments.dataRoot ?? getConfigValue('dataRoot', './data');277const dataRoot = cliArguments.dataRoot ?? getConfigValue('dataRoot', './data');
278/** @type {boolean} */
252const disableCsrf = cliArguments.disableCsrf ?? getConfigValue('disableCsrfProtection', DEFAULT_CSRF_DISABLED);279const disableCsrf = cliArguments.disableCsrf ?? getConfigValue('disableCsrfProtection', DEFAULT_CSRF_DISABLED);
253const basicAuthMode = cliArguments.basicAuthMode ?? getConfigValue('basicAuthMode', DEFAULT_BASIC_AUTH);280const basicAuthMode = cliArguments.basicAuthMode ?? getConfigValue('basicAuthMode', DEFAULT_BASIC_AUTH);
254const perUserBasicAuth = getConfigValue('perUserBasicAuth', DEFAULT_PER_USER_BASIC_AUTH);281const perUserBasicAuth = getConfigValue('perUserBasicAuth', DEFAULT_PER_USER_BASIC_AUTH);
282/** @type {boolean} */
255const enableAccounts = getConfigValue('enableUserAccounts', DEFAULT_ACCOUNTS);283const enableAccounts = getConfigValue('enableUserAccounts', DEFAULT_ACCOUNTS);
256284
257const uploadsPath = path.join(dataRoot, UPLOADS_DIRECTORY);285const uploadsPath = path.join(dataRoot, UPLOADS_DIRECTORY);
258286
259const enableIPv6 = cliArguments.enableIPv6 ?? getConfigValue('protocol.ipv6', DEFAULT_ENABLE_IPV6);
260const enableIPv4 = cliArguments.enableIPv4 ?? getConfigValue('protocol.ipv4', DEFAULT_ENABLE_IPV4);
261287
288/** @type {boolean | "auto"} */
289let enableIPv6 = stringToBool(cliArguments.enableIPv6) ?? getConfigValue('protocol.ipv6', DEFAULT_ENABLE_IPV6);
290/** @type {boolean | "auto"} */
291let enableIPv4 = stringToBool(cliArguments.enableIPv4) ?? getConfigValue('protocol.ipv4', DEFAULT_ENABLE_IPV4);
292
293/** @type {string} */
262const autorunHostname = cliArguments.autorunHostname ?? getConfigValue('autorunHostname', DEFAULT_AUTORUN_HOSTNAME);294const autorunHostname = cliArguments.autorunHostname ?? getConfigValue('autorunHostname', DEFAULT_AUTORUN_HOSTNAME);
295/** @type {number} */
263const autorunPortOverride = cliArguments.autorunPortOverride ?? getConfigValue('autorunPortOverride', DEFAULT_AUTORUN_PORT);296const autorunPortOverride = cliArguments.autorunPortOverride ?? getConfigValue('autorunPortOverride', DEFAULT_AUTORUN_PORT);
264297
298/** @type {boolean} */
265const dnsPreferIPv6 = cliArguments.dnsPreferIPv6 ?? getConfigValue('dnsPreferIPv6', DEFAULT_PREFER_IPV6);299const dnsPreferIPv6 = cliArguments.dnsPreferIPv6 ?? getConfigValue('dnsPreferIPv6', DEFAULT_PREFER_IPV6);
266300
301/** @type {boolean} */
267const avoidLocalhost = cliArguments.avoidLocalhost ?? getConfigValue('avoidLocalhost', DEFAULT_AVOID_LOCALHOST);302const avoidLocalhost = cliArguments.avoidLocalhost ?? getConfigValue('avoidLocalhost', DEFAULT_AVOID_LOCALHOST);
268303
269const proxyEnabled = cliArguments.requestProxyEnabled ?? getConfigValue('requestProxy.enabled', DEFAULT_PROXY_ENABLED);304const proxyEnabled = cliArguments.requestProxyEnabled ?? getConfigValue('requestProxy.enabled', DEFAULT_PROXY_ENABLED);
@@ -280,7 +315,19 @@ if (dnsPreferIPv6) {
280 console.log('Preferring IPv4 for DNS resolution');315 console.log('Preferring IPv4 for DNS resolution');
281}316}
282317
283if (!enableIPv6 && !enableIPv4) {318
319const ipOptions = [true, 'auto', false];
320
321if (!ipOptions.includes(enableIPv6)) {
322 console.warn(color.red('`protocol: ipv6` option invalid'), '\n use:', ipOptions, '\n setting to:', DEFAULT_ENABLE_IPV6);
323 enableIPv6 = DEFAULT_ENABLE_IPV6;
324}
325if (!ipOptions.includes(enableIPv4)) {
326 console.warn(color.red('`protocol: ipv4` option invalid'), '\n use:', ipOptions, '\n setting to:', DEFAULT_ENABLE_IPV4);
327 enableIPv4 = DEFAULT_ENABLE_IPV4;
328}
329
330if (enableIPv6 === false && enableIPv4 === false) {
284 console.error('error: You can\'t disable all internet protocols: at least IPv6 or IPv4 must be enabled.');331 console.error('error: You can\'t disable all internet protocols: at least IPv6 or IPv4 must be enabled.');
285 process.exit(1);332 process.exit(1);
286}333}
@@ -347,8 +394,8 @@ if (enableCorsProxy) {
347}394}
348395
349function getSessionCookieAge() {396function getSessionCookieAge() {
350 // Defaults to 24 hours in seconds if not set397 // Defaults to "no expiration" if not set
351 const configValue = getConfigValue('sessionTimeout', 24 * 60 * 60);398 const configValue = getConfigValue('sessionTimeout', -1);
352399
353 // Convert to milliseconds400 // Convert to milliseconds
354 if (configValue > 0) {401 if (configValue > 0) {
@@ -365,6 +412,55 @@ function getSessionCookieAge() {
365 return undefined;412 return undefined;
366}413}
367414
415/**
416 * Checks the network interfaces to determine the presence of IPv6 and IPv4 addresses.
417 *
418 * @returns {Promise<[boolean, boolean, boolean, boolean]>} A promise that resolves to an array containing:
419 * - [0]: `hasIPv6` (boolean) - Whether the computer has any IPv6 address, including (`::1`).
420 * - [1]: `hasIPv4` (boolean) - Whether the computer has any IPv4 address, including (`127.0.0.1`).
421 * - [2]: `hasIPv6Local` (boolean) - Whether the computer has local IPv6 address (`::1`).
422 * - [3]: `hasIPv4Local` (boolean) - Whether the computer has local IPv4 address (`127.0.0.1`).
423 */
424async function getHasIP() {
425 let hasIPv6 = false;
426 let hasIPv6Local = false;
427
428 let hasIPv4 = false;
429 let hasIPv4Local = false;
430
431 const interfaces = os.networkInterfaces();
432
433 for (const iface of Object.values(interfaces)) {
434 if (iface === undefined) {
435 continue;
436 }
437
438 for (const info of iface) {
439 if (info.family === 'IPv6') {
440 hasIPv6 = true;
441 if (info.address === '::1') {
442 hasIPv6Local = true;
443 }
444 }
445
446 if (info.family === 'IPv4') {
447 hasIPv4 = true;
448 if (info.address === '127.0.0.1') {
449 hasIPv4Local = true;
450 }
451 }
452 if (hasIPv6 && hasIPv4 && hasIPv6Local && hasIPv4Local) break;
453 }
454 if (hasIPv6 && hasIPv4 && hasIPv6Local && hasIPv4Local) break;
455 }
456 return [
457 hasIPv6,
458 hasIPv4,
459 hasIPv6Local,
460 hasIPv4Local,
461 ];
462}
463
368app.use(cookieSession({464app.use(cookieSession({
369 name: getCookieSessionName(),465 name: getCookieSessionName(),
370 sameSite: 'strict',466 sameSite: 'strict',
@@ -377,27 +473,38 @@ app.use(setUserDataMiddleware);
377473
378// CSRF Protection //474// CSRF Protection //
379if (!disableCsrf) {475if (!disableCsrf) {
380 const COOKIES_SECRET = getCookieSecret();476 const csrfSyncProtection = csrfSync({
381477 getTokenFromState: (req) => {
382 const { generateToken, doubleCsrfProtection } = doubleCsrf({478 if (!req.session) {
383 getSecret: getCsrfSecret,479 console.error('(CSRF error) getTokenFromState: Session object not initialized');
384 cookieName: 'X-CSRF-Token',480 return;
385 cookieOptions: {481 }
386 sameSite: 'strict',482 return req.session.csrfToken;
387 secure: false,483 },
484 getTokenFromRequest: (req) => {
485 return req.headers['x-csrf-token']?.toString();
486 },
487 storeTokenInState: (req, token) => {
488 if (!req.session) {
489 console.error('(CSRF error) storeTokenInState: Session object not initialized');
490 return;
491 }
492 req.session.csrfToken = token;
388 },493 },
389 size: 64,494 size: 32,
390 getTokenFromRequest: (req) => req.headers['x-csrf-token'],
391 });495 });
392496
393 app.get('/csrf-token', (req, res) => {497 app.get('/csrf-token', (req, res) => {
394 res.json({498 res.json({
395 'token': generateToken(res, req),499 'token': csrfSyncProtection.generateToken(req),
396 });500 });
397 });501 });
398502
399 app.use(cookieParser(COOKIES_SECRET));503 // Customize the error message
400 app.use(doubleCsrfProtection);504 csrfSyncProtection.invalidCsrfTokenError.message = color.red('Invalid CSRF token. Please refresh the page and try again.');
505 csrfSyncProtection.invalidCsrfTokenError.stack = undefined;
506
507 app.use(csrfSyncProtection.csrfSynchronisedProtection);
401} else {508} else {
402 console.warn('\nCSRF protection is disabled. This will make your server vulnerable to CSRF attacks.\n');509 console.warn('\nCSRF protection is disabled. This will make your server vulnerable to CSRF attacks.\n');
403 app.get('/csrf-token', (req, res) => {510 app.get('/csrf-token', (req, res) => {
@@ -409,7 +516,7 @@ if (!disableCsrf) {
409516
410// Static files517// Static files
411// Host index page518// Host index page
412app.get('/', (request, response) => {519app.get('/', getCacheBusterMiddleware(), (request, response) => {
413 if (shouldRedirectToLogin(request)) {520 if (shouldRedirectToLogin(request)) {
414 const query = request.url.split('?')[1];521 const query = request.url.split('?')[1];
415 const redirectUrl = query ? `/login?${query}` : '/login';522 const redirectUrl = query ? `/login?${query}` : '/login';
@@ -617,13 +724,13 @@ app.use('/api/azure', azureRouter);
617724
618const tavernUrlV6 = new URL(725const tavernUrlV6 = new URL(
619 (cliArguments.ssl ? 'https://' : 'http://') +726 (cliArguments.ssl ? 'https://' : 'http://') +
620 (listen ? '[::]' : '[::1]') +727 (listen ? (ipRegex.v6({ exact: true }).test(listenAddressIPv6) ? listenAddressIPv6 : '[::]') : '[::1]') +
621 (':' + server_port),728 (':' + server_port),
622);729);
623730
624const tavernUrl = new URL(731const tavernUrl = new URL(
625 (cliArguments.ssl ? 'https://' : 'http://') +732 (cliArguments.ssl ? 'https://' : 'http://') +
626 (listen ? '0.0.0.0' : '127.0.0.1') +733 (listen ? (ipRegex.v4({ exact: true }).test(listenAddressIPv4) ? listenAddressIPv4 : '0.0.0.0') : '127.0.0.1') +
627 (':' + server_port),734 (':' + server_port),
628);735);
629736
@@ -683,20 +790,23 @@ const preSetupTasks = async function () {
683790
684/**791/**
685 * Gets the hostname to use for autorun in the browser.792 * Gets the hostname to use for autorun in the browser.
686 * @returns {string} The hostname to use for autorun793 * @param {boolean} useIPv6 If use IPv6
794 * @param {boolean} useIPv4 If use IPv4
795 * @returns Promise<string> The hostname to use for autorun
687 */796 */
688function getAutorunHostname() {797async function getAutorunHostname(useIPv6, useIPv4) {
689 if (autorunHostname === 'auto') {798 if (autorunHostname === 'auto') {
690 if (enableIPv6 && enableIPv4) {799 let localhostResolve = await canResolve('localhost', useIPv6, useIPv4);
691 if (avoidLocalhost) return '[::1]';800
692 return 'localhost';801 if (useIPv6 && useIPv4) {
802 return (avoidLocalhost || !localhostResolve) ? '[::1]' : 'localhost';
693 }803 }
694804
695 if (enableIPv6) {805 if (useIPv6) {
696 return '[::1]';806 return '[::1]';
697 }807 }
698808
699 if (enableIPv4) {809 if (useIPv4) {
700 return '127.0.0.1';810 return '127.0.0.1';
701 }811 }
702 }812 }
@@ -708,11 +818,13 @@ function getAutorunHostname() {
708 * Tasks that need to be run after the server starts listening.818 * Tasks that need to be run after the server starts listening.
709 * @param {boolean} v6Failed If the server failed to start on IPv6819 * @param {boolean} v6Failed If the server failed to start on IPv6
710 * @param {boolean} v4Failed If the server failed to start on IPv4820 * @param {boolean} v4Failed If the server failed to start on IPv4
821 * @param {boolean} useIPv6 If the server is using IPv6
822 * @param {boolean} useIPv4 If the server is using IPv4
711 */823 */
712const postSetupTasks = async function (v6Failed, v4Failed) {824const postSetupTasks = async function (v6Failed, v4Failed, useIPv6, useIPv4) {
713 const autorunUrl = new URL(825 const autorunUrl = new URL(
714 (cliArguments.ssl ? 'https://' : 'http://') +826 (cliArguments.ssl ? 'https://' : 'http://') +
715 (getAutorunHostname()) +827 (await getAutorunHostname(useIPv6, useIPv4)) +
716 (':') +828 (':') +
717 ((autorunPortOverride >= 0) ? autorunPortOverride : server_port),829 ((autorunPortOverride >= 0) ? autorunPortOverride : server_port),
718 );830 );
@@ -725,36 +837,48 @@ const postSetupTasks = async function (v6Failed, v4Failed) {
725837
726 let logListen = 'SillyTavern is listening on';838 let logListen = 'SillyTavern is listening on';
727839
728 if (enableIPv6 && !v6Failed) {840 if (useIPv6 && !v6Failed) {
729 logListen += color.green(' IPv6: ' + tavernUrlV6.host);841 logListen += color.green(
842 ' IPv6: ' + tavernUrlV6.host,
843 );
730 }844 }
731845
732 if (enableIPv4 && !v4Failed) {846 if (useIPv4 && !v4Failed) {
733 logListen += color.green(' IPv4: ' + tavernUrl.host);847 logListen += color.green(
848 ' IPv4: ' + tavernUrl.host,
849 );
734 }850 }
735851
736 const goToLog = 'Go to: ' + color.blue(autorunUrl) + ' to open SillyTavern';852 const goToLog = 'Go to: ' + color.blue(autorunUrl) + ' to open SillyTavern';
737 const plainGoToLog = removeColorFormatting(goToLog);853 const plainGoToLog = removeColorFormatting(goToLog);
738854
739 console.log(logListen);855 console.log(logListen);
856 if (listen) {
857 console.log();
858 console.log('To limit connections to internal localhost only ([::1] or 127.0.0.1), change the setting in config.yaml to "listen: false".');
859 console.log('Check the "access.log" file in the SillyTavern directory to inspect incoming connections.');
860 }
740 console.log('\n' + getSeparator(plainGoToLog.length) + '\n');861 console.log('\n' + getSeparator(plainGoToLog.length) + '\n');
741 console.log(goToLog);862 console.log(goToLog);
742 console.log('\n' + getSeparator(plainGoToLog.length) + '\n');863 console.log('\n' + getSeparator(plainGoToLog.length) + '\n');
743864
744 if (listen) {
745 console.log('[::] or 0.0.0.0 means SillyTavern is listening on all network interfaces (Wi-Fi, LAN, localhost). If you want to limit it only to internal localhost ([::1] or 127.0.0.1), change the setting in config.yaml to "listen: false". Check "access.log" file in the SillyTavern directory if you want to inspect incoming connections.\n');
746 }
747865
748 if (basicAuthMode) {866 if (basicAuthMode) {
749 if (perUserBasicAuth && !enableAccounts) {867 if (perUserBasicAuth && !enableAccounts) {
750 console.error(color.red('Per-user basic authentication is enabled, but user accounts are disabled. This configuration may be insecure.'));868 console.error(color.red(
869 'Per-user basic authentication is enabled, but user accounts are disabled. This configuration may be insecure.',
870 ));
751 } else if (!perUserBasicAuth) {871 } else if (!perUserBasicAuth) {
752 const basicAuthUser = getConfigValue('basicAuthUser', {});872 const basicAuthUser = getConfigValue('basicAuthUser', {});
753 if (!basicAuthUser?.username || !basicAuthUser?.password) {873 if (!basicAuthUser?.username || !basicAuthUser?.password) {
754 console.warn(color.yellow('Basic Authentication is enabled, but username or password is not set or empty!'));874 console.warn(color.yellow(
875 'Basic Authentication is enabled, but username or password is not set or empty!',
876 ));
755 }877 }
756 }878 }
757 }879 }
880
881 setupLogLevel();
758};882};
759883
760/**884/**
@@ -804,14 +928,16 @@ function logSecurityAlert(message) {
804 * Handles the case where the server failed to start on one or both protocols.928 * Handles the case where the server failed to start on one or both protocols.
805 * @param {boolean} v6Failed If the server failed to start on IPv6929 * @param {boolean} v6Failed If the server failed to start on IPv6
806 * @param {boolean} v4Failed If the server failed to start on IPv4930 * @param {boolean} v4Failed If the server failed to start on IPv4
931 * @param {boolean} useIPv6 If use IPv6
932 * @param {boolean} useIPv4 If use IPv4
807 */933 */
808function handleServerListenFail(v6Failed, v4Failed) {934function handleServerListenFail(v6Failed, v4Failed, useIPv6, useIPv4) {
809 if (v6Failed && !enableIPv4) {935 if (v6Failed && !useIPv4) {
810 console.error(color.red('fatal error: Failed to start server on IPv6 and IPv4 disabled'));936 console.error(color.red('fatal error: Failed to start server on IPv6 and IPv4 disabled'));
811 process.exit(1);937 process.exit(1);
812 }938 }
813939
814 if (v4Failed && !enableIPv6) {940 if (v4Failed && !useIPv6) {
815 console.error(color.red('fatal error: Failed to start server on IPv4 and IPv6 disabled'));941 console.error(color.red('fatal error: Failed to start server on IPv4 and IPv6 disabled'));
816 process.exit(1);942 process.exit(1);
817 }943 }
@@ -825,10 +951,11 @@ function handleServerListenFail(v6Failed, v4Failed) {
825/**951/**
826 * Creates an HTTPS server.952 * Creates an HTTPS server.
827 * @param {URL} url The URL to listen on953 * @param {URL} url The URL to listen on
954 * @param {number} ipVersion the ip version to use
828 * @returns {Promise<void>} A promise that resolves when the server is listening955 * @returns {Promise<void>} A promise that resolves when the server is listening
829 * @throws {Error} If the server fails to start956 * @throws {Error} If the server fails to start
830 */957 */
831function createHttpsServer(url) {958function createHttpsServer(url, ipVersion) {
832 return new Promise((resolve, reject) => {959 return new Promise((resolve, reject) => {
833 const server = https.createServer(960 const server = https.createServer(
834 {961 {
@@ -837,34 +964,56 @@ function createHttpsServer(url) {
837 }, app);964 }, app);
838 server.on('error', reject);965 server.on('error', reject);
839 server.on('listening', resolve);966 server.on('listening', resolve);
840 server.listen(Number(url.port || 443), url.hostname);967
968 let host = url.hostname;
969 if (ipVersion === 6) host = urlHostnameToIPv6(url.hostname);
970 server.listen({
971 host: host,
972 port: Number(url.port || 443),
973 // see https://nodejs.org/api/net.html#serverlisten for why ipv6Only is used
974 ipv6Only: true,
975 });
841 });976 });
842}977}
843978
844/**979/**
845 * Creates an HTTP server.980 * Creates an HTTP server.
846 * @param {URL} url The URL to listen on981 * @param {URL} url The URL to listen on
982 * @param {number} ipVersion the ip version to use
847 * @returns {Promise<void>} A promise that resolves when the server is listening983 * @returns {Promise<void>} A promise that resolves when the server is listening
848 * @throws {Error} If the server fails to start984 * @throws {Error} If the server fails to start
849 */985 */
850function createHttpServer(url) {986function createHttpServer(url, ipVersion) {
851 return new Promise((resolve, reject) => {987 return new Promise((resolve, reject) => {
852 const server = http.createServer(app);988 const server = http.createServer(app);
853 server.on('error', reject);989 server.on('error', reject);
854 server.on('listening', resolve);990 server.on('listening', resolve);
855 server.listen(Number(url.port || 80), url.hostname);991
992 let host = url.hostname;
993 if (ipVersion === 6) host = urlHostnameToIPv6(url.hostname);
994 server.listen({
995 host: host,
996 port: Number(url.port || 80),
997 // see https://nodejs.org/api/net.html#serverlisten for why ipv6Only is used
998 ipv6Only: true,
999 });
856 });1000 });
857}1001}
8581002
859async function startHTTPorHTTPS() {1003/**
1004 * Starts the server using http or https depending on config
1005 * @param {boolean} useIPv6 If use IPv6
1006 * @param {boolean} useIPv4 If use IPv4
1007 */
1008async function startHTTPorHTTPS(useIPv6, useIPv4) {
860 let v6Failed = false;1009 let v6Failed = false;
861 let v4Failed = false;1010 let v4Failed = false;
8621011
863 const createFunc = cliArguments.ssl ? createHttpsServer : createHttpServer;1012 const createFunc = cliArguments.ssl ? createHttpsServer : createHttpServer;
8641013
865 if (enableIPv6) {1014 if (useIPv6) {
866 try {1015 try {
867 await createFunc(tavernUrlV6);1016 await createFunc(tavernUrlV6, 6);
868 } catch (error) {1017 } catch (error) {
869 console.error('non-fatal error: failed to start server on IPv6');1018 console.error('non-fatal error: failed to start server on IPv6');
870 console.error(error);1019 console.error(error);
@@ -873,9 +1022,9 @@ async function startHTTPorHTTPS() {
873 }1022 }
874 }1023 }
8751024
876 if (enableIPv4) {1025 if (useIPv4) {
877 try {1026 try {
878 await createFunc(tavernUrl);1027 await createFunc(tavernUrl, 4);
879 } catch (error) {1028 } catch (error) {
880 console.error('non-fatal error: failed to start server on IPv4');1029 console.error('non-fatal error: failed to start server on IPv4');
881 console.error(error);1030 console.error(error);
@@ -888,10 +1037,59 @@ async function startHTTPorHTTPS() {
888}1037}
8891038
890async function startServer() {1039async function startServer() {
891 const [v6Failed, v4Failed] = await startHTTPorHTTPS();1040 let useIPv6 = (enableIPv6 === true);
1041 let useIPv4 = (enableIPv4 === true);
1042
1043 let hasIPv6 = false,
1044 hasIPv4 = false,
1045 hasIPv6Local = false,
1046 hasIPv4Local = false,
1047 hasIPv6Any = false,
1048 hasIPv4Any = false;
1049
1050 if (enableIPv6 === 'auto' || enableIPv4 === 'auto') {
1051 [hasIPv6Any, hasIPv4Any, hasIPv6Local, hasIPv4Local] = await getHasIP();
1052
1053 hasIPv6 = listen ? hasIPv6Any : hasIPv6Local;
1054 if (enableIPv6 === 'auto') {
1055 useIPv6 = hasIPv6;
1056 }
1057 if (hasIPv6) {
1058 if (useIPv6) {
1059 console.log(color.green('IPv6 support detected'));
1060 } else {
1061 console.log('IPv6 support detected (but disabled)');
1062 }
1063 }
1064
1065 hasIPv4 = listen ? hasIPv4Any : hasIPv4Local;
1066 if (enableIPv4 === 'auto') {
1067 useIPv4 = hasIPv4;
1068 }
1069 if (hasIPv4) {
1070 if (useIPv4) {
1071 console.log(color.green('IPv4 support detected'));
1072 } else {
1073 console.log('IPv4 support detected (but disabled)');
1074 }
1075 }
8921076
893 handleServerListenFail(v6Failed, v4Failed);1077 if (enableIPv6 === 'auto' && enableIPv4 === 'auto') {
894 postSetupTasks(v6Failed, v4Failed);1078 if (!hasIPv6 && !hasIPv4) {
1079 console.error('Both IPv6 and IPv4 are not detected');
1080 process.exit(1);
1081 }
1082 }
1083 }
1084
1085 if (!useIPv6 && !useIPv4) {
1086 console.error('Both IPv6 and IPv4 are disabled,\nP.S. you should never see this error, at least at one point it was checked for before this, with the rest of the config options');
1087 process.exit(1);
1088 }
1089
1090 const [v6Failed, v4Failed] = await startHTTPorHTTPS(useIPv6, useIPv4);
1091 handleServerListenFail(v6Failed, v4Failed, useIPv6, useIPv4);
1092 postSetupTasks(v6Failed, v4Failed, useIPv6, useIPv4);
895}1093}
8961094
897async function verifySecuritySettings() {1095async function verifySecuritySettings() {
@@ -901,7 +1099,7 @@ async function verifySecuritySettings() {
901 }1099 }
9021100
903 if (!enableAccounts) {1101 if (!enableAccounts) {
904 logSecurityAlert('Your SillyTavern is currently insecurely open to the public. Enable whitelisting, basic authentication or user accounts.');1102 logSecurityAlert('Your current SillyTavern configuration is insecure (listening to non-localhost). Enable whitelisting, basic authentication or user accounts.');
905 }1103 }
9061104
907 const users = await getAllEnabledUsers();1105 const users = await getAllEnabledUsers();
@@ -921,6 +1119,16 @@ async function verifySecuritySettings() {
921 }1119 }
922}1120}
9231121
1122/**
1123 * Registers a not-found error response if a not-found error page exists. Should only be called after all other middlewares have been registered.
1124 */
1125function apply404Middleware() {
1126 const notFoundWebpage = safeReadFileSync('./public/error/url-not-found.html') ?? '';
1127 app.use((req, res) => {
1128 res.status(404).send(notFoundWebpage);
1129 });
1130}
1131
924// User storage module needs to be initialized before starting the server1132// User storage module needs to be initialized before starting the server
925initUserStorage(dataRoot)1133initUserStorage(dataRoot)
926 .then(ensurePublicDirectoriesExist)1134 .then(ensurePublicDirectoriesExist)
@@ -928,4 +1136,5 @@ initUserStorage(dataRoot)
928 .then(migrateSystemPrompts)1136 .then(migrateSystemPrompts)
929 .then(verifySecuritySettings)1137 .then(verifySecuritySettings)
930 .then(preSetupTasks)1138 .then(preSetupTasks)
1139 .then(apply404Middleware)
931 .finally(startServer);1140 .finally(startServer);
src/constants.js+13 -4
@@ -139,19 +139,19 @@ export const UNSAFE_EXTENSIONS = [
139export const GEMINI_SAFETY = [139export const GEMINI_SAFETY = [
140 {140 {
141 category: 'HARM_CATEGORY_HARASSMENT',141 category: 'HARM_CATEGORY_HARASSMENT',
142 threshold: 'BLOCK_NONE',142 threshold: 'OFF',
143 },143 },
144 {144 {
145 category: 'HARM_CATEGORY_HATE_SPEECH',145 category: 'HARM_CATEGORY_HATE_SPEECH',
146 threshold: 'BLOCK_NONE',146 threshold: 'OFF',
147 },147 },
148 {148 {
149 category: 'HARM_CATEGORY_SEXUALLY_EXPLICIT',149 category: 'HARM_CATEGORY_SEXUALLY_EXPLICIT',
150 threshold: 'BLOCK_NONE',150 threshold: 'OFF',
151 },151 },
152 {152 {
153 category: 'HARM_CATEGORY_DANGEROUS_CONTENT',153 category: 'HARM_CATEGORY_DANGEROUS_CONTENT',
154 threshold: 'BLOCK_NONE',154 threshold: 'OFF',
155 },155 },
156 {156 {
157 category: 'HARM_CATEGORY_CIVIC_INTEGRITY',157 category: 'HARM_CATEGORY_CIVIC_INTEGRITY',
@@ -304,6 +304,7 @@ export const TOGETHERAI_KEYS = [
304export const OLLAMA_KEYS = [304export const OLLAMA_KEYS = [
305 'num_predict',305 'num_predict',
306 'num_ctx',306 'num_ctx',
307 'num_batch',
307 'stop',308 'stop',
308 'temperature',309 'temperature',
309 'repeat_penalty',310 'repeat_penalty',
@@ -369,6 +370,7 @@ export const OPENROUTER_KEYS = [
369 'prompt',370 'prompt',
370 'stop',371 'stop',
371 'provider',372 'provider',
373 'include_reasoning',
372];374];
373375
374// https://github.com/vllm-project/vllm/blob/0f8a91401c89ac0a8018def3756829611b57727f/vllm/entrypoints/openai/protocol.py#L220376// https://github.com/vllm-project/vllm/blob/0f8a91401c89ac0a8018def3756829611b57727f/vllm/entrypoints/openai/protocol.py#L220
@@ -413,3 +415,10 @@ export const VLLM_KEYS = [
413 'guided_decoding_backend',415 'guided_decoding_backend',
414 'guided_whitespace_pattern',416 'guided_whitespace_pattern',
415];417];
418
419export const LOG_LEVELS = {
420 DEBUG: 0,
421 INFO: 1,
422 WARN: 2,
423 ERROR: 3,
424};
src/endpoints/anthropic.js+3 -3
@@ -32,7 +32,7 @@ router.post('/caption-image', jsonParser, async (request, response) => {
32 max_tokens: 4096,32 max_tokens: 4096,
33 };33 };
3434
35 console.log('Multimodal captioning request', body);35 console.debug('Multimodal captioning request', body);
3636
37 const result = await fetch(url, {37 const result = await fetch(url, {
38 body: JSON.stringify(body),38 body: JSON.stringify(body),
@@ -46,14 +46,14 @@ router.post('/caption-image', jsonParser, async (request, response) => {
4646
47 if (!result.ok) {47 if (!result.ok) {
48 const text = await result.text();48 const text = await result.text();
49 console.log(`Claude API returned error: ${result.status} ${result.statusText}`, text);49 console.warn(`Claude API returned error: ${result.status} ${result.statusText}`, text);
50 return response.status(result.status).send({ error: true });50 return response.status(result.status).send({ error: true });
51 }51 }
5252
53 /** @type {any} */53 /** @type {any} */
54 const generateResponseJson = await result.json();54 const generateResponseJson = await result.json();
55 const caption = generateResponseJson.content[0].text;55 const caption = generateResponseJson.content[0].text;
56 console.log('Claude response:', generateResponseJson);56 console.debug('Claude response:', generateResponseJson);
5757
58 if (!caption) {58 if (!caption) {
59 return response.status(500).send('No caption found');59 return response.status(500).send('No caption found');
src/endpoints/assets.js+13 -12
@@ -176,7 +176,7 @@ router.post('/get', jsonParser, async (request, response) => {
176 }176 }
177 }177 }
178 catch (err) {178 catch (err) {
179 console.log(err);179 console.error(err);
180 }180 }
181 return response.send(output);181 return response.send(output);
182});182});
@@ -200,7 +200,7 @@ router.post('/download', jsonParser, async (request, response) => {
200 category = i;200 category = i;
201201
202 if (category === null) {202 if (category === null) {
203 console.debug('Bad request: unsupported asset category.');203 console.error('Bad request: unsupported asset category.');
204 return response.sendStatus(400);204 return response.sendStatus(400);
205 }205 }
206206
@@ -212,7 +212,7 @@ router.post('/download', jsonParser, async (request, response) => {
212212
213 const temp_path = path.join(request.user.directories.assets, 'temp', request.body.filename);213 const temp_path = path.join(request.user.directories.assets, 'temp', request.body.filename);
214 const file_path = path.join(request.user.directories.assets, category, request.body.filename);214 const file_path = path.join(request.user.directories.assets, category, request.body.filename);
215 console.debug('Request received to download', url, 'to', file_path);215 console.info('Request received to download', url, 'to', file_path);
216216
217 try {217 try {
218 // Download to temp218 // Download to temp
@@ -241,13 +241,13 @@ router.post('/download', jsonParser, async (request, response) => {
241 }241 }
242242
243 // Move into asset place243 // Move into asset place
244 console.debug('Download finished, moving file from', temp_path, 'to', file_path);244 console.info('Download finished, moving file from', temp_path, 'to', file_path);
245 fs.copyFileSync(temp_path, file_path);245 fs.copyFileSync(temp_path, file_path);
246 fs.rmSync(temp_path);246 fs.rmSync(temp_path);
247 response.sendStatus(200);247 response.sendStatus(200);
248 }248 }
249 catch (error) {249 catch (error) {
250 console.log(error);250 console.error(error);
251 response.sendStatus(500);251 response.sendStatus(500);
252 }252 }
253});253});
@@ -270,7 +270,7 @@ router.post('/delete', jsonParser, async (request, response) => {
270 category = i;270 category = i;
271271
272 if (category === null) {272 if (category === null) {
273 console.debug('Bad request: unsupported asset category.');273 console.error('Bad request: unsupported asset category.');
274 return response.sendStatus(400);274 return response.sendStatus(400);
275 }275 }
276276
@@ -280,7 +280,7 @@ router.post('/delete', jsonParser, async (request, response) => {
280 return response.status(400).send(validation.message);280 return response.status(400).send(validation.message);
281281
282 const file_path = path.join(request.user.directories.assets, category, request.body.filename);282 const file_path = path.join(request.user.directories.assets, category, request.body.filename);
283 console.debug('Request received to delete', category, file_path);283 console.info('Request received to delete', category, file_path);
284284
285 try {285 try {
286 // Delete if previous download failed286 // Delete if previous download failed
@@ -288,17 +288,17 @@ router.post('/delete', jsonParser, async (request, response) => {
288 fs.unlink(file_path, (err) => {288 fs.unlink(file_path, (err) => {
289 if (err) throw err;289 if (err) throw err;
290 });290 });
291 console.debug('Asset deleted.');291 console.info('Asset deleted.');
292 }292 }
293 else {293 else {
294 console.debug('Asset not found.');294 console.error('Asset not found.');
295 response.sendStatus(400);295 response.sendStatus(400);
296 }296 }
297 // Move into asset place297 // Move into asset place
298 response.sendStatus(200);298 response.sendStatus(200);
299 }299 }
300 catch (error) {300 catch (error) {
301 console.log(error);301 console.error(error);
302 response.sendStatus(500);302 response.sendStatus(500);
303 }303 }
304});304});
@@ -314,6 +314,7 @@ router.post('/delete', jsonParser, async (request, response) => {
314 */314 */
315router.post('/character', jsonParser, async (request, response) => {315router.post('/character', jsonParser, async (request, response) => {
316 if (request.query.name === undefined) return response.sendStatus(400);316 if (request.query.name === undefined) return response.sendStatus(400);
317
317 // For backwards compatibility, don't reject invalid character names, just sanitize them318 // For backwards compatibility, don't reject invalid character names, just sanitize them
318 const name = sanitize(request.query.name.toString());319 const name = sanitize(request.query.name.toString());
319 const inputCategory = request.query.category;320 const inputCategory = request.query.category;
@@ -325,7 +326,7 @@ router.post('/character', jsonParser, async (request, response) => {
325 category = i;326 category = i;
326327
327 if (category === null) {328 if (category === null) {
328 console.debug('Bad request: unsupported asset category.');329 console.error('Bad request: unsupported asset category.');
329 return response.sendStatus(400);330 return response.sendStatus(400);
330 }331 }
331332
@@ -364,7 +365,7 @@ router.post('/character', jsonParser, async (request, response) => {
364 return response.send(output);365 return response.send(output);
365 }366 }
366 catch (err) {367 catch (err) {
367 console.log(err);368 console.error(err);
368 return response.sendStatus(500);369 return response.sendStatus(500);
369 }370 }
370});371});
src/endpoints/avatars.js+2 -1
@@ -9,6 +9,7 @@ import { sync as writeFileAtomicSync } from 'write-file-atomic';
9import { jsonParser, urlencodedParser } from '../express-common.js';9import { jsonParser, urlencodedParser } from '../express-common.js';
10import { AVATAR_WIDTH, AVATAR_HEIGHT } from '../constants.js';10import { AVATAR_WIDTH, AVATAR_HEIGHT } from '../constants.js';
11import { getImages, tryParse } from '../util.js';11import { getImages, tryParse } from '../util.js';
12import { getFileNameValidationFunction } from '../middleware/validateFileName.js';
1213
13export const router = express.Router();14export const router = express.Router();
1415
@@ -17,7 +18,7 @@ router.post('/get', jsonParser, function (request, response) {
17 response.send(JSON.stringify(images));18 response.send(JSON.stringify(images));
18});19});
1920
20router.post('/delete', jsonParser, function (request, response) {21router.post('/delete', jsonParser, getFileNameValidationFunction('avatar'), function (request, response) {
21 if (!request.body) return response.sendStatus(400);22 if (!request.body) return response.sendStatus(400);
2223
23 if (request.body.avatar !== sanitize(request.body.avatar)) {24 if (request.body.avatar !== sanitize(request.body.avatar)) {
src/endpoints/azure.js+6 -6
@@ -11,14 +11,14 @@ router.post('/list', jsonParser, async (req, res) => {
11 const key = readSecret(req.user.directories, SECRET_KEYS.AZURE_TTS);11 const key = readSecret(req.user.directories, SECRET_KEYS.AZURE_TTS);
1212
13 if (!key) {13 if (!key) {
14 console.error('Azure TTS API Key not set');14 console.warn('Azure TTS API Key not set');
15 return res.sendStatus(403);15 return res.sendStatus(403);
16 }16 }
1717
18 const region = req.body.region;18 const region = req.body.region;
1919
20 if (!region) {20 if (!region) {
21 console.error('Azure TTS region not set');21 console.warn('Azure TTS region not set');
22 return res.sendStatus(400);22 return res.sendStatus(400);
23 }23 }
2424
@@ -32,7 +32,7 @@ router.post('/list', jsonParser, async (req, res) => {
32 });32 });
3333
34 if (!response.ok) {34 if (!response.ok) {
35 console.error('Azure Request failed', response.status, response.statusText);35 console.warn('Azure Request failed', response.status, response.statusText);
36 return res.sendStatus(500);36 return res.sendStatus(500);
37 }37 }
3838
@@ -49,13 +49,13 @@ router.post('/generate', jsonParser, async (req, res) => {
49 const key = readSecret(req.user.directories, SECRET_KEYS.AZURE_TTS);49 const key = readSecret(req.user.directories, SECRET_KEYS.AZURE_TTS);
5050
51 if (!key) {51 if (!key) {
52 console.error('Azure TTS API Key not set');52 console.warn('Azure TTS API Key not set');
53 return res.sendStatus(403);53 return res.sendStatus(403);
54 }54 }
5555
56 const { text, voice, region } = req.body;56 const { text, voice, region } = req.body;
57 if (!text || !voice || !region) {57 if (!text || !voice || !region) {
58 console.error('Missing required parameters');58 console.warn('Missing required parameters');
59 return res.sendStatus(400);59 return res.sendStatus(400);
60 }60 }
6161
@@ -75,7 +75,7 @@ router.post('/generate', jsonParser, async (req, res) => {
75 });75 });
7676
77 if (!response.ok) {77 if (!response.ok) {
78 console.error('Azure Request failed', response.status, response.statusText);78 console.warn('Azure Request failed', response.status, response.statusText);
79 return res.sendStatus(500);79 return res.sendStatus(500);
80 }80 }
8181
src/endpoints/backends/chat-completions.js+180 -77
@@ -37,6 +37,8 @@ import {
37 getTiktokenTokenizer,37 getTiktokenTokenizer,
38 sentencepieceTokenizers,38 sentencepieceTokenizers,
39 TEXT_COMPLETION_MODELS,39 TEXT_COMPLETION_MODELS,
40 webTokenizers,
41 getWebTokenizer,
40} from '../tokenizers.js';42} from '../tokenizers.js';
4143
42const API_OPENAI = 'https://api.openai.com/v1';44const API_OPENAI = 'https://api.openai.com/v1';
@@ -61,6 +63,7 @@ const API_DEEPSEEK = 'https://api.deepseek.com/beta';
61 * @returns63 * @returns
62 */64 */
63function postProcessPrompt(messages, type, names) {65function postProcessPrompt(messages, type, names) {
66 const addAssistantPrefix = x => x.length && (x[x.length - 1].role !== 'assistant' || (x[x.length - 1].prefix = true)) ? x : x;
64 switch (type) {67 switch (type) {
65 case 'merge':68 case 'merge':
66 case 'claude':69 case 'claude':
@@ -70,7 +73,9 @@ function postProcessPrompt(messages, type, names) {
70 case 'strict':73 case 'strict':
71 return mergeMessages(messages, names, true, true);74 return mergeMessages(messages, names, true, true);
72 case 'deepseek':75 case 'deepseek':
73 return (x => x.length && (x[x.length - 1].role !== 'assistant' || (x[x.length - 1].prefix = true)) ? x : x)(mergeMessages(messages, names, true, false));76 return addAssistantPrefix(mergeMessages(messages, names, true, false));
77 case 'deepseek-reasoner':
78 return addAssistantPrefix(mergeMessages(messages, names, true, true));
74 default:79 default:
75 return messages;80 return messages;
76 }81 }
@@ -109,7 +114,7 @@ async function sendClaudeRequest(request, response) {
109 }114 }
110115
111 if (!apiKey) {116 if (!apiKey) {
112 console.log(color.red(`Claude API key is missing.\n${divider}`));117 console.warn(color.red(`Claude API key is missing.\n${divider}`));
113 return response.status(400).send({ error: true });118 return response.status(400).send({ error: true });
114 }119 }
115120
@@ -174,7 +179,7 @@ async function sendClaudeRequest(request, response) {
174 additionalHeaders['anthropic-beta'] = 'prompt-caching-2024-07-31';179 additionalHeaders['anthropic-beta'] = 'prompt-caching-2024-07-31';
175 }180 }
176181
177 console.log('Claude request:', requestBody);182 console.debug('Claude request:', requestBody);
178183
179 const generateResponse = await fetch(apiUrl + '/messages', {184 const generateResponse = await fetch(apiUrl + '/messages', {
180 method: 'POST',185 method: 'POST',
@@ -194,21 +199,21 @@ async function sendClaudeRequest(request, response) {
194 } else {199 } else {
195 if (!generateResponse.ok) {200 if (!generateResponse.ok) {
196 const generateResponseText = await generateResponse.text();201 const generateResponseText = await generateResponse.text();
197 console.log(color.red(`Claude API returned error: ${generateResponse.status} ${generateResponse.statusText}\n${generateResponseText}\n${divider}`));202 console.warn(color.red(`Claude API returned error: ${generateResponse.status} ${generateResponse.statusText}\n${generateResponseText}\n${divider}`));
198 return response.status(generateResponse.status).send({ error: true });203 return response.status(generateResponse.status).send({ error: true });
199 }204 }
200205
201 /** @type {any} */206 /** @type {any} */
202 const generateResponseJson = await generateResponse.json();207 const generateResponseJson = await generateResponse.json();
203 const responseText = generateResponseJson?.content?.[0]?.text || '';208 const responseText = generateResponseJson?.content?.[0]?.text || '';
204 console.log('Claude response:', generateResponseJson);209 console.debug('Claude response:', generateResponseJson);
205210
206 // Wrap it back to OAI format + save the original content211 // Wrap it back to OAI format + save the original content
207 const reply = { choices: [{ 'message': { 'content': responseText } }], content: generateResponseJson.content };212 const reply = { choices: [{ 'message': { 'content': responseText } }], content: generateResponseJson.content };
208 return response.send(reply);213 return response.send(reply);
209 }214 }
210 } catch (error) {215 } catch (error) {
211 console.log(color.red(`Error communicating with Claude: ${error}\n${divider}`));216 console.error(color.red(`Error communicating with Claude: ${error}\n${divider}`));
212 if (!response.headersSent) {217 if (!response.headersSent) {
213 return response.status(500).send({ error: true });218 return response.status(500).send({ error: true });
214 }219 }
@@ -225,12 +230,12 @@ async function sendScaleRequest(request, response) {
225 const apiKey = readSecret(request.user.directories, SECRET_KEYS.SCALE);230 const apiKey = readSecret(request.user.directories, SECRET_KEYS.SCALE);
226231
227 if (!apiKey) {232 if (!apiKey) {
228 console.log('Scale API key is missing.');233 console.warn('Scale API key is missing.');
229 return response.status(400).send({ error: true });234 return response.status(400).send({ error: true });
230 }235 }
231236
232 const requestPrompt = convertTextCompletionPrompt(request.body.messages);237 const requestPrompt = convertTextCompletionPrompt(request.body.messages);
233 console.log('Scale request:', requestPrompt);238 console.debug('Scale request:', requestPrompt);
234239
235 try {240 try {
236 const controller = new AbortController();241 const controller = new AbortController();
@@ -249,18 +254,18 @@ async function sendScaleRequest(request, response) {
249 });254 });
250255
251 if (!generateResponse.ok) {256 if (!generateResponse.ok) {
252 console.log(`Scale API returned error: ${generateResponse.status} ${generateResponse.statusText} ${await generateResponse.text()}`);257 console.warn(`Scale API returned error: ${generateResponse.status} ${generateResponse.statusText} ${await generateResponse.text()}`);
253 return response.status(500).send({ error: true });258 return response.status(500).send({ error: true });
254 }259 }
255260
256 /** @type {any} */261 /** @type {any} */
257 const generateResponseJson = await generateResponse.json();262 const generateResponseJson = await generateResponse.json();
258 console.log('Scale response:', generateResponseJson);263 console.debug('Scale response:', generateResponseJson);
259264
260 const reply = { choices: [{ 'message': { 'content': generateResponseJson.output } }] };265 const reply = { choices: [{ 'message': { 'content': generateResponseJson.output } }] };
261 return response.send(reply);266 return response.send(reply);
262 } catch (error) {267 } catch (error) {
263 console.log(error);268 console.error(error);
264 if (!response.headersSent) {269 if (!response.headersSent) {
265 return response.status(500).send({ error: true });270 return response.status(500).send({ error: true });
266 }271 }
@@ -277,13 +282,13 @@ async function sendMakerSuiteRequest(request, response) {
277 const apiKey = request.body.reverse_proxy ? request.body.proxy_password : readSecret(request.user.directories, SECRET_KEYS.MAKERSUITE);282 const apiKey = request.body.reverse_proxy ? request.body.proxy_password : readSecret(request.user.directories, SECRET_KEYS.MAKERSUITE);
278283
279 if (!request.body.reverse_proxy && !apiKey) {284 if (!request.body.reverse_proxy && !apiKey) {
280 console.log('Google AI Studio API key is missing.');285 console.warn('Google AI Studio API key is missing.');
281 return response.status(400).send({ error: true });286 return response.status(400).send({ error: true });
282 }287 }
283288
284 const model = String(request.body.model);289 const model = String(request.body.model);
285 const stream = Boolean(request.body.stream);290 const stream = Boolean(request.body.stream);
286 const showThoughts = Boolean(request.body.show_thoughts);291 const isThinking = model.includes('thinking');
287292
288 const generationConfig = {293 const generationConfig = {
289 stopSequences: request.body.stop,294 stopSequences: request.body.stop,
@@ -300,8 +305,9 @@ async function sendMakerSuiteRequest(request, response) {
300 }305 }
301306
302 const should_use_system_prompt = (307 const should_use_system_prompt = (
308 model.includes('gemini-2.0-pro') ||
309 model.includes('gemini-2.0-flash') ||
303 model.includes('gemini-2.0-flash-thinking-exp') ||310 model.includes('gemini-2.0-flash-thinking-exp') ||
304 model.includes('gemini-2.0-flash-exp') ||
305 model.includes('gemini-1.5-flash') ||311 model.includes('gemini-1.5-flash') ||
306 model.includes('gemini-1.5-pro') ||312 model.includes('gemini-1.5-pro') ||
307 model.startsWith('gemini-exp')313 model.startsWith('gemini-exp')
@@ -310,9 +316,15 @@ async function sendMakerSuiteRequest(request, response) {
310 const prompt = convertGooglePrompt(request.body.messages, model, should_use_system_prompt, getPromptNames(request));316 const prompt = convertGooglePrompt(request.body.messages, model, should_use_system_prompt, getPromptNames(request));
311 let safetySettings = GEMINI_SAFETY;317 let safetySettings = GEMINI_SAFETY;
312318
313 if (model.includes('gemini-2.0-flash-exp')) {319 // These old models do not support setting the threshold to OFF at all.
320 if (['gemini-1.5-pro-001', 'gemini-1.5-flash-001', 'gemini-1.5-flash-8b-exp-0827', 'gemini-1.5-flash-8b-exp-0924', 'gemini-pro', 'gemini-1.0-pro', 'gemini-1.0-pro-001'].includes(model)) {
321 safetySettings = GEMINI_SAFETY.map(setting => ({ ...setting, threshold: 'BLOCK_NONE' }));
322 }
323 // Interestingly, Gemini 2.0 Flash does support setting the threshold for HARM_CATEGORY_CIVIC_INTEGRITY to OFF.
324 else if (['gemini-2.0-flash', 'gemini-2.0-flash-001', 'gemini-2.0-flash-exp'].includes(model)) {
314 safetySettings = GEMINI_SAFETY.map(setting => ({ ...setting, threshold: 'OFF' }));325 safetySettings = GEMINI_SAFETY.map(setting => ({ ...setting, threshold: 'OFF' }));
315 }326 }
327 // Most of the other models allow for setting the threshold of filters, except for HARM_CATEGORY_CIVIC_INTEGRITY, to OFF.
316328
317 let body = {329 let body = {
318 contents: prompt.contents,330 contents: prompt.contents,
@@ -328,7 +340,7 @@ async function sendMakerSuiteRequest(request, response) {
328 }340 }
329341
330 const body = getGeminiBody();342 const body = getGeminiBody();
331 console.log('Google AI Studio request:', body);343 console.debug('Google AI Studio request:', body);
332344
333 try {345 try {
334 const controller = new AbortController();346 const controller = new AbortController();
@@ -337,7 +349,6 @@ async function sendMakerSuiteRequest(request, response) {
337 controller.abort();349 controller.abort();
338 });350 });
339351
340 const isThinking = model.includes('thinking');
341 const apiVersion = isThinking ? 'v1alpha' : 'v1beta';352 const apiVersion = isThinking ? 'v1alpha' : 'v1beta';
342 const responseType = (stream ? 'streamGenerateContent' : 'generateContent');353 const responseType = (stream ? 'streamGenerateContent' : 'generateContent');
343354
@@ -355,14 +366,14 @@ async function sendMakerSuiteRequest(request, response) {
355 // Pipe remote SSE stream to Express response366 // Pipe remote SSE stream to Express response
356 forwardFetchResponse(generateResponse, response);367 forwardFetchResponse(generateResponse, response);
357 } catch (error) {368 } catch (error) {
358 console.log('Error forwarding streaming response:', error);369 console.error('Error forwarding streaming response:', error);
359 if (!response.headersSent) {370 if (!response.headersSent) {
360 return response.status(500).send({ error: true });371 return response.status(500).send({ error: true });
361 }372 }
362 }373 }
363 } else {374 } else {
364 if (!generateResponse.ok) {375 if (!generateResponse.ok) {
365 console.log(`Google AI Studio API returned error: ${generateResponse.status} ${generateResponse.statusText} ${await generateResponse.text()}`);376 console.warn(`Google AI Studio API returned error: ${generateResponse.status} ${generateResponse.statusText} ${await generateResponse.text()}`);
366 return response.status(generateResponse.status).send({ error: true });377 return response.status(generateResponse.status).send({ error: true });
367 }378 }
368379
@@ -372,7 +383,7 @@ async function sendMakerSuiteRequest(request, response) {
372 const candidates = generateResponseJson?.candidates;383 const candidates = generateResponseJson?.candidates;
373 if (!candidates || candidates.length === 0) {384 if (!candidates || candidates.length === 0) {
374 let message = 'Google AI Studio API returned no candidate';385 let message = 'Google AI Studio API returned no candidate';
375 console.log(message, generateResponseJson);386 console.warn(message, generateResponseJson);
376 if (generateResponseJson?.promptFeedback?.blockReason) {387 if (generateResponseJson?.promptFeedback?.blockReason) {
377 message += `\nPrompt was blocked due to : ${generateResponseJson.promptFeedback.blockReason}`;388 message += `\nPrompt was blocked due to : ${generateResponseJson.promptFeedback.blockReason}`;
378 }389 }
@@ -380,25 +391,21 @@ async function sendMakerSuiteRequest(request, response) {
380 }391 }
381392
382 const responseContent = candidates[0].content ?? candidates[0].output;393 const responseContent = candidates[0].content ?? candidates[0].output;
383 console.log('Google AI Studio response:', responseContent);394 console.warn('Google AI Studio response:', responseContent);
384395
385 if (Array.isArray(responseContent?.parts) && isThinking && !showThoughts) {396 const responseText = typeof responseContent === 'string' ? responseContent : responseContent?.parts?.filter(part => !part.thought)?.map(part => part.text)?.join('\n\n');
386 responseContent.parts = responseContent.parts.filter(part => !part.thought);
387 }
388
389 const responseText = typeof responseContent === 'string' ? responseContent : responseContent?.parts?.map(part => part.text)?.join('\n\n');
390 if (!responseText) {397 if (!responseText) {
391 let message = 'Google AI Studio Candidate text empty';398 let message = 'Google AI Studio Candidate text empty';
392 console.log(message, generateResponseJson);399 console.warn(message, generateResponseJson);
393 return response.send({ error: { message } });400 return response.send({ error: { message } });
394 }401 }
395402
396 // Wrap it back to OAI format403 // Wrap it back to OAI format
397 const reply = { choices: [{ 'message': { 'content': responseText } }] };404 const reply = { choices: [{ 'message': { 'content': responseText } }], responseContent };
398 return response.send(reply);405 return response.send(reply);
399 }406 }
400 } catch (error) {407 } catch (error) {
401 console.log('Error communicating with Google AI Studio API: ', error);408 console.error('Error communicating with Google AI Studio API: ', error);
402 if (!response.headersSent) {409 if (!response.headersSent) {
403 return response.status(500).send({ error: true });410 return response.status(500).send({ error: true });
404 }411 }
@@ -412,8 +419,9 @@ async function sendMakerSuiteRequest(request, response) {
412 */419 */
413async function sendAI21Request(request, response) {420async function sendAI21Request(request, response) {
414 if (!request.body) return response.sendStatus(400);421 if (!request.body) return response.sendStatus(400);
422
415 const controller = new AbortController();423 const controller = new AbortController();
416 console.log(request.body.messages);424 console.debug(request.body.messages);
417 request.socket.removeAllListeners('close');425 request.socket.removeAllListeners('close');
418 request.socket.on('close', function () {426 request.socket.on('close', function () {
419 controller.abort();427 controller.abort();
@@ -439,7 +447,7 @@ async function sendAI21Request(request, response) {
439 signal: controller.signal,447 signal: controller.signal,
440 };448 };
441449
442 console.log('AI21 request:', body);450 console.debug('AI21 request:', body);
443451
444 try {452 try {
445 const generateResponse = await fetch(API_AI21 + '/chat/completions', options);453 const generateResponse = await fetch(API_AI21 + '/chat/completions', options);
@@ -448,16 +456,16 @@ async function sendAI21Request(request, response) {
448 } else {456 } else {
449 if (!generateResponse.ok) {457 if (!generateResponse.ok) {
450 const errorText = await generateResponse.text();458 const errorText = await generateResponse.text();
451 console.log(`AI21 API returned error: ${generateResponse.status} ${generateResponse.statusText} ${errorText}`);459 console.warn(`AI21 API returned error: ${generateResponse.status} ${generateResponse.statusText} ${errorText}`);
452 const errorJson = tryParse(errorText) ?? { error: true };460 const errorJson = tryParse(errorText) ?? { error: true };
453 return response.status(500).send(errorJson);461 return response.status(500).send(errorJson);
454 }462 }
455 const generateResponseJson = await generateResponse.json();463 const generateResponseJson = await generateResponse.json();
456 console.log('AI21 response:', generateResponseJson);464 console.debug('AI21 response:', generateResponseJson);
457 return response.send(generateResponseJson);465 return response.send(generateResponseJson);
458 }466 }
459 } catch (error) {467 } catch (error) {
460 console.log('Error communicating with AI21 API: ', error);468 console.error('Error communicating with AI21 API: ', error);
461 if (!response.headersSent) {469 if (!response.headersSent) {
462 response.send({ error: true });470 response.send({ error: true });
463 } else {471 } else {
@@ -476,7 +484,7 @@ async function sendMistralAIRequest(request, response) {
476 const apiKey = request.body.reverse_proxy ? request.body.proxy_password : readSecret(request.user.directories, SECRET_KEYS.MISTRALAI);484 const apiKey = request.body.reverse_proxy ? request.body.proxy_password : readSecret(request.user.directories, SECRET_KEYS.MISTRALAI);
477485
478 if (!apiKey) {486 if (!apiKey) {
479 console.log('MistralAI API key is missing.');487 console.warn('MistralAI API key is missing.');
480 return response.status(400).send({ error: true });488 return response.status(400).send({ error: true });
481 }489 }
482490
@@ -517,7 +525,7 @@ async function sendMistralAIRequest(request, response) {
517 timeout: 0,525 timeout: 0,
518 };526 };
519527
520 console.log('MisralAI request:', requestBody);528 console.debug('MisralAI request:', requestBody);
521529
522 const generateResponse = await fetch(apiUrl + '/chat/completions', config);530 const generateResponse = await fetch(apiUrl + '/chat/completions', config);
523 if (request.body.stream) {531 if (request.body.stream) {
@@ -525,16 +533,16 @@ async function sendMistralAIRequest(request, response) {
525 } else {533 } else {
526 if (!generateResponse.ok) {534 if (!generateResponse.ok) {
527 const errorText = await generateResponse.text();535 const errorText = await generateResponse.text();
528 console.log(`MistralAI API returned error: ${generateResponse.status} ${generateResponse.statusText} ${errorText}`);536 console.warn(`MistralAI API returned error: ${generateResponse.status} ${generateResponse.statusText} ${errorText}`);
529 const errorJson = tryParse(errorText) ?? { error: true };537 const errorJson = tryParse(errorText) ?? { error: true };
530 return response.status(500).send(errorJson);538 return response.status(500).send(errorJson);
531 }539 }
532 const generateResponseJson = await generateResponse.json();540 const generateResponseJson = await generateResponse.json();
533 console.log('MistralAI response:', generateResponseJson);541 console.debug('MistralAI response:', generateResponseJson);
534 return response.send(generateResponseJson);542 return response.send(generateResponseJson);
535 }543 }
536 } catch (error) {544 } catch (error) {
537 console.log('Error communicating with MistralAI API: ', error);545 console.error('Error communicating with MistralAI API: ', error);
538 if (!response.headersSent) {546 if (!response.headersSent) {
539 response.send({ error: true });547 response.send({ error: true });
540 } else {548 } else {
@@ -557,7 +565,7 @@ async function sendCohereRequest(request, response) {
557 });565 });
558566
559 if (!apiKey) {567 if (!apiKey) {
560 console.log('Cohere API key is missing.');568 console.warn('Cohere API key is missing.');
561 return response.status(400).send({ error: true });569 return response.status(400).send({ error: true });
562 }570 }
563571
@@ -596,7 +604,7 @@ async function sendCohereRequest(request, response) {
596 requestBody.safety_mode = 'OFF';604 requestBody.safety_mode = 'OFF';
597 }605 }
598606
599 console.log('Cohere request:', requestBody);607 console.debug('Cohere request:', requestBody);
600608
601 const config = {609 const config = {
602 method: 'POST',610 method: 'POST',
@@ -618,16 +626,16 @@ async function sendCohereRequest(request, response) {
618 const generateResponse = await fetch(apiUrl, config);626 const generateResponse = await fetch(apiUrl, config);
619 if (!generateResponse.ok) {627 if (!generateResponse.ok) {
620 const errorText = await generateResponse.text();628 const errorText = await generateResponse.text();
621 console.log(`Cohere API returned error: ${generateResponse.status} ${generateResponse.statusText} ${errorText}`);629 console.warn(`Cohere API returned error: ${generateResponse.status} ${generateResponse.statusText} ${errorText}`);
622 const errorJson = tryParse(errorText) ?? { error: true };630 const errorJson = tryParse(errorText) ?? { error: true };
623 return response.status(500).send(errorJson);631 return response.status(500).send(errorJson);
624 }632 }
625 const generateResponseJson = await generateResponse.json();633 const generateResponseJson = await generateResponse.json();
626 console.log('Cohere response:', generateResponseJson);634 console.debug('Cohere response:', generateResponseJson);
627 return response.send(generateResponseJson);635 return response.send(generateResponseJson);
628 }636 }
629 } catch (error) {637 } catch (error) {
630 console.log('Error communicating with Cohere API: ', error);638 console.error('Error communicating with Cohere API: ', error);
631 if (!response.headersSent) {639 if (!response.headersSent) {
632 response.send({ error: true });640 response.send({ error: true });
633 } else {641 } else {
@@ -636,6 +644,94 @@ async function sendCohereRequest(request, response) {
636 }644 }
637}645}
638646
647/**
648 * Sends a request to DeepSeek API.
649 * @param {express.Request} request Express request
650 * @param {express.Response} response Express response
651 */
652async function sendDeepSeekRequest(request, response) {
653 const apiUrl = new URL(request.body.reverse_proxy || API_DEEPSEEK).toString();
654 const apiKey = request.body.reverse_proxy ? request.body.proxy_password : readSecret(request.user.directories, SECRET_KEYS.DEEPSEEK);
655
656 if (!apiKey && !request.body.reverse_proxy) {
657 console.warn('DeepSeek API key is missing.');
658 return response.status(400).send({ error: true });
659 }
660
661 const controller = new AbortController();
662 request.socket.removeAllListeners('close');
663 request.socket.on('close', function () {
664 controller.abort();
665 });
666
667 try {
668 let bodyParams = {};
669
670 if (request.body.logprobs > 0) {
671 bodyParams['top_logprobs'] = request.body.logprobs;
672 bodyParams['logprobs'] = true;
673 }
674
675 if (Array.isArray(request.body.tools) && request.body.tools.length > 0) {
676 bodyParams['tools'] = request.body.tools;
677 bodyParams['tool_choice'] = request.body.tool_choice;
678 }
679
680 const postProcessType = String(request.body.model).endsWith('-reasoner') ? 'deepseek-reasoner' : 'deepseek';
681 const processedMessages = postProcessPrompt(request.body.messages, postProcessType, getPromptNames(request));
682
683 const requestBody = {
684 'messages': processedMessages,
685 'model': request.body.model,
686 'temperature': request.body.temperature,
687 'max_tokens': request.body.max_tokens,
688 'stream': request.body.stream,
689 'presence_penalty': request.body.presence_penalty,
690 'frequency_penalty': request.body.frequency_penalty,
691 'top_p': request.body.top_p,
692 'stop': request.body.stop,
693 'seed': request.body.seed,
694 ...bodyParams,
695 };
696
697 const config = {
698 method: 'POST',
699 headers: {
700 'Content-Type': 'application/json',
701 'Authorization': 'Bearer ' + apiKey,
702 },
703 body: JSON.stringify(requestBody),
704 signal: controller.signal,
705 };
706
707 console.debug('DeepSeek request:', requestBody);
708
709 const generateResponse = await fetch(apiUrl + '/chat/completions', config);
710
711 if (request.body.stream) {
712 forwardFetchResponse(generateResponse, response);
713 } else {
714 if (!generateResponse.ok) {
715 const errorText = await generateResponse.text();
716 console.warn(`DeepSeek API returned error: ${generateResponse.status} ${generateResponse.statusText} ${errorText}`);
717 const errorJson = tryParse(errorText) ?? { error: true };
718 return response.status(500).send(errorJson);
719 }
720 const generateResponseJson = await generateResponse.json();
721 console.debug('DeepSeek response:', generateResponseJson);
722 return response.send(generateResponseJson);
723 }
724 } catch (error) {
725 console.error('Error communicating with DeepSeek API: ', error);
726 if (!response.headersSent) {
727 response.send({ error: true });
728 } else {
729 response.end();
730 }
731 }
732}
733
734
639export const router = express.Router();735export const router = express.Router();
640736
641router.post('/status', jsonParser, async function (request, response_getstatus_openai) {737router.post('/status', jsonParser, async function (request, response_getstatus_openai) {
@@ -680,16 +776,16 @@ router.post('/status', jsonParser, async function (request, response_getstatus_o
680 api_key_openai = readSecret(request.user.directories, SECRET_KEYS.NANOGPT);776 api_key_openai = readSecret(request.user.directories, SECRET_KEYS.NANOGPT);
681 headers = {};777 headers = {};
682 } else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.DEEPSEEK) {778 } else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.DEEPSEEK) {
683 api_url = API_DEEPSEEK.replace('/beta', '');779 api_url = new URL(request.body.reverse_proxy || API_DEEPSEEK.replace('/beta', ''));
684 api_key_openai = readSecret(request.user.directories, SECRET_KEYS.DEEPSEEK);780 api_key_openai = request.body.reverse_proxy ? request.body.proxy_password : readSecret(request.user.directories, SECRET_KEYS.DEEPSEEK);
685 headers = {};781 headers = {};
686 } else {782 } else {
687 console.log('This chat completion source is not supported yet.');783 console.warn('This chat completion source is not supported yet.');
688 return response_getstatus_openai.status(400).send({ error: true });784 return response_getstatus_openai.status(400).send({ error: true });
689 }785 }
690786
691 if (!api_key_openai && !request.body.reverse_proxy && request.body.chat_completion_source !== CHAT_COMPLETION_SOURCES.CUSTOM) {787 if (!api_key_openai && !request.body.reverse_proxy && request.body.chat_completion_source !== CHAT_COMPLETION_SOURCES.CUSTOM) {
692 console.log('Chat Completion API key is missing.');788 console.warn('Chat Completion API key is missing.');
693 return response_getstatus_openai.status(400).send({ error: true });789 return response_getstatus_openai.status(400).send({ error: true });
694 }790 }
695791
@@ -724,23 +820,23 @@ router.post('/status', jsonParser, async function (request, response_getstatus_o
724 };820 };
725 });821 });
726822
727 console.log('Available OpenRouter models:', models);823 console.info('Available OpenRouter models:', models);
728 } else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.MISTRALAI) {824 } else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.MISTRALAI) {
729 const models = data?.data;825 const models = data?.data;
730 console.log(models);826 console.info(models);
731 } else {827 } else {
732 const models = data?.data;828 const models = data?.data;
733829
734 if (Array.isArray(models)) {830 if (Array.isArray(models)) {
735 const modelIds = models.filter(x => x && typeof x === 'object').map(x => x.id).sort();831 const modelIds = models.filter(x => x && typeof x === 'object').map(x => x.id).sort();
736 console.log('Available models:', modelIds);832 console.info('Available models:', modelIds);
737 } else {833 } else {
738 console.log('Chat Completion endpoint did not return a list of models.');834 console.warn('Chat Completion endpoint did not return a list of models.');
739 }835 }
740 }836 }
741 }837 }
742 else {838 else {
743 console.log('Chat Completion status check failed. Either Access Token is incorrect or API endpoint is down.');839 console.error('Chat Completion status check failed. Either Access Token is incorrect or API endpoint is down.');
744 response_getstatus_openai.send({ error: true, can_bypass: true, data: { data: [] } });840 response_getstatus_openai.send({ error: true, can_bypass: true, data: { data: [] } });
745 }841 }
746 } catch (e) {842 } catch (e) {
@@ -773,10 +869,18 @@ router.post('/bias', jsonParser, async function (request, response) {
773 const tokenizer = getSentencepiceTokenizer(model);869 const tokenizer = getSentencepiceTokenizer(model);
774 const instance = await tokenizer?.get();870 const instance = await tokenizer?.get();
775 if (!instance) {871 if (!instance) {
776 console.warn('Tokenizer not initialized:', model);872 console.error('Tokenizer not initialized:', model);
777 return response.send({});873 return response.send({});
778 }874 }
779 encodeFunction = (text) => new Uint32Array(instance.encodeIds(text));875 encodeFunction = (text) => new Uint32Array(instance.encodeIds(text));
876 } else if (webTokenizers.includes(model)) {
877 const tokenizer = getWebTokenizer(model);
878 const instance = await tokenizer?.get();
879 if (!instance) {
880 console.warn('Tokenizer not initialized:', model);
881 return response.send({});
882 }
883 encodeFunction = (text) => new Uint32Array(instance.encode(text));
780 } else {884 } else {
781 const tokenizer = getTiktokenTokenizer(model);885 const tokenizer = getTiktokenTokenizer(model);
782 encodeFunction = (tokenizer.encode.bind(tokenizer));886 encodeFunction = (tokenizer.encode.bind(tokenizer));
@@ -841,6 +945,7 @@ router.post('/generate', jsonParser, function (request, response) {
841 case CHAT_COMPLETION_SOURCES.MAKERSUITE: return sendMakerSuiteRequest(request, response);945 case CHAT_COMPLETION_SOURCES.MAKERSUITE: return sendMakerSuiteRequest(request, response);
842 case CHAT_COMPLETION_SOURCES.MISTRALAI: return sendMistralAIRequest(request, response);946 case CHAT_COMPLETION_SOURCES.MISTRALAI: return sendMistralAIRequest(request, response);
843 case CHAT_COMPLETION_SOURCES.COHERE: return sendCohereRequest(request, response);947 case CHAT_COMPLETION_SOURCES.COHERE: return sendCohereRequest(request, response);
948 case CHAT_COMPLETION_SOURCES.DEEPSEEK: return sendDeepSeekRequest(request, response);
844 }949 }
845950
846 let apiUrl;951 let apiUrl;
@@ -899,6 +1004,10 @@ router.post('/generate', jsonParser, function (request, response) {
899 bodyParams['route'] = 'fallback';1004 bodyParams['route'] = 'fallback';
900 }1005 }
9011006
1007 if (request.body.include_reasoning) {
1008 bodyParams['include_reasoning'] = true;
1009 }
1010
902 let cachingAtDepth = getConfigValue('claude.cachingAtDepth', -1);1011 let cachingAtDepth = getConfigValue('claude.cachingAtDepth', -1);
903 if (Number.isInteger(cachingAtDepth) && cachingAtDepth >= 0 && request.body.model?.startsWith('anthropic/claude-3')) {1012 if (Number.isInteger(cachingAtDepth) && cachingAtDepth >= 0 && request.body.model?.startsWith('anthropic/claude-3')) {
904 cachingAtDepthForOpenRouterClaude(request.body.messages, cachingAtDepth);1013 cachingAtDepthForOpenRouterClaude(request.body.messages, cachingAtDepth);
@@ -922,7 +1031,7 @@ router.post('/generate', jsonParser, function (request, response) {
922 mergeObjectWithYaml(headers, request.body.custom_include_headers);1031 mergeObjectWithYaml(headers, request.body.custom_include_headers);
9231032
924 if (request.body.custom_prompt_post_processing) {1033 if (request.body.custom_prompt_post_processing) {
925 console.log('Applying custom prompt post-processing of type', request.body.custom_prompt_post_processing);1034 console.info('Applying custom prompt post-processing of type', request.body.custom_prompt_post_processing);
926 request.body.messages = postProcessPrompt(1035 request.body.messages = postProcessPrompt(
927 request.body.messages,1036 request.body.messages,
928 request.body.custom_prompt_post_processing,1037 request.body.custom_prompt_post_processing,
@@ -954,25 +1063,20 @@ router.post('/generate', jsonParser, function (request, response) {
954 apiKey = readSecret(request.user.directories, SECRET_KEYS.BLOCKENTROPY);1063 apiKey = readSecret(request.user.directories, SECRET_KEYS.BLOCKENTROPY);
955 headers = {};1064 headers = {};
956 bodyParams = {};1065 bodyParams = {};
957 } else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.DEEPSEEK) {
958 apiUrl = API_DEEPSEEK;
959 apiKey = readSecret(request.user.directories, SECRET_KEYS.DEEPSEEK);
960 headers = {};
961 bodyParams = {};
962
963 if (request.body.logprobs > 0) {
964 bodyParams['top_logprobs'] = request.body.logprobs;
965 bodyParams['logprobs'] = true;
966 }
967
968 request.body.messages = postProcessPrompt(request.body.messages, 'deepseek', getPromptNames(request));
969 } else {1066 } else {
970 console.log('This chat completion source is not supported yet.');1067 console.warn('This chat completion source is not supported yet.');
971 return response.status(400).send({ error: true });1068 return response.status(400).send({ error: true });
972 }1069 }
9731070
1071 // A few of OpenAIs reasoning models support reasoning effort
1072 if ([CHAT_COMPLETION_SOURCES.CUSTOM, CHAT_COMPLETION_SOURCES.OPENAI].includes(request.body.chat_completion_source)) {
1073 if (['o1', 'o3-mini', 'o3-mini-2025-01-31'].includes(request.body.model)) {
1074 bodyParams['reasoning_effort'] = request.body.reasoning_effort;
1075 }
1076 }
1077
974 if (!apiKey && !request.body.reverse_proxy && request.body.chat_completion_source !== CHAT_COMPLETION_SOURCES.CUSTOM) {1078 if (!apiKey && !request.body.reverse_proxy && request.body.chat_completion_source !== CHAT_COMPLETION_SOURCES.CUSTOM) {
975 console.log('OpenAI API key is missing.');1079 console.warn('OpenAI API key is missing.');
976 return response.status(400).send({ error: true });1080 return response.status(400).send({ error: true });
977 }1081 }
9781082
@@ -1032,7 +1136,7 @@ router.post('/generate', jsonParser, function (request, response) {
1032 signal: controller.signal,1136 signal: controller.signal,
1033 };1137 };
10341138
1035 console.log(requestBody);1139 console.debug(requestBody);
10361140
1037 makeRequest(config, response, request);1141 makeRequest(config, response, request);
10381142
@@ -1049,7 +1153,7 @@ router.post('/generate', jsonParser, function (request, response) {
1049 const fetchResponse = await fetch(endpointUrl, config);1153 const fetchResponse = await fetch(endpointUrl, config);
10501154
1051 if (request.body.stream) {1155 if (request.body.stream) {
1052 console.log('Streaming request in progress');1156 console.info('Streaming request in progress');
1053 forwardFetchResponse(fetchResponse, response);1157 forwardFetchResponse(fetchResponse, response);
1054 return;1158 return;
1055 }1159 }
@@ -1058,10 +1162,10 @@ router.post('/generate', jsonParser, function (request, response) {
1058 /** @type {any} */1162 /** @type {any} */
1059 let json = await fetchResponse.json();1163 let json = await fetchResponse.json();
1060 response.send(json);1164 response.send(json);
1061 console.log(json);1165 console.debug(json);
1062 console.log(json?.choices?.[0]?.message);1166 console.debug(json?.choices?.[0]?.message);
1063 } else if (fetchResponse.status === 429 && retries > 0) {1167 } else if (fetchResponse.status === 429 && retries > 0) {
1064 console.log(`Out of quota, retrying in ${Math.round(timeout / 1000)}s`);1168 console.warn(`Out of quota, retrying in ${Math.round(timeout / 1000)}s`);
1065 setTimeout(() => {1169 setTimeout(() => {
1066 timeout *= 2;1170 timeout *= 2;
1067 makeRequest(config, response, request, retries - 1, timeout);1171 makeRequest(config, response, request, retries - 1, timeout);
@@ -1070,7 +1174,7 @@ router.post('/generate', jsonParser, function (request, response) {
1070 await handleErrorResponse(fetchResponse);1174 await handleErrorResponse(fetchResponse);
1071 }1175 }
1072 } catch (error) {1176 } catch (error) {
1073 console.log('Generation failed', error);1177 console.error('Generation failed', error);
1074 const message = error.code === 'ECONNREFUSED'1178 const message = error.code === 'ECONNREFUSED'
1075 ? `Connection refused: ${error.message}`1179 ? `Connection refused: ${error.message}`
1076 : error.message || 'Unknown error occurred';1180 : error.message || 'Unknown error occurred';
@@ -1092,7 +1196,7 @@ router.post('/generate', jsonParser, function (request, response) {
10921196
1093 const message = errorResponse.statusText || 'Unknown error occurred';1197 const message = errorResponse.statusText || 'Unknown error occurred';
1094 const quota_error = errorResponse.status === 429 && errorData?.error?.type === 'insufficient_quota';1198 const quota_error = errorResponse.status === 429 && errorData?.error?.type === 'insufficient_quota';
1095 console.log('Chat completion request error: ', message, responseText);1199 console.error('Chat completion request error: ', message, responseText);
10961200
1097 if (!response.headersSent) {1201 if (!response.headersSent) {
1098 response.send({ error: { message }, quota_error: quota_error });1202 response.send({ error: { message }, quota_error: quota_error });
@@ -1103,4 +1207,3 @@ router.post('/generate', jsonParser, function (request, response) {
1103 }1207 }
1104 }1208 }
1105});1209});
1106
src/endpoints/backends/kobold.js+14 -14
@@ -22,17 +22,17 @@ router.post('/generate', jsonParser, async function (request, response_generate)
22 request.socket.on('close', async function () {22 request.socket.on('close', async function () {
23 if (request.body.can_abort && !response_generate.writableEnded) {23 if (request.body.can_abort && !response_generate.writableEnded) {
24 try {24 try {
25 console.log('Aborting Kobold generation...');25 console.info('Aborting Kobold generation...');
26 // send abort signal to koboldcpp26 // send abort signal to koboldcpp
27 const abortResponse = await fetch(`${request.body.api_server}/extra/abort`, {27 const abortResponse = await fetch(`${request.body.api_server}/extra/abort`, {
28 method: 'POST',28 method: 'POST',
29 });29 });
3030
31 if (!abortResponse.ok) {31 if (!abortResponse.ok) {
32 console.log('Error sending abort request to Kobold:', abortResponse.status);32 console.error('Error sending abort request to Kobold:', abortResponse.status);
33 }33 }
34 } catch (error) {34 } catch (error) {
35 console.log(error);35 console.error(error);
36 }36 }
37 }37 }
38 controller.abort();38 controller.abort();
@@ -81,7 +81,7 @@ router.post('/generate', jsonParser, async function (request, response_generate)
81 }81 }
82 }82 }
8383
84 console.log(this_settings);84 console.debug(this_settings);
85 const args = {85 const args = {
86 body: JSON.stringify(this_settings),86 body: JSON.stringify(this_settings),
87 headers: Object.assign(87 headers: Object.assign(
@@ -105,7 +105,7 @@ router.post('/generate', jsonParser, async function (request, response_generate)
105 } else {105 } else {
106 if (!response.ok) {106 if (!response.ok) {
107 const errorText = await response.text();107 const errorText = await response.text();
108 console.log(`Kobold returned error: ${response.status} ${response.statusText} ${errorText}`);108 console.warn(`Kobold returned error: ${response.status} ${response.statusText} ${errorText}`);
109109
110 try {110 try {
111 const errorJson = JSON.parse(errorText);111 const errorJson = JSON.parse(errorText);
@@ -117,7 +117,7 @@ router.post('/generate', jsonParser, async function (request, response_generate)
117 }117 }
118118
119 const data = await response.json();119 const data = await response.json();
120 console.log('Endpoint response:', data);120 console.debug('Endpoint response:', data);
121 return response_generate.send(data);121 return response_generate.send(data);
122 }122 }
123 } catch (error) {123 } catch (error) {
@@ -125,19 +125,19 @@ router.post('/generate', jsonParser, async function (request, response_generate)
125 switch (error?.status) {125 switch (error?.status) {
126 case 403:126 case 403:
127 case 503: // retry in case of temporary service issue, possibly caused by a queue failure?127 case 503: // retry in case of temporary service issue, possibly caused by a queue failure?
128 console.debug(`KoboldAI is busy. Retry attempt ${i + 1} of ${MAX_RETRIES}...`);128 console.warn(`KoboldAI is busy. Retry attempt ${i + 1} of ${MAX_RETRIES}...`);
129 await delay(delayAmount);129 await delay(delayAmount);
130 break;130 break;
131 default:131 default:
132 if ('status' in error) {132 if ('status' in error) {
133 console.log('Status Code from Kobold:', error.status);133 console.error('Status Code from Kobold:', error.status);
134 }134 }
135 return response_generate.send({ error: true });135 return response_generate.send({ error: true });
136 }136 }
137 }137 }
138 }138 }
139139
140 console.log('Max retries exceeded. Giving up.');140 console.error('Max retries exceeded. Giving up.');
141 return response_generate.send({ error: true });141 return response_generate.send({ error: true });
142});142});
143143
@@ -193,16 +193,16 @@ router.post('/transcribe-audio', urlencodedParser, async function (request, resp
193 const server = request.body.server;193 const server = request.body.server;
194194
195 if (!server) {195 if (!server) {
196 console.log('Server is not set');196 console.error('Server is not set');
197 return response.sendStatus(400);197 return response.sendStatus(400);
198 }198 }
199199
200 if (!request.file) {200 if (!request.file) {
201 console.log('No audio file found');201 console.error('No audio file found');
202 return response.sendStatus(400);202 return response.sendStatus(400);
203 }203 }
204204
205 console.log('Transcribing audio with KoboldCpp', server);205 console.debug('Transcribing audio with KoboldCpp', server);
206206
207 const fileBase64 = fs.readFileSync(request.file.path).toString('base64');207 const fileBase64 = fs.readFileSync(request.file.path).toString('base64');
208 fs.rmSync(request.file.path);208 fs.rmSync(request.file.path);
@@ -226,12 +226,12 @@ router.post('/transcribe-audio', urlencodedParser, async function (request, resp
226226
227 if (!result.ok) {227 if (!result.ok) {
228 const text = await result.text();228 const text = await result.text();
229 console.log('KoboldCpp request failed', result.statusText, text);229 console.error('KoboldCpp request failed', result.statusText, text);
230 return response.status(500).send(text);230 return response.status(500).send(text);
231 }231 }
232232
233 const data = await result.json();233 const data = await result.json();
234 console.log('KoboldCpp transcription response', data);234 console.debug('KoboldCpp transcription response', data);
235 return response.json(data);235 return response.json(data);
236 } catch (error) {236 } catch (error) {
237 console.error('KoboldCpp transcription failed', error);237 console.error('KoboldCpp transcription failed', error);
src/endpoints/backends/scale-alt.js+5 -5
@@ -13,7 +13,7 @@ router.post('/generate', jsonParser, async function (request, response) {
13 const cookie = readSecret(request.user.directories, SECRET_KEYS.SCALE_COOKIE);13 const cookie = readSecret(request.user.directories, SECRET_KEYS.SCALE_COOKIE);
1414
15 if (!cookie) {15 if (!cookie) {
16 console.log('No Scale cookie found');16 console.error('No Scale cookie found');
17 return response.sendStatus(400);17 return response.sendStatus(400);
18 }18 }
1919
@@ -62,7 +62,7 @@ router.post('/generate', jsonParser, async function (request, response) {
62 },62 },
63 };63 };
6464
65 console.log('Scale request:', body);65 console.debug('Scale request:', body);
6666
67 const result = await fetch('https://dashboard.scale.com/spellbook/api/trpc/v2.variant.run', {67 const result = await fetch('https://dashboard.scale.com/spellbook/api/trpc/v2.variant.run', {
68 method: 'POST',68 method: 'POST',
@@ -75,7 +75,7 @@ router.post('/generate', jsonParser, async function (request, response) {
7575
76 if (!result.ok) {76 if (!result.ok) {
77 const text = await result.text();77 const text = await result.text();
78 console.log('Scale request failed', result.statusText, text);78 console.error('Scale request failed', result.statusText, text);
79 return response.status(500).send({ error: { message: result.statusText } });79 return response.status(500).send({ error: { message: result.statusText } });
80 }80 }
8181
@@ -83,7 +83,7 @@ router.post('/generate', jsonParser, async function (request, response) {
83 const data = await result.json();83 const data = await result.json();
84 const output = data?.result?.data?.json?.outputs?.[0] || '';84 const output = data?.result?.data?.json?.outputs?.[0] || '';
8585
86 console.log('Scale response:', data);86 console.debug('Scale response:', data);
8787
88 if (!output) {88 if (!output) {
89 console.warn('Scale response is empty');89 console.warn('Scale response is empty');
@@ -92,7 +92,7 @@ router.post('/generate', jsonParser, async function (request, response) {
9292
93 return response.json({ output });93 return response.json({ output });
94 } catch (error) {94 } catch (error) {
95 console.log(error);95 console.error(error);
96 return response.sendStatus(500);96 return response.sendStatus(500);
97 }97 }
98});98});
src/endpoints/backends/text-completions.js+43 -45
@@ -58,12 +58,12 @@ async function parseOllamaStream(jsonStream, request, response) {
58 });58 });
5959
60 jsonStream.body.on('end', () => {60 jsonStream.body.on('end', () => {
61 console.log('Streaming request finished');61 console.info('Streaming request finished');
62 response.write('data: [DONE]\n\n');62 response.write('data: [DONE]\n\n');
63 response.end();63 response.end();
64 });64 });
65 } catch (error) {65 } catch (error) {
66 console.log('Error forwarding streaming response:', error);66 console.error('Error forwarding streaming response:', error);
67 if (!response.headersSent) {67 if (!response.headersSent) {
68 return response.status(500).send({ error: true });68 return response.status(500).send({ error: true });
69 } else {69 } else {
@@ -79,16 +79,16 @@ async function parseOllamaStream(jsonStream, request, response) {
79 */79 */
80async function abortKoboldCppRequest(url) {80async function abortKoboldCppRequest(url) {
81 try {81 try {
82 console.log('Aborting Kobold generation...');82 console.info('Aborting Kobold generation...');
83 const abortResponse = await fetch(`${url}/api/extra/abort`, {83 const abortResponse = await fetch(`${url}/api/extra/abort`, {
84 method: 'POST',84 method: 'POST',
85 });85 });
8686
87 if (!abortResponse.ok) {87 if (!abortResponse.ok) {
88 console.log('Error sending abort request to Kobold:', abortResponse.status, abortResponse.statusText);88 console.error('Error sending abort request to Kobold:', abortResponse.status, abortResponse.statusText);
89 }89 }
90 } catch (error) {90 } catch (error) {
91 console.log(error);91 console.error(error);
92 }92 }
93}93}
9494
@@ -101,7 +101,7 @@ router.post('/status', jsonParser, async function (request, response) {
101 request.body.api_server = request.body.api_server.replace('localhost', '127.0.0.1');101 request.body.api_server = request.body.api_server.replace('localhost', '127.0.0.1');
102 }102 }
103103
104 console.log('Trying to connect to API:', request.body);104 console.debug('Trying to connect to API', request.body);
105 const baseUrl = trimV1(request.body.api_server);105 const baseUrl = trimV1(request.body.api_server);
106106
107 const args = {107 const args = {
@@ -123,6 +123,7 @@ router.post('/status', jsonParser, async function (request, response) {
123 case TEXTGEN_TYPES.LLAMACPP:123 case TEXTGEN_TYPES.LLAMACPP:
124 case TEXTGEN_TYPES.INFERMATICAI:124 case TEXTGEN_TYPES.INFERMATICAI:
125 case TEXTGEN_TYPES.OPENROUTER:125 case TEXTGEN_TYPES.OPENROUTER:
126 case TEXTGEN_TYPES.FEATHERLESS:
126 url += '/v1/models';127 url += '/v1/models';
127 break;128 break;
128 case TEXTGEN_TYPES.DREAMGEN:129 case TEXTGEN_TYPES.DREAMGEN:
@@ -140,9 +141,6 @@ router.post('/status', jsonParser, async function (request, response) {
140 case TEXTGEN_TYPES.OLLAMA:141 case TEXTGEN_TYPES.OLLAMA:
141 url += '/api/tags';142 url += '/api/tags';
142 break;143 break;
143 case TEXTGEN_TYPES.FEATHERLESS:
144 url += '/v1/models';
145 break;
146 case TEXTGEN_TYPES.HUGGINGFACE:144 case TEXTGEN_TYPES.HUGGINGFACE:
147 url += '/info';145 url += '/info';
148 break;146 break;
@@ -152,7 +150,7 @@ router.post('/status', jsonParser, async function (request, response) {
152 const isPossiblyLmStudio = modelsReply.headers.get('x-powered-by') === 'Express';150 const isPossiblyLmStudio = modelsReply.headers.get('x-powered-by') === 'Express';
153151
154 if (!modelsReply.ok) {152 if (!modelsReply.ok) {
155 console.log('Models endpoint is offline.');153 console.error('Models endpoint is offline.');
156 return response.sendStatus(400);154 return response.sendStatus(400);
157 }155 }
158156
@@ -173,12 +171,12 @@ router.post('/status', jsonParser, async function (request, response) {
173 }171 }
174172
175 if (!Array.isArray(data.data)) {173 if (!Array.isArray(data.data)) {
176 console.log('Models response is not an array.');174 console.error('Models response is not an array.');
177 return response.sendStatus(400);175 return response.sendStatus(400);
178 }176 }
179177
180 const modelIds = data.data.map(x => x.id);178 const modelIds = data.data.map(x => x.id);
181 console.log('Models available:', modelIds);179 console.info('Models available:', modelIds);
182180
183 // Set result to the first model ID181 // Set result to the first model ID
184 result = modelIds[0] || 'Valid';182 result = modelIds[0] || 'Valid';
@@ -191,7 +189,7 @@ router.post('/status', jsonParser, async function (request, response) {
191 if (modelInfoReply.ok) {189 if (modelInfoReply.ok) {
192 /** @type {any} */190 /** @type {any} */
193 const modelInfo = await modelInfoReply.json();191 const modelInfo = await modelInfoReply.json();
194 console.log('Ooba model info:', modelInfo);192 console.debug('Ooba model info:', modelInfo);
195193
196 const modelName = modelInfo?.model_name;194 const modelName = modelInfo?.model_name;
197 result = modelName || result;195 result = modelName || result;
@@ -208,7 +206,7 @@ router.post('/status', jsonParser, async function (request, response) {
208 if (modelInfoReply.ok) {206 if (modelInfoReply.ok) {
209 /** @type {any} */207 /** @type {any} */
210 const modelInfo = await modelInfoReply.json();208 const modelInfo = await modelInfoReply.json();
211 console.log('Tabby model info:', modelInfo);209 console.debug('Tabby model info:', modelInfo);
212210
213 const modelName = modelInfo?.id;211 const modelName = modelInfo?.id;
214 result = modelName || result;212 result = modelName || result;
@@ -255,7 +253,7 @@ router.post('/props', jsonParser, async function (request, response) {
255 props['chat_template'] = props['chat_template'].slice(0, -1) + '\n';253 props['chat_template'] = props['chat_template'].slice(0, -1) + '\n';
256 }254 }
257 props['chat_template_hash'] = createHash('sha256').update(props['chat_template']).digest('hex');255 props['chat_template_hash'] = createHash('sha256').update(props['chat_template']).digest('hex');
258 console.log(`Model properties: ${JSON.stringify(props)}`);256 console.debug(`Model properties: ${JSON.stringify(props)}`);
259 return response.send(props);257 return response.send(props);
260 } catch (error) {258 } catch (error) {
261 console.error(error);259 console.error(error);
@@ -273,7 +271,7 @@ router.post('/generate', jsonParser, async function (request, response) {
273271
274 const apiType = request.body.api_type;272 const apiType = request.body.api_type;
275 const baseUrl = request.body.api_server;273 const baseUrl = request.body.api_server;
276 console.log(request.body);274 console.debug(request.body);
277275
278 const controller = new AbortController();276 const controller = new AbortController();
279 request.socket.removeAllListeners('close');277 request.socket.removeAllListeners('close');
@@ -375,6 +373,10 @@ router.post('/generate', jsonParser, async function (request, response) {
375373
376 if (request.body.api_type === TEXTGEN_TYPES.OLLAMA) {374 if (request.body.api_type === TEXTGEN_TYPES.OLLAMA) {
377 const keepAlive = getConfigValue('ollama.keepAlive', -1);375 const keepAlive = getConfigValue('ollama.keepAlive', -1);
376 const numBatch = getConfigValue('ollama.batchSize', -1);
377 if (numBatch > 0) {
378 request.body['num_batch'] = numBatch;
379 }
378 args.body = JSON.stringify({380 args.body = JSON.stringify({
379 model: request.body.model,381 model: request.body.model,
380 prompt: request.body.prompt,382 prompt: request.body.prompt,
@@ -399,7 +401,7 @@ router.post('/generate', jsonParser, async function (request, response) {
399 if (completionsReply.ok) {401 if (completionsReply.ok) {
400 /** @type {any} */402 /** @type {any} */
401 const data = await completionsReply.json();403 const data = await completionsReply.json();
402 console.log('Endpoint response:', data);404 console.debug('Endpoint response:', data);
403405
404 // Map InfermaticAI response to OAI completions format406 // Map InfermaticAI response to OAI completions format
405 if (apiType === TEXTGEN_TYPES.INFERMATICAI) {407 if (apiType === TEXTGEN_TYPES.INFERMATICAI) {
@@ -411,24 +413,20 @@ router.post('/generate', jsonParser, async function (request, response) {
411 const text = await completionsReply.text();413 const text = await completionsReply.text();
412 const errorBody = { error: true, status: completionsReply.status, response: text };414 const errorBody = { error: true, status: completionsReply.status, response: text };
413415
414 if (!response.headersSent) {416 return !response.headersSent
415 return response.send(errorBody);417 ? response.send(errorBody)
416 }418 : response.end();
417
418 return response.end();
419 }419 }
420 }420 }
421 } catch (error) {421 } catch (error) {
422 const status = error?.status ?? error?.code ?? 'UNKNOWN';422 const status = error?.status ?? error?.code ?? 'UNKNOWN';
423 const text = error?.error ?? error?.statusText ?? error?.message ?? 'Unknown error on /generate endpoint';423 const text = error?.error ?? error?.statusText ?? error?.message ?? 'Unknown error on /generate endpoint';
424 let value = { error: true, status: status, response: text };424 let value = { error: true, status: status, response: text };
425 console.log('Endpoint error:', error);425 console.error('Endpoint error:', error);
426426
427 if (!response.headersSent) {427 return !response.headersSent
428 return response.send(value);428 ? response.send(value)
429 }429 : response.end();
430
431 return response.end();
432 }430 }
433});431});
434432
@@ -451,7 +449,7 @@ ollama.post('/download', jsonParser, async function (request, response) {
451 });449 });
452450
453 if (!fetchResponse.ok) {451 if (!fetchResponse.ok) {
454 console.log('Download error:', fetchResponse.status, fetchResponse.statusText);452 console.error('Download error:', fetchResponse.status, fetchResponse.statusText);
455 return response.status(fetchResponse.status).send({ error: true });453 return response.status(fetchResponse.status).send({ error: true });
456 }454 }
457455
@@ -468,7 +466,7 @@ ollama.post('/caption-image', jsonParser, async function (request, response) {
468 return response.sendStatus(400);466 return response.sendStatus(400);
469 }467 }
470468
471 console.log('Ollama caption request:', request.body);469 console.debug('Ollama caption request:', request.body);
472 const baseUrl = trimV1(request.body.server_url);470 const baseUrl = trimV1(request.body.server_url);
473471
474 const fetchResponse = await fetch(`${baseUrl}/api/generate`, {472 const fetchResponse = await fetch(`${baseUrl}/api/generate`, {
@@ -483,18 +481,18 @@ ollama.post('/caption-image', jsonParser, async function (request, response) {
483 });481 });
484482
485 if (!fetchResponse.ok) {483 if (!fetchResponse.ok) {
486 console.log('Ollama caption error:', fetchResponse.status, fetchResponse.statusText);484 console.error('Ollama caption error:', fetchResponse.status, fetchResponse.statusText);
487 return response.status(500).send({ error: true });485 return response.status(500).send({ error: true });
488 }486 }
489487
490 /** @type {any} */488 /** @type {any} */
491 const data = await fetchResponse.json();489 const data = await fetchResponse.json();
492 console.log('Ollama caption response:', data);490 console.debug('Ollama caption response:', data);
493491
494 const caption = data?.response || '';492 const caption = data?.response || '';
495493
496 if (!caption) {494 if (!caption) {
497 console.log('Ollama caption is empty.');495 console.error('Ollama caption is empty.');
498 return response.status(500).send({ error: true });496 return response.status(500).send({ error: true });
499 }497 }
500498
@@ -513,7 +511,7 @@ llamacpp.post('/caption-image', jsonParser, async function (request, response) {
513 return response.sendStatus(400);511 return response.sendStatus(400);
514 }512 }
515513
516 console.log('LlamaCpp caption request:', request.body);514 console.debug('LlamaCpp caption request:', request.body);
517 const baseUrl = trimV1(request.body.server_url);515 const baseUrl = trimV1(request.body.server_url);
518516
519 const fetchResponse = await fetch(`${baseUrl}/completion`, {517 const fetchResponse = await fetch(`${baseUrl}/completion`, {
@@ -529,18 +527,18 @@ llamacpp.post('/caption-image', jsonParser, async function (request, response) {
529 });527 });
530528
531 if (!fetchResponse.ok) {529 if (!fetchResponse.ok) {
532 console.log('LlamaCpp caption error:', fetchResponse.status, fetchResponse.statusText);530 console.error('LlamaCpp caption error:', fetchResponse.status, fetchResponse.statusText);
533 return response.status(500).send({ error: true });531 return response.status(500).send({ error: true });
534 }532 }
535533
536 /** @type {any} */534 /** @type {any} */
537 const data = await fetchResponse.json();535 const data = await fetchResponse.json();
538 console.log('LlamaCpp caption response:', data);536 console.debug('LlamaCpp caption response:', data);
539537
540 const caption = data?.content || '';538 const caption = data?.content || '';
541539
542 if (!caption) {540 if (!caption) {
543 console.log('LlamaCpp caption is empty.');541 console.error('LlamaCpp caption is empty.');
544 return response.status(500).send({ error: true });542 return response.status(500).send({ error: true });
545 }543 }
546544
@@ -558,7 +556,7 @@ llamacpp.post('/props', jsonParser, async function (request, response) {
558 return response.sendStatus(400);556 return response.sendStatus(400);
559 }557 }
560558
561 console.log('LlamaCpp props request:', request.body);559 console.debug('LlamaCpp props request:', request.body);
562 const baseUrl = trimV1(request.body.server_url);560 const baseUrl = trimV1(request.body.server_url);
563561
564 const fetchResponse = await fetch(`${baseUrl}/props`, {562 const fetchResponse = await fetch(`${baseUrl}/props`, {
@@ -566,12 +564,12 @@ llamacpp.post('/props', jsonParser, async function (request, response) {
566 });564 });
567565
568 if (!fetchResponse.ok) {566 if (!fetchResponse.ok) {
569 console.log('LlamaCpp props error:', fetchResponse.status, fetchResponse.statusText);567 console.error('LlamaCpp props error:', fetchResponse.status, fetchResponse.statusText);
570 return response.status(500).send({ error: true });568 return response.status(500).send({ error: true });
571 }569 }
572570
573 const data = await fetchResponse.json();571 const data = await fetchResponse.json();
574 console.log('LlamaCpp props response:', data);572 console.debug('LlamaCpp props response:', data);
575573
576 return response.send(data);574 return response.send(data);
577575
@@ -590,7 +588,7 @@ llamacpp.post('/slots', jsonParser, async function (request, response) {
590 return response.sendStatus(400);588 return response.sendStatus(400);
591 }589 }
592590
593 console.log('LlamaCpp slots request:', request.body);591 console.debug('LlamaCpp slots request:', request.body);
594 const baseUrl = trimV1(request.body.server_url);592 const baseUrl = trimV1(request.body.server_url);
595593
596 let fetchResponse;594 let fetchResponse;
@@ -616,12 +614,12 @@ llamacpp.post('/slots', jsonParser, async function (request, response) {
616 }614 }
617615
618 if (!fetchResponse.ok) {616 if (!fetchResponse.ok) {
619 console.log('LlamaCpp slots error:', fetchResponse.status, fetchResponse.statusText);617 console.error('LlamaCpp slots error:', fetchResponse.status, fetchResponse.statusText);
620 return response.status(500).send({ error: true });618 return response.status(500).send({ error: true });
621 }619 }
622620
623 const data = await fetchResponse.json();621 const data = await fetchResponse.json();
624 console.log('LlamaCpp slots response:', data);622 console.debug('LlamaCpp slots response:', data);
625623
626 return response.send(data);624 return response.send(data);
627625
@@ -659,14 +657,14 @@ tabby.post('/download', jsonParser, async function (request, response) {
659 return response.status(403).send({ error: true });657 return response.status(403).send({ error: true });
660 }658 }
661 } else {659 } else {
662 console.log('API Permission error:', permissionResponse.status, permissionResponse.statusText);660 console.error('API Permission error:', permissionResponse.status, permissionResponse.statusText);
663 return response.status(permissionResponse.status).send({ error: true });661 return response.status(permissionResponse.status).send({ error: true });
664 }662 }
665663
666 const fetchResponse = await fetch(`${baseUrl}/v1/download`, args);664 const fetchResponse = await fetch(`${baseUrl}/v1/download`, args);
667665
668 if (!fetchResponse.ok) {666 if (!fetchResponse.ok) {
669 console.log('Download error:', fetchResponse.status, fetchResponse.statusText);667 console.error('Download error:', fetchResponse.status, fetchResponse.statusText);
670 return response.status(fetchResponse.status).send({ error: true });668 return response.status(fetchResponse.status).send({ error: true });
671 }669 }
672670
src/endpoints/backgrounds.js+5 -4
@@ -7,6 +7,7 @@ import sanitize from 'sanitize-filename';
7import { jsonParser, urlencodedParser } from '../express-common.js';7import { jsonParser, urlencodedParser } from '../express-common.js';
8import { invalidateThumbnail } from './thumbnails.js';8import { invalidateThumbnail } from './thumbnails.js';
9import { getImages } from '../util.js';9import { getImages } from '../util.js';
10import { getFileNameValidationFunction } from '../middleware/validateFileName.js';
1011
11export const router = express.Router();12export const router = express.Router();
1213
@@ -15,7 +16,7 @@ router.post('/all', jsonParser, function (request, response) {
15 response.send(JSON.stringify(images));16 response.send(JSON.stringify(images));
16});17});
1718
18router.post('/delete', jsonParser, function (request, response) {19router.post('/delete', jsonParser, getFileNameValidationFunction('bg'), function (request, response) {
19 if (!request.body) return response.sendStatus(400);20 if (!request.body) return response.sendStatus(400);
2021
21 if (request.body.bg !== sanitize(request.body.bg)) {22 if (request.body.bg !== sanitize(request.body.bg)) {
@@ -26,7 +27,7 @@ router.post('/delete', jsonParser, function (request, response) {
26 const fileName = path.join(request.user.directories.backgrounds, sanitize(request.body.bg));27 const fileName = path.join(request.user.directories.backgrounds, sanitize(request.body.bg));
2728
28 if (!fs.existsSync(fileName)) {29 if (!fs.existsSync(fileName)) {
29 console.log('BG file not found');30 console.error('BG file not found');
30 return response.sendStatus(400);31 return response.sendStatus(400);
31 }32 }
3233
@@ -42,12 +43,12 @@ router.post('/rename', jsonParser, function (request, response) {
42 const newFileName = path.join(request.user.directories.backgrounds, sanitize(request.body.new_bg));43 const newFileName = path.join(request.user.directories.backgrounds, sanitize(request.body.new_bg));
4344
44 if (!fs.existsSync(oldFileName)) {45 if (!fs.existsSync(oldFileName)) {
45 console.log('BG file not found');46 console.error('BG file not found');
46 return response.sendStatus(400);47 return response.sendStatus(400);
47 }48 }
4849
49 if (fs.existsSync(newFileName)) {50 if (fs.existsSync(newFileName)) {
50 console.log('New BG file already exists');51 console.error('New BG file already exists');
51 return response.sendStatus(400);52 return response.sendStatus(400);
52 }53 }
5354
src/endpoints/caption.js+4 -4
@@ -2,10 +2,10 @@ import express from 'express';
2import { jsonParser } from '../express-common.js';2import { jsonParser } from '../express-common.js';
3import { getPipeline, getRawImage } from '../transformers.js';3import { getPipeline, getRawImage } from '../transformers.js';
44
5const TASK = 'image-to-text';
6
7export const router = express.Router();5export const router = express.Router();
86
7const TASK = 'image-to-text';
8
9router.post('/', jsonParser, async (req, res) => {9router.post('/', jsonParser, async (req, res) => {
10 try {10 try {
11 const { image } = req.body;11 const { image } = req.body;
@@ -13,14 +13,14 @@ router.post('/', jsonParser, async (req, res) => {
13 const rawImage = await getRawImage(image);13 const rawImage = await getRawImage(image);
1414
15 if (!rawImage) {15 if (!rawImage) {
16 console.log('Failed to parse captioned image');16 console.warn('Failed to parse captioned image');
17 return res.sendStatus(400);17 return res.sendStatus(400);
18 }18 }
1919
20 const pipe = await getPipeline(TASK);20 const pipe = await getPipeline(TASK);
21 const result = await pipe(rawImage);21 const result = await pipe(rawImage);
22 const text = result[0].generated_text;22 const text = result[0].generated_text;
23 console.log('Image caption:', text);23 console.info('Image caption:', text);
2424
25 return res.json({ caption: text });25 return res.json({ caption: text });
26 } catch (error) {26 } catch (error) {
src/endpoints/characters.js+56 -47
@@ -14,6 +14,7 @@ import jimp from 'jimp';
1414
15import { AVATAR_WIDTH, AVATAR_HEIGHT } from '../constants.js';15import { AVATAR_WIDTH, AVATAR_HEIGHT } from '../constants.js';
16import { jsonParser, urlencodedParser } from '../express-common.js';16import { jsonParser, urlencodedParser } from '../express-common.js';
17import { default as validateAvatarUrlMiddleware, getFileNameValidationFunction } from '../middleware/validateFileName.js';
17import { deepMerge, humanizedISO8601DateTime, tryParse, extractFileFromZipBuffer, MemoryLimitedMap, getConfigValue } from '../util.js';18import { deepMerge, humanizedISO8601DateTime, tryParse, extractFileFromZipBuffer, MemoryLimitedMap, getConfigValue } from '../util.js';
18import { TavernCardValidator } from '../validator/TavernCardValidator.js';19import { TavernCardValidator } from '../validator/TavernCardValidator.js';
19import { parse, write } from '../character-card-parser.js';20import { parse, write } from '../character-card-parser.js';
@@ -73,12 +74,18 @@ async function writeCharacterData(inputFile, data, outputFile, request, crop = u
73 * Read the image, resize, and save it as a PNG into the buffer.74 * Read the image, resize, and save it as a PNG into the buffer.
74 * @returns {Promise<Buffer>} Image buffer75 * @returns {Promise<Buffer>} Image buffer
75 */76 */
76 function getInputImage() {77 async function getInputImage() {
78 try {
77 if (Buffer.isBuffer(inputFile)) {79 if (Buffer.isBuffer(inputFile)) {
78 return parseImageBuffer(inputFile, crop);80 return await parseImageBuffer(inputFile, crop);
79 }81 }
8082
81 return tryReadImage(inputFile, crop);83 return await tryReadImage(inputFile, crop);
84 } catch (error) {
85 const message = Buffer.isBuffer(inputFile) ? 'Failed to read image buffer.' : `Failed to read image: ${inputFile}.`;
86 console.warn(message, 'Using a fallback image.', error);
87 return await fs.promises.readFile(defaultAvatarPath);
88 }
82 }89 }
8390
84 const inputImage = await getInputImage();91 const inputImage = await getInputImage();
@@ -90,7 +97,7 @@ async function writeCharacterData(inputFile, data, outputFile, request, crop = u
90 writeFileAtomicSync(outputImagePath, outputImage);97 writeFileAtomicSync(outputImagePath, outputImage);
91 return true;98 return true;
92 } catch (err) {99 } catch (err) {
93 console.log(err);100 console.error(err);
94 return false;101 return false;
95 }102 }
96}103}
@@ -159,7 +166,7 @@ async function tryReadImage(imgPath, crop) {
159 }166 }
160 // If it's an unsupported type of image (APNG) - just read the file as buffer167 // If it's an unsupported type of image (APNG) - just read the file as buffer
161 catch (error) {168 catch (error) {
162 console.log(`Failed to read image: ${imgPath}`, error);169 console.error(`Failed to read image: ${imgPath}`, error);
163 return fs.readFileSync(imgPath);170 return fs.readFileSync(imgPath);
164 }171 }
165}172}
@@ -222,12 +229,12 @@ const processCharacter = async (item, directories) => {
222 return character;229 return character;
223 }230 }
224 catch (err) {231 catch (err) {
225 console.log(`Could not process character: ${item}`);232 console.error(`Could not process character: ${item}`);
226233
227 if (err instanceof SyntaxError) {234 if (err instanceof SyntaxError) {
228 console.log(`${item} does not contain a valid JSON object.`);235 console.error(`${item} does not contain a valid JSON object.`);
229 } else {236 } else {
230 console.log('An unexpected error occurred: ', err);237 console.error('An unexpected error occurred: ', err);
231 }238 }
232239
233 return {240 return {
@@ -315,7 +322,7 @@ function readFromV2(char) {
315 };322 };
316323
317 _.forEach(fieldMappings, (v2Path, charField) => {324 _.forEach(fieldMappings, (v2Path, charField) => {
318 //console.log(`Migrating field: ${charField} from ${v2Path}`);325 //console.info(`Migrating field: ${charField} from ${v2Path}`);
319 const v2Value = _.get(char.data, v2Path);326 const v2Value = _.get(char.data, v2Path);
320 if (_.isUndefined(v2Value)) {327 if (_.isUndefined(v2Value)) {
321 let defaultValue = undefined;328 let defaultValue = undefined;
@@ -330,15 +337,15 @@ function readFromV2(char) {
330 }337 }
331338
332 if (!_.isUndefined(defaultValue)) {339 if (!_.isUndefined(defaultValue)) {
333 //console.debug(`Spec v2 extension data missing for field: ${charField}, using default value: ${defaultValue}`);340 //console.warn(`Spec v2 extension data missing for field: ${charField}, using default value: ${defaultValue}`);
334 char[charField] = defaultValue;341 char[charField] = defaultValue;
335 } else {342 } else {
336 console.debug(`Char ${char['name']} has Spec v2 data missing for unknown field: ${charField}`);343 console.warn(`Char ${char['name']} has Spec v2 data missing for unknown field: ${charField}`);
337 return;344 return;
338 }345 }
339 }346 }
340 if (!_.isUndefined(char[charField]) && !_.isUndefined(v2Value) && String(char[charField]) !== String(v2Value)) {347 if (!_.isUndefined(char[charField]) && !_.isUndefined(v2Value) && String(char[charField]) !== String(v2Value)) {
341 console.debug(`Char ${char['name']} has Spec v2 data mismatch with Spec v1 for field: ${charField}`, char[charField], v2Value);348 console.warn(`Char ${char['name']} has Spec v2 data mismatch with Spec v1 for field: ${charField}`, char[charField], v2Value);
342 }349 }
343 char[charField] = v2Value;350 char[charField] = v2Value;
344 });351 });
@@ -435,7 +442,7 @@ function charaFormatData(data, directories) {
435 }442 }
436443
437 } catch {444 } catch {
438 console.debug(`Failed to read world info file: ${data.world}. Character book will not be available.`);445 console.warn(`Failed to read world info file: ${data.world}. Character book will not be available.`);
439 }446 }
440 }447 }
441448
@@ -445,7 +452,7 @@ function charaFormatData(data, directories) {
445 // Deep merge the extensions object452 // Deep merge the extensions object
446 _.set(char, 'data.extensions', deepMerge(char.data.extensions, extensions));453 _.set(char, 'data.extensions', deepMerge(char.data.extensions, extensions));
447 } catch {454 } catch {
448 console.debug(`Failed to parse extensions JSON: ${data.extensions}`);455 console.warn(`Failed to parse extensions JSON: ${data.extensions}`);
449 }456 }
450 }457 }
451458
@@ -519,7 +526,7 @@ async function importFromYaml(uploadPath, context, preservedFileName) {
519 const fileText = fs.readFileSync(uploadPath, 'utf8');526 const fileText = fs.readFileSync(uploadPath, 'utf8');
520 fs.rmSync(uploadPath);527 fs.rmSync(uploadPath);
521 const yamlData = yaml.parse(fileText);528 const yamlData = yaml.parse(fileText);
522 console.log('Importing from YAML');529 console.info('Importing from YAML');
523 yamlData.name = sanitize(yamlData.name);530 yamlData.name = sanitize(yamlData.name);
524 const fileName = preservedFileName || getPngName(yamlData.name, context.request.user.directories);531 const fileName = preservedFileName || getPngName(yamlData.name, context.request.user.directories);
525 let char = convertToV2({532 let char = convertToV2({
@@ -552,7 +559,7 @@ async function importFromYaml(uploadPath, context, preservedFileName) {
552async function importFromCharX(uploadPath, { request }, preservedFileName) {559async function importFromCharX(uploadPath, { request }, preservedFileName) {
553 const data = fs.readFileSync(uploadPath).buffer;560 const data = fs.readFileSync(uploadPath).buffer;
554 fs.rmSync(uploadPath);561 fs.rmSync(uploadPath);
555 console.log('Importing from CharX');562 console.info('Importing from CharX');
556 const cardBuffer = await extractFileFromZipBuffer(data, 'card.json');563 const cardBuffer = await extractFileFromZipBuffer(data, 'card.json');
557564
558 if (!cardBuffer) {565 if (!cardBuffer) {
@@ -601,7 +608,7 @@ async function importFromJson(uploadPath, { request }, preservedFileName) {
601 let jsonData = JSON.parse(data);608 let jsonData = JSON.parse(data);
602609
603 if (jsonData.spec !== undefined) {610 if (jsonData.spec !== undefined) {
604 console.log(`Importing from ${jsonData.spec} json`);611 console.info(`Importing from ${jsonData.spec} json`);
605 importRisuSprites(request.user.directories, jsonData);612 importRisuSprites(request.user.directories, jsonData);
606 unsetFavFlag(jsonData);613 unsetFavFlag(jsonData);
607 jsonData = readFromV2(jsonData);614 jsonData = readFromV2(jsonData);
@@ -611,7 +618,7 @@ async function importFromJson(uploadPath, { request }, preservedFileName) {
611 const result = await writeCharacterData(defaultAvatarPath, char, pngName, request);618 const result = await writeCharacterData(defaultAvatarPath, char, pngName, request);
612 return result ? pngName : '';619 return result ? pngName : '';
613 } else if (jsonData.name !== undefined) {620 } else if (jsonData.name !== undefined) {
614 console.log('Importing from v1 json');621 console.info('Importing from v1 json');
615 jsonData.name = sanitize(jsonData.name);622 jsonData.name = sanitize(jsonData.name);
616 if (jsonData.creator_notes) {623 if (jsonData.creator_notes) {
617 jsonData.creator_notes = jsonData.creator_notes.replace('Creator\'s notes go here.', '');624 jsonData.creator_notes = jsonData.creator_notes.replace('Creator\'s notes go here.', '');
@@ -637,7 +644,7 @@ async function importFromJson(uploadPath, { request }, preservedFileName) {
637 const result = await writeCharacterData(defaultAvatarPath, charJSON, pngName, request);644 const result = await writeCharacterData(defaultAvatarPath, charJSON, pngName, request);
638 return result ? pngName : '';645 return result ? pngName : '';
639 } else if (jsonData.char_name !== undefined) {//json Pygmalion notepad646 } else if (jsonData.char_name !== undefined) {//json Pygmalion notepad
640 console.log('Importing from gradio json');647 console.info('Importing from gradio json');
641 jsonData.char_name = sanitize(jsonData.char_name);648 jsonData.char_name = sanitize(jsonData.char_name);
642 if (jsonData.creator_notes) {649 if (jsonData.creator_notes) {
643 jsonData.creator_notes = jsonData.creator_notes.replace('Creator\'s notes go here.', '');650 jsonData.creator_notes = jsonData.creator_notes.replace('Creator\'s notes go here.', '');
@@ -684,7 +691,7 @@ async function importFromPng(uploadPath, { request }, preservedFileName) {
684 const pngName = preservedFileName || getPngName(jsonData.name, request.user.directories);691 const pngName = preservedFileName || getPngName(jsonData.name, request.user.directories);
685692
686 if (jsonData.spec !== undefined) {693 if (jsonData.spec !== undefined) {
687 console.log(`Found a ${jsonData.spec} character file.`);694 console.info(`Found a ${jsonData.spec} character file.`);
688 importRisuSprites(request.user.directories, jsonData);695 importRisuSprites(request.user.directories, jsonData);
689 unsetFavFlag(jsonData);696 unsetFavFlag(jsonData);
690 jsonData = readFromV2(jsonData);697 jsonData = readFromV2(jsonData);
@@ -694,7 +701,7 @@ async function importFromPng(uploadPath, { request }, preservedFileName) {
694 fs.unlinkSync(uploadPath);701 fs.unlinkSync(uploadPath);
695 return result ? pngName : '';702 return result ? pngName : '';
696 } else if (jsonData.name !== undefined) {703 } else if (jsonData.name !== undefined) {
697 console.log('Found a v1 character file.');704 console.info('Found a v1 character file.');
698705
699 if (jsonData.creator_notes) {706 if (jsonData.creator_notes) {
700 jsonData.creator_notes = jsonData.creator_notes.replace('Creator\'s notes go here.', '');707 jsonData.creator_notes = jsonData.creator_notes.replace('Creator\'s notes go here.', '');
@@ -756,7 +763,7 @@ router.post('/create', urlencodedParser, async function (request, response) {
756 }763 }
757});764});
758765
759router.post('/rename', jsonParser, async function (request, response) {766router.post('/rename', jsonParser, validateAvatarUrlMiddleware, async function (request, response) {
760 if (!request.body.avatar_url || !request.body.new_name) {767 if (!request.body.avatar_url || !request.body.new_name) {
761 return response.sendStatus(400);768 return response.sendStatus(400);
762 }769 }
@@ -803,15 +810,15 @@ router.post('/rename', jsonParser, async function (request, response) {
803 }810 }
804});811});
805812
806router.post('/edit', urlencodedParser, async function (request, response) {813router.post('/edit', urlencodedParser, validateAvatarUrlMiddleware, async function (request, response) {
807 if (!request.body) {814 if (!request.body) {
808 console.error('Error: no response body detected');815 console.warn('Error: no response body detected');
809 response.status(400).send('Error: no response body detected');816 response.status(400).send('Error: no response body detected');
810 return;817 return;
811 }818 }
812819
813 if (request.body.ch_name === '' || request.body.ch_name === undefined || request.body.ch_name === '.') {820 if (request.body.ch_name === '' || request.body.ch_name === undefined || request.body.ch_name === '.') {
814 console.error('Error: invalid name.');821 console.warn('Error: invalid name.');
815 response.status(400).send('Error: invalid name.');822 response.status(400).send('Error: invalid name.');
816 return;823 return;
817 }824 }
@@ -832,6 +839,9 @@ router.post('/edit', urlencodedParser, async function (request, response) {
832 invalidateThumbnail(request.user.directories, 'avatar', request.body.avatar_url);839 invalidateThumbnail(request.user.directories, 'avatar', request.body.avatar_url);
833 await writeCharacterData(newAvatarPath, char, targetFile, request, crop);840 await writeCharacterData(newAvatarPath, char, targetFile, request, crop);
834 fs.unlinkSync(newAvatarPath);841 fs.unlinkSync(newAvatarPath);
842
843 // Bust cache to reload the new avatar
844 response.setHeader('Clear-Site-Data', '"cache"');
835 }845 }
836846
837 return response.sendStatus(200);847 return response.sendStatus(200);
@@ -852,15 +862,15 @@ router.post('/edit', urlencodedParser, async function (request, response) {
852 * @param {Object} response - The HTTP response object.862 * @param {Object} response - The HTTP response object.
853 * @returns {void}863 * @returns {void}
854 */864 */
855router.post('/edit-attribute', jsonParser, async function (request, response) {865router.post('/edit-attribute', jsonParser, validateAvatarUrlMiddleware, async function (request, response) {
856 console.log(request.body);866 console.debug(request.body);
857 if (!request.body) {867 if (!request.body) {
858 console.error('Error: no response body detected');868 console.warn('Error: no response body detected');
859 return response.status(400).send('Error: no response body detected');869 return response.status(400).send('Error: no response body detected');
860 }870 }
861871
862 if (request.body.ch_name === '' || request.body.ch_name === undefined || request.body.ch_name === '.') {872 if (request.body.ch_name === '' || request.body.ch_name === undefined || request.body.ch_name === '.') {
863 console.error('Error: invalid name.');873 console.warn('Error: invalid name.');
864 return response.status(400).send('Error: invalid name.');874 return response.status(400).send('Error: invalid name.');
865 }875 }
866876
@@ -872,7 +882,7 @@ router.post('/edit-attribute', jsonParser, async function (request, response) {
872 const char = JSON.parse(charJSON);882 const char = JSON.parse(charJSON);
873 //check if the field exists883 //check if the field exists
874 if (char[request.body.field] === undefined && char.data[request.body.field] === undefined) {884 if (char[request.body.field] === undefined && char.data[request.body.field] === undefined) {
875 console.error('Error: invalid field.');885 console.warn('Error: invalid field.');
876 response.status(400).send('Error: invalid field.');886 response.status(400).send('Error: invalid field.');
877 return;887 return;
878 }888 }
@@ -898,7 +908,7 @@ router.post('/edit-attribute', jsonParser, async function (request, response) {
898 *908 *
899 * @returns {void}909 * @returns {void}
900 * */910 * */
901router.post('/merge-attributes', jsonParser, async function (request, response) {911router.post('/merge-attributes', jsonParser, getFileNameValidationFunction('avatar'), async function (request, response) {
902 try {912 try {
903 const update = request.body;913 const update = request.body;
904 const avatarPath = path.join(request.user.directories.characters, update.avatar);914 const avatarPath = path.join(request.user.directories.characters, update.avatar);
@@ -921,7 +931,7 @@ router.post('/merge-attributes', jsonParser, async function (request, response)
921 await writeCharacterData(avatarPath, JSON.stringify(character), targetImg, request);931 await writeCharacterData(avatarPath, JSON.stringify(character), targetImg, request);
922 response.sendStatus(200);932 response.sendStatus(200);
923 } else {933 } else {
924 console.log(validator.lastValidationError);934 console.warn(validator.lastValidationError);
925 response.status(400).send({ message: `Validation failed for ${character.name}`, error: validator.lastValidationError });935 response.status(400).send({ message: `Validation failed for ${character.name}`, error: validator.lastValidationError });
926 }936 }
927 } catch (exception) {937 } catch (exception) {
@@ -929,7 +939,7 @@ router.post('/merge-attributes', jsonParser, async function (request, response)
929 }939 }
930});940});
931941
932router.post('/delete', jsonParser, async function (request, response) {942router.post('/delete', jsonParser, validateAvatarUrlMiddleware, async function (request, response) {
933 if (!request.body || !request.body.avatar_url) {943 if (!request.body || !request.body.avatar_url) {
934 return response.sendStatus(400);944 return response.sendStatus(400);
935 }945 }
@@ -992,7 +1002,7 @@ router.post('/all', jsonParser, async function (request, response) {
992 }1002 }
993});1003});
9941004
995router.post('/get', jsonParser, async function (request, response) {1005router.post('/get', jsonParser, validateAvatarUrlMiddleware, async function (request, response) {
996 try {1006 try {
997 if (!request.body) return response.sendStatus(400);1007 if (!request.body) return response.sendStatus(400);
998 const item = request.body.avatar_url;1008 const item = request.body.avatar_url;
@@ -1011,7 +1021,7 @@ router.post('/get', jsonParser, async function (request, response) {
1011 }1021 }
1012});1022});
10131023
1014router.post('/chats', jsonParser, async function (request, response) {1024router.post('/chats', jsonParser, validateAvatarUrlMiddleware, async function (request, response) {
1015 if (!request.body) return response.sendStatus(400);1025 if (!request.body) return response.sendStatus(400);
10161026
1017 const characterDirectory = (request.body.avatar_url).replace('.png', '');1027 const characterDirectory = (request.body.avatar_url).replace('.png', '');
@@ -1043,7 +1053,7 @@ router.post('/chats', jsonParser, async function (request, response) {
1043 const fileSizeInKB = `${(stats.size / 1024).toFixed(2)}kb`;1053 const fileSizeInKB = `${(stats.size / 1024).toFixed(2)}kb`;
10441054
1045 if (stats.size === 0) {1055 if (stats.size === 0) {
1046 console.log(`Found an empty chat file: ${pathToFile}`);1056 console.warn(`Found an empty chat file: ${pathToFile}`);
1047 res({});1057 res({});
1048 return;1058 return;
1049 }1059 }
@@ -1075,7 +1085,7 @@ router.post('/chats', jsonParser, async function (request, response) {
10751085
1076 res(chatData);1086 res(chatData);
1077 } else {1087 } else {
1078 console.log('Found an invalid or corrupted chat file:', pathToFile);1088 console.warn('Found an invalid or corrupted chat file:', pathToFile);
1079 res({});1089 res({});
1080 }1090 }
1081 }1091 }
@@ -1088,7 +1098,7 @@ router.post('/chats', jsonParser, async function (request, response) {
10881098
1089 return response.send(validFiles);1099 return response.send(validFiles);
1090 } catch (error) {1100 } catch (error) {
1091 console.log(error);1101 console.error(error);
1092 return response.send({ error: true });1102 return response.send({ error: true });
1093 }1103 }
1094});1104});
@@ -1145,7 +1155,7 @@ router.post('/import', urlencodedParser, async function (request, response) {
1145 const fileName = await importFunction(uploadPath, { request, response }, preservedFileName);1155 const fileName = await importFunction(uploadPath, { request, response }, preservedFileName);
11461156
1147 if (!fileName) {1157 if (!fileName) {
1148 console.error('Failed to import character');1158 console.warn('Failed to import character');
1149 return response.sendStatus(400);1159 return response.sendStatus(400);
1150 }1160 }
11511161
@@ -1155,22 +1165,21 @@ router.post('/import', urlencodedParser, async function (request, response) {
11551165
1156 response.send({ file_name: fileName });1166 response.send({ file_name: fileName });
1157 } catch (err) {1167 } catch (err) {
1158 console.log(err);1168 console.error(err);
1159 response.send({ error: true });1169 response.send({ error: true });
1160 }1170 }
1161});1171});
11621172
1163router.post('/duplicate', jsonParser, async function (request, response) {1173router.post('/duplicate', jsonParser, validateAvatarUrlMiddleware, async function (request, response) {
1164 try {1174 try {
1165 if (!request.body.avatar_url) {1175 if (!request.body.avatar_url) {
1166 console.log('avatar URL not found in request body');1176 console.warn('avatar URL not found in request body');
1167 console.log(request.body);1177 console.debug(request.body);
1168 return response.sendStatus(400);1178 return response.sendStatus(400);
1169 }1179 }
1170 let filename = path.join(request.user.directories.characters, sanitize(request.body.avatar_url));1180 let filename = path.join(request.user.directories.characters, sanitize(request.body.avatar_url));
1171 if (!fs.existsSync(filename)) {1181 if (!fs.existsSync(filename)) {
1172 console.log('file for dupe not found');1182 console.error('file for dupe not found', filename);
1173 console.log(filename);
1174 return response.sendStatus(404);1183 return response.sendStatus(404);
1175 }1184 }
1176 let suffix = 1;1185 let suffix = 1;
@@ -1198,7 +1207,7 @@ router.post('/duplicate', jsonParser, async function (request, response) {
1198 }1207 }
11991208
1200 fs.copyFileSync(filename, newFilename);1209 fs.copyFileSync(filename, newFilename);
1201 console.log(`${filename} was copied to ${newFilename}`);1210 console.info(`${filename} was copied to ${newFilename}`);
1202 response.send({ path: path.parse(newFilename).base });1211 response.send({ path: path.parse(newFilename).base });
1203 }1212 }
1204 catch (error) {1213 catch (error) {
@@ -1207,7 +1216,7 @@ router.post('/duplicate', jsonParser, async function (request, response) {
1207 }1216 }
1208});1217});
12091218
1210router.post('/export', jsonParser, async function (request, response) {1219router.post('/export', jsonParser, validateAvatarUrlMiddleware, async function (request, response) {
1211 try {1220 try {
1212 if (!request.body.format || !request.body.avatar_url) {1221 if (!request.body.format || !request.body.avatar_url) {
1213 return response.sendStatus(400);1222 return response.sendStatus(400);
src/endpoints/chats.js+25 -25
@@ -9,6 +9,7 @@ import { sync as writeFileAtomicSync } from 'write-file-atomic';
9import _ from 'lodash';9import _ from 'lodash';
1010
11import { jsonParser, urlencodedParser } from '../express-common.js';11import { jsonParser, urlencodedParser } from '../express-common.js';
12import validateAvatarUrlMiddleware from '../middleware/validateFileName.js';
12import {13import {
13 getConfigValue,14 getConfigValue,
14 humanizedISO8601DateTime,15 humanizedISO8601DateTime,
@@ -49,7 +50,7 @@ function backupChat(directory, name, chat) {
4950
50 removeOldBackups(directory, 'chat_', maxTotalChatBackups);51 removeOldBackups(directory, 'chat_', maxTotalChatBackups);
51 } catch (err) {52 } catch (err) {
52 console.log(`Could not backup chat for ${name}`, err);53 console.error(`Could not backup chat for ${name}`, err);
53 }54 }
54}55}
5556
@@ -294,7 +295,7 @@ function importRisuChat(userName, characterName, jsonData) {
294295
295export const router = express.Router();296export const router = express.Router();
296297
297router.post('/save', jsonParser, function (request, response) {298router.post('/save', jsonParser, validateAvatarUrlMiddleware, function (request, response) {
298 try {299 try {
299 const directoryName = String(request.body.avatar_url).replace('.png', '');300 const directoryName = String(request.body.avatar_url).replace('.png', '');
300 const chatData = request.body.chat;301 const chatData = request.body.chat;
@@ -305,12 +306,12 @@ router.post('/save', jsonParser, function (request, response) {
305 getBackupFunction(request.user.profile.handle)(request.user.directories.backups, directoryName, jsonlData);306 getBackupFunction(request.user.profile.handle)(request.user.directories.backups, directoryName, jsonlData);
306 return response.send({ result: 'ok' });307 return response.send({ result: 'ok' });
307 } catch (error) {308 } catch (error) {
308 response.send(error);309 console.error(error);
309 return console.log(error);310 return response.send(error);
310 }311 }
311});312});
312313
313router.post('/get', jsonParser, function (request, response) {314router.post('/get', jsonParser, validateAvatarUrlMiddleware, function (request, response) {
314 try {315 try {
315 const dirName = String(request.body.avatar_url).replace('.png', '');316 const dirName = String(request.body.avatar_url).replace('.png', '');
316 const directoryPath = path.join(request.user.directories.chats, dirName);317 const directoryPath = path.join(request.user.directories.chats, dirName);
@@ -347,7 +348,7 @@ router.post('/get', jsonParser, function (request, response) {
347});348});
348349
349350
350router.post('/rename', jsonParser, async function (request, response) {351router.post('/rename', jsonParser, validateAvatarUrlMiddleware, async function (request, response) {
351 if (!request.body || !request.body.original_file || !request.body.renamed_file) {352 if (!request.body || !request.body.original_file || !request.body.renamed_file) {
352 return response.sendStatus(400);353 return response.sendStatus(400);
353 }354 }
@@ -358,37 +359,37 @@ router.post('/rename', jsonParser, async function (request, response) {
358 const pathToOriginalFile = path.join(pathToFolder, sanitize(request.body.original_file));359 const pathToOriginalFile = path.join(pathToFolder, sanitize(request.body.original_file));
359 const pathToRenamedFile = path.join(pathToFolder, sanitize(request.body.renamed_file));360 const pathToRenamedFile = path.join(pathToFolder, sanitize(request.body.renamed_file));
360 const sanitizedFileName = path.parse(pathToRenamedFile).name;361 const sanitizedFileName = path.parse(pathToRenamedFile).name;
361 console.log('Old chat name', pathToOriginalFile);362 console.info('Old chat name', pathToOriginalFile);
362 console.log('New chat name', pathToRenamedFile);363 console.info('New chat name', pathToRenamedFile);
363364
364 if (!fs.existsSync(pathToOriginalFile) || fs.existsSync(pathToRenamedFile)) {365 if (!fs.existsSync(pathToOriginalFile) || fs.existsSync(pathToRenamedFile)) {
365 console.log('Either Source or Destination files are not available');366 console.error('Either Source or Destination files are not available');
366 return response.status(400).send({ error: true });367 return response.status(400).send({ error: true });
367 }368 }
368369
369 fs.copyFileSync(pathToOriginalFile, pathToRenamedFile);370 fs.copyFileSync(pathToOriginalFile, pathToRenamedFile);
370 fs.rmSync(pathToOriginalFile);371 fs.rmSync(pathToOriginalFile);
371 console.log('Successfully renamed.');372 console.info('Successfully renamed.');
372 return response.send({ ok: true, sanitizedFileName });373 return response.send({ ok: true, sanitizedFileName });
373});374});
374375
375router.post('/delete', jsonParser, function (request, response) {376router.post('/delete', jsonParser, validateAvatarUrlMiddleware, function (request, response) {
376 const dirName = String(request.body.avatar_url).replace('.png', '');377 const dirName = String(request.body.avatar_url).replace('.png', '');
377 const fileName = String(request.body.chatfile);378 const fileName = String(request.body.chatfile);
378 const filePath = path.join(request.user.directories.chats, dirName, sanitize(fileName));379 const filePath = path.join(request.user.directories.chats, dirName, sanitize(fileName));
379 const chatFileExists = fs.existsSync(filePath);380 const chatFileExists = fs.existsSync(filePath);
380381
381 if (!chatFileExists) {382 if (!chatFileExists) {
382 console.log(`Chat file not found '${filePath}'`);383 console.error(`Chat file not found '${filePath}'`);
383 return response.sendStatus(400);384 return response.sendStatus(400);
384 }385 }
385386
386 fs.rmSync(filePath);387 fs.rmSync(filePath);
387 console.log('Deleted chat file: ' + filePath);388 console.info(`Deleted chat file: ${filePath}`);
388 return response.send('ok');389 return response.send('ok');
389});390});
390391
391router.post('/export', jsonParser, async function (request, response) {392router.post('/export', jsonParser, validateAvatarUrlMiddleware, async function (request, response) {
392 if (!request.body.file || (!request.body.avatar_url && request.body.is_group === false)) {393 if (!request.body.file || (!request.body.avatar_url && request.body.is_group === false)) {
393 return response.sendStatus(400);394 return response.sendStatus(400);
394 }395 }
@@ -401,7 +402,7 @@ router.post('/export', jsonParser, async function (request, response) {
401 const errorMessage = {402 const errorMessage = {
402 message: `Could not find JSONL file to export. Source chat file: ${filename}.`,403 message: `Could not find JSONL file to export. Source chat file: ${filename}.`,
403 };404 };
404 console.log(errorMessage.message);405 console.error(errorMessage.message);
405 return response.status(404).json(errorMessage);406 return response.status(404).json(errorMessage);
406 }407 }
407 try {408 try {
@@ -414,14 +415,14 @@ router.post('/export', jsonParser, async function (request, response) {
414 result: rawFile,415 result: rawFile,
415 };416 };
416417
417 console.log(`Chat exported as ${exportfilename}`);418 console.info(`Chat exported as ${exportfilename}`);
418 return response.status(200).json(successMessage);419 return response.status(200).json(successMessage);
419 } catch (err) {420 } catch (err) {
420 console.error(err);421 console.error(err);
421 const errorMessage = {422 const errorMessage = {
422 message: `Could not read JSONL file to export. Source chat file: ${filename}.`,423 message: `Could not read JSONL file to export. Source chat file: ${filename}.`,
423 };424 };
424 console.log(errorMessage.message);425 console.error(errorMessage.message);
425 return response.status(500).json(errorMessage);426 return response.status(500).json(errorMessage);
426 }427 }
427 }428 }
@@ -448,12 +449,11 @@ router.post('/export', jsonParser, async function (request, response) {
448 message: `Chat saved to ${exportfilename}`,449 message: `Chat saved to ${exportfilename}`,
449 result: buffer,450 result: buffer,
450 };451 };
451 console.log(`Chat exported as ${exportfilename}`);452 console.info(`Chat exported as ${exportfilename}`);
452 return response.status(200).json(successMessage);453 return response.status(200).json(successMessage);
453 });454 });
454 } catch (err) {455 } catch (err) {
455 console.log('chat export failed.');456 console.error('chat export failed.', err);
456 console.log(err);
457 return response.sendStatus(400);457 return response.sendStatus(400);
458 }458 }
459});459});
@@ -478,7 +478,7 @@ router.post('/group/import', urlencodedParser, function (request, response) {
478 }478 }
479});479});
480480
481router.post('/import', urlencodedParser, function (request, response) {481router.post('/import', urlencodedParser, validateAvatarUrlMiddleware, function (request, response) {
482 if (!request.body) return response.sendStatus(400);482 if (!request.body) return response.sendStatus(400);
483483
484 const format = request.body.file_type;484 const format = request.body.file_type;
@@ -512,7 +512,7 @@ router.post('/import', urlencodedParser, function (request, response) {
512 } else if (jsonData.type === 'risuChat') { // RisuAI format512 } else if (jsonData.type === 'risuChat') { // RisuAI format
513 importFunc = importRisuChat;513 importFunc = importRisuChat;
514 } else { // Unknown format514 } else { // Unknown format
515 console.log('Incorrect chat format .json');515 console.error('Incorrect chat format .json');
516 return response.send({ error: true });516 return response.send({ error: true });
517 }517 }
518518
@@ -540,7 +540,7 @@ router.post('/import', urlencodedParser, function (request, response) {
540 const jsonData = JSON.parse(header);540 const jsonData = JSON.parse(header);
541541
542 if (!(jsonData.user_name !== undefined || jsonData.name !== undefined)) {542 if (!(jsonData.user_name !== undefined || jsonData.name !== undefined)) {
543 console.log('Incorrect chat format .jsonl');543 console.error('Incorrect chat format .jsonl');
544 return response.send({ error: true });544 return response.send({ error: true });
545 }545 }
546546
@@ -626,7 +626,7 @@ router.post('/group/save', jsonParser, (request, response) => {
626 return response.send({ ok: true });626 return response.send({ ok: true });
627});627});
628628
629router.post('/search', jsonParser, function (request, response) {629router.post('/search', jsonParser, validateAvatarUrlMiddleware, function (request, response) {
630 try {630 try {
631 const { query, avatar_url, group_id } = request.body;631 const { query, avatar_url, group_id } = request.body;
632 let chatFiles = [];632 let chatFiles = [];
@@ -646,7 +646,7 @@ router.post('/search', jsonParser, function (request, response) {
646 break;646 break;
647 }647 }
648 } catch (error) {648 } catch (error) {
649 console.error(groupFile, 'group file is corrupted:', error);649 console.warn(groupFile, 'group file is corrupted:', error);
650 }650 }
651 }651 }
652652
src/endpoints/classify.js+2 -2
@@ -44,9 +44,9 @@ router.post('/', jsonParser, async (req, res) => {
44 }44 }
45 }45 }
4646
47 console.log('Classify input:', text);47 console.debug('Classify input:', text);
48 const result = await getResult(text);48 const result = await getResult(text);
49 console.log('Classify output:', result);49 console.debug('Classify output:', result);
5050
51 return res.json({ classification: result });51 return res.json({ classification: result });
52 } catch (error) {52 } catch (error) {
src/endpoints/content-manager.js+26 -26
@@ -71,7 +71,7 @@ export function getDefaultPresets(directories) {
7171
72 return presets;72 return presets;
73 } catch (err) {73 } catch (err) {
74 console.log('Failed to get default presets', err);74 console.warn('Failed to get default presets', err);
75 return [];75 return [];
76 }76 }
77}77}
@@ -92,7 +92,7 @@ export function getDefaultPresetFile(filename) {
92 const fileContent = fs.readFileSync(contentPath, 'utf8');92 const fileContent = fs.readFileSync(contentPath, 'utf8');
93 return JSON.parse(fileContent);93 return JSON.parse(fileContent);
94 } catch (err) {94 } catch (err) {
95 console.log(`Failed to get default file ${filename}`, err);95 console.warn(`Failed to get default file ${filename}`, err);
96 return null;96 return null;
97 }97 }
98}98}
@@ -121,21 +121,21 @@ async function seedContentForUser(contentIndex, directories, forceCategories) {
121 }121 }
122122
123 if (!contentItem.folder) {123 if (!contentItem.folder) {
124 console.log(`Content file ${contentItem.filename} has no parent folder`);124 console.warn(`Content file ${contentItem.filename} has no parent folder`);
125 continue;125 continue;
126 }126 }
127127
128 const contentPath = path.join(contentItem.folder, contentItem.filename);128 const contentPath = path.join(contentItem.folder, contentItem.filename);
129129
130 if (!fs.existsSync(contentPath)) {130 if (!fs.existsSync(contentPath)) {
131 console.log(`Content file ${contentItem.filename} is missing`);131 console.warn(`Content file ${contentItem.filename} is missing`);
132 continue;132 continue;
133 }133 }
134134
135 const contentTarget = getTargetByType(contentItem.type, directories);135 const contentTarget = getTargetByType(contentItem.type, directories);
136136
137 if (!contentTarget) {137 if (!contentTarget) {
138 console.log(`Content file ${contentItem.filename} has unknown type ${contentItem.type}`);138 console.warn(`Content file ${contentItem.filename} has unknown type ${contentItem.type}`);
139 continue;139 continue;
140 }140 }
141141
@@ -144,12 +144,12 @@ async function seedContentForUser(contentIndex, directories, forceCategories) {
144 contentLog.push(contentItem.filename);144 contentLog.push(contentItem.filename);
145145
146 if (fs.existsSync(targetPath)) {146 if (fs.existsSync(targetPath)) {
147 console.log(`Content file ${contentItem.filename} already exists in ${contentTarget}`);147 console.warn(`Content file ${contentItem.filename} already exists in ${contentTarget}`);
148 continue;148 continue;
149 }149 }
150150
151 fs.cpSync(contentPath, targetPath, { recursive: true, force: false });151 fs.cpSync(contentPath, targetPath, { recursive: true, force: false });
152 console.log(`Content file ${contentItem.filename} copied to ${contentTarget}`);152 console.info(`Content file ${contentItem.filename} copied to ${contentTarget}`);
153 anyContentAdded = true;153 anyContentAdded = true;
154 }154 }
155155
@@ -182,12 +182,12 @@ export async function checkForNewContent(directoriesList, forceCategories = [])
182 }182 }
183183
184 if (anyContentAdded && !contentCheckSkip && forceCategories?.length === 0) {184 if (anyContentAdded && !contentCheckSkip && forceCategories?.length === 0) {
185 console.log();185 console.info();
186 console.log(`${color.blue('If you don\'t want to receive content updates in the future, set')} ${color.yellow('skipContentCheck')} ${color.blue('to true in the config.yaml file.')}`);186 console.info(`${color.blue('If you don\'t want to receive content updates in the future, set')} ${color.yellow('skipContentCheck')} ${color.blue('to true in the config.yaml file.')}`);
187 console.log();187 console.info();
188 }188 }
189 } catch (err) {189 } catch (err) {
190 console.log('Content check failed', err);190 console.error('Content check failed', err);
191 }191 }
192}192}
193193
@@ -331,7 +331,7 @@ async function downloadChubLorebook(id) {
331331
332 if (!result.ok) {332 if (!result.ok) {
333 const text = await result.text();333 const text = await result.text();
334 console.log('Chub returned error', result.statusText, text);334 console.error('Chub returned error', result.statusText, text);
335 throw new Error('Failed to download lorebook');335 throw new Error('Failed to download lorebook');
336 }336 }
337337
@@ -355,7 +355,7 @@ async function downloadChubCharacter(id) {
355355
356 if (!result.ok) {356 if (!result.ok) {
357 const text = await result.text();357 const text = await result.text();
358 console.log('Chub returned error', result.statusText, text);358 console.error('Chub returned error', result.statusText, text);
359 throw new Error('Failed to download character');359 throw new Error('Failed to download character');
360 }360 }
361361
@@ -376,7 +376,7 @@ async function downloadPygmalionCharacter(id) {
376376
377 if (!result.ok) {377 if (!result.ok) {
378 const text = await result.text();378 const text = await result.text();
379 console.log('Pygsite returned error', result.status, text);379 console.error('Pygsite returned error', result.status, text);
380 throw new Error('Failed to download character');380 throw new Error('Failed to download character');
381 }381 }
382382
@@ -485,7 +485,7 @@ async function downloadJannyCharacter(uuid) {
485 }485 }
486 }486 }
487487
488 console.log('Janny returned error', result.statusText, await result.text());488 console.error('Janny returned error', result.statusText, await result.text());
489 throw new Error('Failed to download character');489 throw new Error('Failed to download character');
490}490}
491491
@@ -577,7 +577,7 @@ async function downloadRisuCharacter(uuid) {
577577
578 if (!result.ok) {578 if (!result.ok) {
579 const text = await result.text();579 const text = await result.text();
580 console.log('RisuAI returned error', result.statusText, text);580 console.error('RisuAI returned error', result.statusText, text);
581 throw new Error('Failed to download character');581 throw new Error('Failed to download character');
582 }582 }
583583
@@ -673,11 +673,11 @@ router.post('/importURL', jsonParser, async (request, response) => {
673 type = chubParsed?.type;673 type = chubParsed?.type;
674674
675 if (chubParsed?.type === 'character') {675 if (chubParsed?.type === 'character') {
676 console.log('Downloading chub character:', chubParsed.id);676 console.info('Downloading chub character:', chubParsed.id);
677 result = await downloadChubCharacter(chubParsed.id);677 result = await downloadChubCharacter(chubParsed.id);
678 }678 }
679 else if (chubParsed?.type === 'lorebook') {679 else if (chubParsed?.type === 'lorebook') {
680 console.log('Downloading chub lorebook:', chubParsed.id);680 console.info('Downloading chub lorebook:', chubParsed.id);
681 result = await downloadChubLorebook(chubParsed.id);681 result = await downloadChubLorebook(chubParsed.id);
682 }682 }
683 else {683 else {
@@ -692,7 +692,7 @@ router.post('/importURL', jsonParser, async (request, response) => {
692 type = 'character';692 type = 'character';
693 result = await downloadRisuCharacter(uuid);693 result = await downloadRisuCharacter(uuid);
694 } else if (isGeneric) {694 } else if (isGeneric) {
695 console.log('Downloading from generic url.');695 console.info('Downloading from generic url.');
696 type = 'character';696 type = 'character';
697 result = await downloadGenericPng(url);697 result = await downloadGenericPng(url);
698 } else {698 } else {
@@ -708,7 +708,7 @@ router.post('/importURL', jsonParser, async (request, response) => {
708 response.set('X-Custom-Content-Type', type);708 response.set('X-Custom-Content-Type', type);
709 return response.send(result.buffer);709 return response.send(result.buffer);
710 } catch (error) {710 } catch (error) {
711 console.log('Importing custom content failed', error);711 console.error('Importing custom content failed', error);
712 return response.sendStatus(500);712 return response.sendStatus(500);
713 }713 }
714});714});
@@ -728,22 +728,22 @@ router.post('/importUUID', jsonParser, async (request, response) => {
728 const uuidType = uuid.includes('lorebook') ? 'lorebook' : 'character';728 const uuidType = uuid.includes('lorebook') ? 'lorebook' : 'character';
729729
730 if (isPygmalion) {730 if (isPygmalion) {
731 console.log('Downloading Pygmalion character:', uuid);731 console.info('Downloading Pygmalion character:', uuid);
732 result = await downloadPygmalionCharacter(uuid);732 result = await downloadPygmalionCharacter(uuid);
733 } else if (isJannny) {733 } else if (isJannny) {
734 console.log('Downloading Janitor character:', uuid.split('_')[0]);734 console.info('Downloading Janitor character:', uuid.split('_')[0]);
735 result = await downloadJannyCharacter(uuid.split('_')[0]);735 result = await downloadJannyCharacter(uuid.split('_')[0]);
736 } else if (isAICC) {736 } else if (isAICC) {
737 const [, author, card] = uuid.split('/');737 const [, author, card] = uuid.split('/');
738 console.log('Downloading AICC character:', `${author}/${card}`);738 console.info('Downloading AICC character:', `${author}/${card}`);
739 result = await downloadAICCCharacter(`${author}/${card}`);739 result = await downloadAICCCharacter(`${author}/${card}`);
740 } else {740 } else {
741 if (uuidType === 'character') {741 if (uuidType === 'character') {
742 console.log('Downloading chub character:', uuid);742 console.info('Downloading chub character:', uuid);
743 result = await downloadChubCharacter(uuid);743 result = await downloadChubCharacter(uuid);
744 }744 }
745 else if (uuidType === 'lorebook') {745 else if (uuidType === 'lorebook') {
746 console.log('Downloading chub lorebook:', uuid);746 console.info('Downloading chub lorebook:', uuid);
747 result = await downloadChubLorebook(uuid);747 result = await downloadChubLorebook(uuid);
748 }748 }
749 else {749 else {
@@ -756,7 +756,7 @@ router.post('/importUUID', jsonParser, async (request, response) => {
756 response.set('X-Custom-Content-Type', uuidType);756 response.set('X-Custom-Content-Type', uuidType);
757 return response.send(result.buffer);757 return response.send(result.buffer);
758 } catch (error) {758 } catch (error) {
759 console.log('Importing custom content failed', error);759 console.error('Importing custom content failed', error);
760 return response.sendStatus(500);760 return response.sendStatus(500);
761 }761 }
762});762});
src/endpoints/extensions.js+16 -16
@@ -80,7 +80,7 @@ router.post('/install', jsonParser, async (request, response) => {
80 const { url, global } = request.body;80 const { url, global } = request.body;
8181
82 if (global && !request.user.profile.admin) {82 if (global && !request.user.profile.admin) {
83 console.warn(`User ${request.user.profile.handle} does not have permission to install global extensions.`);83 console.error(`User ${request.user.profile.handle} does not have permission to install global extensions.`);
84 return response.status(403).send('Forbidden: No permission to install global extensions.');84 return response.status(403).send('Forbidden: No permission to install global extensions.');
85 }85 }
8686
@@ -92,13 +92,13 @@ router.post('/install', jsonParser, async (request, response) => {
92 }92 }
9393
94 await git.clone(url, extensionPath, { '--depth': 1 });94 await git.clone(url, extensionPath, { '--depth': 1 });
95 console.log(`Extension has been cloned at ${extensionPath}`);95 console.info(`Extension has been cloned at ${extensionPath}`);
9696
97 const { version, author, display_name } = await getManifest(extensionPath);97 const { version, author, display_name } = await getManifest(extensionPath);
9898
99 return response.send({ version, author, display_name, extensionPath });99 return response.send({ version, author, display_name, extensionPath });
100 } catch (error) {100 } catch (error) {
101 console.log('Importing custom content failed', error);101 console.error('Importing custom content failed', error);
102 return response.status(500).send(`Server Error: ${error.message}`);102 return response.status(500).send(`Server Error: ${error.message}`);
103 }103 }
104});104});
@@ -124,7 +124,7 @@ router.post('/update', jsonParser, async (request, response) => {
124 const { extensionName, global } = request.body;124 const { extensionName, global } = request.body;
125125
126 if (global && !request.user.profile.admin) {126 if (global && !request.user.profile.admin) {
127 console.warn(`User ${request.user.profile.handle} does not have permission to update global extensions.`);127 console.error(`User ${request.user.profile.handle} does not have permission to update global extensions.`);
128 return response.status(403).send('Forbidden: No permission to update global extensions.');128 return response.status(403).send('Forbidden: No permission to update global extensions.');
129 }129 }
130130
@@ -139,9 +139,9 @@ router.post('/update', jsonParser, async (request, response) => {
139 const currentBranch = await git.cwd(extensionPath).branch();139 const currentBranch = await git.cwd(extensionPath).branch();
140 if (!isUpToDate) {140 if (!isUpToDate) {
141 await git.cwd(extensionPath).pull('origin', currentBranch.current);141 await git.cwd(extensionPath).pull('origin', currentBranch.current);
142 console.log(`Extension has been updated at ${extensionPath}`);142 console.info(`Extension has been updated at ${extensionPath}`);
143 } else {143 } else {
144 console.log(`Extension is up to date at ${extensionPath}`);144 console.info(`Extension is up to date at ${extensionPath}`);
145 }145 }
146 await git.cwd(extensionPath).fetch('origin');146 await git.cwd(extensionPath).fetch('origin');
147 const fullCommitHash = await git.cwd(extensionPath).revparse(['HEAD']);147 const fullCommitHash = await git.cwd(extensionPath).revparse(['HEAD']);
@@ -150,7 +150,7 @@ router.post('/update', jsonParser, async (request, response) => {
150 return response.send({ shortCommitHash, extensionPath, isUpToDate, remoteUrl });150 return response.send({ shortCommitHash, extensionPath, isUpToDate, remoteUrl });
151151
152 } catch (error) {152 } catch (error) {
153 console.log('Updating custom content failed', error);153 console.error('Updating custom content failed', error);
154 return response.status(500).send(`Server Error: ${error.message}`);154 return response.status(500).send(`Server Error: ${error.message}`);
155 }155 }
156});156});
@@ -164,7 +164,7 @@ router.post('/move', jsonParser, async (request, response) => {
164 }164 }
165165
166 if (!request.user.profile.admin) {166 if (!request.user.profile.admin) {
167 console.warn(`User ${request.user.profile.handle} does not have permission to move extensions.`);167 console.error(`User ${request.user.profile.handle} does not have permission to move extensions.`);
168 return response.status(403).send('Forbidden: No permission to move extensions.');168 return response.status(403).send('Forbidden: No permission to move extensions.');
169 }169 }
170170
@@ -190,11 +190,11 @@ router.post('/move', jsonParser, async (request, response) => {
190190
191 fs.cpSync(sourcePath, destinationPath, { recursive: true, force: true });191 fs.cpSync(sourcePath, destinationPath, { recursive: true, force: true });
192 fs.rmSync(sourcePath, { recursive: true, force: true });192 fs.rmSync(sourcePath, { recursive: true, force: true });
193 console.log(`Extension has been moved from ${sourcePath} to ${destinationPath}`);193 console.info(`Extension has been moved from ${sourcePath} to ${destinationPath}`);
194194
195 return response.sendStatus(204);195 return response.sendStatus(204);
196 } catch (error) {196 } catch (error) {
197 console.log('Moving extension failed', error);197 console.error('Moving extension failed', error);
198 return response.status(500).send('Internal Server Error. Try again later.');198 return response.status(500).send('Internal Server Error. Try again later.');
199 }199 }
200});200});
@@ -237,13 +237,13 @@ router.post('/version', jsonParser, async (request, response) => {
237 // get only the working branch237 // get only the working branch
238 const currentBranchName = currentBranch.current;238 const currentBranchName = currentBranch.current;
239 await git.cwd(extensionPath).fetch('origin');239 await git.cwd(extensionPath).fetch('origin');
240 console.log(extensionName, currentBranchName, currentCommitHash);240 console.debug(extensionName, currentBranchName, currentCommitHash);
241 const { isUpToDate, remoteUrl } = await checkIfRepoIsUpToDate(extensionPath);241 const { isUpToDate, remoteUrl } = await checkIfRepoIsUpToDate(extensionPath);
242242
243 return response.send({ currentBranchName, currentCommitHash, isUpToDate, remoteUrl });243 return response.send({ currentBranchName, currentCommitHash, isUpToDate, remoteUrl });
244244
245 } catch (error) {245 } catch (error) {
246 console.log('Getting extension version failed', error);246 console.error('Getting extension version failed', error);
247 return response.status(500).send(`Server Error: ${error.message}`);247 return response.status(500).send(`Server Error: ${error.message}`);
248 }248 }
249});249});
@@ -265,7 +265,7 @@ router.post('/delete', jsonParser, async (request, response) => {
265 const { extensionName, global } = request.body;265 const { extensionName, global } = request.body;
266266
267 if (global && !request.user.profile.admin) {267 if (global && !request.user.profile.admin) {
268 console.warn(`User ${request.user.profile.handle} does not have permission to delete global extensions.`);268 console.error(`User ${request.user.profile.handle} does not have permission to delete global extensions.`);
269 return response.status(403).send('Forbidden: No permission to delete global extensions.');269 return response.status(403).send('Forbidden: No permission to delete global extensions.');
270 }270 }
271271
@@ -277,12 +277,12 @@ router.post('/delete', jsonParser, async (request, response) => {
277 }277 }
278278
279 await fs.promises.rm(extensionPath, { recursive: true });279 await fs.promises.rm(extensionPath, { recursive: true });
280 console.log(`Extension has been deleted at ${extensionPath}`);280 console.info(`Extension has been deleted at ${extensionPath}`);
281281
282 return response.send(`Extension has been deleted at ${extensionPath}`);282 return response.send(`Extension has been deleted at ${extensionPath}`);
283283
284 } catch (error) {284 } catch (error) {
285 console.log('Deleting custom content failed', error);285 console.error('Deleting custom content failed', error);
286 return response.status(500).send(`Server Error: ${error.message}`);286 return response.status(500).send(`Server Error: ${error.message}`);
287 }287 }
288});288});
@@ -323,7 +323,7 @@ router.get('/discover', jsonParser, function (request, response) {
323323
324 // Combine all extensions324 // Combine all extensions
325 const allExtensions = [...builtInExtensions, ...userExtensions, ...globalExtensions];325 const allExtensions = [...builtInExtensions, ...userExtensions, ...globalExtensions];
326 console.log('Extensions available for', request.user.profile.handle, allExtensions);326 console.info('Extensions available for', request.user.profile.handle, allExtensions);
327327
328 return response.send(allExtensions);328 return response.send(allExtensions);
329});329});
src/endpoints/files.js+7 -7
@@ -21,7 +21,7 @@ router.post('/sanitize-filename', jsonParser, async (request, response) => {
21 const sanitizedFilename = sanitize(fileName);21 const sanitizedFilename = sanitize(fileName);
22 return response.send({ fileName: sanitizedFilename });22 return response.send({ fileName: sanitizedFilename });
23 } catch (error) {23 } catch (error) {
24 console.log(error);24 console.error(error);
25 return response.sendStatus(500);25 return response.sendStatus(500);
26 }26 }
27});27});
@@ -44,10 +44,10 @@ router.post('/upload', jsonParser, async (request, response) => {
44 const pathToUpload = path.join(request.user.directories.files, request.body.name);44 const pathToUpload = path.join(request.user.directories.files, request.body.name);
45 writeFileSyncAtomic(pathToUpload, request.body.data, 'base64');45 writeFileSyncAtomic(pathToUpload, request.body.data, 'base64');
46 const url = clientRelativePath(request.user.directories.root, pathToUpload);46 const url = clientRelativePath(request.user.directories.root, pathToUpload);
47 console.log(`Uploaded file: ${url} from ${request.user.profile.handle}`);47 console.info(`Uploaded file: ${url} from ${request.user.profile.handle}`);
48 return response.send({ path: url });48 return response.send({ path: url });
49 } catch (error) {49 } catch (error) {
50 console.log(error);50 console.error(error);
51 return response.sendStatus(500);51 return response.sendStatus(500);
52 }52 }
53});53});
@@ -68,10 +68,10 @@ router.post('/delete', jsonParser, async (request, response) => {
68 }68 }
6969
70 fs.rmSync(pathToDelete);70 fs.rmSync(pathToDelete);
71 console.log(`Deleted file: ${request.body.path} from ${request.user.profile.handle}`);71 console.info(`Deleted file: ${request.body.path} from ${request.user.profile.handle}`);
72 return response.sendStatus(200);72 return response.sendStatus(200);
73 } catch (error) {73 } catch (error) {
74 console.log(error);74 console.error(error);
75 return response.sendStatus(500);75 return response.sendStatus(500);
76 }76 }
77});77});
@@ -87,7 +87,7 @@ router.post('/verify', jsonParser, async (request, response) => {
87 for (const url of request.body.urls) {87 for (const url of request.body.urls) {
88 const pathToVerify = path.join(request.user.directories.root, url);88 const pathToVerify = path.join(request.user.directories.root, url);
89 if (!pathToVerify.startsWith(request.user.directories.files)) {89 if (!pathToVerify.startsWith(request.user.directories.files)) {
90 console.debug(`File verification: Invalid path: ${pathToVerify}`);90 console.warn(`File verification: Invalid path: ${pathToVerify}`);
91 continue;91 continue;
92 }92 }
93 const fileExists = fs.existsSync(pathToVerify);93 const fileExists = fs.existsSync(pathToVerify);
@@ -96,7 +96,7 @@ router.post('/verify', jsonParser, async (request, response) => {
9696
97 return response.send(verified);97 return response.send(verified);
98 } catch (error) {98 } catch (error) {
99 console.log(error);99 console.error(error);
100 return response.sendStatus(500);100 return response.sendStatus(500);
101 }101 }
102});102});
src/endpoints/google.js+3 -3
@@ -34,7 +34,7 @@ router.post('/caption-image', jsonParser, async (request, response) => {
34 generationConfig: { maxOutputTokens: 1000 },34 generationConfig: { maxOutputTokens: 1000 },
35 };35 };
3636
37 console.log('Multimodal captioning request', model, body);37 console.debug('Multimodal captioning request', model, body);
3838
39 const result = await fetch(url, {39 const result = await fetch(url, {
40 body: JSON.stringify(body),40 body: JSON.stringify(body),
@@ -46,13 +46,13 @@ router.post('/caption-image', jsonParser, async (request, response) => {
4646
47 if (!result.ok) {47 if (!result.ok) {
48 const error = await result.json();48 const error = await result.json();
49 console.log(`Google AI Studio API returned error: ${result.status} ${result.statusText}`, error);49 console.error(`Google AI Studio API returned error: ${result.status} ${result.statusText}`, error);
50 return response.status(result.status).send({ error: true });50 return response.status(result.status).send({ error: true });
51 }51 }
5252
53 /** @type {any} */53 /** @type {any} */
54 const data = await result.json();54 const data = await result.json();
55 console.log('Multimodal captioning response', data);55 console.info('Multimodal captioning response', data);
5656
57 const candidates = data?.candidates;57 const candidates = data?.candidates;
58 if (!candidates) {58 if (!candidates) {
src/endpoints/groups.js+1 -1
@@ -114,7 +114,7 @@ router.post('/delete', jsonParser, async (request, response) => {
114114
115 if (group && Array.isArray(group.chats)) {115 if (group && Array.isArray(group.chats)) {
116 for (const chat of group.chats) {116 for (const chat of group.chats) {
117 console.log('Deleting group chat', chat);117 console.info('Deleting group chat', chat);
118 const pathToFile = path.join(request.user.directories.groupChats, `${id}.jsonl`);118 const pathToFile = path.join(request.user.directories.groupChats, `${id}.jsonl`);
119119
120 if (fs.existsSync(pathToFile)) {120 if (fs.existsSync(pathToFile)) {
src/endpoints/horde.js+3 -3
@@ -155,7 +155,7 @@ router.post('/cancel-task', jsonParser, async (request, response) => {
155 });155 });
156156
157 const data = await fetchResult.json();157 const data = await fetchResult.json();
158 console.log(`Cancelled Horde task ${taskId}`);158 console.info(`Cancelled Horde task ${taskId}`);
159 return response.send(data);159 return response.send(data);
160 } catch (error) {160 } catch (error) {
161 console.error(error);161 console.error(error);
@@ -174,7 +174,7 @@ router.post('/task-status', jsonParser, async (request, response) => {
174 });174 });
175175
176 const data = await fetchResult.json();176 const data = await fetchResult.json();
177 console.log(`Horde task ${taskId} status:`, data);177 console.info(`Horde task ${taskId} status:`, data);
178 return response.send(data);178 return response.send(data);
179 } catch (error) {179 } catch (error) {
180 console.error(error);180 console.error(error);
@@ -187,7 +187,7 @@ router.post('/generate-text', jsonParser, async (request, response) => {
187 const url = 'https://aihorde.net/api/v2/generate/text/async';187 const url = 'https://aihorde.net/api/v2/generate/text/async';
188 const agent = await getClientAgent();188 const agent = await getClientAgent();
189189
190 console.log(request.body);190 console.debug(request.body);
191 try {191 try {
192 const result = await fetch(url, {192 const result = await fetch(url, {
src/endpoints/images.js+0 -0
src/endpoints/novelai.js+0 -0
src/endpoints/openai.js+0 -0
src/endpoints/openrouter.js+0 -0
src/endpoints/presets.js+0 -0
src/endpoints/search.js+0 -0
src/endpoints/secrets.js+0 -0
src/endpoints/settings.js+0 -0
src/endpoints/speech.js+0 -0
src/endpoints/sprites.js+0 -0
src/endpoints/stable-diffusion.js+0 -0
src/endpoints/stats.js+0 -0
src/endpoints/thumbnails.js+0 -0
src/endpoints/tokenizers.js+0 -0
src/endpoints/translate.js+0 -0
src/endpoints/users-admin.js+0 -0
src/endpoints/users-private.js+0 -0
src/endpoints/users-public.js+0 -0
src/endpoints/vectors.js+0 -0
src/endpoints/worldinfo.js+0 -0
src/middleware/basicAuth.js+0 -0
src/middleware/cacheBuster.js+0 -0
src/middleware/validateFileName.js+0 -0
src/middleware/whitelist.js+0 -0
src/plugin-loader.js+0 -0
src/prompt-converters.js+0 -0
src/request-proxy.js+0 -0
src/transformers.js+0 -0
src/users.js+0 -0
src/util.js+0 -0
src/vectors/cohere-vectors.js+0 -0
src/vectors/extras-vectors.js+0 -0
src/vectors/makersuite-vectors.js+0 -0
src/vectors/nomicai-vectors.js+0 -0
src/vectors/openai-vectors.js+0 -0
Diff truncated