Merge branch 'staging' into staging-3071-v2

552a418bae064f6eaad47dcba09bae389f22efa4

Eradev <admin@eradev.com>

74 files changed, +1549 -282Showing 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+16 -14
@@ -72,7 +72,7 @@ minLogLevel: 0
72## Set to a positive number to expire session after a certain time of inactivity72## Set to a positive number to expire session after a certain time of inactivity
73## Set to 0 to expire session when the browser is closed73## Set to 0 to expire session when the browser is closed
74## Set to a negative number to disable session expiration74## Set to a negative number to disable session expiration
75sessionTimeout: 8640075sessionTimeout: -1
76# Used to sign session cookies. Will be auto-generated if not set76# Used to sign session cookies. Will be auto-generated if not set
77cookieSecret: ''77cookieSecret: ''
78# Disable CSRF protection - NOT RECOMMENDED78# Disable CSRF protection - NOT RECOMMENDED
@@ -135,24 +135,26 @@ whitelistImportDomains:
135## headers:135## headers:
136## User-Agent: "Googlebot/2.1 (+http://www.google.com/bot.html)"136## User-Agent: "Googlebot/2.1 (+http://www.google.com/bot.html)"
137requestOverrides: []137requestOverrides: []
138# -- EXTENSIONS CONFIGURATION --138
139# EXTENSIONS CONFIGURATION
140extensions:
139 # Enable UI extensions141 # Enable UI extensions
140enableExtensions: true142 enabled: true
141 # Automatically update extensions when a release version changes143 # Automatically update extensions when a release version changes
142enableExtensionsAutoUpdate: true144 autoUpdate: true
145 models:
146 # Enables automatic model download from HuggingFace
147 autoDownload: true
148 # Additional models for extensions. Expects model IDs from HuggingFace model hub in ONNX format
149 classification: Cohee/distilbert-base-uncased-go-emotions-onnx
150 captioning: Xenova/vit-gpt2-image-captioning
151 embedding: Cohee/jina-embeddings-v2-base-en
152 speechToText: Xenova/whisper-small
153 textToSpeech: Xenova/speecht5_tts
154
143# Additional model tokenizers can be downloaded on demand.155# Additional model tokenizers can be downloaded on demand.
144# Disabling will fallback to another locally available tokenizer.156# Disabling will fallback to another locally available tokenizer.
145enableDownloadableTokenizers: true157enableDownloadableTokenizers: true
146# Extension settings
147extras:
148 # Disables automatic model download from HuggingFace
149 disableAutoDownload: false
150 # Extra models for plugins. Expects model IDs from HuggingFace model hub in ONNX format
151 classificationModel: Cohee/distilbert-base-uncased-go-emotions-onnx
152 captioningModel: Xenova/vit-gpt2-image-captioning
153 embeddingModel: Cohee/jina-embeddings-v2-base-en
154 speechToTextModel: Xenova/whisper-small
155 textToSpeechModel: Xenova/speecht5_tts
156# -- OPENAI CONFIGURATION --158# -- OPENAI CONFIGURATION --
157# A placeholder message to use in strict prompt post-processing mode when the prompt doesn't start with a user message159# A placeholder message to use in strict prompt post-processing mode when the prompt doesn't start with a user message
158promptPlaceholder: "[Start a new chat]"160promptPlaceholder: "[Start a new chat]"
default/content/backgrounds/_black.jpg+0 -0

Binary file

default/content/backgrounds/_white.jpg+0 -0

Binary file

default/content/index.json+8 -0
@@ -782,5 +782,13 @@
782 {782 {
783 "filename": "presets/context/Mistral V7.json",783 "filename": "presets/context/Mistral V7.json",
784 "type": "context"784 "type": "context"
785 },
786 {
787 "filename": "presets/instruct/DeepSeek-V2.5.json",
788 "type": "instruct"
789 },
790 {
791 "filename": "presets/context/DeepSeek-V2.5.json",
792 "type": "context"
785 }793 }
786]794]
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/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+7 -7
@@ -1,12 +1,12 @@
1{1{
2 "name": "sillytavern",2 "name": "sillytavern",
3 "version": "1.12.10",3 "version": "1.12.11",
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.10",9 "version": "1.12.11",
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",
@@ -2987,10 +2987,10 @@
2987 "node": "*"2987 "node": "*"
2988 }2988 }
2989 },2989 },
2990 "node_modules/csrf-csrf": {2990 "node_modules/csrf-sync": {
2991 "version": "2.2.4",2991 "version": "4.0.3",
2992 "resolved": "https://registry.npmjs.org/csrf-csrf/-/csrf-csrf-2.2.4.tgz",2992 "resolved": "https://registry.npmjs.org/csrf-sync/-/csrf-sync-4.0.3.tgz",
2993 "integrity": "sha512-LuhBmy5RfRmEfeqeYqgaAuS1eDpVtKZB/Eiec9xiKQLBynJxrGVRdM2yRT/YMl1Njo/yKh2L9AYsIwSlTPnx2A==",2993 "integrity": "sha512-wXzltBBzt/7imzDt6ZT7G/axQG7jo4Sm0uXDUzFY8hR59qhDHdjqpW2hojS4oAVIZDzwlMQloIVCTJoDDh0wwA==",
2994 "license": "ISC",2994 "license": "ISC",
2995 "dependencies": {2995 "dependencies": {
2996 "http-errors": "^2.0.0"2996 "http-errors": "^2.0.0"
package.json+2 -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",
@@ -86,7 +86,7 @@
86 "type": "git",86 "type": "git",
87 "url": "https://github.com/SillyTavern/SillyTavern.git"87 "url": "https://github.com/SillyTavern/SillyTavern.git"
88 },88 },
89 "version": "1.12.10",89 "version": "1.12.11",
90 "scripts": {90 "scripts": {
91 "start": "node server.js",91 "start": "node server.js",
92 "start:deno": "deno run --allow-run --allow-net --allow-read --allow-write --allow-sys --allow-env server.js",92 "start:deno": "deno run --allow-run --allow-net --allow-read --allow-write --allow-sys --allow-env server.js",
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/img/vllm.svg+1 -0
@@ -0,0 +1 @@
1<svg version="1.0" width="565.752" height="586.537" viewBox="0 0 424.314 439.903" preserveAspectRatio="xMidYMid" xmlns="http://www.w3.org/2000/svg"><path d="M344.6 41.703c-43.4 23-79.3 42.4-79.7 43.2-.8 1.4-72.3 276.1-84.3 323.5-6.2 24.9-7.1 27.7-8 25.5-.4-.8-.5-77.7-.4-170.8l.3-169.2H86.3c-47.5 0-86.3.2-86.3.5 0 1 162.2 325 166.9 333.3 2.5 4.6 5 9.2 5.6 10.3.9 1.8 2.7 1.9 68.2 1.9 63.1 0 67.2-.1 67.7-1.8.3-.9 24.2-90.8 53-199.7 51-192.7 58.9-222.4 62-233.3.8-2.8 1.1-5.2.8-5.1-.4 0-36.2 18.8-79.6 41.7z"/></svg>
\ No newline at end of file1 \ No newline at end of file
public/index.html+62 -11
@@ -1977,12 +1977,12 @@
1977 </span>1977 </span>
1978 </div>1978 </div>
1979 </div>1979 </div>
1980 <div class="range-block" data-source="makersuite">1980 <div class="range-block" data-source="makersuite,deepseek,openrouter">
1981 <label for="openai_show_thoughts" class="checkbox_label widthFreeExpand">1981 <label for="openai_show_thoughts" class="checkbox_label widthFreeExpand">
1982 <input id="openai_show_thoughts" type="checkbox" />1982 <input id="openai_show_thoughts" type="checkbox" />
1983 <span>1983 <span>
1984 <span data-i18n="Show model thoughts">Show model thoughts</span>1984 <span data-i18n="Show model reasoning">Show model reasoning</span>
1985 <i class="opacity50p fa-solid fa-circle-info" title="Gemini 2.0 Thinking"></i>1985 <i class="opacity50p fa-solid fa-circle-info" title="Gemini 2.0 Thinking / DeepSeek Reasoner"></i>
1986 </span>1986 </span>
1987 </label>1987 </label>
1988 <div class="toggle-description justifyLeft marginBot5">1988 <div class="toggle-description justifyLeft marginBot5">
@@ -2692,7 +2692,7 @@
2692 <option value="windowai">Window AI</option>2692 <option value="windowai">Window AI</option>
2693 </optgroup>2693 </optgroup>
2694 </select>2694 </select>
2695 <div class="inline-drawer wide100p" data-source="openai,claude,mistralai,makersuite">2695 <div class="inline-drawer wide100p" data-source="openai,claude,mistralai,makersuite,deepseek">
2696 <div class="inline-drawer-toggle inline-drawer-header">2696 <div class="inline-drawer-toggle inline-drawer-header">
2697 <b data-i18n="Reverse Proxy">Reverse Proxy</b>2697 <b data-i18n="Reverse Proxy">Reverse Proxy</b>
2698 <div class="fa-solid fa-circle-chevron-down inline-drawer-icon down"></div>2698 <div class="fa-solid fa-circle-chevron-down inline-drawer-icon down"></div>
@@ -2755,7 +2755,7 @@
2755 </div>2755 </div>
2756 </div>2756 </div>
2757 </div>2757 </div>
2758 <div id="ReverseProxyWarningMessage" data-source="openai,claude,mistralai,makersuite">2758 <div id="ReverseProxyWarningMessage" data-source="openai,claude,mistralai,makersuite,deepseek">
2759 <div class="reverse_proxy_warning">2759 <div class="reverse_proxy_warning">
2760 <b>2760 <b>
2761 <div data-i18n="Using a proxy that you're not running yourself is a risk to your data privacy.">2761 <div data-i18n="Using a proxy that you're not running yourself is a risk to your data privacy.">
@@ -3062,7 +3062,9 @@
3062 <option value="gemini-1.0-ultra-latest">Gemini 1.0 Ultra</option>3062 <option value="gemini-1.0-ultra-latest">Gemini 1.0 Ultra</option>
3063 </optgroup>3063 </optgroup>
3064 <optgroup label="Subversions">3064 <optgroup label="Subversions">
3065 <option value="gemini-2.0-flash-thinking-exp-1219">Gemini 2.0 Flash Thinking Experimental</option>3065 <option value="gemini-2.0-flash-thinking-exp">Gemini 2.0 Flash Thinking Experimental</option>
3066 <option value="gemini-2.0-flash-thinking-exp-01-21">Gemini 2.0 Flash Thinking Experimental 2025-01-21</option>
3067 <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>3068 <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>3069 <option value="gemini-exp-1114">Gemini Experimental 2024-11-14</option>
3068 <option value="gemini-exp-1121">Gemini Experimental 2024-11-21</option>3070 <option value="gemini-exp-1121">Gemini Experimental 2024-11-21</option>
@@ -3209,6 +3211,7 @@
3209 <select id="model_deepseek_select">3211 <select id="model_deepseek_select">
3210 <option value="deepseek-chat">deepseek-chat</option>3212 <option value="deepseek-chat">deepseek-chat</option>
3211 <option value="deepseek-coder">deepseek-coder</option>3213 <option value="deepseek-coder">deepseek-coder</option>
3214 <option value="deepseek-reasoner">deepseek-reasoner</option>
3212 </select>3215 </select>
3213 </div>3216 </div>
3214 </div>3217 </div>
@@ -3797,6 +3800,39 @@
3797 </div>3800 </div>
3798 </div>3801 </div>
3799 <div>3802 <div>
3803 <h4 class="standoutHeader">
3804 <span data-i18n="Reasoning">Reasoning</span>
3805 </h4>
3806 <div>
3807 <label class="checkbox_label" for="reasoning_add_to_prompts" title="Add existing reasoning blocks to prompts. To add a new reasoning block, use the message edit menu." data-i18n="[title]reasoning_add_to_prompts">
3808 <input id="reasoning_add_to_prompts" type="checkbox" />
3809 <small data-i18n="Add Reasoning to Prompts">
3810 Add Reasoning to Prompts
3811 </small>
3812 </label>
3813 <div class="flex-container">
3814 <div class="flex1" title="Inserted before the reasoning content." data-i18n="[title]reasoning_prefix">
3815 <small data-i18n="Prefix">Prefix</small>
3816 <textarea id="reasoning_prefix" class="text_pole textarea_compact autoSetHeight"></textarea>
3817 </div>
3818 <div class="flex1" title="Inserted after the reasoning content." data-i18n="[title]reasoning_suffix">
3819 <small data-i18n="Suffix">Suffix</small>
3820 <textarea id="reasoning_suffix" class="text_pole textarea_compact autoSetHeight"></textarea>
3821 </div>
3822 </div>
3823 <div class="flex-container">
3824 <div class="flex1" title="Inserted between the reasoning and the message content." data-i18n="[title]reasoning_separator">
3825 <small data-i18n="Separator">Separator</small>
3826 <textarea id="reasoning_separator" class="text_pole textarea_compact autoSetHeight"></textarea>
3827 </div>
3828 <div class="flex1" title="Maximum number of reasoning blocks to be added per prompt, counting from the last message." data-i18n="[title]reasoning_max_additions">
3829 <small data-i18n="Max Additions">Max Additions</small>
3830 <input id="reasoning_max_additions" class="text_pole textarea_compact" type="number" min="0" max="999"></textarea>
3831 </div>
3832 </div>
3833 </div>
3834 </div>
3835 <div>
3800 <h4 class="standoutHeader" data-i18n="Miscellaneous">Miscellaneous</h4>3836 <h4 class="standoutHeader" data-i18n="Miscellaneous">Miscellaneous</h4>
3801 <div>3837 <div>
3802 <small>3838 <small>
@@ -6218,14 +6254,26 @@
6218 <div class="mes_edit_buttons">6254 <div class="mes_edit_buttons">
6219 <div class="mes_edit_done menu_button fa-solid fa-check" title="Confirm" data-i18n="[title]Confirm"></div>6255 <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>6256 <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">6257 <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>6258 <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>6259 <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">6260 <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>6261 <div class="mes_edit_cancel menu_button fa-solid fa-xmark" title="Cancel" data-i18n="[title]Cancel"></div>
6227 </div>6262 </div>
6228 </div>6263 </div>
6264 <details class="mes_reasoning_details">
6265 <summary class="mes_reasoning_summary">
6266 <span data-i18n="Reasoning">Reasoning</span>
6267 <div class="mes_reasoning_actions">
6268 <div class="mes_reasoning_edit_done mes_button fa-solid fa-check" title="Confirm" data-i18n="[title]Confirmedit"></div>
6269 <div class="mes_reasoning_edit_cancel mes_button fa-solid fa-xmark" title="Cancel edit" data-i18n="[title]Cancel edit"></div>
6270 <div class="mes_reasoning_edit mes_button fa-solid fa-pencil" title="Edit reasoning" data-i18n="[title]Edit reasoning"></div>
6271 <div class="mes_reasoning_copy mes_button fa-solid fa-copy" title="Copy reasoning" data-i18n="[title]Copy reasoning"></div>
6272 <div class="mes_reasoning_delete mes_button fa-solid fa-trash-can" title="Remove reasoning" data-i18n="[title]Remove reasoning"></div>
6273 </div>
6274 </summary>
6275 <div class="mes_reasoning"></div>
6276 </details>
6229 <div class="mes_text"></div>6277 <div class="mes_text"></div>
6230 <div class="mes_img_container">6278 <div class="mes_img_container">
6231 <div class="mes_img_controls">6279 <div class="mes_img_controls">
@@ -6325,7 +6373,10 @@
6325 <img alt="Avatar" src="" />6373 <img alt="Avatar" src="" />
6326 </div>6374 </div>
6327 <div class="group_member_name">6375 <div class="group_member_name">
6328 <div class="ch_name"></div>6376 <div class="character_name_block">
6377 <span class="ch_name"></span>
6378 <small class="ch_additional_info character_version"></small>
6379 </div>
6329 <div class="tags tags_inline"></div>6380 <div class="tags tags_inline"></div>
6330 </div>6381 </div>
6331 <input class="ch_fav" value="" hidden />6382 <input class="ch_fav" value="" hidden />
public/locales/ar-sa.json+1 -1
@@ -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+1 -1
@@ -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+1 -1
@@ -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+2 -2
@@ -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,7 +1385,7 @@
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 "Show model reasoning": "Afficher 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 "Display the model's internal thoughts in the response.": "Afficher les pensées internes du modèle dans la réponse.",
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",
public/locales/is-is.json+1 -1
@@ -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+1 -1
@@ -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+1 -1
@@ -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+1 -1
@@ -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 캐릭터 (직접링크)",
public/locales/nl-nl.json+1 -1
@@ -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+1 -1
@@ -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+1 -1
@@ -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+1 -1
@@ -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+1 -1
@@ -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+10 -10
@@ -266,7 +266,7 @@
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 "Show model reasoning": "展示思维链",
270 "Display the model's internal thoughts in the response.": "展示模型在回复时的内部思维链。",270 "Display the model's internal thoughts in the response.": "展示模型在回复时的内部思维链。",
271 "Assistant Prefill": "AI预填",271 "Assistant Prefill": "AI预填",
272 "Expand the editor": "展开编辑器",272 "Expand the editor": "展开编辑器",
@@ -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": "查看角色卡片",
@@ -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": "显示提示词差异",
@@ -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+77 -11
@@ -28,14 +28,14 @@
28 "Prose Augmenter": "散文增強器",28 "Prose Augmenter": "散文增強器",
29 "Text Adventure": "文字冒險",29 "Text Adventure": "文字冒險",
30 "response legth(tokens)": "回應長度(符元數)",30 "response legth(tokens)": "回應長度(符元數)",
31 "Streaming": "串流",31 "Streaming": "即時串流",
32 "Streaming_desc": "生成時逐位顯示回應。當此功能關閉時,回應將在完成後一次顯示。",32 "Streaming_desc": "逐字顯示生成中的回應內容。關閉時,回應將在生成完成後一次性顯示。",
33 "context size(tokens)": "上下文長度(符元數)",33 "context size(tokens)": "上下文長度(符元數)",
34 "unlocked": "解鎖",34 "unlocked": "解鎖",
35 "Only enable this if your model supports context sizes greater than 8192 tokens": "僅在您的模型支援超過 8192 個符元的上下文長度時啟用此功能",35 "Only enable this if your model supports context sizes greater than 8192 tokens": "僅在您的模型支援超過 8192 個符元的上下文長度時啟用此功能",
36 "Max prompt cost:": "最大提示詞費用",36 "Max prompt cost:": "最大提示詞費用:",
37 "Display the response bit by bit as it is generated.": "生成時逐位顯示回應。",37 "Display the response bit by bit as it is generated.": "逐字顯示生成中的回應內容。",
38 "When this is off, responses will be displayed all at once when they are complete.": "關閉時,回應將在完成後一次性顯示。",38 "When this is off, responses will be displayed all at once when they are complete.": "關閉時,回應將在生成完成後一次性顯示。",
39 "Temperature": "溫度",39 "Temperature": "溫度",
40 "rep.pen": "重複懲罰",40 "rep.pen": "重複懲罰",
41 "Rep. Pen. Range.": "重複懲罰範圍",41 "Rep. Pen. Range.": "重複懲罰範圍",
@@ -880,7 +880,7 @@
880 "popup-button-no": "取消",880 "popup-button-no": "取消",
881 "popup-button-cancel": "關閉",881 "popup-button-cancel": "關閉",
882 "popup-button-import": "匯入",882 "popup-button-import": "匯入",
883 "Advanced Definitions": "- 進階定義",883 "Advanced Definitions": "進階定義",
884 "Prompt Overrides": "提示詞覆寫",884 "Prompt Overrides": "提示詞覆寫",
885 "(For Chat Completion and Instruct Mode)": "(用於聊天補全和指令模式)",885 "(For Chat Completion and Instruct Mode)": "(用於聊天補全和指令模式)",
886 "Insert {{original}} into either box to include the respective default prompt from system settings.": "在任一框中插入 {{original}} 以包含系統設定中的預設提示詞。",886 "Insert {{original}} into either box to include the respective default prompt from system settings.": "在任一框中插入 {{original}} 以包含系統設定中的預設提示詞。",
@@ -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角色(直接連結)",
@@ -1745,7 +1745,7 @@
1745 "Sprite Folder Override": "表情立繪資料夾覆蓋",1745 "Sprite Folder Override": "表情立繪資料夾覆蓋",
1746 "Sprite set:": "立繪組:",1746 "Sprite set:": "立繪組:",
1747 "Show Gallery": "查看圖庫",1747 "Show Gallery": "查看圖庫",
1748 "Sticky": "固定",1748 "Sticky": "黏性",
1749 "Style Preset": "預設樣式",1749 "Style Preset": "預設樣式",
1750 "Summarize chat messages for vector generation": "摘要聊天訊息以進行向量化處理",1750 "Summarize chat messages for vector generation": "摘要聊天訊息以進行向量化處理",
1751 "Summarize chat messages when sending": "傳送時摘要聊天內容",1751 "Summarize chat messages when sending": "傳送時摘要聊天內容",
@@ -2124,7 +2124,7 @@
2124 "from other websites": "以從其他網站新增。",2124 "from other websites": "以從其他網站新增。",
2125 "Go to the": "前往",2125 "Go to the": "前往",
2126 "to install additional features.": "以安裝更多功能。",2126 "to install additional features.": "以安裝更多功能。",
2127 "If you're connected to an API, try asking me something!": "若您已連接 API,嘗試問我一些問題吧!",2127 "If you're connected to an API, try asking me something!": "若您已連線 API,嘗試問我一些問題吧!",
2128 "Title/Memo": "標題/備註",2128 "Title/Memo": "標題/備註",
2129 "Strategy": "插入策略",2129 "Strategy": "插入策略",
2130 "Position": "位置",2130 "Position": "位置",
@@ -2357,7 +2357,7 @@
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 "Show model reasoning": "顯示模型思維鏈",
2361 "Display the model's internal thoughts in the response.": "在回應中顯示模型的思維鏈(內部思考過程)。",2361 "Display the model's internal thoughts in the response.": "在回應中顯示模型的思維鏈(內部思考過程)。",
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(可選)",
@@ -2386,5 +2386,71 @@
2386 "Your preset contains proxy and/or custom endpoint settings.": "此預設包含代理和/或自訂端點設定。",2386 "Your preset contains proxy and/or custom endpoint settings.": "此預設包含代理和/或自訂端點設定。",
2387 "Do you want to remove these fields before exporting?": "是否要在匯出前移除這些欄位?",2387 "Do you want to remove these fields before exporting?": "是否要在匯出前移除這些欄位?",
2388 "Delete the preset? This action is irreversible and your current settings will be overwritten.": "確定刪除此預設?刪除後無法復原,且此設定將被覆蓋。",2388 "Delete the preset? This action is irreversible and your current settings will be overwritten.": "確定刪除此預設?刪除後無法復原,且此設定將被覆蓋。",
2389 "Update all": "全部更新"2389 "Update all": "全部更新",
2390 "Automatically chooses an alternative provider if chosen providers can't serve your request.": "當所選提供者無法滿足您的請求時,自動選擇替代提供者。",
2391 "Use extension settings": "使用擴充功能設定",
2392 "To use instruct formatting, switch to OpenRouter under Text Completion API.": "若要使用指令格式,請在文本補全 API 下切換至 OpenRouter。",
2393 "Automatically 'continue' a response if the model stopped before reaching a certain amount of tokens.": "如果模型在達到一定數量的符元前停止,則自動繼續生成回應。",
2394 "Toggle entry's active state.": "切換條目的啟用狀態。",
2395 "Non-sticky": "無黏性",
2396 "No cooldown": "無冷卻時間",
2397 "No delay": "無延遲",
2398 "Included settings:": "包含設定:",
2399 "Click on the setting name to omit it from the profile.": "點擊設定名稱以從設定檔中省略。",
2400 "Tints the chat background and/or character sprites.": "調整聊天背景或角色圖片的色調。",
2401 "Only chunk on custom boundary": "僅在自定邊界進行分塊(chunk)",
2402 "help_macros_firstDisplayedMessageId": "載入到可見聊天中的第一則訊息的 ID。",
2403 "Couldn't import tags:": "無法匯入標籤:",
2404 "Select providers. No selection = all providers.": "選擇供應商。未選擇=所有供應商。",
2405 "Select a model": "選擇模型",
2406 "Search models...": "搜尋模型⋯",
2407 "[Currently loaded]": "[當前加載]",
2408 "Search providers...": "搜尋供應商⋯",
2409 "No-sticky": "無固定",
2410 "Create a new World Info": "建立新世界資訊",
2411 "Enter a name for the new file:": "輸入新檔案的名稱:",
2412 "Valid World Info file name is required": "需要有效的世界資訊檔案名稱",
2413 "World Info file has an invalid format": "世界資訊檔案格式無效",
2414 "World Info file has no entries": "世界資訊檔案中無任何條目",
2415 "Character not found.": "未找到該角色。",
2416 "Open a chat to get a name of the chat-bound lorebook": "開啟聊天以取得綁定聊天的知識書名稱。",
2417 "File is not valid: ${0}": "檔案無效或格式不正確:${0}",
2418 "The world with ${0} is invalid or corrupted.": "世界 ${0} 無效或已損壞。",
2419 "Deactivated all worlds": "已停用所有世界",
2420 "No world found named: ${0}": "未找到名為 ${0} 的世界",
2421 "Activated world: ${0}": "已啟用世界:${0}",
2422 "Deactivated world: ${0}": "已停用世界:${0}",
2423 "World was not active: ${0}": "世界 ${0} 未啟用",
2424 "The world '${0}' has been imported and linked to the character successfully.": "世界「${0}」已成功匯入並綁定至角色。",
2425 "World/Lorebook imported": "世界/知識書已匯入",
2426 "Are you sure you want to import '${0}'?": "確定要匯入「${0}」嗎?",
2427 "Built-in Extensions:": "內建擴充功能:",
2428 "Installed Extensions:": "已安裝的擴充功能:",
2429 "Loading third-party extensions... Please wait...": "正在載入第三方擴充功能,請稍候⋯",
2430 "The page will be reloaded shortly...": "頁面即將重新載入⋯",
2431 "Extensions state changed": "擴充功能狀態已更改",
2432 "Error loading extensions. See browser console for details.": "載入擴充功能時出現錯誤。詳情請查看瀏覽器控制台。",
2433 "You don't have permission to update global extensions.": "您無權更新全域擴充功能。",
2434 "Extension update failed": "擴充功能更新失敗",
2435 "Extension ${0} updated to ${1}": "擴充功能 ${0} 已更新至 ${1}",
2436 "Reload the page to apply updates": "重新加載頁面以使用更新",
2437 "You don't have permission to delete global extensions.": "您無權刪除全域擴充功能。",
2438 "Are you sure you want to delete ${0}?": "確定要刪除 ${0} 嗎?",
2439 "You don't have permission to move extensions.": "您無權移動擴充功能。",
2440 "Are you sure you want to move ${0} to your local extensions? This will make it available only for you.": "確定要將 ${0} 移至本地擴充功能嗎?此後僅您可使用。",
2441 "Are you sure you want to move ${0} to the global extensions? This will make it available for all users.": "確定要將 ${0} 移至全域擴充功能嗎?此後所有使用者皆可使用。",
2442 "Extension ${0} moved.": "擴充功能 ${0} 已移動。",
2443 "Extension ${0} deleted": "擴充功能 ${0} 已刪除。",
2444 "Please wait...": "請稍候⋯",
2445 "Installing extension": "正在安裝擴充功能",
2446 "Extension installation failed": "擴充功能安裝失敗",
2447 "Extension '${0}' by ${1} (version ${2}) has been installed successfully!": "擴充功能 '${0}' 由 ${1} 提供(版本 ${2})已成功安裝!",
2448 "Extension installation successful": "擴充功能安裝成功",
2449 "Extension updates available": "有可用的擴充功能更新",
2450 "Auto-updating extensions. This may take several minutes.": "正在自動更新擴充功能。這可能需要幾分鐘時間。",
2451 "Install": "安裝",
2452 "Modules provided by your Extras API:": "由您的 Extras API 提供的模組:",
2453 "Not connected to the API!": "未連線到 API!",
2454 "ext_type_system": "這是內建的擴充功能,無法刪除,且會跟隨系統更新。",
2455 "Valid": "已驗證"
2390}2456}
public/script.js+140 -39
@@ -168,6 +168,7 @@ import {
168 isTrueBoolean,168 isTrueBoolean,
169 toggleDrawer,169 toggleDrawer,
170 isElementInViewport,170 isElementInViewport,
171 copyText,
171} from './scripts/utils.js';172} from './scripts/utils.js';
172import { debounce_timeout } from './scripts/constants.js';173import { debounce_timeout } from './scripts/constants.js';
173174
@@ -266,6 +267,7 @@ import { initSettingsSearch } from './scripts/setting-search.js';
266import { initBulkEdit } from './scripts/bulk-edit.js';267import { initBulkEdit } from './scripts/bulk-edit.js';
267import { deriveTemplatesFromChatTemplate } from './scripts/chat-templates.js';268import { deriveTemplatesFromChatTemplate } from './scripts/chat-templates.js';
268import { getContext } from './scripts/st-context.js';269import { getContext } from './scripts/st-context.js';
270import { initReasoning, PromptReasoning } from './scripts/reasoning.js';
269271
270// API OBJECT FOR EXTERNAL WIRING272// API OBJECT FOR EXTERNAL WIRING
271globalThis.SillyTavern = {273globalThis.SillyTavern = {
@@ -442,6 +444,7 @@ export const event_types = {
442 MESSAGE_DELETED: 'message_deleted',444 MESSAGE_DELETED: 'message_deleted',
443 MESSAGE_UPDATED: 'message_updated',445 MESSAGE_UPDATED: 'message_updated',
444 MESSAGE_FILE_EMBEDDED: 'message_file_embedded',446 MESSAGE_FILE_EMBEDDED: 'message_file_embedded',
447 MORE_MESSAGES_LOADED: 'more_messages_loaded',
445 IMPERSONATE_READY: 'impersonate_ready',448 IMPERSONATE_READY: 'impersonate_ready',
446 CHAT_CHANGED: 'chat_id_changed',449 CHAT_CHANGED: 'chat_id_changed',
447 GENERATION_AFTER_COMMANDS: 'GENERATION_AFTER_COMMANDS',450 GENERATION_AFTER_COMMANDS: 'GENERATION_AFTER_COMMANDS',
@@ -722,6 +725,7 @@ async function getSystemMessages() {
722 is_user: false,725 is_user: false,
723 is_system: true,726 is_system: true,
724 mes: await renderTemplateAsync('assistantNote'),727 mes: await renderTemplateAsync('assistantNote'),
728 uses_system_ui: true,
725 extra: {729 extra: {
726 isSmallSys: true,730 isSmallSys: true,
727 },731 },
@@ -979,6 +983,7 @@ async function firstLoadInit() {
979 initServerHistory();983 initServerHistory();
980 initSettingsSearch();984 initSettingsSearch();
981 initBulkEdit();985 initBulkEdit();
986 initReasoning();
982 await initScrapers();987 await initScrapers();
983 doDailyExtensionUpdatesCheck();988 doDailyExtensionUpdatesCheck();
984 await hideLoader();989 await hideLoader();
@@ -1828,7 +1833,7 @@ export async function replaceCurrentChat() {
1828 }1833 }
1829}1834}
18301835
1831export function showMoreMessages(messagesToLoad = null) {1836export async function showMoreMessages(messagesToLoad = null) {
1832 const firstDisplayedMesId = $('#chat').children('.mes').first().attr('mesid');1837 const firstDisplayedMesId = $('#chat').children('.mes').first().attr('mesid');
1833 let messageId = Number(firstDisplayedMesId);1838 let messageId = Number(firstDisplayedMesId);
1834 let count = messagesToLoad || power_user.chat_truncation || Number.MAX_SAFE_INTEGER;1839 let count = messagesToLoad || power_user.chat_truncation || Number.MAX_SAFE_INTEGER;
@@ -1858,6 +1863,8 @@ export function showMoreMessages(messagesToLoad = null) {
1858 const newHeight = $('#chat').prop('scrollHeight');1863 const newHeight = $('#chat').prop('scrollHeight');
1859 $('#chat').scrollTop(newHeight - prevHeight);1864 $('#chat').scrollTop(newHeight - prevHeight);
1860 }1865 }
1866
1867 await eventSource.emit(event_types.MORE_MESSAGES_LOADED);
1861}1868}
18621869
1863export async function printMessages() {1870export async function printMessages() {
@@ -2195,6 +2202,7 @@ function getMessageFromTemplate({
2195 isUser,2202 isUser,
2196 avatarImg,2203 avatarImg,
2197 bias,2204 bias,
2205 reasoning,
2198 isSystem,2206 isSystem,
2199 title,2207 title,
2200 timerValue,2208 timerValue,
@@ -2219,6 +2227,7 @@ function getMessageFromTemplate({
2219 mes.find('.avatar img').attr('src', avatarImg);2227 mes.find('.avatar img').attr('src', avatarImg);
2220 mes.find('.ch_name .name_text').text(characterName);2228 mes.find('.ch_name .name_text').text(characterName);
2221 mes.find('.mes_bias').html(bias);2229 mes.find('.mes_bias').html(bias);
2230 mes.find('.mes_reasoning').html(reasoning);
2222 mes.find('.timestamp').text(timestamp).attr('title', `${extra?.api ? extra.api + ' - ' : ''}${extra?.model ?? ''}`);2231 mes.find('.timestamp').text(timestamp).attr('title', `${extra?.api ? extra.api + ' - ' : ''}${extra?.model ?? ''}`);
2223 mes.find('.mesIDDisplay').text(`#${mesId}`);2232 mes.find('.mesIDDisplay').text(`#${mesId}`);
2224 tokenCount && mes.find('.tokenCounterDisplay').text(`${tokenCount}t`);2233 tokenCount && mes.find('.tokenCounterDisplay').text(`${tokenCount}t`);
@@ -2233,10 +2242,16 @@ function getMessageFromTemplate({
2233 return mes;2242 return mes;
2234}2243}
22352244
2245/**
2246 * Re-renders a message block with updated content.
2247 * @param {number} messageId Message ID
2248 * @param {object} message Message object
2249 */
2236export function updateMessageBlock(messageId, message) {2250export function updateMessageBlock(messageId, message) {
2237 const messageElement = $(`#chat [mesid="${messageId}"]`);2251 const messageElement = $(`#chat [mesid="${messageId}"]`);
2238 const text = message?.extra?.display_text ?? message.mes;2252 const text = message?.extra?.display_text ?? message.mes;
2239 messageElement.find('.mes_text').html(messageFormatting(text, message.name, message.is_system, message.is_user, messageId));2253 messageElement.find('.mes_text').html(messageFormatting(text, message.name, message.is_system, message.is_user, messageId));
2254 messageElement.find('.mes_reasoning').html(messageFormatting(message.extra?.reasoning ?? '', '', false, false, -1));
2240 addCopyToCodeBlocks(messageElement);2255 addCopyToCodeBlocks(messageElement);
2241 appendMediaToMessage(message, messageElement);2256 appendMediaToMessage(message, messageElement);
2242}2257}
@@ -2315,18 +2330,17 @@ export function addCopyToCodeBlocks(messageElement) {
2315 const codeBlocks = $(messageElement).find('pre code');2330 const codeBlocks = $(messageElement).find('pre code');
2316 for (let i = 0; i < codeBlocks.length; i++) {2331 for (let i = 0; i < codeBlocks.length; i++) {
2317 hljs.highlightElement(codeBlocks.get(i));2332 hljs.highlightElement(codeBlocks.get(i));
2318 if (navigator.clipboard !== undefined) {
2319 const copyButton = document.createElement('i');2333 const copyButton = document.createElement('i');
2320 copyButton.classList.add('fa-solid', 'fa-copy', 'code-copy', 'interactable');2334 copyButton.classList.add('fa-solid', 'fa-copy', 'code-copy', 'interactable');
2321 copyButton.title = 'Copy code';2335 copyButton.title = 'Copy code';
2322 codeBlocks.get(i).appendChild(copyButton);2336 codeBlocks.get(i).appendChild(copyButton);
2323 copyButton.addEventListener('pointerup', function (event) {2337 copyButton.addEventListener('pointerup', async function () {
2324 navigator.clipboard.writeText(codeBlocks.get(i).innerText);2338 const text = codeBlocks.get(i).innerText;
2339 await copyText(text);
2325 toastr.info(t`Copied!`, '', { timeOut: 2000 });2340 toastr.info(t`Copied!`, '', { timeOut: 2000 });
2326 });2341 });
2327 }2342 }
2328}2343}
2329}
23302344
23312345
2332/**2346/**
@@ -2396,6 +2410,7 @@ export function addOneMessage(mes, { type = 'normal', insertAfter = null, scroll
2396 sanitizerOverrides,2410 sanitizerOverrides,
2397 );2411 );
2398 const bias = messageFormatting(mes.extra?.bias ?? '', '', false, false, -1);2412 const bias = messageFormatting(mes.extra?.bias ?? '', '', false, false, -1);
2413 const reasoning = messageFormatting(mes.extra?.reasoning ?? '', '', false, false, -1);
2399 let bookmarkLink = mes?.extra?.bookmark_link ?? '';2414 let bookmarkLink = mes?.extra?.bookmark_link ?? '';
24002415
2401 let params = {2416 let params = {
@@ -2405,6 +2420,7 @@ export function addOneMessage(mes, { type = 'normal', insertAfter = null, scroll
2405 isUser: mes.is_user,2420 isUser: mes.is_user,
2406 avatarImg: avatarImg,2421 avatarImg: avatarImg,
2407 bias: bias,2422 bias: bias,
2423 reasoning: reasoning,
2408 isSystem: isSystem,2424 isSystem: isSystem,
2409 title: title,2425 title: title,
2410 bookmarkLink: bookmarkLink,2426 bookmarkLink: bookmarkLink,
@@ -2464,6 +2480,7 @@ export function addOneMessage(mes, { type = 'normal', insertAfter = null, scroll
2464 const swipeMessage = chatElement.find(`[mesid="${chat.length - 1}"]`);2480 const swipeMessage = chatElement.find(`[mesid="${chat.length - 1}"]`);
2465 swipeMessage.attr('swipeid', params.swipeId);2481 swipeMessage.attr('swipeid', params.swipeId);
2466 swipeMessage.find('.mes_text').html(messageText).attr('title', title);2482 swipeMessage.find('.mes_text').html(messageText).attr('title', title);
2483 swipeMessage.find('.mes_reasoning').html(reasoning);
2467 swipeMessage.find('.timestamp').text(timestamp).attr('title', `${params.extra.api} - ${params.extra.model}`);2484 swipeMessage.find('.timestamp').text(timestamp).attr('title', `${params.extra.api} - ${params.extra.model}`);
2468 appendMediaToMessage(mes, swipeMessage);2485 appendMediaToMessage(mes, swipeMessage);
2469 if (power_user.timestamp_model_icon && params.extra?.api) {2486 if (power_user.timestamp_model_icon && params.extra?.api) {
@@ -3074,6 +3091,7 @@ class StreamingProcessor {
3074 this.messageTextDom = null;3091 this.messageTextDom = null;
3075 this.messageTimerDom = null;3092 this.messageTimerDom = null;
3076 this.messageTokenCounterDom = null;3093 this.messageTokenCounterDom = null;
3094 this.messageReasoningDom = null;
3077 /** @type {HTMLTextAreaElement} */3095 /** @type {HTMLTextAreaElement} */
3078 this.sendTextarea = document.querySelector('#send_textarea');3096 this.sendTextarea = document.querySelector('#send_textarea');
3079 this.type = type;3097 this.type = type;
@@ -3089,6 +3107,7 @@ class StreamingProcessor {
3089 /** @type {import('./scripts/logprobs.js').TokenLogprobs[]} */3107 /** @type {import('./scripts/logprobs.js').TokenLogprobs[]} */
3090 this.messageLogprobs = [];3108 this.messageLogprobs = [];
3091 this.toolCalls = [];3109 this.toolCalls = [];
3110 this.reasoning = '';
3092 }3111 }
30933112
3094 #checkDomElements(messageId) {3113 #checkDomElements(messageId) {
@@ -3097,6 +3116,7 @@ class StreamingProcessor {
3097 this.messageTextDom = this.messageDom?.querySelector('.mes_text');3116 this.messageTextDom = this.messageDom?.querySelector('.mes_text');
3098 this.messageTimerDom = this.messageDom?.querySelector('.mes_timer');3117 this.messageTimerDom = this.messageDom?.querySelector('.mes_timer');
3099 this.messageTokenCounterDom = this.messageDom?.querySelector('.tokenCounterDisplay');3118 this.messageTokenCounterDom = this.messageDom?.querySelector('.tokenCounterDisplay');
3119 this.messageReasoningDom = this.messageDom?.querySelector('.mes_reasoning');
3100 }3120 }
3101 }3121 }
31023122
@@ -3174,18 +3194,27 @@ class StreamingProcessor {
3174 this.#checkDomElements(messageId);3194 this.#checkDomElements(messageId);
3175 this.#updateMessageBlockVisibility();3195 this.#updateMessageBlockVisibility();
3176 const currentTime = new Date();3196 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;3197 chat[messageId]['mes'] = processedText;
3181 chat[messageId]['gen_started'] = this.timeStarted;3198 chat[messageId]['gen_started'] = this.timeStarted;
3182 chat[messageId]['gen_finished'] = currentTime;3199 chat[messageId]['gen_finished'] = currentTime;
31833200
3184 if (currentTokenCount) {
3185 if (!chat[messageId]['extra']) {3201 if (!chat[messageId]['extra']) {
3186 chat[messageId]['extra'] = {};3202 chat[messageId]['extra'] = {};
3187 }3203 }
31883204
3205 if (this.reasoning) {
3206 chat[messageId]['extra']['reasoning'] = this.reasoning;
3207 if (this.messageReasoningDom instanceof HTMLElement) {
3208 const formattedReasoning = messageFormatting(this.reasoning, '', false, false, -1);
3209 this.messageReasoningDom.innerHTML = formattedReasoning;
3210 }
3211 }
3212
3213 // Don't waste time calculating token count for streaming
3214 const tokenCountText = (this.reasoning || '') + processedText;
3215 const currentTokenCount = isFinal && power_user.message_token_count_enabled ? getTokenCount(tokenCountText, 0) : 0;
3216
3217 if (currentTokenCount) {
3189 chat[messageId]['extra']['token_count'] = currentTokenCount;3218 chat[messageId]['extra']['token_count'] = currentTokenCount;
3190 if (this.messageTokenCounterDom instanceof HTMLElement) {3219 if (this.messageTokenCounterDom instanceof HTMLElement) {
3191 this.messageTokenCounterDom.textContent = `${currentTokenCount}t`;3220 this.messageTokenCounterDom.textContent = `${currentTokenCount}t`;
@@ -3207,10 +3236,13 @@ class StreamingProcessor {
3207 if (this.messageTextDom instanceof HTMLElement) {3236 if (this.messageTextDom instanceof HTMLElement) {
3208 this.messageTextDom.innerHTML = formattedText;3237 this.messageTextDom.innerHTML = formattedText;
3209 }3238 }
3239
3240 const timePassed = formatGenerationTimer(this.timeStarted, currentTime, currentTokenCount);
3210 if (this.messageTimerDom instanceof HTMLElement) {3241 if (this.messageTimerDom instanceof HTMLElement) {
3211 this.messageTimerDom.textContent = timePassed.timerValue;3242 this.messageTimerDom.textContent = timePassed.timerValue;
3212 this.messageTimerDom.title = timePassed.timerTitle;3243 this.messageTimerDom.title = timePassed.timerTitle;
3213 }3244 }
3245
3214 this.setFirstSwipe(messageId);3246 this.setFirstSwipe(messageId);
3215 }3247 }
32163248
@@ -3317,7 +3349,7 @@ class StreamingProcessor {
3317 }3349 }
33183350
3319 /**3351 /**
3320 * @returns {Generator<{ text: string, swipes: string[], logprobs: import('./scripts/logprobs.js').TokenLogprobs, toolCalls: any[] }, void, void>}3352 * @returns {Generator<{ text: string, swipes: string[], logprobs: import('./scripts/logprobs.js').TokenLogprobs, toolCalls: any[], state: any }, void, void>}
3321 */3353 */
3322 *nullStreamingGeneration() {3354 *nullStreamingGeneration() {
3323 throw new Error('Generation function for streaming is not hooked up');3355 throw new Error('Generation function for streaming is not hooked up');
@@ -3339,7 +3371,7 @@ class StreamingProcessor {
3339 try {3371 try {
3340 const sw = new Stopwatch(1000 / power_user.streaming_fps);3372 const sw = new Stopwatch(1000 / power_user.streaming_fps);
3341 const timestamps = [];3373 const timestamps = [];
3342 for await (const { text, swipes, logprobs, toolCalls } of this.generator()) {3374 for await (const { text, swipes, logprobs, toolCalls, state } of this.generator()) {
3343 timestamps.push(Date.now());3375 timestamps.push(Date.now());
3344 if (this.isStopped) {3376 if (this.isStopped) {
3345 return;3377 return;
@@ -3351,6 +3383,7 @@ class StreamingProcessor {
3351 if (logprobs) {3383 if (logprobs) {
3352 this.messageLogprobs.push(...(Array.isArray(logprobs) ? logprobs : [logprobs]));3384 this.messageLogprobs.push(...(Array.isArray(logprobs) ? logprobs : [logprobs]));
3353 }3385 }
3386 this.reasoning = state?.reasoning ?? '';
3354 await eventSource.emit(event_types.STREAM_TOKEN_RECEIVED, text);3387 await eventSource.emit(event_types.STREAM_TOKEN_RECEIVED, text);
3355 await sw.tick(() => this.onProgressStreaming(this.messageId, this.continueMessage + text));3388 await sw.tick(() => this.onProgressStreaming(this.messageId, this.continueMessage + text));
3356 }3389 }
@@ -3817,6 +3850,14 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
3817 coreChat.pop();3850 coreChat.pop();
3818 }3851 }
38193852
3853 const reasoning = new PromptReasoning();
3854 for (let i = coreChat.length - 1; i >= 0; i--) {
3855 if (reasoning.isLimitReached()) {
3856 break;
3857 }
3858 coreChat[i] = { ...coreChat[i], mes: reasoning.addToMessage(coreChat[i].mes, coreChat[i].extra?.reasoning) };
3859 }
3860
3820 coreChat = await Promise.all(coreChat.map(async (chatItem, index) => {3861 coreChat = await Promise.all(coreChat.map(async (chatItem, index) => {
3821 let message = chatItem.mes;3862 let message = chatItem.mes;
3822 let regexType = chatItem.is_user ? regex_placement.USER_INPUT : regex_placement.AI_OUTPUT;3863 let regexType = chatItem.is_user ? regex_placement.USER_INPUT : regex_placement.AI_OUTPUT;
@@ -3902,7 +3943,7 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
3902 * @returns {string[]} Examples array with block heading3943 * @returns {string[]} Examples array with block heading
3903 */3944 */
3904 function parseMesExamples(examplesStr) {3945 function parseMesExamples(examplesStr) {
3905 if (examplesStr.length === 0 || examplesStr === '<START>') {3946 if (!examplesStr || examplesStr.length === 0 || examplesStr === '<START>') {
3906 return [];3947 return [];
3907 }3948 }
39083949
@@ -4738,6 +4779,7 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
4738 //const getData = await response.json();4779 //const getData = await response.json();
4739 let getMessage = extractMessageFromData(data);4780 let getMessage = extractMessageFromData(data);
4740 let title = extractTitleFromData(data);4781 let title = extractTitleFromData(data);
4782 let reasoning = extractReasoningFromData(data);
4741 kobold_horde_model = title;4783 kobold_horde_model = title;
47424784
4743 const swipes = extractMultiSwipes(data, type);4785 const swipes = extractMultiSwipes(data, type);
@@ -4764,10 +4806,10 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
4764 else {4806 else {
4765 // Without streaming we'll be having a full message on continuation. Treat it as a last chunk.4807 // Without streaming we'll be having a full message on continuation. Treat it as a last chunk.
4766 if (originalType !== 'continue') {4808 if (originalType !== 'continue') {
4767 ({ type, getMessage } = await saveReply(type, getMessage, false, title, swipes));4809 ({ type, getMessage } = await saveReply(type, getMessage, false, title, swipes, reasoning));
4768 }4810 }
4769 else {4811 else {
4770 ({ type, getMessage } = await saveReply('appendFinal', getMessage, false, title, swipes));4812 ({ type, getMessage } = await saveReply('appendFinal', getMessage, false, title, swipes, reasoning));
4771 }4813 }
47724814
4773 // This relies on `saveReply` having been called to add the message to the chat, so it must be last.4815 // This relies on `saveReply` having been called to add the message to the chat, so it must be last.
@@ -5469,7 +5511,7 @@ async function promptItemize(itemizedPrompts, requestedMesId) {
5469 } else {5511 } else {
5470 diffPrevPrompt.style.display = 'none';5512 diffPrevPrompt.style.display = 'none';
5471 }5513 }
5472 popup.dlg.querySelector('#copyPromptToClipboard').addEventListener('click', function () {5514 popup.dlg.querySelector('#copyPromptToClipboard').addEventListener('pointerup', async function () {
5473 let rawPrompt = itemizedPrompts[PromptArrayItemForRawPromptDisplay].rawPrompt;5515 let rawPrompt = itemizedPrompts[PromptArrayItemForRawPromptDisplay].rawPrompt;
5474 let rawPromptValues = rawPrompt;5516 let rawPromptValues = rawPrompt;
54755517
@@ -5477,7 +5519,7 @@ async function promptItemize(itemizedPrompts, requestedMesId) {
5477 rawPromptValues = rawPrompt.map(x => x.content).join('\n');5519 rawPromptValues = rawPrompt.map(x => x.content).join('\n');
5478 }5520 }
54795521
5480 navigator.clipboard.writeText(rawPromptValues);5522 await copyText(rawPromptValues);
5481 toastr.info(t`Copied!`);5523 toastr.info(t`Copied!`);
5482 });5524 });
54835525
@@ -5672,6 +5714,26 @@ function extractMessageFromData(data) {
5672}5714}
56735715
5674/**5716/**
5717 * Extracts the reasoning from the response data.
5718 * @param {object} data Response data
5719 * @returns {string} Extracted reasoning
5720 */
5721function extractReasoningFromData(data) {
5722 if (main_api === 'openai' && oai_settings.show_thoughts) {
5723 switch (oai_settings.chat_completion_source) {
5724 case chat_completion_sources.DEEPSEEK:
5725 return data?.choices?.[0]?.message?.reasoning_content ?? '';
5726 case chat_completion_sources.OPENROUTER:
5727 return data?.choices?.[0]?.message?.reasoning ?? '';
5728 case chat_completion_sources.MAKERSUITE:
5729 return data?.responseContent?.parts?.filter(part => part.thought)?.map(part => part.text)?.join('\n\n') ?? '';
5730 }
5731 }
5732
5733 return '';
5734}
5735
5736/**
5675 * Extracts multiswipe swipes from the response data.5737 * Extracts multiswipe swipes from the response data.
5676 * @param {Object} data Response data5738 * @param {Object} data Response data
5677 * @param {string} type Type of generation5739 * @param {string} type Type of generation
@@ -5851,7 +5913,7 @@ export function cleanUpMessage(getMessage, isImpersonate, isContinue, displayInc
5851 return getMessage;5913 return getMessage;
5852}5914}
58535915
5854export async function saveReply(type, getMessage, fromStreaming, title, swipes) {5916export 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 ||5917 if (type != 'append' && type != 'continue' && type != 'appendFinal' && chat.length && (chat[chat.length - 1]['swipe_id'] === undefined ||
5856 chat[chat.length - 1]['is_user'])) {5918 chat[chat.length - 1]['is_user'])) {
5857 type = 'normal';5919 type = 'normal';
@@ -5876,8 +5938,10 @@ export async function saveReply(type, getMessage, fromStreaming, title, swipes)
5876 chat[chat.length - 1]['send_date'] = getMessageTimeStamp();5938 chat[chat.length - 1]['send_date'] = getMessageTimeStamp();
5877 chat[chat.length - 1]['extra']['api'] = getGeneratingApi();5939 chat[chat.length - 1]['extra']['api'] = getGeneratingApi();
5878 chat[chat.length - 1]['extra']['model'] = getGeneratingModel();5940 chat[chat.length - 1]['extra']['model'] = getGeneratingModel();
5941 chat[chat.length - 1]['extra']['reasoning'] = reasoning;
5879 if (power_user.message_token_count_enabled) {5942 if (power_user.message_token_count_enabled) {
5880 chat[chat.length - 1]['extra']['token_count'] = await getTokenCountAsync(chat[chat.length - 1]['mes'], 0);5943 const tokenCountText = (reasoning || '') + chat[chat.length - 1]['mes'];
5944 chat[chat.length - 1]['extra']['token_count'] = await getTokenCountAsync(tokenCountText, 0);
5881 }5945 }
5882 const chat_id = (chat.length - 1);5946 const chat_id = (chat.length - 1);
5883 await eventSource.emit(event_types.MESSAGE_RECEIVED, chat_id);5947 await eventSource.emit(event_types.MESSAGE_RECEIVED, chat_id);
@@ -5896,8 +5960,10 @@ export async function saveReply(type, getMessage, fromStreaming, title, swipes)
5896 chat[chat.length - 1]['send_date'] = getMessageTimeStamp();5960 chat[chat.length - 1]['send_date'] = getMessageTimeStamp();
5897 chat[chat.length - 1]['extra']['api'] = getGeneratingApi();5961 chat[chat.length - 1]['extra']['api'] = getGeneratingApi();
5898 chat[chat.length - 1]['extra']['model'] = getGeneratingModel();5962 chat[chat.length - 1]['extra']['model'] = getGeneratingModel();
5963 chat[chat.length - 1]['extra']['reasoning'] += reasoning;
5899 if (power_user.message_token_count_enabled) {5964 if (power_user.message_token_count_enabled) {
5900 chat[chat.length - 1]['extra']['token_count'] = await getTokenCountAsync(chat[chat.length - 1]['mes'], 0);5965 const tokenCountText = (reasoning || '') + chat[chat.length - 1]['mes'];
5966 chat[chat.length - 1]['extra']['token_count'] = await getTokenCountAsync(tokenCountText, 0);
5901 }5967 }
5902 const chat_id = (chat.length - 1);5968 const chat_id = (chat.length - 1);
5903 await eventSource.emit(event_types.MESSAGE_RECEIVED, chat_id);5969 await eventSource.emit(event_types.MESSAGE_RECEIVED, chat_id);
@@ -5913,8 +5979,10 @@ export async function saveReply(type, getMessage, fromStreaming, title, swipes)
5913 chat[chat.length - 1]['send_date'] = getMessageTimeStamp();5979 chat[chat.length - 1]['send_date'] = getMessageTimeStamp();
5914 chat[chat.length - 1]['extra']['api'] = getGeneratingApi();5980 chat[chat.length - 1]['extra']['api'] = getGeneratingApi();
5915 chat[chat.length - 1]['extra']['model'] = getGeneratingModel();5981 chat[chat.length - 1]['extra']['model'] = getGeneratingModel();
5982 chat[chat.length - 1]['extra']['reasoning'] += reasoning;
5916 if (power_user.message_token_count_enabled) {5983 if (power_user.message_token_count_enabled) {
5917 chat[chat.length - 1]['extra']['token_count'] = await getTokenCountAsync(chat[chat.length - 1]['mes'], 0);5984 const tokenCountText = (reasoning || '') + chat[chat.length - 1]['mes'];
5985 chat[chat.length - 1]['extra']['token_count'] = await getTokenCountAsync(tokenCountText, 0);
5918 }5986 }
5919 const chat_id = (chat.length - 1);5987 const chat_id = (chat.length - 1);
5920 await eventSource.emit(event_types.MESSAGE_RECEIVED, chat_id);5988 await eventSource.emit(event_types.MESSAGE_RECEIVED, chat_id);
@@ -5930,6 +5998,7 @@ export async function saveReply(type, getMessage, fromStreaming, title, swipes)
5930 chat[chat.length - 1]['send_date'] = getMessageTimeStamp();5998 chat[chat.length - 1]['send_date'] = getMessageTimeStamp();
5931 chat[chat.length - 1]['extra']['api'] = getGeneratingApi();5999 chat[chat.length - 1]['extra']['api'] = getGeneratingApi();
5932 chat[chat.length - 1]['extra']['model'] = getGeneratingModel();6000 chat[chat.length - 1]['extra']['model'] = getGeneratingModel();
6001 chat[chat.length - 1]['extra']['reasoning'] = reasoning;
5933 if (power_user.trim_spaces) {6002 if (power_user.trim_spaces) {
5934 getMessage = getMessage.trim();6003 getMessage = getMessage.trim();
5935 }6004 }
@@ -5939,7 +6008,8 @@ export async function saveReply(type, getMessage, fromStreaming, title, swipes)
5939 chat[chat.length - 1]['gen_finished'] = generationFinished;6008 chat[chat.length - 1]['gen_finished'] = generationFinished;
59406009
5941 if (power_user.message_token_count_enabled) {6010 if (power_user.message_token_count_enabled) {
5942 chat[chat.length - 1]['extra']['token_count'] = await getTokenCountAsync(chat[chat.length - 1]['mes'], 0);6011 const tokenCountText = (reasoning || '') + chat[chat.length - 1]['mes'];
6012 chat[chat.length - 1]['extra']['token_count'] = await getTokenCountAsync(tokenCountText, 0);
5943 }6013 }
59446014
5945 if (selected_group) {6015 if (selected_group) {
@@ -6004,6 +6074,19 @@ export async function saveReply(type, getMessage, fromStreaming, title, swipes)
6004 return { type, getMessage };6074 return { type, getMessage };
6005}6075}
60066076
6077export function syncCurrentSwipeInfoExtras() {
6078 if (!chat.length) {
6079 return;
6080 }
6081 const currentMessage = chat[chat.length - 1];
6082 if (currentMessage && Array.isArray(currentMessage.swipe_info) && typeof currentMessage.swipe_id === 'number') {
6083 const swipeInfo = currentMessage.swipe_info[currentMessage.swipe_id];
6084 if (swipeInfo && typeof swipeInfo === 'object') {
6085 swipeInfo.extra = structuredClone(currentMessage.extra);
6086 }
6087 }
6088}
6089
6007function saveImageToMessage(img, mes) {6090function saveImageToMessage(img, mes) {
6008 if (mes && img.image) {6091 if (mes && img.image) {
6009 if (!mes.extra || typeof mes.extra !== 'object') {6092 if (!mes.extra || typeof mes.extra !== 'object') {
@@ -7982,11 +8065,25 @@ function updateEditArrowClasses() {
7982 }8065 }
7983}8066}
79848067
7985function closeMessageEditor() {8068/**
8069 * Closes the message editor.
8070 * @param {'message'|'reasoning'|'all'} what What to close. Default is 'all'.
8071 */
8072export function closeMessageEditor(what = 'all') {
8073 if (what === 'message' || what === 'all') {
7986 if (this_edit_mes_id) {8074 if (this_edit_mes_id) {
7987 $(`#chat .mes[mesid="${this_edit_mes_id}"] .mes_edit_cancel`).click();8075 $(`#chat .mes[mesid="${this_edit_mes_id}"] .mes_edit_cancel`).click();
7988 }8076 }
7989 }8077 }
8078 if (what === 'reasoning' || what === 'all') {
8079 document.querySelectorAll('.reasoning_edit_textarea').forEach((el) => {
8080 const cancelButton = el.closest('.mes')?.querySelector('.mes_reasoning_edit_cancel');
8081 if (cancelButton instanceof HTMLElement) {
8082 cancelButton.click();
8083 }
8084 });
8085 }
8086}
79908087
7991export function setGenerationProgress(progress) {8088export function setGenerationProgress(progress) {
7992 if (!progress) {8089 if (!progress) {
@@ -8417,6 +8514,9 @@ function swipe_left() { // when we swipe left..but no generation.
8417 streamingProcessor.onStopStreaming();8514 streamingProcessor.onStopStreaming();
8418 }8515 }
84198516
8517 // Make sure ad-hoc changes to extras are saved before swiping away
8518 syncCurrentSwipeInfoExtras();
8519
8420 const swipe_duration = 120;8520 const swipe_duration = 120;
8421 const swipe_range = '700px';8521 const swipe_range = '700px';
8422 chat[chat.length - 1]['swipe_id']--;8522 chat[chat.length - 1]['swipe_id']--;
@@ -8468,7 +8568,8 @@ function swipe_left() { // when we swipe left..but no generation.
8468 }8568 }
84698569
8470 const swipeMessage = $('#chat').find(`[mesid="${chat.length - 1}"]`);8570 const swipeMessage = $('#chat').find(`[mesid="${chat.length - 1}"]`);
8471 const tokenCount = await getTokenCountAsync(chat[chat.length - 1].mes, 0);8571 const tokenCountText = (chat[chat.length - 1]?.extra?.reasoning || '') + chat[chat.length - 1].mes;
8572 const tokenCount = await getTokenCountAsync(tokenCountText, 0);
8472 chat[chat.length - 1]['extra']['token_count'] = tokenCount;8573 chat[chat.length - 1]['extra']['token_count'] = tokenCount;
8473 swipeMessage.find('.tokenCounterDisplay').text(`${tokenCount}t`);8574 swipeMessage.find('.tokenCounterDisplay').text(`${tokenCount}t`);
8474 }8575 }
@@ -8551,6 +8652,9 @@ const swipe_right = () => {
8551 return unblockGeneration();8652 return unblockGeneration();
8552 }8653 }
85538654
8655 // Make sure ad-hoc changes to extras are saved before swiping away
8656 syncCurrentSwipeInfoExtras();
8657
8554 const swipe_duration = 200;8658 const swipe_duration = 200;
8555 const swipe_range = 700;8659 const swipe_range = 700;
8556 //console.log(swipe_range);8660 //console.log(swipe_range);
@@ -8632,6 +8736,7 @@ const swipe_right = () => {
8632 // resets the timer8736 // resets the timer
8633 swipeMessage.find('.mes_timer').html('');8737 swipeMessage.find('.mes_timer').html('');
8634 swipeMessage.find('.tokenCounterDisplay').text('');8738 swipeMessage.find('.tokenCounterDisplay').text('');
8739 swipeMessage.find('.mes_reasoning').html('');
8635 } else {8740 } else {
8636 //console.log('showing previously generated swipe candidate, or "..."');8741 //console.log('showing previously generated swipe candidate, or "..."');
8637 //console.log('onclick right swipe calling addOneMessage');8742 //console.log('onclick right swipe calling addOneMessage');
@@ -8642,7 +8747,8 @@ const swipe_right = () => {
8642 chat[chat.length - 1].extra = {};8747 chat[chat.length - 1].extra = {};
8643 }8748 }
86448749
8645 const tokenCount = await getTokenCountAsync(chat[chat.length - 1].mes, 0);8750 const tokenCountText = (chat[chat.length - 1]?.extra?.reasoning || '') + chat[chat.length - 1].mes;
8751 const tokenCount = await getTokenCountAsync(tokenCountText, 0);
8646 chat[chat.length - 1]['extra']['token_count'] = tokenCount;8752 chat[chat.length - 1]['extra']['token_count'] = tokenCount;
8647 swipeMessage.find('.tokenCounterDisplay').text(`${tokenCount}t`);8753 swipeMessage.find('.tokenCounterDisplay').text(`${tokenCount}t`);
8648 }8754 }
@@ -9444,7 +9550,8 @@ function addDebugFunctions() {
9444 message.extra = {};9550 message.extra = {};
9445 }9551 }
94469552
9447 message.extra.token_count = await getTokenCountAsync(message.mes, 0);9553 const tokenCountText = (message?.extra?.reasoning || '') + message.mes;
9554 message.extra.token_count = await getTokenCountAsync(tokenCountText, 0);
9448 }9555 }
94499556
9450 await saveChatConditional();9557 await saveChatConditional();
@@ -9507,7 +9614,7 @@ API Settings: ${JSON.stringify(getSettingsContents[getSettingsContents.main_api
9507 //console.log(logMessage);9614 //console.log(logMessage);
95089615
9509 try {9616 try {
9510 await navigator.clipboard.writeText(logMessage);9617 await copyText(logMessage);
9511 toastr.info('Your ST API setup data has been copied to the clipboard.');9618 toastr.info('Your ST API setup data has been copied to the clipboard.');
9512 } catch (error) {9619 } catch (error) {
9513 toastr.error('Failed to copy ST Setup to clipboard:', error);9620 toastr.error('Failed to copy ST Setup to clipboard:', error);
@@ -10572,24 +10679,18 @@ jQuery(async function () {
10572 setTimeout(function () { $('#shadow_select_chat_popup').css('display', 'none'); }, animation_duration);10679 setTimeout(function () { $('#shadow_select_chat_popup').css('display', 'none'); }, animation_duration);
10573 });10680 });
1057410681
10575 if (navigator.clipboard === undefined) {10682 $(document).on('pointerup', '.mes_copy', async function () {
10576 // No clipboard support
10577 $('.mes_copy').remove();
10578 }
10579 else {
10580 $(document).on('pointerup', '.mes_copy', function () {
10581 if (this_chid !== undefined || selected_group || name2 === neutralCharacterName) {10683 if (this_chid !== undefined || selected_group || name2 === neutralCharacterName) {
10582 try {10684 try {
10583 const messageId = $(this).closest('.mes').attr('mesid');10685 const messageId = $(this).closest('.mes').attr('mesid');
10584 const text = chat[messageId]['mes'];10686 const text = chat[messageId]['mes'];
10585 navigator.clipboard.writeText(text);10687 await copyText(text);
10586 toastr.info('Copied!', '', { timeOut: 2000 });10688 toastr.info('Copied!', '', { timeOut: 2000 });
10587 } catch (err) {10689 } catch (err) {
10588 console.error('Failed to copy: ', err);10690 console.error('Failed to copy: ', err);
10589 }10691 }
10590 }10692 }
10591 });10693 });
10592 }
1059310694
10594 $(document).on('pointerup', '.mes_prompt', async function () {10695 $(document).on('pointerup', '.mes_prompt', async function () {
10595 let mesIdForItemization = $(this).closest('.mes').attr('mesId');10696 let mesIdForItemization = $(this).closest('.mes').attr('mesId');
@@ -11219,14 +11320,15 @@ jQuery(async function () {
1121911320
11220 $(document).keyup(function (e) {11321 $(document).keyup(function (e) {
11221 if (e.key === 'Escape') {11322 if (e.key === 'Escape') {
11222 const isEditVisible = $('#curEditTextarea').is(':visible');11323 const isEditVisible = $('#curEditTextarea').is(':visible') || $('.reasoning_edit_textarea').length > 0;
11223 if (isEditVisible && power_user.auto_save_msg_edits === false) {11324 if (isEditVisible && power_user.auto_save_msg_edits === false) {
11224 closeMessageEditor();11325 closeMessageEditor('all');
11225 $('#send_textarea').focus();11326 $('#send_textarea').focus();
11226 return;11327 return;
11227 }11328 }
11228 if (isEditVisible && power_user.auto_save_msg_edits === true) {11329 if (isEditVisible && power_user.auto_save_msg_edits === true) {
11229 $(`#chat .mes[mesid="${this_edit_mes_id}"] .mes_edit_done`).click();11330 $(`#chat .mes[mesid="${this_edit_mes_id}"] .mes_edit_done`).click();
11331 closeMessageEditor('reasoning');
11230 $('#send_textarea').focus();11332 $('#send_textarea').focus();
11231 return;11333 return;
11232 }11334 }
@@ -11460,8 +11562,8 @@ jQuery(async function () {
11460 $('#avatar-and-name-block').slideToggle();11562 $('#avatar-and-name-block').slideToggle();
11461 });11563 });
1146211564
11463 $(document).on('mouseup touchend', '#show_more_messages', () => {11565 $(document).on('mouseup touchend', '#show_more_messages', async function () {
11464 showMoreMessages();11566 await showMoreMessages();
11465 });11567 });
1146611568
11467 $(document).on('click', '.open_characters_library', async function () {11569 $(document).on('click', '.open_characters_library', async function () {
@@ -11483,4 +11585,3 @@ jQuery(async function () {
1148311585
11484 initCustomSelectedSamplers();11586 initCustomSelectedSamplers();
11485});11587});
11486
public/scripts/backgrounds.js+4 -4
@@ -482,10 +482,10 @@ function highlightNewBackground(bg) {
482 */482 */
483function setFittingClass(fitting) {483function setFittingClass(fitting) {
484 const backgrounds = $('#bg1, #bg_custom');484 const backgrounds = $('#bg1, #bg_custom');
485 backgrounds.toggleClass('cover', fitting === 'cover');485 for (const option of ['cover', 'contain', 'stretch', 'center']) {
486 backgrounds.toggleClass('contain', fitting === 'contain');486 backgrounds.toggleClass(option, option === fitting);
487 backgrounds.toggleClass('stretch', fitting === 'stretch');487 }
488 backgrounds.toggleClass('center', fitting === 'center');488 background_settings.fitting = fitting;
489}489}
490490
491function onBackgroundFilterInput() {491function onBackgroundFilterInput() {
public/scripts/chat-templates.js+10 -0
@@ -60,6 +60,16 @@ const hash_derivations = {
60 // Tulu-3-70B60 // Tulu-3-70B
61 'Tulu'61 'Tulu'
62 ,62 ,
63
64 // DeepSeek V2.5
65 '54d400beedcd17f464e10063e0577f6f798fa896266a912d8a366f8a2fcc0bca':
66 'DeepSeek-V2.5'
67 ,
68
69 // DeepSeek R1
70 'b6835114b7303ddd78919a82e4d9f7d8c26ed0d7dfc36beeb12d524f6144eab1':
71 'DeepSeek-V2.5'
72 ,
63};73};
6474
65const substr_derivations = {75const substr_derivations = {
public/scripts/chats.js+17 -0
@@ -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,7 @@ 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';
4448
45/**49/**
46 * @typedef {Object} FileAttachment50 * @typedef {Object} FileAttachment
@@ -1437,6 +1441,19 @@ jQuery(function () {
1437 await viewMessageFile(messageId);1441 await viewMessageFile(messageId);
1438 });1442 });
14391443
1444 $(document).on('click', '.assistant_note_export', async function () {
1445 const chatToSave = [
1446 {
1447 user_name: name1,
1448 character_name: name2,
1449 chat_metadata: chat_metadata,
1450 },
1451 ...chat.filter(x => x?.extra?.type !== system_message_types.ASSISTANT_NOTE),
1452 ];
1453
1454 download(JSON.stringify(chatToSave, null, 4), `Assistant - ${humanizedDateTime()}.json`, 'application/json');
1455 });
1456
1440 // Do not change. #attachFile is added by extension.1457 // Do not change. #attachFile is added by extension.
1441 $(document).on('click', '#attachFile', function () {1458 $(document).on('click', '#attachFile', function () {
1442 $('#file_form_input').trigger('click');1459 $('#file_form_input').trigger('click');
public/scripts/extensions.js+23 -9
@@ -38,7 +38,8 @@ export let modules = [];
38let activeExtensions = new Set();38let activeExtensions = new Set();
3939
40const getApiUrl = () => extension_settings.apiUrl;40const getApiUrl = () => extension_settings.apiUrl;
41const sortManifests = (a, b) => parseInt(a.loading_order) - parseInt(b.loading_order) || String(a.display_name).localeCompare(String(b.display_name));41const sortManifestsByOrder = (a, b) => parseInt(a.loading_order) - parseInt(b.loading_order) || String(a.display_name).localeCompare(String(b.display_name));
42const sortManifestsByName = (a, b) => String(a.display_name).localeCompare(String(b.display_name)) || parseInt(a.loading_order) - parseInt(b.loading_order);
42let connectedToApi = false;43let connectedToApi = false;
4344
44/**45/**
@@ -355,7 +356,7 @@ async function getManifests(names) {
355 * @returns {Promise<void>}356 * @returns {Promise<void>}
356 */357 */
357async function activateExtensions() {358async function activateExtensions() {
358 const extensions = Object.entries(manifests).sort((a, b) => sortManifests(a[1], b[1]));359 const extensions = Object.entries(manifests).sort((a, b) => sortManifestsByOrder(a[1], b[1]));
359 const promises = [];360 const promises = [];
360361
361 for (let entry of extensions) {362 for (let entry of extensions) {
@@ -712,7 +713,10 @@ async function showExtensionsDetails() {
712713
713 htmlExternal.append(htmlLoading);714 htmlExternal.append(htmlLoading);
714715
715 const extensions = Object.entries(manifests).sort((a, b) => sortManifests(a[1], b[1])).map(getExtensionData);716 const sortOrderKey = 'extensions_sortByName';
717 const sortByName = localStorage.getItem(sortOrderKey) === 'true';
718 const sortFn = sortByName ? sortManifestsByName : sortManifestsByOrder;
719 const extensions = Object.entries(manifests).sort((a, b) => sortFn(a[1], b[1])).map(getExtensionData);
716720
717 extensions.forEach(value => {721 extensions.forEach(value => {
718 const { isExternal, extensionHtml } = value;722 const { isExternal, extensionHtml } = value;
@@ -729,7 +733,6 @@ async function showExtensionsDetails() {
729 /** @type {import('./popup.js').CustomPopupButton} */733 /** @type {import('./popup.js').CustomPopupButton} */
730 const updateAllButton = {734 const updateAllButton = {
731 text: t`Update all`,735 text: t`Update all`,
732 appendAtEnd: true,
733 action: async () => {736 action: async () => {
734 requiresReload = true;737 requiresReload = true;
735 await autoUpdateExtensions(true);738 await autoUpdateExtensions(true);
@@ -737,13 +740,23 @@ async function showExtensionsDetails() {
737 },740 },
738 };741 };
739742
743 /** @type {import('./popup.js').CustomPopupButton} */
744 const sortOrderButton = {
745 text: sortByName ? t`Sort: Display Name` : t`Sort: Loading Order`,
746 action: async () => {
747 abortController.abort();
748 localStorage.setItem(sortOrderKey, sortByName ? 'false' : 'true');
749 await showExtensionsDetails();
750 },
751 };
752
740 let waitingForSave = false;753 let waitingForSave = false;
741754
742 const popup = new Popup(html, POPUP_TYPE.TEXT, '', {755 const popup = new Popup(html, POPUP_TYPE.TEXT, '', {
743 okButton: t`Close`,756 okButton: t`Close`,
744 wide: true,757 wide: true,
745 large: true,758 large: true,
746 customButtons: [updateAllButton],759 customButtons: [sortOrderButton, updateAllButton],
747 allowVerticalScrolling: true,760 allowVerticalScrolling: true,
748 onClosing: async () => {761 onClosing: async () => {
749 if (waitingForSave) {762 if (waitingForSave) {
@@ -762,7 +775,7 @@ async function showExtensionsDetails() {
762 });775 });
763 popupPromise = popup.show();776 popupPromise = popup.show();
764 popup.content.scrollTop = initialScrollTop;777 popup.content.scrollTop = initialScrollTop;
765 checkForUpdatesManual(abortController.signal).finally(() => htmlLoading.remove());778 checkForUpdatesManual(sortFn, abortController.signal).finally(() => htmlLoading.remove());
766 } catch (error) {779 } catch (error) {
767 toastr.error(t`Error loading extensions. See browser console for details.`);780 toastr.error(t`Error loading extensions. See browser console for details.`);
768 console.error(error);781 console.error(error);
@@ -1073,12 +1086,13 @@ function processVersionCheckQueue() {
10731086
1074/**1087/**
1075 * Performs a manual check for updates on all 3rd-party extensions.1088 * Performs a manual check for updates on all 3rd-party extensions.
1089 * @param {function} sortFn Sort function
1076 * @param {AbortSignal} abortSignal Signal to abort the operation1090 * @param {AbortSignal} abortSignal Signal to abort the operation
1077 * @returns {Promise<any[]>}1091 * @returns {Promise<any[]>}
1078 */1092 */
1079async function checkForUpdatesManual(abortSignal) {1093async function checkForUpdatesManual(sortFn, abortSignal) {
1080 const promises = [];1094 const promises = [];
1081 for (const id of Object.keys(manifests).filter(x => x.startsWith('third-party')).sort((a, b) => sortManifests(manifests[a], manifests[b]))) {1095 for (const id of Object.keys(manifests).filter(x => x.startsWith('third-party')).sort((a, b) => sortFn(manifests[a], manifests[b]))) {
1082 const externalId = id.replace('third-party', '');1096 const externalId = id.replace('third-party', '');
1083 const promise = enqueueVersionCheck(async () => {1097 const promise = enqueueVersionCheck(async () => {
1084 try {1098 try {
@@ -1223,7 +1237,7 @@ export async function runGenerationInterceptors(chat, contextSize, type) {
1223 exitImmediately = immediately;1237 exitImmediately = immediately;
1224 };1238 };
12251239
1226 for (const manifest of Object.values(manifests).filter(x => x.generate_interceptor).sort((a, b) => sortManifests(a, b))) {1240 for (const manifest of Object.values(manifests).filter(x => x.generate_interceptor).sort((a, b) => sortManifestsByOrder(a, b))) {
1227 const interceptorKey = manifest.generate_interceptor;1241 const interceptorKey = manifest.generate_interceptor;
1228 if (typeof globalThis[interceptorKey] === 'function') {1242 if (typeof globalThis[interceptorKey] === 'function') {
1229 try {1243 try {
public/scripts/extensions/caption/settings.html+2 -0
@@ -54,6 +54,8 @@
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-flash-exp">gemini-2.0-flash-exp</option>56 <option data-type="google" value="gemini-2.0-flash-exp">gemini-2.0-flash-exp</option>
57 <option data-type="google" value="gemini-2.0-flash-thinking-exp">gemini-2.0-flash-thinking-exp</option>
58 <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>59 <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>60 <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>61 <option data-type="google" value="gemini-1.5-flash-latest">gemini-1.5-flash-latest</option>
public/scripts/extensions/tts/index.js+1 -1
@@ -466,7 +466,7 @@ async function processTtsQueue() {
466 }466 }
467467
468 if (extension_settings.tts.skip_tags) {468 if (extension_settings.tts.skip_tags) {
469 text = text.replace(/<.*?>.*?<\/.*?>/g, '').trim();469 text = text.replace(/<.*?>[\s\S]*?<\/.*?>/g, '').trim();
470 }470 }
471471
472 if (!extension_settings.tts.pass_asterisks) {472 if (!extension_settings.tts.pass_asterisks) {
public/scripts/group-chats.js+10 -0
@@ -81,6 +81,7 @@ import { t } from './i18n.js';
8181
82export {82export {
83 selected_group,83 selected_group,
84 openGroupId,
84 is_group_automode_enabled,85 is_group_automode_enabled,
85 hideMutedSprites,86 hideMutedSprites,
86 is_group_generating,87 is_group_generating,
@@ -1367,6 +1368,15 @@ function getGroupCharacterBlock(character) {
1367 template.find('.ch_fav').val(isFav);1368 template.find('.ch_fav').val(isFav);
1368 template.toggleClass('is_fav', isFav);1369 template.toggleClass('is_fav', isFav);
13691370
1371 const auxFieldName = power_user.aux_field || 'character_version';
1372 const auxFieldValue = (character.data && character.data[auxFieldName]) || '';
1373 if (auxFieldValue) {
1374 template.find('.character_version').text(auxFieldValue);
1375 }
1376 else {
1377 template.find('.character_version').hide();
1378 }
1379
1370 let queuePosition = groupChatQueueOrder.get(character.avatar);1380 let queuePosition = groupChatQueueOrder.get(character.avatar);
1371 if (queuePosition) {1381 if (queuePosition) {
1372 template.find('.queue_position').text(queuePosition);1382 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/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+43 -11
@@ -1096,8 +1096,8 @@ async function preparePromptsForChatCompletion({ Scenario, charPersonality, name
1096 // Unordered prompts without marker1096 // Unordered prompts without marker
1097 { role: 'system', content: impersonationPrompt, identifier: 'impersonate' },1097 { role: 'system', content: impersonationPrompt, identifier: 'impersonate' },
1098 { role: 'system', content: quietPrompt, identifier: 'quietPrompt' },1098 { role: 'system', content: quietPrompt, identifier: 'quietPrompt' },
1099 { role: 'system', content: bias, identifier: 'bias' },
1100 { role: 'system', content: groupNudge, identifier: 'groupNudge' },1099 { role: 'system', content: groupNudge, identifier: 'groupNudge' },
1100 { role: 'assistant', content: bias, identifier: 'bias' },
1101 ];1101 ];
11021102
1103 // Tavern Extras - Summary1103 // Tavern Extras - Summary
@@ -1922,7 +1922,7 @@ async function sendOpenAIRequest(type, messages, signal) {
1922 }1922 }
19231923
1924 // Proxy is only supported for Claude, OpenAI, Mistral, and Google MakerSuite1924 // 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)) {1925 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();1926 await validateReverseProxy();
1927 generate_data['reverse_proxy'] = oai_settings.reverse_proxy;1927 generate_data['reverse_proxy'] = oai_settings.reverse_proxy;
1928 generate_data['proxy_password'] = oai_settings.proxy_password;1928 generate_data['proxy_password'] = oai_settings.proxy_password;
@@ -2030,6 +2030,16 @@ async function sendOpenAIRequest(type, messages, signal) {
2030 // https://api-docs.deepseek.com/api/create-chat-completion2030 // https://api-docs.deepseek.com/api/create-chat-completion
2031 if (isDeepSeek) {2031 if (isDeepSeek) {
2032 generate_data.top_p = generate_data.top_p || Number.EPSILON;2032 generate_data.top_p = generate_data.top_p || Number.EPSILON;
2033
2034 if (generate_data.model.endsWith('-reasoner')) {
2035 delete generate_data.top_p;
2036 delete generate_data.temperature;
2037 delete generate_data.frequency_penalty;
2038 delete generate_data.presence_penalty;
2039 delete generate_data.top_logprobs;
2040 delete generate_data.logprobs;
2041 delete generate_data.logit_bias;
2042 }
2033 }2043 }
20342044
2035 if ((isOAI || isOpenRouter || isMistral || isCustom || isCohere || isNano) && oai_settings.seed >= 0) {2045 if ((isOAI || isOpenRouter || isMistral || isCustom || isCohere || isNano) && oai_settings.seed >= 0) {
@@ -2085,6 +2095,7 @@ async function sendOpenAIRequest(type, messages, signal) {
2085 let text = '';2095 let text = '';
2086 const swipes = [];2096 const swipes = [];
2087 const toolCalls = [];2097 const toolCalls = [];
2098 const state = { reasoning: '' };
2088 while (true) {2099 while (true) {
2089 const { done, value } = await reader.read();2100 const { done, value } = await reader.read();
2090 if (done) return;2101 if (done) return;
@@ -2095,14 +2106,14 @@ async function sendOpenAIRequest(type, messages, signal) {
20952106
2096 if (Array.isArray(parsed?.choices) && parsed?.choices?.[0]?.index > 0) {2107 if (Array.isArray(parsed?.choices) && parsed?.choices?.[0]?.index > 0) {
2097 const swipeIndex = parsed.choices[0].index - 1;2108 const swipeIndex = parsed.choices[0].index - 1;
2098 swipes[swipeIndex] = (swipes[swipeIndex] || '') + getStreamingReply(parsed);2109 swipes[swipeIndex] = (swipes[swipeIndex] || '') + getStreamingReply(parsed, state);
2099 } else {2110 } else {
2100 text += getStreamingReply(parsed);2111 text += getStreamingReply(parsed, state);
2101 }2112 }
21022113
2103 ToolManager.parseToolCalls(toolCalls, parsed);2114 ToolManager.parseToolCalls(toolCalls, parsed);
21042115
2105 yield { text, swipes: swipes, logprobs: parseChatCompletionLogprobs(parsed), toolCalls: toolCalls };2116 yield { text, swipes: swipes, logprobs: parseChatCompletionLogprobs(parsed), toolCalls: toolCalls, state: state };
2106 }2117 }
2107 };2118 };
2108 }2119 }
@@ -2129,13 +2140,32 @@ async function sendOpenAIRequest(type, messages, signal) {
2129 }2140 }
2130}2141}
21312142
2132function getStreamingReply(data) {2143/**
2144 * Extracts the reply from the response data from a chat completions-like source
2145 * @param {object} data Response data from the chat completions-like source
2146 * @param {object} state Additional state to keep track of
2147 * @returns {string} The reply extracted from the response data
2148 */
2149function getStreamingReply(data, state) {
2133 if (oai_settings.chat_completion_source === chat_completion_sources.CLAUDE) {2150 if (oai_settings.chat_completion_source === chat_completion_sources.CLAUDE) {
2134 return data?.delta?.text || '';2151 return data?.delta?.text || '';
2135 } else if (oai_settings.chat_completion_source === chat_completion_sources.MAKERSUITE) {2152 } 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') || '';2153 if (oai_settings.show_thoughts) {
2154 state.reasoning += (data?.candidates?.[0]?.content?.parts?.filter(x => x.thought)?.map(x => x.text)?.[0] || '');
2155 }
2156 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) {2157 } else if (oai_settings.chat_completion_source === chat_completion_sources.COHERE) {
2138 return data?.delta?.message?.content?.text || data?.delta?.message?.tool_plan || '';2158 return data?.delta?.message?.content?.text || data?.delta?.message?.tool_plan || '';
2159 } else if (oai_settings.chat_completion_source === chat_completion_sources.DEEPSEEK) {
2160 if (oai_settings.show_thoughts) {
2161 state.reasoning += (data.choices?.filter(x => x?.delta?.reasoning_content)?.[0]?.delta?.reasoning_content || '');
2162 }
2163 return data.choices?.[0]?.delta?.content || '';
2164 } else if (oai_settings.chat_completion_source === chat_completion_sources.OPENROUTER) {
2165 if (oai_settings.show_thoughts) {
2166 state.reasoning += (data.choices?.filter(x => x?.delta?.reasoning)?.[0]?.delta?.reasoning || '');
2167 }
2168 return data.choices?.[0]?.delta?.content ?? data.choices?.[0]?.message?.content ?? data.choices?.[0]?.text ?? '';
2139 } else {2169 } else {
2140 return data.choices?.[0]?.delta?.content ?? data.choices?.[0]?.message?.content ?? data.choices?.[0]?.text ?? '';2170 return data.choices?.[0]?.delta?.content ?? data.choices?.[0]?.message?.content ?? data.choices?.[0]?.text ?? '';
2141 }2171 }
@@ -3346,7 +3376,7 @@ async function getStatusOpen() {
3346 chat_completion_source: oai_settings.chat_completion_source,3376 chat_completion_source: oai_settings.chat_completion_source,
3347 };3377 };
33483378
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)) {3379 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();3380 await validateReverseProxy();
3351 }3381 }
33523382
@@ -4204,7 +4234,7 @@ async function onModelChange() {
4204 $('#openai_max_context').attr('max', max_32k);4234 $('#openai_max_context').attr('max', max_32k);
4205 } else if (value.includes('gemini-1.5-pro') || value.includes('gemini-exp-1206')) {4235 } else if (value.includes('gemini-1.5-pro') || value.includes('gemini-exp-1206')) {
4206 $('#openai_max_context').attr('max', max_2mil);4236 $('#openai_max_context').attr('max', max_2mil);
4207 } else if (value.includes('gemini-1.5-flash') || value.includes('gemini-2.0-flash-exp')) {4237 } else if (value.includes('gemini-1.5-flash') || value.includes('gemini-2.0-flash-exp') || value.includes('gemini-2.0-flash-thinking-exp')) {
4208 $('#openai_max_context').attr('max', max_1mil);4238 $('#openai_max_context').attr('max', max_1mil);
4209 } else if (value.includes('gemini-1.0-pro') || value === 'gemini-pro') {4239 } else if (value.includes('gemini-1.0-pro') || value === 'gemini-pro') {
4210 $('#openai_max_context').attr('max', max_32k);4240 $('#openai_max_context').attr('max', max_32k);
@@ -4488,7 +4518,7 @@ async function onModelChange() {
4488 if (oai_settings.chat_completion_source === chat_completion_sources.DEEPSEEK) {4518 if (oai_settings.chat_completion_source === chat_completion_sources.DEEPSEEK) {
4489 if (oai_settings.max_context_unlocked) {4519 if (oai_settings.max_context_unlocked) {
4490 $('#openai_max_context').attr('max', unlocked_max);4520 $('#openai_max_context').attr('max', unlocked_max);
4491 } else if (oai_settings.deepseek_model == 'deepseek-chat') {4521 } else if (['deepseek-reasoner', 'deepseek-chat'].includes(oai_settings.deepseek_model)) {
4492 $('#openai_max_context').attr('max', max_64k);4522 $('#openai_max_context').attr('max', max_64k);
4493 } else if (oai_settings.deepseek_model == 'deepseek-coder') {4523 } else if (oai_settings.deepseek_model == 'deepseek-coder') {
4494 $('#openai_max_context').attr('max', max_16k);4524 $('#openai_max_context').attr('max', max_16k);
@@ -4725,7 +4755,7 @@ async function onConnectButtonClick(e) {
4725 await writeSecret(SECRET_KEYS.DEEPSEEK, api_key_deepseek);4755 await writeSecret(SECRET_KEYS.DEEPSEEK, api_key_deepseek);
4726 }4756 }
47274757
4728 if (!secret_state[SECRET_KEYS.DEEPSEEK]) {4758 if (!secret_state[SECRET_KEYS.DEEPSEEK] && !oai_settings.reverse_proxy) {
4729 console.log('No secret key saved for DeepSeek');4759 console.log('No secret key saved for DeepSeek');
4730 return;4760 return;
4731 }4761 }
@@ -4901,6 +4931,8 @@ export function isImageInliningSupported() {
4901 const visionSupportedModels = [4931 const visionSupportedModels = [
4902 'gpt-4-vision',4932 'gpt-4-vision',
4903 'gemini-2.0-flash-thinking-exp-1219',4933 'gemini-2.0-flash-thinking-exp-1219',
4934 'gemini-2.0-flash-thinking-exp-01-21',
4935 'gemini-2.0-flash-thinking-exp',
4904 'gemini-2.0-flash-exp',4936 'gemini-2.0-flash-exp',
4905 'gemini-1.5-flash',4937 'gemini-1.5-flash',
4906 'gemini-1.5-flash-latest',4938 'gemini-1.5-flash-latest',
public/scripts/power-user.js+9 -1
@@ -253,6 +253,14 @@ let power_user = {
253 content: 'Write {{char}}\'s next reply in a fictional chat between {{char}} and {{user}}.',253 content: 'Write {{char}}\'s next reply in a fictional chat between {{char}} and {{user}}.',
254 },254 },
255255
256 reasoning: {
257 add_to_prompts: false,
258 prefix: '<think>\n',
259 suffix: '\n</think>',
260 separator: '\n\n',
261 max_additions: 1,
262 },
263
256 personas: {},264 personas: {},
257 default_persona: null,265 default_persona: null,
258 persona_descriptions: {},266 persona_descriptions: {},
@@ -2534,7 +2542,7 @@ async function loadUntilMesId(mesId) {
2534 let target;2542 let target;
25352543
2536 while (getFirstDisplayedMessageId() > mesId && getFirstDisplayedMessageId() !== 0) {2544 while (getFirstDisplayedMessageId() > mesId && getFirstDisplayedMessageId() !== 0) {
2537 showMoreMessages();2545 await showMoreMessages();
2538 await delay(1);2546 await delay(1);
2539 target = $('#chat').find(`.mes[mesid=${mesId}]`);2547 target = $('#chat').find(`.mes[mesid=${mesId}]`);
25402548
public/scripts/reasoning.js+297 -0
@@ -0,0 +1,297 @@
1import { chat, closeMessageEditor, saveChatConditional, saveSettingsDebounced, substituteParams, updateMessageBlock } from '../script.js';
2import { t } from './i18n.js';
3import { MacrosParser } from './macros.js';
4import { Popup } from './popup.js';
5import { power_user } from './power-user.js';
6import { SlashCommand } from './slash-commands/SlashCommand.js';
7import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from './slash-commands/SlashCommandArgument.js';
8import { commonEnumProviders } from './slash-commands/SlashCommandCommonEnumsProvider.js';
9import { SlashCommandParser } from './slash-commands/SlashCommandParser.js';
10import { copyText } from './utils.js';
11
12/**
13 * Gets a message from a jQuery element.
14 * @param {Element} element
15 * @returns {{messageId: number, message: object, messageBlock: JQuery<HTMLElement>}}
16 */
17function getMessageFromJquery(element) {
18 const messageBlock = $(element).closest('.mes');
19 const messageId = Number(messageBlock.attr('mesid'));
20 const message = chat[messageId];
21 return { messageId: messageId, message, messageBlock };
22}
23
24/**
25 * Helper class for adding reasoning to messages.
26 * Keeps track of the number of reasoning additions.
27 */
28export class PromptReasoning {
29 static REASONING_PLACEHOLDER = '\u200B';
30 static REASONING_PLACEHOLDER_REGEX = new RegExp(`${PromptReasoning.REASONING_PLACEHOLDER}$`);
31
32 constructor() {
33 this.counter = 0;
34 }
35
36 /**
37 * Checks if the limit of reasoning additions has been reached.
38 * @returns {boolean} True if the limit of reasoning additions has been reached, false otherwise.
39 */
40 isLimitReached() {
41 if (!power_user.reasoning.add_to_prompts) {
42 return true;
43 }
44
45 return this.counter >= power_user.reasoning.max_additions;
46 }
47
48 /**
49 * Add reasoning to a message according to the power user settings.
50 * @param {string} content Message content
51 * @param {string} reasoning Message reasoning
52 * @returns {string} Message content with reasoning
53 */
54 addToMessage(content, reasoning) {
55 // Disabled or reached limit of additions
56 if (!power_user.reasoning.add_to_prompts || this.counter >= power_user.reasoning.max_additions) {
57 return content;
58 }
59
60 // No reasoning provided or a placeholder
61 if (!reasoning || reasoning === PromptReasoning.REASONING_PLACEHOLDER) {
62 return content;
63 }
64
65 // Increment the counter
66 this.counter++;
67
68 // Substitute macros in variable parts
69 const prefix = substituteParams(power_user.reasoning.prefix || '');
70 const separator = substituteParams(power_user.reasoning.separator || '');
71 const suffix = substituteParams(power_user.reasoning.suffix || '');
72
73 // Combine parts with reasoning and content
74 return `${prefix}${reasoning}${suffix}${separator}${content}`;
75 }
76}
77
78function loadReasoningSettings() {
79 $('#reasoning_add_to_prompts').prop('checked', power_user.reasoning.add_to_prompts);
80 $('#reasoning_add_to_prompts').on('change', function () {
81 power_user.reasoning.add_to_prompts = !!$(this).prop('checked');
82 saveSettingsDebounced();
83 });
84
85 $('#reasoning_prefix').val(power_user.reasoning.prefix);
86 $('#reasoning_prefix').on('input', function () {
87 power_user.reasoning.prefix = String($(this).val());
88 saveSettingsDebounced();
89 });
90
91 $('#reasoning_suffix').val(power_user.reasoning.suffix);
92 $('#reasoning_suffix').on('input', function () {
93 power_user.reasoning.suffix = String($(this).val());
94 saveSettingsDebounced();
95 });
96
97 $('#reasoning_separator').val(power_user.reasoning.separator);
98 $('#reasoning_separator').on('input', function () {
99 power_user.reasoning.separator = String($(this).val());
100 saveSettingsDebounced();
101 });
102
103 $('#reasoning_max_additions').val(power_user.reasoning.max_additions);
104 $('#reasoning_max_additions').on('input', function () {
105 power_user.reasoning.max_additions = Number($(this).val());
106 saveSettingsDebounced();
107 });
108}
109
110function registerReasoningSlashCommands() {
111 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
112 name: 'reasoning-get',
113 returns: ARGUMENT_TYPE.STRING,
114 helpString: t`Get the contents of a reasoning block of a message. Returns an empty string if the message does not have a reasoning block.`,
115 unnamedArgumentList: [
116 SlashCommandArgument.fromProps({
117 description: 'Message ID. If not provided, the message ID of the last message is used.',
118 typeList: ARGUMENT_TYPE.NUMBER,
119 enumProvider: commonEnumProviders.messages(),
120 }),
121 ],
122 callback: (_args, value) => {
123 const messageId = !isNaN(Number(value)) ? Number(value) : chat.length - 1;
124 const message = chat[messageId];
125 const reasoning = String(message?.extra?.reasoning ?? '');
126 return reasoning.replace(PromptReasoning.REASONING_PLACEHOLDER_REGEX, '');
127 },
128 }));
129
130 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
131 name: 'reasoning-set',
132 returns: ARGUMENT_TYPE.STRING,
133 helpString: t`Set the reasoning block of a message. Returns the reasoning block content.`,
134 namedArgumentList: [
135 SlashCommandNamedArgument.fromProps({
136 name: 'at',
137 description: 'Message ID. If not provided, the message ID of the last message is used.',
138 typeList: ARGUMENT_TYPE.NUMBER,
139 enumProvider: commonEnumProviders.messages(),
140 }),
141 ],
142 unnamedArgumentList: [
143 SlashCommandArgument.fromProps({
144 description: 'Reasoning block content.',
145 typeList: ARGUMENT_TYPE.STRING,
146 }),
147 ],
148 callback: async (args, value) => {
149 const messageId = !isNaN(Number(args[0])) ? Number(args[0]) : chat.length - 1;
150 const message = chat[messageId];
151 if (!message?.extra) {
152 return '';
153 }
154
155 message.extra.reasoning = String(value ?? '');
156 await saveChatConditional();
157
158 closeMessageEditor('reasoning');
159 updateMessageBlock(messageId, message);
160 return message.extra.reasoning;
161 },
162 }));
163}
164
165function registerReasoningMacros() {
166 MacrosParser.registerMacro('reasoningPrefix', () => power_user.reasoning.prefix, t`Reasoning Prefix`);
167 MacrosParser.registerMacro('reasoningSuffix', () => power_user.reasoning.suffix, t`Reasoning Suffix`);
168 MacrosParser.registerMacro('reasoningSeparator', () => power_user.reasoning.separator, t`Reasoning Separator`);
169}
170
171function setReasoningEventHandlers(){
172 $(document).on('click', '.mes_reasoning_copy', (e) => {
173 e.stopPropagation();
174 e.preventDefault();
175 });
176
177 $(document).on('click', '.mes_reasoning_edit', function (e) {
178 e.stopPropagation();
179 e.preventDefault();
180 const { message, messageBlock } = getMessageFromJquery(this);
181 if (!message?.extra) {
182 return;
183 }
184
185 const reasoning = String(message?.extra?.reasoning ?? '');
186 const chatElement = document.getElementById('chat');
187 const textarea = document.createElement('textarea');
188 const reasoningBlock = messageBlock.find('.mes_reasoning');
189 textarea.classList.add('reasoning_edit_textarea');
190 textarea.value = reasoning.replace(PromptReasoning.REASONING_PLACEHOLDER_REGEX, '');
191 $(textarea).insertBefore(reasoningBlock);
192
193 if (!CSS.supports('field-sizing', 'content')) {
194 const resetHeight = function () {
195 const scrollTop = chatElement.scrollTop;
196 textarea.style.height = '0px';
197 textarea.style.height = `${textarea.scrollHeight}px`;
198 chatElement.scrollTop = scrollTop;
199 };
200
201 textarea.addEventListener('input', resetHeight);
202 resetHeight();
203 }
204
205 textarea.focus();
206 textarea.setSelectionRange(textarea.value.length, textarea.value.length);
207
208 const textareaRect = textarea.getBoundingClientRect();
209 const chatRect = chatElement.getBoundingClientRect();
210
211 // Scroll if textarea bottom is below visible area
212 if (textareaRect.bottom > chatRect.bottom) {
213 const scrollOffset = textareaRect.bottom - chatRect.bottom;
214 chatElement.scrollTop += scrollOffset;
215 }
216 });
217
218 $(document).on('click', '.mes_reasoning_edit_done', async function (e) {
219 e.stopPropagation();
220 e.preventDefault();
221 const { message, messageId, messageBlock } = getMessageFromJquery(this);
222 if (!message?.extra) {
223 return;
224 }
225
226 const textarea = messageBlock.find('.reasoning_edit_textarea');
227 const reasoning = String(textarea.val());
228 message.extra.reasoning = reasoning;
229 await saveChatConditional();
230 updateMessageBlock(messageId, message);
231 textarea.remove();
232 });
233
234 $(document).on('click', '.mes_reasoning_edit_cancel', function (e) {
235 e.stopPropagation();
236 e.preventDefault();
237
238 const { messageBlock } = getMessageFromJquery(this);
239 const textarea = messageBlock.find('.reasoning_edit_textarea');
240 textarea.remove();
241 });
242
243 $(document).on('click', '.mes_edit_add_reasoning', async function () {
244 const { message, messageId } = getMessageFromJquery(this);
245 if (!message?.extra) {
246 return;
247 }
248
249 if (message.extra.reasoning) {
250 toastr.info(t`Reasoning already exists.`, t`Edit Message`);
251 return;
252 }
253
254 message.extra.reasoning = PromptReasoning.REASONING_PLACEHOLDER;
255 await saveChatConditional();
256 closeMessageEditor();
257 updateMessageBlock(messageId, message);
258 });
259
260 $(document).on('click', '.mes_reasoning_delete', async function (e) {
261 e.stopPropagation();
262 e.preventDefault();
263
264 const confirm = await Popup.show.confirm(t`Are you sure you want to clear the reasoning?`, t`Visible message contents will stay intact.`);
265
266 if (!confirm) {
267 return;
268 }
269
270 const { message, messageId } = getMessageFromJquery(this);
271 if (!message?.extra) {
272 return;
273 }
274 message.extra.reasoning = '';
275 await saveChatConditional();
276 updateMessageBlock(messageId, message);
277 });
278
279 $(document).on('pointerup', '.mes_reasoning_copy', async function () {
280 const { message } = getMessageFromJquery(this);
281 const reasoning = String(message?.extra?.reasoning ?? '').replace(PromptReasoning.REASONING_PLACEHOLDER_REGEX, '');
282
283 if (!reasoning) {
284 return;
285 }
286
287 await copyText(reasoning);
288 toastr.info(t`Copied!`, '', { timeOut: 2000 });
289 });
290}
291
292export function initReasoning() {
293 loadReasoningSettings();
294 setReasoningEventHandlers();
295 registerReasoningSlashCommands();
296 registerReasoningMacros();
297}
public/scripts/slash-commands.js+6 -2
@@ -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,
@@ -1968,8 +1969,8 @@ export function initDefaultSlashCommands() {
1968 SlashCommandParser.addCommandObject(SlashCommand.fromProps({1969 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
1969 name: 'chat-render',1970 name: 'chat-render',
1970 helpString: 'Renders a specified number of messages into the chat window. Displays all messages if no argument is provided.',1971 helpString: 'Renders a specified number of messages into the chat window. Displays all messages if no argument is provided.',
1971 callback: (args, number) => {1972 callback: async (args, number) => {
1972 showMoreMessages(number && !isNaN(Number(number)) ? Number(number) : Number.MAX_SAFE_INTEGER);1973 await showMoreMessages(number && !isNaN(Number(number)) ? Number(number) : Number.MAX_SAFE_INTEGER);
1973 if (isTrueBoolean(String(args?.scroll ?? ''))) {1974 if (isTrueBoolean(String(args?.scroll ?? ''))) {
1974 $('#chat').scrollTop(0);1975 $('#chat').scrollTop(0);
1975 }1976 }
@@ -2814,8 +2815,11 @@ async function addSwipeCallback(args, value) {
2814 const newSwipeId = lastMessage.swipes.length - 1;2815 const newSwipeId = lastMessage.swipes.length - 1;
28152816
2816 if (isTrueBoolean(args.switch)) {2817 if (isTrueBoolean(args.switch)) {
2818 // Make sure ad-hoc changes to extras are saved before swiping away
2819 syncCurrentSwipeInfoExtras();
2817 lastMessage.swipe_id = newSwipeId;2820 lastMessage.swipe_id = newSwipeId;
2818 lastMessage.mes = lastMessage.swipes[newSwipeId];2821 lastMessage.mes = lastMessage.swipes[newSwipeId];
2822 lastMessage.extra = structuredClone(lastMessage.swipe_info?.[newSwipeId]?.extra ?? lastMessage.extra ?? {});
2819 }2823 }
28202824
2821 await saveChatConditional();2825 await saveChatConditional();
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+10 -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,7 +68,7 @@ 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 { timestampToMoment, uuidv4 } from './utils.js';
6972
70export function getContext() {73export function getContext() {
71 return {74 return {
@@ -167,6 +170,11 @@ export function getContext() {
167 chatCompletionSettings: oai_settings,170 chatCompletionSettings: oai_settings,
168 textCompletionSettings: textgenerationwebui_settings,171 textCompletionSettings: textgenerationwebui_settings,
169 powerUserSettings: power_user,172 powerUserSettings: power_user,
173 getCharacters,
174 uuidv4,
175 humanizedDateTime,
176 updateMessageBlock,
177 appendMediaToMessage,
170 };178 };
171}179}
172180
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/textgen-models.js+11 -11
@@ -319,8 +319,6 @@ export async function loadFeatherlessModels(data) {
319 return;319 return;
320 }320 }
321321
322 // Sort the data by model id (default A-Z)
323 data.sort((a, b) => a.id.localeCompare(b.id));
324 originalModels = data; // Store the original data for search322 originalModels = data; // Store the original data for search
325 featherlessModels = data;323 featherlessModels = data;
326324
@@ -334,10 +332,8 @@ export async function loadFeatherlessModels(data) {
334 // Retrieve the stored number of items per page or default to 10332 // Retrieve the stored number of items per page or default to 10
335 const perPage = Number(localStorage.getItem(storageKey)) || 10;333 const perPage = Number(localStorage.getItem(storageKey)) || 10;
336334
337 // Initialize pagination with the full set of models335 // Initialize pagination
338 const currentModelIndex = data.findIndex(x => x.id === textgen_settings.featherless_model);336 applyFiltersAndSort();
339 featherlessCurrentPage = currentModelIndex >= 0 ? (currentModelIndex / perPage) + 1 : 1;
340 setupPagination(originalModels, perPage);
341337
342 // Function to set up pagination (also used for filtered results)338 // Function to set up pagination (also used for filtered results)
343 function setupPagination(models, perPage, pageNumber = featherlessCurrentPage) {339 function setupPagination(models, perPage, pageNumber = featherlessCurrentPage) {
@@ -383,7 +379,7 @@ export async function loadFeatherlessModels(data) {
383379
384 const dateAddedDiv = document.createElement('div');380 const dateAddedDiv = document.createElement('div');
385 dateAddedDiv.classList.add('model-date-added');381 dateAddedDiv.classList.add('model-date-added');
386 dateAddedDiv.textContent = `Added On: ${new Date(model.updated_at).toLocaleDateString()}`;382 dateAddedDiv.textContent = `Added On: ${new Date(model.created * 1000).toLocaleDateString()}`;
387383
388 detailsContainer.appendChild(modelClassDiv);384 detailsContainer.appendChild(modelClassDiv);
389 detailsContainer.appendChild(contextLengthDiv);385 detailsContainer.appendChild(contextLengthDiv);
@@ -472,6 +468,7 @@ export async function loadFeatherlessModels(data) {
472 featherlessTop = await fetchFeatherlessStats();468 featherlessTop = await fetchFeatherlessStats();
473 }469 }
474 const featherlessIds = featherlessTop.map(stat => stat.id);470 const featherlessIds = featherlessTop.map(stat => stat.id);
471
475 if (selectedCategory === 'New') {472 if (selectedCategory === 'New') {
476 featherlessNew = await fetchFeatherlessNew();473 featherlessNew = await fetchFeatherlessNew();
477 }474 }
@@ -493,7 +490,7 @@ export async function loadFeatherlessModels(data) {
493 return matchesSearch && matchesClass && matchesNew;490 return matchesSearch && matchesClass && matchesNew;
494 }491 }
495 else {492 else {
496 return matchesSearch;493 return matchesSearch && matchesClass;
497 }494 }
498 });495 });
499496
@@ -502,11 +499,14 @@ export async function loadFeatherlessModels(data) {
502 } else if (selectedSortOrder === 'desc') {499 } else if (selectedSortOrder === 'desc') {
503 filteredModels.sort((a, b) => b.id.localeCompare(a.id));500 filteredModels.sort((a, b) => b.id.localeCompare(a.id));
504 } else if (selectedSortOrder === 'date_asc') {501 } else if (selectedSortOrder === 'date_asc') {
505 filteredModels.sort((a, b) => a.updated_at.localeCompare(b.updated_at));502 filteredModels.sort((a, b) => a.created - b.created);
506 } else if (selectedSortOrder === 'date_desc') {503 } else if (selectedSortOrder === 'date_desc') {
507 filteredModels.sort((a, b) => b.updated_at.localeCompare(a.updated_at));504 filteredModels.sort((a, b) => b.created - a.created);
508 }505 }
509506
507 const currentModelIndex = filteredModels.findIndex(x => x.id === textgen_settings.featherless_model);
508 featherlessCurrentPage = currentModelIndex >= 0 ? (currentModelIndex / perPage) + 1 : 1;
509
510 setupPagination(filteredModels, Number(localStorage.getItem(storageKey)) || perPage, featherlessCurrentPage);510 setupPagination(filteredModels, Number(localStorage.getItem(storageKey)) || perPage, featherlessCurrentPage);
511 }511 }
512512
@@ -528,7 +528,7 @@ async function fetchFeatherlessStats() {
528}528}
529529
530async function fetchFeatherlessNew() {530async function fetchFeatherlessNew() {
531 const response = await fetch('https://api.featherless.ai/feather/models?sort=-created_at&perPage=10');531 const response = await fetch('https://api.featherless.ai/feather/models?sort=-created_at&perPage=20');
532 const data = await response.json();532 const data = await response.json();
533 return data.items;533 return data.items;
534}534}
public/scripts/textgen-settings.js+4 -3
@@ -986,6 +986,7 @@ export async function generateTextGenWithStreaming(generate_data, signal) {
986 let logprobs = null;986 let logprobs = null;
987 const swipes = [];987 const swipes = [];
988 const toolCalls = [];988 const toolCalls = [];
989 const state = {};
989 while (true) {990 while (true) {
990 const { done, value } = await reader.read();991 const { done, value } = await reader.read();
991 if (done) return;992 if (done) return;
@@ -1004,7 +1005,7 @@ export async function generateTextGenWithStreaming(generate_data, signal) {
1004 logprobs = parseTextgenLogprobs(newText, data.choices?.[0]?.logprobs || data?.completion_probabilities);1005 logprobs = parseTextgenLogprobs(newText, data.choices?.[0]?.logprobs || data?.completion_probabilities);
1005 }1006 }
10061007
1007 yield { text, swipes, logprobs, toolCalls };1008 yield { text, swipes, logprobs, toolCalls, state };
1008 }1009 }
1009 };1010 };
1010}1011}
@@ -1231,7 +1232,7 @@ export function getTextGenGenerationData(finalPrompt, maxTokens, isImpersonate,
1231 'top_p': settings.top_p,1232 'top_p': settings.top_p,
1232 'typical_p': settings.typical_p,1233 'typical_p': settings.typical_p,
1233 'typical': settings.typical_p,1234 'typical': settings.typical_p,
1234 'sampler_seed': settings.seed,1235 'sampler_seed': settings.seed >= 0 ? settings.seed : undefined,
1235 'min_p': settings.min_p,1236 'min_p': settings.min_p,
1236 'repetition_penalty': settings.rep_pen,1237 'repetition_penalty': settings.rep_pen,
1237 'frequency_penalty': settings.freq_pen,1238 'frequency_penalty': settings.freq_pen,
@@ -1294,7 +1295,7 @@ export function getTextGenGenerationData(finalPrompt, maxTokens, isImpersonate,
1294 'temperature_last': (settings.type === OOBA || settings.type === APHRODITE || settings.type == TABBY) ? settings.temperature_last : undefined,1295 '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,1296 'speculative_ngram': settings.type === TABBY ? settings.speculative_ngram : undefined,
1296 'do_sample': settings.type === OOBA ? settings.do_sample : undefined,1297 'do_sample': settings.type === OOBA ? settings.do_sample : undefined,
1297 'seed': settings.seed,1298 'seed': settings.seed >= 0 ? settings.seed : undefined,
1298 'guidance_scale': cfgValues?.guidanceScale?.value ?? settings.guidance_scale ?? 1,1299 'guidance_scale': cfgValues?.guidanceScale?.value ?? settings.guidance_scale ?? 1,
1299 'negative_prompt': cfgValues?.negativePrompt ?? substituteParams(settings.negative_prompt) ?? '',1300 'negative_prompt': cfgValues?.negativePrompt ?? substituteParams(settings.negative_prompt) ?? '',
1300 'grammar_string': settings.grammar_string,1301 'grammar_string': settings.grammar_string,
public/scripts/utils.js+20 -0
@@ -392,6 +392,26 @@ export function getStringHash(str, seed = 0) {
392}392}
393393
394/**394/**
395 * Copy text to clipboard. Use navigator.clipboard.writeText if available, otherwise use document.execCommand.
396 * @param {string} text - The text to copy to the clipboard.
397 * @returns {Promise<void>} A promise that resolves when the text has been copied to the clipboard.
398 */
399export function copyText(text) {
400 if (navigator.clipboard) {
401 return navigator.clipboard.writeText(text);
402 }
403
404 const parent = document.querySelector('dialog[open]:last-of-type') ?? document.body;
405 const textArea = document.createElement('textarea');
406 textArea.value = text;
407 parent.appendChild(textArea);
408 textArea.focus();
409 textArea.select();
410 document.execCommand('copy');
411 parent.removeChild(textArea);
412}
413
414/**
395 * Map of debounced functions to their timers.415 * Map of debounced functions to their timers.
396 * Weak map is used to avoid memory leaks.416 * Weak map is used to avoid memory leaks.
397 * @type {WeakMap<function, any>}417 * @type {WeakMap<function, any>}
public/style.css+131 -24
@@ -292,36 +292,44 @@ input[type='checkbox']:focus-visible {
292 filter: grayscale(25%);292 filter: grayscale(25%);
293}293}
294294
295.mes_text table {295.mes_text table,
296.mes_reasoning table {
296 border-spacing: 0;297 border-spacing: 0;
297 border-collapse: collapse;298 border-collapse: collapse;
298 margin-bottom: 10px;299 margin-bottom: 10px;
299}300}
300301
301.mes_text td,302.mes_text td,
302.mes_text th {303.mes_text th,
304.mes_reasoning td,
305.mes_reasoning th {
303 border: 1px solid;306 border: 1px solid;
304 border-collapse: collapse;307 border-collapse: collapse;
305 padding: 0.25em;308 padding: 0.25em;
306}309}
307310
308.mes_text p {311.mes_text p,
312.mes_reasoning p {
309 margin-top: 0;313 margin-top: 0;
310 margin-bottom: 10px;314 margin-bottom: 10px;
311}315}
312316
313.mes_text li tt {317.mes_text li tt,
318.mes_reasoning li tt {
314 display: inline-block;319 display: inline-block;
315}320}
316321
317.mes_text ol,322.mes_text ol,
318.mes_text ul {323.mes_text ul,
324.mes_reasoning ol,
325.mes_reasoning ul {
319 margin-top: 5px;326 margin-top: 5px;
320 margin-bottom: 5px;327 margin-bottom: 5px;
321}328}
322329
323.mes_text br,330.mes_text br,
324.mes_bias br {331.mes_bias br,
332.mes_reasoning br {
325 content: ' ';333 content: ' ';
326}334}
327335
@@ -332,25 +340,83 @@ input[type='checkbox']:focus-visible {
332 color: var(--SmartThemeQuoteColor);340 color: var(--SmartThemeQuoteColor);
333}341}
334342
343.mes_reasoning {
344 display: block;
345 border: 1px solid var(--SmartThemeBorderColor);
346 background-color: var(--black30a);
347 border-radius: 5px;
348 padding: 5px;
349 margin: 5px 0;
350 overflow-y: auto;
351}
352
353.mes_reasoning_summary {
354 cursor: pointer;
355 position: relative;
356 margin: 2px;
357}
358
359@supports not selector(:has(*)) {
360 .mes_reasoning_details {
361 display: none !important;
362 }
363}
364
365.mes_bias:empty,
366.mes_reasoning:empty,
367.mes_reasoning_details:has(.mes_reasoning:empty),
368.mes_block:has(.edit_textarea) .mes_reasoning_details,
369.mes_reasoning_details:not([open]) .mes_reasoning_actions,
370.mes_reasoning_details:has(.reasoning_edit_textarea) .mes_reasoning,
371.mes_reasoning_details:not(:has(.reasoning_edit_textarea)) .mes_reasoning_actions .mes_button.mes_reasoning_edit_done,
372.mes_reasoning_details:not(:has(.reasoning_edit_textarea)) .mes_reasoning_actions .mes_button.mes_reasoning_edit_cancel,
373.mes_reasoning_details:has(.reasoning_edit_textarea) .mes_reasoning_actions .mes_button:not(.mes_reasoning_edit_done, .mes_reasoning_edit_cancel) {
374 display: none;
375}
376
377.mes_reasoning_actions {
378 position: absolute;
379 right: 0;
380 top: 0;
381
382 display: flex;
383 gap: 4px;
384 flex-wrap: nowrap;
385 justify-content: flex-end;
386 transition: all 200ms;
387 overflow-x: hidden;
388 padding: 1px;
389}
390
391.mes_reasoning_summary>span {
392 margin-left: 0.5em;
393}
394
335.mes_text i,395.mes_text i,
336.mes_text em {396.mes_text em,
397.mes_reasoning i,
398.mes_reasoning em {
337 color: var(--SmartThemeEmColor);399 color: var(--SmartThemeEmColor);
338}400}
339401
340.mes_text u {402.mes_text u,
403.mes_reasoning u {
341 color: var(--SmartThemeUnderlineColor);404 color: var(--SmartThemeUnderlineColor);
342}405}
343406
344.mes_text q {407.mes_text q,
408.mes_reasoning q {
345 color: var(--SmartThemeQuoteColor);409 color: var(--SmartThemeQuoteColor);
346}410}
347411
348.mes_text font[color] em,412.mes_text font[color] em,
349.mes_text font[color] i {413.mes_text font[color] i,
350 color: inherit;414.mes_text font[color] u,
351}415.mes_text font[color] q,
352416.mes_reasoning font[color] em,
353.mes_text font[color] q {417.mes_reasoning font[color] i,
418.mes_reasoning font[color] u,
419.mes_reasoning font[color] q {
354 color: inherit;420 color: inherit;
355}421}
356422
@@ -358,7 +424,8 @@ input[type='checkbox']:focus-visible {
358 display: block;424 display: block;
359}425}
360426
361.mes_text blockquote {427.mes_text blockquote,
428.mes_reasoning blockquote {
362 border-left: 3px solid var(--SmartThemeQuoteColor);429 border-left: 3px solid var(--SmartThemeQuoteColor);
363 padding-left: 10px;430 padding-left: 10px;
364 background-color: var(--black30a);431 background-color: var(--black30a);
@@ -368,18 +435,24 @@ input[type='checkbox']:focus-visible {
368.mes_text strong em,435.mes_text strong em,
369.mes_text strong,436.mes_text strong,
370.mes_text h2,437.mes_text h2,
371.mes_text h1 {438.mes_text h1,
439.mes_reasoning strong em,
440.mes_reasoning strong,
441.mes_reasoning h2,
442.mes_reasoning h1 {
372 font-weight: bold;443 font-weight: bold;
373}444}
374445
375.mes_text pre code {446.mes_text pre code,
447.mes_reasoning pre code {
376 position: relative;448 position: relative;
377 display: block;449 display: block;
378 overflow-x: auto;450 overflow-x: auto;
379 padding: 1em;451 padding: 1em;
380}452}
381453
382.mes_text img:not(.mes_img) {454.mes_text img:not(.mes_img),
455.mes_reasoning img:not(.mes_img) {
383 max-width: 100%;456 max-width: 100%;
384 max-height: var(--doc-height);457 max-height: var(--doc-height);
385}458}
@@ -1022,6 +1095,11 @@ body .panelControlBar {
1022 /*only affects bubblechat to make it sit nicely at the bottom*/1095 /*only affects bubblechat to make it sit nicely at the bottom*/
1023}1096}
10241097
1098.last_mes:has(.mes_text:empty):has(.mes_reasoning_details[open]) .mes_reasoning:not(:empty) {
1099 margin-bottom: 30px;
1100}
1101
1102.last_mes .mes_reasoning,
1025.last_mes .mes_text {1103.last_mes .mes_text {
1026 padding-right: 30px;1104 padding-right: 30px;
1027}1105}
@@ -1235,14 +1313,18 @@ body.swipeAllMessages .mes:not(.last_mes) .swipes-counter {
1235 overflow-y: clip;1313 overflow-y: clip;
1236}1314}
12371315
1238.mes_text {1316.mes_text,
1317.mes_reasoning {
1239 font-weight: 500;1318 font-weight: 500;
1240 line-height: calc(var(--mainFontSize) + .5rem);1319 line-height: calc(var(--mainFontSize) + .5rem);
1320 max-width: 100%;
1321 overflow-wrap: anywhere;
1322}
1323
1324.mes_text {
1241 padding-left: 0;1325 padding-left: 0;
1242 padding-top: 5px;1326 padding-top: 5px;
1243 padding-bottom: 5px;1327 padding-bottom: 5px;
1244 max-width: 100%;
1245 overflow-wrap: anywhere;
1246}1328}
12471329
1248br {1330br {
@@ -2921,7 +3003,7 @@ input[type=search]:focus::-webkit-search-cancel-button {
2921 position: relative;3003 position: relative;
2922}3004}
29233005
2924#rm_print_characters_block .ch_name,3006.character_name_block .ch_name,
2925.avatar-container .ch_name {3007.avatar-container .ch_name {
2926 flex: 1 1 auto;3008 flex: 1 1 auto;
2927 white-space: nowrap;3009 white-space: nowrap;
@@ -2931,6 +3013,13 @@ input[type=search]:focus::-webkit-search-cancel-button {
2931 display: block;3013 display: block;
2932}3014}
29333015
3016.character_name_block .character_version {
3017 text-overflow: ellipsis;
3018 overflow: hidden;
3019 text-wrap: nowrap;
3020 max-width: 50%;
3021}
3022
2934#rm_print_characters_block .character_name_block> :last-child {3023#rm_print_characters_block .character_name_block> :last-child {
2935 flex: 0 100000 auto;3024 flex: 0 100000 auto;
2936 /* Force shrinking first */3025 /* Force shrinking first */
@@ -4143,10 +4232,12 @@ input[type="range"]::-webkit-slider-thumb {
4143 align-items: center;4232 align-items: center;
4144}4233}
41454234
4235.mes_reasoning_edit_cancel,
4146.mes_edit_cancel.menu_button {4236.mes_edit_cancel.menu_button {
4147 background-color: var(--crimson70a);4237 background-color: var(--crimson70a);
4148}4238}
41494239
4240.mes_reasoning_edit_done,
4150.mes_edit_done.menu_button {4241.mes_edit_done.menu_button {
4151 background-color: var(--okGreen70a);4242 background-color: var(--okGreen70a);
4152}4243}
@@ -4155,6 +4246,7 @@ input[type="range"]::-webkit-slider-thumb {
4155 opacity: 1;4246 opacity: 1;
4156}4247}
41574248
4249.reasoning_edit_textarea,
4158.edit_textarea {4250.edit_textarea {
4159 padding: 5px;4251 padding: 5px;
4160 margin: 0;4252 margin: 0;
@@ -5643,6 +5735,7 @@ body:not(.movingUI) .drawer-content.maximized {
56435735
5644.model-card .details-container {5736.model-card .details-container {
5645 text-align: right;5737 text-align: right;
5738 line-height: 0.9;
5646}5739}
56475740
5648.model-card:hover {5741.model-card:hover {
@@ -5665,7 +5758,7 @@ body:not(.movingUI) .drawer-content.maximized {
5665}5758}
56665759
5667.model-title {5760.model-title {
5668 font-size: 13px;5761 font-size: calc(var(--mainFontSize) * 0.95);
5669 font-weight: bold;5762 font-weight: bold;
5670 overflow: hidden;5763 overflow: hidden;
5671}5764}
@@ -5681,7 +5774,7 @@ body:not(.movingUI) .drawer-content.maximized {
5681.model-class,5774.model-class,
5682.model-context-length,5775.model-context-length,
5683.model-date-added {5776.model-date-added {
5684 font-size: 10px;5777 font-size: calc(var(--mainFontSize) * 0.75);
5685}5778}
56865779
5687.model-class,5780.model-class,
@@ -5763,3 +5856,17 @@ body:not(.movingUI) .drawer-content.maximized {
5763.alternate_greetings_list {5856.alternate_greetings_list {
5764 overflow-y: scroll;5857 overflow-y: scroll;
5765}5858}
5859
5860.mes_text div[data-type="assistant_note"]:has(.assistant_note_export) {
5861 display: flex;
5862 flex-direction: row;
5863 flex-wrap: nowrap;
5864 justify-content: space-between;
5865 align-items: center;
5866 gap: 10px;
5867 padding: 0 2px;
5868}
5869
5870.mes_text div[data-type="assistant_note"]:has(.assistant_note_export)>div:not(.assistant_note_export) {
5871 flex: 1;
5872}
server.js+39 -18
@@ -18,10 +18,9 @@ import { hideBin } from 'yargs/helpers';
1818
19// express/server related library imports19// express/server related library imports
20import cors from 'cors';20import cors from 'cors';
21import { doubleCsrf } from 'csrf-csrf';21import { csrfSync } from 'csrf-sync';
22import express from 'express';22import express from 'express';
23import compression from 'compression';23import compression from 'compression';
24import cookieParser from 'cookie-parser';
25import cookieSession from 'cookie-session';24import cookieSession from 'cookie-session';
26import multer from 'multer';25import multer from 'multer';
27import responseTime from 'response-time';26import responseTime from 'response-time';
@@ -40,7 +39,6 @@ util.inspect.defaultOptions.depth = 4;
40import { loadPlugins } from './src/plugin-loader.js';39import { loadPlugins } from './src/plugin-loader.js';
41import {40import {
42 initUserStorage,41 initUserStorage,
43 getCsrfSecret,
44 getCookieSecret,42 getCookieSecret,
45 getCookieSessionName,43 getCookieSessionName,
46 getAllEnabledUsers,44 getAllEnabledUsers,
@@ -67,6 +65,7 @@ import {
67 forwardFetchResponse,65 forwardFetchResponse,
68 removeColorFormatting,66 removeColorFormatting,
69 getSeparator,67 getSeparator,
68 safeReadFileSync,
70 setupLogLevel,69 setupLogLevel,
71} from './src/util.js';70} from './src/util.js';
72import { UPLOADS_DIRECTORY } from './src/constants.js';71import { UPLOADS_DIRECTORY } from './src/constants.js';
@@ -348,8 +347,8 @@ if (enableCorsProxy) {
348}347}
349348
350function getSessionCookieAge() {349function getSessionCookieAge() {
351 // Defaults to 24 hours in seconds if not set350 // Defaults to "no expiration" if not set
352 const configValue = getConfigValue('sessionTimeout', 24 * 60 * 60);351 const configValue = getConfigValue('sessionTimeout', -1);
353352
354 // Convert to milliseconds353 // Convert to milliseconds
355 if (configValue > 0) {354 if (configValue > 0) {
@@ -378,27 +377,38 @@ app.use(setUserDataMiddleware);
378377
379// CSRF Protection //378// CSRF Protection //
380if (!disableCsrf) {379if (!disableCsrf) {
381 const COOKIES_SECRET = getCookieSecret();380 const csrfSyncProtection = csrfSync({
382381 getTokenFromState: (req) => {
383 const { generateToken, doubleCsrfProtection } = doubleCsrf({382 if (!req.session) {
384 getSecret: getCsrfSecret,383 console.error('(CSRF error) getTokenFromState: Session object not initialized');
385 cookieName: 'X-CSRF-Token',384 return;
386 cookieOptions: {385 }
387 sameSite: 'strict',386 return req.session.csrfToken;
388 secure: false,387 },
388 getTokenFromRequest: (req) => {
389 return req.headers['x-csrf-token']?.toString();
390 },
391 storeTokenInState: (req, token) => {
392 if (!req.session) {
393 console.error('(CSRF error) storeTokenInState: Session object not initialized');
394 return;
395 }
396 req.session.csrfToken = token;
389 },397 },
390 size: 64,398 size: 32,
391 getTokenFromRequest: (req) => req.headers['x-csrf-token'],
392 });399 });
393400
394 app.get('/csrf-token', (req, res) => {401 app.get('/csrf-token', (req, res) => {
395 res.json({402 res.json({
396 'token': generateToken(res, req),403 'token': csrfSyncProtection.generateToken(req),
397 });404 });
398 });405 });
399406
400 app.use(cookieParser(COOKIES_SECRET));407 // Customize the error message
401 app.use(doubleCsrfProtection);408 csrfSyncProtection.invalidCsrfTokenError.message = color.red('Invalid CSRF token. Please refresh the page and try again.');
409 csrfSyncProtection.invalidCsrfTokenError.stack = undefined;
410
411 app.use(csrfSyncProtection.csrfSynchronisedProtection);
402} else {412} else {
403 console.warn('\nCSRF protection is disabled. This will make your server vulnerable to CSRF attacks.\n');413 console.warn('\nCSRF protection is disabled. This will make your server vulnerable to CSRF attacks.\n');
404 app.get('/csrf-token', (req, res) => {414 app.get('/csrf-token', (req, res) => {
@@ -925,6 +935,16 @@ async function verifySecuritySettings() {
925 }935 }
926}936}
927937
938/**
939 * Registers a not-found error response if a not-found error page exists. Should only be called after all other middlewares have been registered.
940 */
941function apply404Middleware() {
942 const notFoundWebpage = safeReadFileSync('./public/error/url-not-found.html') ?? '';
943 app.use((req, res) => {
944 res.status(404).send(notFoundWebpage);
945 });
946}
947
928// User storage module needs to be initialized before starting the server948// User storage module needs to be initialized before starting the server
929initUserStorage(dataRoot)949initUserStorage(dataRoot)
930 .then(ensurePublicDirectoriesExist)950 .then(ensurePublicDirectoriesExist)
@@ -932,4 +952,5 @@ initUserStorage(dataRoot)
932 .then(migrateSystemPrompts)952 .then(migrateSystemPrompts)
933 .then(verifySecuritySettings)953 .then(verifySecuritySettings)
934 .then(preSetupTasks)954 .then(preSetupTasks)
955 .then(apply404Middleware)
935 .finally(startServer);956 .finally(startServer);
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/backends/chat-completions.js+113 -23
@@ -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 }
@@ -284,6 +289,7 @@ async function sendMakerSuiteRequest(request, response) {
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 showThoughts = Boolean(request.body.show_thoughts);
292 const isThinking = model.includes('thinking');
287293
288 const generationConfig = {294 const generationConfig = {
289 stopSequences: request.body.stop,295 stopSequences: request.body.stop,
@@ -324,6 +330,12 @@ async function sendMakerSuiteRequest(request, response) {
324 body.systemInstruction = prompt.system_instruction;330 body.systemInstruction = prompt.system_instruction;
325 }331 }
326332
333 if (isThinking && showThoughts) {
334 generationConfig.thinkingConfig = {
335 includeThoughts: true,
336 };
337 }
338
327 return body;339 return body;
328 }340 }
329341
@@ -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
@@ -382,11 +393,7 @@ async function sendMakerSuiteRequest(request, response) {
382 const responseContent = candidates[0].content ?? candidates[0].output;393 const responseContent = candidates[0].content ?? candidates[0].output;
383 console.error('Google AI Studio response:', responseContent);394 console.error('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.error(message, generateResponseJson);399 console.error(message, generateResponseJson);
@@ -394,7 +401,7 @@ async function sendMakerSuiteRequest(request, response) {
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) {
@@ -639,6 +646,89 @@ async function sendCohereRequest(request, response) {
639 }646 }
640}647}
641648
649/**
650 * Sends a request to DeepSeek API.
651 * @param {express.Request} request Express request
652 * @param {express.Response} response Express response
653 */
654async function sendDeepSeekRequest(request, response) {
655 const apiUrl = new URL(request.body.reverse_proxy || API_DEEPSEEK).toString();
656 const apiKey = request.body.reverse_proxy ? request.body.proxy_password : readSecret(request.user.directories, SECRET_KEYS.DEEPSEEK);
657
658 if (!apiKey && !request.body.reverse_proxy) {
659 console.log('DeepSeek API key is missing.');
660 return response.status(400).send({ error: true });
661 }
662
663 const controller = new AbortController();
664 request.socket.removeAllListeners('close');
665 request.socket.on('close', function () {
666 controller.abort();
667 });
668
669 try {
670 let bodyParams = {};
671
672 if (request.body.logprobs > 0) {
673 bodyParams['top_logprobs'] = request.body.logprobs;
674 bodyParams['logprobs'] = true;
675 }
676
677 const postProcessType = String(request.body.model).endsWith('-reasoner') ? 'deepseek-reasoner' : 'deepseek';
678 const processedMessages = postProcessPrompt(request.body.messages, postProcessType, getPromptNames(request));
679
680 const requestBody = {
681 'messages': processedMessages,
682 'model': request.body.model,
683 'temperature': request.body.temperature,
684 'max_tokens': request.body.max_tokens,
685 'stream': request.body.stream,
686 'presence_penalty': request.body.presence_penalty,
687 'frequency_penalty': request.body.frequency_penalty,
688 'top_p': request.body.top_p,
689 'stop': request.body.stop,
690 'seed': request.body.seed,
691 ...bodyParams,
692 };
693
694 const config = {
695 method: 'POST',
696 headers: {
697 'Content-Type': 'application/json',
698 'Authorization': 'Bearer ' + apiKey,
699 },
700 body: JSON.stringify(requestBody),
701 signal: controller.signal,
702 };
703
704 console.log('DeepSeek request:', requestBody);
705
706 const generateResponse = await fetch(apiUrl + '/chat/completions', config);
707
708 if (request.body.stream) {
709 forwardFetchResponse(generateResponse, response);
710 } else {
711 if (!generateResponse.ok) {
712 const errorText = await generateResponse.text();
713 console.log(`DeepSeek API returned error: ${generateResponse.status} ${generateResponse.statusText} ${errorText}`);
714 const errorJson = tryParse(errorText) ?? { error: true };
715 return response.status(500).send(errorJson);
716 }
717 const generateResponseJson = await generateResponse.json();
718 console.log('DeepSeek response:', generateResponseJson);
719 return response.send(generateResponseJson);
720 }
721 } catch (error) {
722 console.log('Error communicating with DeepSeek API: ', error);
723 if (!response.headersSent) {
724 response.send({ error: true });
725 } else {
726 response.end();
727 }
728 }
729}
730
731
642export const router = express.Router();732export const router = express.Router();
643733
644router.post('/status', jsonParser, async function (request, response_getstatus_openai) {734router.post('/status', jsonParser, async function (request, response_getstatus_openai) {
@@ -683,8 +773,8 @@ router.post('/status', jsonParser, async function (request, response_getstatus_o
683 api_key_openai = readSecret(request.user.directories, SECRET_KEYS.NANOGPT);773 api_key_openai = readSecret(request.user.directories, SECRET_KEYS.NANOGPT);
684 headers = {};774 headers = {};
685 } else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.DEEPSEEK) {775 } else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.DEEPSEEK) {
686 api_url = API_DEEPSEEK.replace('/beta', '');776 api_url = new URL(request.body.reverse_proxy || API_DEEPSEEK.replace('/beta', ''));
687 api_key_openai = readSecret(request.user.directories, SECRET_KEYS.DEEPSEEK);777 api_key_openai = request.body.reverse_proxy ? request.body.proxy_password : readSecret(request.user.directories, SECRET_KEYS.DEEPSEEK);
688 headers = {};778 headers = {};
689 } else {779 } else {
690 console.error('This chat completion source is not supported yet.');780 console.error('This chat completion source is not supported yet.');
@@ -779,6 +869,14 @@ router.post('/bias', jsonParser, async function (request, response) {
779 return response.send({});869 return response.send({});
780 }870 }
781 encodeFunction = (text) => new Uint32Array(instance.encodeIds(text));871 encodeFunction = (text) => new Uint32Array(instance.encodeIds(text));
872 } else if (webTokenizers.includes(model)) {
873 const tokenizer = getWebTokenizer(model);
874 const instance = await tokenizer?.get();
875 if (!instance) {
876 console.warn('Tokenizer not initialized:', model);
877 return response.send({});
878 }
879 encodeFunction = (text) => new Uint32Array(instance.encode(text));
782 } else {880 } else {
783 const tokenizer = getTiktokenTokenizer(model);881 const tokenizer = getTiktokenTokenizer(model);
784 encodeFunction = (tokenizer.encode.bind(tokenizer));882 encodeFunction = (tokenizer.encode.bind(tokenizer));
@@ -843,6 +941,7 @@ router.post('/generate', jsonParser, function (request, response) {
843 case CHAT_COMPLETION_SOURCES.MAKERSUITE: return sendMakerSuiteRequest(request, response);941 case CHAT_COMPLETION_SOURCES.MAKERSUITE: return sendMakerSuiteRequest(request, response);
844 case CHAT_COMPLETION_SOURCES.MISTRALAI: return sendMistralAIRequest(request, response);942 case CHAT_COMPLETION_SOURCES.MISTRALAI: return sendMistralAIRequest(request, response);
845 case CHAT_COMPLETION_SOURCES.COHERE: return sendCohereRequest(request, response);943 case CHAT_COMPLETION_SOURCES.COHERE: return sendCohereRequest(request, response);
944 case CHAT_COMPLETION_SOURCES.DEEPSEEK: return sendDeepSeekRequest(request, response);
846 }945 }
847946
848 let apiUrl;947 let apiUrl;
@@ -901,6 +1000,10 @@ router.post('/generate', jsonParser, function (request, response) {
901 bodyParams['route'] = 'fallback';1000 bodyParams['route'] = 'fallback';
902 }1001 }
9031002
1003 if (request.body.show_thoughts) {
1004 bodyParams['include_reasoning'] = true;
1005 }
1006
904 let cachingAtDepth = getConfigValue('claude.cachingAtDepth', -1);1007 let cachingAtDepth = getConfigValue('claude.cachingAtDepth', -1);
905 if (Number.isInteger(cachingAtDepth) && cachingAtDepth >= 0 && request.body.model?.startsWith('anthropic/claude-3')) {1008 if (Number.isInteger(cachingAtDepth) && cachingAtDepth >= 0 && request.body.model?.startsWith('anthropic/claude-3')) {
906 cachingAtDepthForOpenRouterClaude(request.body.messages, cachingAtDepth);1009 cachingAtDepthForOpenRouterClaude(request.body.messages, cachingAtDepth);
@@ -956,18 +1059,6 @@ router.post('/generate', jsonParser, function (request, response) {
956 apiKey = readSecret(request.user.directories, SECRET_KEYS.BLOCKENTROPY);1059 apiKey = readSecret(request.user.directories, SECRET_KEYS.BLOCKENTROPY);
957 headers = {};1060 headers = {};
958 bodyParams = {};1061 bodyParams = {};
959 } else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.DEEPSEEK) {
960 apiUrl = API_DEEPSEEK;
961 apiKey = readSecret(request.user.directories, SECRET_KEYS.DEEPSEEK);
962 headers = {};
963 bodyParams = {};
964
965 if (request.body.logprobs > 0) {
966 bodyParams['top_logprobs'] = request.body.logprobs;
967 bodyParams['logprobs'] = true;
968 }
969
970 request.body.messages = postProcessPrompt(request.body.messages, 'deepseek', getPromptNames(request));
971 } else {1062 } else {
972 console.error('This chat completion source is not supported yet.');1063 console.error('This chat completion source is not supported yet.');
973 return response.status(400).send({ error: true });1064 return response.status(400).send({ error: true });
@@ -1105,4 +1196,3 @@ router.post('/generate', jsonParser, function (request, response) {
1105 }1196 }
1106 }1197 }
1107});1198});
1108
src/endpoints/backgrounds.js+2 -1
@@ -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)) {
src/endpoints/characters.js+19 -12
@@ -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();
@@ -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,7 +810,7 @@ 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.error('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');
@@ -852,7 +859,7 @@ router.post('/edit', urlencodedParser, async function (request, response) {
852 * @param {Object} response - The HTTP response object.859 * @param {Object} response - The HTTP response object.
853 * @returns {void}860 * @returns {void}
854 */861 */
855router.post('/edit-attribute', jsonParser, async function (request, response) {862router.post('/edit-attribute', jsonParser, validateAvatarUrlMiddleware, async function (request, response) {
856 console.debug(request.body);863 console.debug(request.body);
857 if (!request.body) {864 if (!request.body) {
858 console.error('Error: no response body detected');865 console.error('Error: no response body detected');
@@ -898,7 +905,7 @@ router.post('/edit-attribute', jsonParser, async function (request, response) {
898 *905 *
899 * @returns {void}906 * @returns {void}
900 * */907 * */
901router.post('/merge-attributes', jsonParser, async function (request, response) {908router.post('/merge-attributes', jsonParser, getFileNameValidationFunction('avatar'), async function (request, response) {
902 try {909 try {
903 const update = request.body;910 const update = request.body;
904 const avatarPath = path.join(request.user.directories.characters, update.avatar);911 const avatarPath = path.join(request.user.directories.characters, update.avatar);
@@ -929,7 +936,7 @@ router.post('/merge-attributes', jsonParser, async function (request, response)
929 }936 }
930});937});
931938
932router.post('/delete', jsonParser, async function (request, response) {939router.post('/delete', jsonParser, validateAvatarUrlMiddleware, async function (request, response) {
933 if (!request.body || !request.body.avatar_url) {940 if (!request.body || !request.body.avatar_url) {
934 return response.sendStatus(400);941 return response.sendStatus(400);
935 }942 }
@@ -992,7 +999,7 @@ router.post('/all', jsonParser, async function (request, response) {
992 }999 }
993});1000});
9941001
995router.post('/get', jsonParser, async function (request, response) {1002router.post('/get', jsonParser, validateAvatarUrlMiddleware, async function (request, response) {
996 try {1003 try {
997 if (!request.body) return response.sendStatus(400);1004 if (!request.body) return response.sendStatus(400);
998 const item = request.body.avatar_url;1005 const item = request.body.avatar_url;
@@ -1011,7 +1018,7 @@ router.post('/get', jsonParser, async function (request, response) {
1011 }1018 }
1012});1019});
10131020
1014router.post('/chats', jsonParser, async function (request, response) {1021router.post('/chats', jsonParser, validateAvatarUrlMiddleware, async function (request, response) {
1015 if (!request.body) return response.sendStatus(400);1022 if (!request.body) return response.sendStatus(400);
10161023
1017 const characterDirectory = (request.body.avatar_url).replace('.png', '');1024 const characterDirectory = (request.body.avatar_url).replace('.png', '');
@@ -1160,7 +1167,7 @@ router.post('/import', urlencodedParser, async function (request, response) {
1160 }1167 }
1161});1168});
11621169
1163router.post('/duplicate', jsonParser, async function (request, response) {1170router.post('/duplicate', jsonParser, validateAvatarUrlMiddleware, async function (request, response) {
1164 try {1171 try {
1165 if (!request.body.avatar_url) {1172 if (!request.body.avatar_url) {
1166 console.error('avatar URL not found in request body');1173 console.error('avatar URL not found in request body');
@@ -1206,7 +1213,7 @@ router.post('/duplicate', jsonParser, async function (request, response) {
1206 }1213 }
1207});1214});
12081215
1209router.post('/export', jsonParser, async function (request, response) {1216router.post('/export', jsonParser, validateAvatarUrlMiddleware, async function (request, response) {
1210 try {1217 try {
1211 if (!request.body.format || !request.body.avatar_url) {1218 if (!request.body.format || !request.body.avatar_url) {
1212 return response.sendStatus(400);1219 return response.sendStatus(400);
src/endpoints/chats.js+38 -7
@@ -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,
@@ -264,9 +265,37 @@ function flattenChubChat(userName, characterName, lines) {
264 return (lines ?? []).map(convert).join('\n');265 return (lines ?? []).map(convert).join('\n');
265}266}
266267
268/**
269 * Imports a chat from RisuAI format.
270 * @param {string} userName User name
271 * @param {string} characterName Character name
272 * @param {object} jsonData Imported chat data
273 * @returns {string} Chat data
274 */
275function importRisuChat(userName, characterName, jsonData) {
276 /** @type {object[]} */
277 const chat = [{
278 user_name: userName,
279 character_name: characterName,
280 create_date: humanizedISO8601DateTime(),
281 }];
282
283 for (const message of jsonData.data.message) {
284 const isUser = message.role === 'user';
285 chat.push({
286 name: message.name ?? (isUser ? userName : characterName),
287 is_user: isUser,
288 send_date: Number(message.time ?? Date.now()),
289 mes: message.data ?? '',
290 });
291 }
292
293 return chat.map(obj => JSON.stringify(obj)).join('\n');
294}
295
267export const router = express.Router();296export const router = express.Router();
268297
269router.post('/save', jsonParser, function (request, response) {298router.post('/save', jsonParser, validateAvatarUrlMiddleware, function (request, response) {
270 try {299 try {
271 const directoryName = String(request.body.avatar_url).replace('.png', '');300 const directoryName = String(request.body.avatar_url).replace('.png', '');
272 const chatData = request.body.chat;301 const chatData = request.body.chat;
@@ -282,7 +311,7 @@ router.post('/save', jsonParser, function (request, response) {
282 }311 }
283});312});
284313
285router.post('/get', jsonParser, function (request, response) {314router.post('/get', jsonParser, validateAvatarUrlMiddleware, function (request, response) {
286 try {315 try {
287 const dirName = String(request.body.avatar_url).replace('.png', '');316 const dirName = String(request.body.avatar_url).replace('.png', '');
288 const directoryPath = path.join(request.user.directories.chats, dirName);317 const directoryPath = path.join(request.user.directories.chats, dirName);
@@ -319,7 +348,7 @@ router.post('/get', jsonParser, function (request, response) {
319});348});
320349
321350
322router.post('/rename', jsonParser, async function (request, response) {351router.post('/rename', jsonParser, validateAvatarUrlMiddleware, async function (request, response) {
323 if (!request.body || !request.body.original_file || !request.body.renamed_file) {352 if (!request.body || !request.body.original_file || !request.body.renamed_file) {
324 return response.sendStatus(400);353 return response.sendStatus(400);
325 }354 }
@@ -344,7 +373,7 @@ router.post('/rename', jsonParser, async function (request, response) {
344 return response.send({ ok: true, sanitizedFileName });373 return response.send({ ok: true, sanitizedFileName });
345});374});
346375
347router.post('/delete', jsonParser, function (request, response) {376router.post('/delete', jsonParser, validateAvatarUrlMiddleware, function (request, response) {
348 const dirName = String(request.body.avatar_url).replace('.png', '');377 const dirName = String(request.body.avatar_url).replace('.png', '');
349 const fileName = String(request.body.chatfile);378 const fileName = String(request.body.chatfile);
350 const filePath = path.join(request.user.directories.chats, dirName, sanitize(fileName));379 const filePath = path.join(request.user.directories.chats, dirName, sanitize(fileName));
@@ -360,7 +389,7 @@ router.post('/delete', jsonParser, function (request, response) {
360 return response.send('ok');389 return response.send('ok');
361});390});
362391
363router.post('/export', jsonParser, async function (request, response) {392router.post('/export', jsonParser, validateAvatarUrlMiddleware, async function (request, response) {
364 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)) {
365 return response.sendStatus(400);394 return response.sendStatus(400);
366 }395 }
@@ -449,7 +478,7 @@ router.post('/group/import', urlencodedParser, function (request, response) {
449 }478 }
450});479});
451480
452router.post('/import', urlencodedParser, function (request, response) {481router.post('/import', urlencodedParser, validateAvatarUrlMiddleware, function (request, response) {
453 if (!request.body) return response.sendStatus(400);482 if (!request.body) return response.sendStatus(400);
454483
455 const format = request.body.file_type;484 const format = request.body.file_type;
@@ -480,6 +509,8 @@ router.post('/import', urlencodedParser, function (request, response) {
480 importFunc = importOobaChat;509 importFunc = importOobaChat;
481 } else if (Array.isArray(jsonData.messages)) { // Agnai's format510 } else if (Array.isArray(jsonData.messages)) { // Agnai's format
482 importFunc = importAgnaiChat;511 importFunc = importAgnaiChat;
512 } else if (jsonData.type === 'risuChat') { // RisuAI format
513 importFunc = importRisuChat;
483 } else { // Unknown format514 } else { // Unknown format
484 console.error('Incorrect chat format .json');515 console.error('Incorrect chat format .json');
485 return response.send({ error: true });516 return response.send({ error: true });
@@ -595,7 +626,7 @@ router.post('/group/save', jsonParser, (request, response) => {
595 return response.send({ ok: true });626 return response.send({ ok: true });
596});627});
597628
598router.post('/search', jsonParser, function (request, response) {629router.post('/search', jsonParser, validateAvatarUrlMiddleware, function (request, response) {
599 try {630 try {
600 const { query, avatar_url, group_id } = request.body;631 const { query, avatar_url, group_id } = request.body;
601 let chatFiles = [];632 let chatFiles = [];
src/endpoints/search.js+37 -0
@@ -4,6 +4,8 @@ import express from 'express';
4import { decode } from 'html-entities';4import { decode } from 'html-entities';
5import { readSecret, SECRET_KEYS } from './secrets.js';5import { readSecret, SECRET_KEYS } from './secrets.js';
6import { jsonParser } from '../express-common.js';6import { jsonParser } from '../express-common.js';
7import { trimV1 } from '../util.js';
8import { setAdditionalHeaders } from '../additional-headers.js';
79
8export const router = express.Router();10export const router = express.Router();
911
@@ -257,6 +259,41 @@ router.post('/tavily', jsonParser, async (request, response) => {
257 }259 }
258});260});
259261
262router.post('/koboldcpp', jsonParser, async (request, response) => {
263 try {
264 const { query, url } = request.body;
265
266 if (!url) {
267 console.error('No URL provided for KoboldCpp search');
268 return response.sendStatus(400);
269 }
270
271 console.debug('KoboldCpp search query', query);
272
273 const baseUrl = trimV1(url);
274 const args = {
275 method: 'POST',
276 headers: {},
277 body: JSON.stringify({ q: query }),
278 };
279
280 setAdditionalHeaders(request, args, baseUrl);
281 const result = await fetch(`${baseUrl}/api/extra/websearch`, args);
282
283 if (!result.ok) {
284 const text = await result.text();
285 console.error('KoboldCpp request failed', result.statusText, text);
286 return response.status(500).send(text);
287 }
288
289 const data = await result.json();
290 return response.json(data);
291 } catch (error) {
292 console.error(error);
293 return response.sendStatus(500);
294 }
295});
296
260router.post('/visit', jsonParser, async (request, response) => {297router.post('/visit', jsonParser, async (request, response) => {
261 try {298 try {
262 const url = request.body.url;299 const url = request.body.url;
src/endpoints/settings.js+5 -4
@@ -9,9 +9,10 @@ import { SETTINGS_FILE } from '../constants.js';
9import { getConfigValue, generateTimestamp, removeOldBackups } from '../util.js';9import { getConfigValue, generateTimestamp, removeOldBackups } from '../util.js';
10import { jsonParser } from '../express-common.js';10import { jsonParser } from '../express-common.js';
11import { getAllUserHandles, getUserDirectories } from '../users.js';11import { getAllUserHandles, getUserDirectories } from '../users.js';
12import { getFileNameValidationFunction } from '../middleware/validateFileName.js';
1213
13const ENABLE_EXTENSIONS = getConfigValue('enableExtensions', true);14const ENABLE_EXTENSIONS = !!getConfigValue('extensions.enabled', true);
14const ENABLE_EXTENSIONS_AUTO_UPDATE = getConfigValue('enableExtensionsAutoUpdate', true);15const ENABLE_EXTENSIONS_AUTO_UPDATE = !!getConfigValue('extensions.autoUpdate', true);
15const ENABLE_ACCOUNTS = getConfigValue('enableUserAccounts', false);16const ENABLE_ACCOUNTS = getConfigValue('enableUserAccounts', false);
1617
17// 10 minutes18// 10 minutes
@@ -296,7 +297,7 @@ router.post('/get-snapshots', jsonParser, async (request, response) => {
296 }297 }
297});298});
298299
299router.post('/load-snapshot', jsonParser, async (request, response) => {300router.post('/load-snapshot', jsonParser, getFileNameValidationFunction('name'), async (request, response) => {
300 try {301 try {
301 const userFilesPattern = getFilePrefix(request.user.profile.handle);302 const userFilesPattern = getFilePrefix(request.user.profile.handle);
302303
@@ -330,7 +331,7 @@ router.post('/make-snapshot', jsonParser, async (request, response) => {
330 }331 }
331});332});
332333
333router.post('/restore-snapshot', jsonParser, async (request, response) => {334router.post('/restore-snapshot', jsonParser, getFileNameValidationFunction('name'), async (request, response) => {
334 try {335 try {
335 const userFilesPattern = getFilePrefix(request.user.profile.handle);336 const userFilesPattern = getFilePrefix(request.user.profile.handle);
336337
src/endpoints/tokenizers.js+42 -0
@@ -238,6 +238,15 @@ export const sentencepieceTokenizers = [
238 'jamba',238 'jamba',
239];239];
240240
241export const webTokenizers = [
242 'claude',
243 'llama3',
244 'command-r',
245 'qwen2',
246 'nemo',
247 'deepseek',
248];
249
241/**250/**
242 * Gets the Sentencepiece tokenizer by the model name.251 * Gets the Sentencepiece tokenizer by the model name.
243 * @param {string} model Sentencepiece model name252 * @param {string} model Sentencepiece model name
@@ -276,6 +285,39 @@ export function getSentencepiceTokenizer(model) {
276}285}
277286
278/**287/**
288 * Gets the Web tokenizer by the model name.
289 * @param {string} model Web tokenizer model name
290 * @returns {WebTokenizer|null} Web tokenizer
291 */
292export function getWebTokenizer(model) {
293 if (model.includes('llama3')) {
294 return llama3_tokenizer;
295 }
296
297 if (model.includes('claude')) {
298 return claude_tokenizer;
299 }
300
301 if (model.includes('command-r')) {
302 return commandTokenizer;
303 }
304
305 if (model.includes('qwen2')) {
306 return qwen2Tokenizer;
307 }
308
309 if (model.includes('nemo')) {
310 return nemoTokenizer;
311 }
312
313 if (model.includes('deepseek')) {
314 return deepseekTokenizer;
315 }
316
317 return null;
318}
319
320/**
279 * Counts the token ids for the given text using the Sentencepiece tokenizer.321 * Counts the token ids for the given text using the Sentencepiece tokenizer.
280 * @param {SentencePieceTokenizer} tokenizer Sentencepiece tokenizer322 * @param {SentencePieceTokenizer} tokenizer Sentencepiece tokenizer
281 * @param {string} text Text to tokenize323 * @param {string} text Text to tokenize
src/endpoints/users-private.js+1 -0
@@ -23,6 +23,7 @@ router.post('/logout', async (request, response) => {
23 }23 }
2424
25 request.session.handle = null;25 request.session.handle = null;
26 request.session.csrfToken = null;
26 request.session = null;27 request.session = null;
27 return response.sendStatus(204);28 return response.sendStatus(204);
28 } catch (error) {29 } catch (error) {
src/endpoints/vectors.js+1 -1
@@ -164,7 +164,7 @@ function getSourceSettings(source, request) {
164 };164 };
165 case 'transformers':165 case 'transformers':
166 return {166 return {
167 model: getConfigValue('extras.embeddingModel', ''),167 model: getConfigValue('extensions.models.embedding', ''),
168 };168 };
169 case 'palm':169 case 'palm':
170 return {170 return {
src/middleware/basicAuth.js+4 -3
@@ -5,17 +5,18 @@
5import { Buffer } from 'node:buffer';5import { Buffer } from 'node:buffer';
6import storage from 'node-persist';6import storage from 'node-persist';
7import { getAllUserHandles, toKey, getPasswordHash } from '../users.js';7import { getAllUserHandles, toKey, getPasswordHash } from '../users.js';
8import { getConfig, getConfigValue } from '../util.js';8import { getConfig, getConfigValue, safeReadFileSync } from '../util.js';
99
10const PER_USER_BASIC_AUTH = getConfigValue('perUserBasicAuth', false);10const PER_USER_BASIC_AUTH = getConfigValue('perUserBasicAuth', false);
11const ENABLE_ACCOUNTS = getConfigValue('enableUserAccounts', false);11const ENABLE_ACCOUNTS = getConfigValue('enableUserAccounts', false);
1212
13const basicAuthMiddleware = async function (request, response, callback) {
14 const unauthorizedWebpage = safeReadFileSync('./public/error/unauthorized.html') ?? '';
13 const unauthorizedResponse = (res) => {15 const unauthorizedResponse = (res) => {
14 res.set('WWW-Authenticate', 'Basic realm="SillyTavern", charset="UTF-8"');16 res.set('WWW-Authenticate', 'Basic realm="SillyTavern", charset="UTF-8"');
15 return res.status(401).send('Authentication required');17 return res.status(401).send(unauthorizedWebpage);
16 };18 };
1719
18const basicAuthMiddleware = async function (request, response, callback) {
19 const config = getConfig();20 const config = getConfig();
20 const authHeader = request.headers.authorization;21 const authHeader = request.headers.authorization;
2122
src/middleware/validateFileName.js+34 -0
@@ -0,0 +1,34 @@
1import path from 'node:path';
2
3/**
4 * Gets a middleware function that validates the field in the request body.
5 * @param {string} fieldName Field name
6 * @returns {import('express').RequestHandler} Middleware function
7 */
8export function getFileNameValidationFunction(fieldName) {
9 /**
10 * Validates the field in the request body.
11 * @param {import('express').Request} req Request object
12 * @param {import('express').Response} res Response object
13 * @param {import('express').NextFunction} next Next middleware
14 */
15 return function validateAvatarUrlMiddleware(req, res, next) {
16 if (req.body && fieldName in req.body && typeof req.body[fieldName] === 'string') {
17 const forbiddenRegExp = path.sep === '/' ? /[/\x00]/ : /[/\x00\\]/;
18 if (forbiddenRegExp.test(req.body[fieldName])) {
19 console.error('An error occurred while validating the request body', {
20 handle: req.user.profile.handle,
21 path: req.originalUrl,
22 field: fieldName,
23 value: req.body[fieldName],
24 });
25 return res.sendStatus(400);
26 }
27 }
28
29 next();
30 };
31}
32
33const avatarUrlValidationFunction = getFileNameValidationFunction('avatar_url');
34export default avatarUrlValidationFunction;
src/middleware/whitelist.js+16 -5
@@ -1,10 +1,11 @@
1import path from 'node:path';1import path from 'node:path';
2import fs from 'node:fs';2import fs from 'node:fs';
3import process from 'node:process';3import process from 'node:process';
4import Handlebars from 'handlebars';
4import ipMatching from 'ip-matching';5import ipMatching from 'ip-matching';
56
6import { getIpFromRequest } from '../express-common.js';7import { getIpFromRequest } from '../express-common.js';
7import { color, getConfigValue } from '../util.js';8import { color, getConfigValue, safeReadFileSync } from '../util.js';
89
9const whitelistPath = path.join(process.cwd(), './whitelist.txt');10const whitelistPath = path.join(process.cwd(), './whitelist.txt');
10const enableForwardedWhitelist = getConfigValue('enableForwardedWhitelist', false);11const enableForwardedWhitelist = getConfigValue('enableForwardedWhitelist', false);
@@ -52,12 +53,16 @@ function getForwardedIp(req) {
52 * @returns {import('express').RequestHandler} The middleware function53 * @returns {import('express').RequestHandler} The middleware function
53 */54 */
54export default function whitelistMiddleware(whitelistMode, listen) {55export default function whitelistMiddleware(whitelistMode, listen) {
56 const forbiddenWebpage = Handlebars.compile(
57 safeReadFileSync('./public/error/forbidden-by-whitelist.html') ?? '',
58 );
59
55 return function (req, res, next) {60 return function (req, res, next) {
56 const clientIp = getIpFromRequest(req);61 const clientIp = getIpFromRequest(req);
57 const forwardedIp = getForwardedIp(req);62 const forwardedIp = getForwardedIp(req);
63 const userAgent = req.headers['user-agent'];
5864
59 if (listen && !knownIPs.has(clientIp)) {65 if (listen && !knownIPs.has(clientIp)) {
60 const userAgent = req.headers['user-agent'];
61 console.log(color.yellow(`New connection from ${clientIp}; User Agent: ${userAgent}\n`));66 console.log(color.yellow(`New connection from ${clientIp}; User Agent: ${userAgent}\n`));
62 knownIPs.add(clientIp);67 knownIPs.add(clientIp);
6368
@@ -76,9 +81,15 @@ export default function whitelistMiddleware(whitelistMode, listen) {
76 || forwardedIp && whitelistMode === true && !whitelist.some(x => ipMatching.matches(forwardedIp, ipMatching.getMatch(x)))81 || forwardedIp && whitelistMode === true && !whitelist.some(x => ipMatching.matches(forwardedIp, ipMatching.getMatch(x)))
77 ) {82 ) {
78 // Log the connection attempt with real IP address83 // Log the connection attempt with real IP address
79 const ipDetails = forwardedIp ? `${clientIp} (forwarded from ${forwardedIp})` : clientIp;84 const ipDetails = forwardedIp
80 console.log(color.red('Forbidden: Connection attempt from ' + ipDetails + '. If you are attempting to connect, please add your IP address in whitelist or disable whitelist mode in config.yaml in root of SillyTavern folder.\n'));85 ? `${clientIp} (forwarded from ${forwardedIp})`
81 return res.status(403).send('<b>Forbidden</b>: Connection attempt from <b>' + ipDetails + '</b>. If you are attempting to connect, please add your IP address in whitelist or disable whitelist mode in config.yaml in root of SillyTavern folder.');86 : clientIp;
87 console.log(
88 color.red(
89 `Blocked connection from ${clientIp}; User Agent: ${userAgent}\n\tTo allow this connection, add its IP address to the whitelist or disable whitelist mode by editing config.yaml in the root directory of your SillyTavern installation.\n`,
90 ),
91 );
92 return res.status(403).send(forbiddenWebpage({ ipDetails }));
82 }93 }
83 next();94 next();
84 };95 };
src/prompt-converters.js+2 -0
@@ -360,6 +360,8 @@ export function convertCohereMessages(messages, names) {
360 */360 */
361export function convertGooglePrompt(messages, model, useSysPrompt, names) {361export function convertGooglePrompt(messages, model, useSysPrompt, names) {
362 const visionSupportedModels = [362 const visionSupportedModels = [
363 'gemini-2.0-flash-thinking-exp',
364 'gemini-2.0-flash-thinking-exp-01-21',
363 'gemini-2.0-flash-thinking-exp-1219',365 'gemini-2.0-flash-thinking-exp-1219',
364 'gemini-2.0-flash-exp',366 'gemini-2.0-flash-exp',
365 'gemini-1.5-flash',367 'gemini-1.5-flash',
src/transformers.js+6 -6
@@ -19,31 +19,31 @@ const tasks = {
19 'text-classification': {19 'text-classification': {
20 defaultModel: 'Cohee/distilbert-base-uncased-go-emotions-onnx',20 defaultModel: 'Cohee/distilbert-base-uncased-go-emotions-onnx',
21 pipeline: null,21 pipeline: null,
22 configField: 'extras.classificationModel',22 configField: 'extensions.models.classification',
23 quantized: true,23 quantized: true,
24 },24 },
25 'image-to-text': {25 'image-to-text': {
26 defaultModel: 'Xenova/vit-gpt2-image-captioning',26 defaultModel: 'Xenova/vit-gpt2-image-captioning',
27 pipeline: null,27 pipeline: null,
28 configField: 'extras.captioningModel',28 configField: 'extensions.models.captioning',
29 quantized: true,29 quantized: true,
30 },30 },
31 'feature-extraction': {31 'feature-extraction': {
32 defaultModel: 'Xenova/all-mpnet-base-v2',32 defaultModel: 'Xenova/all-mpnet-base-v2',
33 pipeline: null,33 pipeline: null,
34 configField: 'extras.embeddingModel',34 configField: 'extensions.models.embedding',
35 quantized: true,35 quantized: true,
36 },36 },
37 'automatic-speech-recognition': {37 'automatic-speech-recognition': {
38 defaultModel: 'Xenova/whisper-small',38 defaultModel: 'Xenova/whisper-small',
39 pipeline: null,39 pipeline: null,
40 configField: 'extras.speechToTextModel',40 configField: 'extensions.models.speechToText',
41 quantized: true,41 quantized: true,
42 },42 },
43 'text-to-speech': {43 'text-to-speech': {
44 defaultModel: 'Xenova/speecht5_tts',44 defaultModel: 'Xenova/speecht5_tts',
45 pipeline: null,45 pipeline: null,
46 configField: 'extras.textToSpeechModel',46 configField: 'extensions.models.textToSpeech',
47 quantized: false,47 quantized: false,
48 },48 },
49};49};
@@ -132,7 +132,7 @@ export async function getPipeline(task, forceModel = '') {
132132
133 const cacheDir = path.join(globalThis.DATA_ROOT, '_cache');133 const cacheDir = path.join(globalThis.DATA_ROOT, '_cache');
134 const model = forceModel || getModelForTask(task);134 const model = forceModel || getModelForTask(task);
135 const localOnly = getConfigValue('extras.disableAutoDownload', false);135 const localOnly = !getConfigValue('extensions.models.autoDownload', true);
136 console.log('Initializing transformers.js pipeline for task', task, 'with model', model);136 console.log('Initializing transformers.js pipeline for task', task, 'with model', model);
137 const instance = await pipeline(task, model, { cache_dir: cacheDir, quantized: tasks[task].quantized ?? true, local_files_only: localOnly });137 const instance = await pipeline(task, model, { cache_dir: cacheDir, quantized: tasks[task].quantized ?? true, local_files_only: localOnly });
138 tasks[task].pipeline = instance;138 tasks[task].pipeline = instance;
src/users.js+2 -1
@@ -458,7 +458,8 @@ export function getPasswordSalt() {
458 */458 */
459export function getCookieSessionName() {459export function getCookieSessionName() {
460 // Get server hostname and hash it to generate a session suffix460 // Get server hostname and hash it to generate a session suffix
461 const suffix = crypto.createHash('sha256').update(os.hostname()).digest('hex').slice(0, 8);461 const hostname = os.hostname() || 'localhost';
462 const suffix = crypto.createHash('sha256').update(hostname).digest('hex').slice(0, 8);
462 return `session-${suffix}`;463 return `session-${suffix}`;
463}464}
464465
src/util.js+11 -0
@@ -889,3 +889,14 @@ export class MemoryLimitedMap {
889 return this.map[Symbol.iterator]();889 return this.map[Symbol.iterator]();
890 }890 }
891}891}
892
893/**
894 * A 'safe' version of `fs.readFileSync()`. Returns the contents of a file if it exists, falling back to a default value if not.
895 * @param {string} filePath Path of the file to be read.
896 * @param {Parameters<typeof fs.readFileSync>[1]} options Options object to pass through to `fs.readFileSync()` (default: `{ encoding: 'utf-8' }`).
897 * @returns The contents at `filePath` if it exists, or `null` if not.
898 */
899export function safeReadFileSync(filePath, options = { encoding: 'utf-8' }) {
900 if (fs.existsSync(filePath)) return fs.readFileSync(filePath, options);
901 return null;
902}