Merge branch 'staging' into patch-1

7f321c9cf64e3ae18fa6cde165bbbde9a68c78d0

Ashley Saleem-west <hello@ashleysw.com>

Signed
69 files changed, +1435 -237Showing 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
@@ -70,7 +70,7 @@ perUserBasicAuth: false
70## Set to a positive number to expire session after a certain time of inactivity70## Set to a positive number to expire session after a certain time of inactivity
71## Set to 0 to expire session when the browser is closed71## Set to 0 to expire session when the browser is closed
72## Set to a negative number to disable session expiration72## Set to a negative number to disable session expiration
73sessionTimeout: 8640073sessionTimeout: -1
74# Used to sign session cookies. Will be auto-generated if not set74# Used to sign session cookies. Will be auto-generated if not set
75cookieSecret: ''75cookieSecret: ''
76# Disable CSRF protection - NOT RECOMMENDED76# Disable CSRF protection - NOT RECOMMENDED
@@ -133,24 +133,26 @@ whitelistImportDomains:
133## headers:133## headers:
134## User-Agent: "Googlebot/2.1 (+http://www.google.com/bot.html)"134## User-Agent: "Googlebot/2.1 (+http://www.google.com/bot.html)"
135requestOverrides: []135requestOverrides: []
136# -- EXTENSIONS CONFIGURATION --136
137# EXTENSIONS CONFIGURATION
138extensions:
137 # Enable UI extensions139 # Enable UI extensions
138enableExtensions: true140 enabled: true
139 # Automatically update extensions when a release version changes141 # Automatically update extensions when a release version changes
140enableExtensionsAutoUpdate: true142 autoUpdate: true
143 models:
144 # Enables automatic model download from HuggingFace
145 autoDownload: true
146 # Additional models for extensions. Expects model IDs from HuggingFace model hub in ONNX format
147 classification: Cohee/distilbert-base-uncased-go-emotions-onnx
148 captioning: Xenova/vit-gpt2-image-captioning
149 embedding: Cohee/jina-embeddings-v2-base-en
150 speechToText: Xenova/whisper-small
151 textToSpeech: Xenova/speecht5_tts
152
141# Additional model tokenizers can be downloaded on demand.153# Additional model tokenizers can be downloaded on demand.
142# Disabling will fallback to another locally available tokenizer.154# Disabling will fallback to another locally available tokenizer.
143enableDownloadableTokenizers: true155enableDownloadableTokenizers: true
144# Extension settings
145extras:
146 # Disables automatic model download from HuggingFace
147 disableAutoDownload: false
148 # Extra models for plugins. Expects model IDs from HuggingFace model hub in ONNX format
149 classificationModel: Cohee/distilbert-base-uncased-go-emotions-onnx
150 captioningModel: Xenova/vit-gpt2-image-captioning
151 embeddingModel: Cohee/jina-embeddings-v2-base-en
152 speechToTextModel: Xenova/whisper-small
153 textToSpeechModel: Xenova/speecht5_tts
154# -- OPENAI CONFIGURATION --156# -- OPENAI CONFIGURATION --
155# A placeholder message to use in strict prompt post-processing mode when the prompt doesn't start with a user message157# A placeholder message to use in strict prompt post-processing mode when the prompt doesn't start with a user message
156promptPlaceholder: "[Start a new chat]"158promptPlaceholder: "[Start a new chat]"
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+5 -5
@@ -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+1 -1
@@ -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",
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/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+2 -2
@@ -1381,7 +1381,7 @@
1381 "char_import_2": "Chub Lorebook(直接連結或 ID)",1381 "char_import_2": "Chub Lorebook(直接連結或 ID)",
1382 "char_import_3": "JanitorAI 角色(直接連結或 ID)",1382 "char_import_3": "JanitorAI 角色(直接連結或 ID)",
1383 "char_import_4": "Pygmalion.chat 角色(直接連結或 ID)",1383 "char_import_4": "Pygmalion.chat 角色(直接連結或 ID)",
1384 "char_import_5": "AICharacterCard.com 角色(直接連結或 ID)",1384 "char_import_5": "AICharacterCards.com 角色(直接連結或 ID)",
1385 "char_import_6": "直接 PNG 連結(請參閱",1385 "char_import_6": "直接 PNG 連結(請參閱",
1386 "char_import_7": "對於允許的主機)",1386 "char_import_7": "對於允許的主機)",
1387 "char_import_8": "RisuRealm角色(直接連結)",1387 "char_import_8": "RisuRealm角色(直接連結)",
@@ -2357,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(可選)",
public/script.js+130 -22
@@ -267,6 +267,7 @@ import { initSettingsSearch } from './scripts/setting-search.js';
267import { initBulkEdit } from './scripts/bulk-edit.js';267import { initBulkEdit } from './scripts/bulk-edit.js';
268import { deriveTemplatesFromChatTemplate } from './scripts/chat-templates.js';268import { deriveTemplatesFromChatTemplate } from './scripts/chat-templates.js';
269import { getContext } from './scripts/st-context.js';269import { getContext } from './scripts/st-context.js';
270import { initReasoning, PromptReasoning } from './scripts/reasoning.js';
270271
271// API OBJECT FOR EXTERNAL WIRING272// API OBJECT FOR EXTERNAL WIRING
272globalThis.SillyTavern = {273globalThis.SillyTavern = {
@@ -443,6 +444,7 @@ export const event_types = {
443 MESSAGE_DELETED: 'message_deleted',444 MESSAGE_DELETED: 'message_deleted',
444 MESSAGE_UPDATED: 'message_updated',445 MESSAGE_UPDATED: 'message_updated',
445 MESSAGE_FILE_EMBEDDED: 'message_file_embedded',446 MESSAGE_FILE_EMBEDDED: 'message_file_embedded',
447 MORE_MESSAGES_LOADED: 'more_messages_loaded',
446 IMPERSONATE_READY: 'impersonate_ready',448 IMPERSONATE_READY: 'impersonate_ready',
447 CHAT_CHANGED: 'chat_id_changed',449 CHAT_CHANGED: 'chat_id_changed',
448 GENERATION_AFTER_COMMANDS: 'GENERATION_AFTER_COMMANDS',450 GENERATION_AFTER_COMMANDS: 'GENERATION_AFTER_COMMANDS',
@@ -723,6 +725,7 @@ async function getSystemMessages() {
723 is_user: false,725 is_user: false,
724 is_system: true,726 is_system: true,
725 mes: await renderTemplateAsync('assistantNote'),727 mes: await renderTemplateAsync('assistantNote'),
728 uses_system_ui: true,
726 extra: {729 extra: {
727 isSmallSys: true,730 isSmallSys: true,
728 },731 },
@@ -980,6 +983,7 @@ async function firstLoadInit() {
980 initServerHistory();983 initServerHistory();
981 initSettingsSearch();984 initSettingsSearch();
982 initBulkEdit();985 initBulkEdit();
986 initReasoning();
983 await initScrapers();987 await initScrapers();
984 doDailyExtensionUpdatesCheck();988 doDailyExtensionUpdatesCheck();
985 await hideLoader();989 await hideLoader();
@@ -1829,7 +1833,7 @@ export async function replaceCurrentChat() {
1829 }1833 }
1830}1834}
18311835
1832export function showMoreMessages(messagesToLoad = null) {1836export async function showMoreMessages(messagesToLoad = null) {
1833 const firstDisplayedMesId = $('#chat').children('.mes').first().attr('mesid');1837 const firstDisplayedMesId = $('#chat').children('.mes').first().attr('mesid');
1834 let messageId = Number(firstDisplayedMesId);1838 let messageId = Number(firstDisplayedMesId);
1835 let count = messagesToLoad || power_user.chat_truncation || Number.MAX_SAFE_INTEGER;1839 let count = messagesToLoad || power_user.chat_truncation || Number.MAX_SAFE_INTEGER;
@@ -1859,6 +1863,8 @@ export function showMoreMessages(messagesToLoad = null) {
1859 const newHeight = $('#chat').prop('scrollHeight');1863 const newHeight = $('#chat').prop('scrollHeight');
1860 $('#chat').scrollTop(newHeight - prevHeight);1864 $('#chat').scrollTop(newHeight - prevHeight);
1861 }1865 }
1866
1867 await eventSource.emit(event_types.MORE_MESSAGES_LOADED);
1862}1868}
18631869
1864export async function printMessages() {1870export async function printMessages() {
@@ -2196,6 +2202,7 @@ function getMessageFromTemplate({
2196 isUser,2202 isUser,
2197 avatarImg,2203 avatarImg,
2198 bias,2204 bias,
2205 reasoning,
2199 isSystem,2206 isSystem,
2200 title,2207 title,
2201 timerValue,2208 timerValue,
@@ -2220,6 +2227,7 @@ function getMessageFromTemplate({
2220 mes.find('.avatar img').attr('src', avatarImg);2227 mes.find('.avatar img').attr('src', avatarImg);
2221 mes.find('.ch_name .name_text').text(characterName);2228 mes.find('.ch_name .name_text').text(characterName);
2222 mes.find('.mes_bias').html(bias);2229 mes.find('.mes_bias').html(bias);
2230 mes.find('.mes_reasoning').html(reasoning);
2223 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 ?? ''}`);
2224 mes.find('.mesIDDisplay').text(`#${mesId}`);2232 mes.find('.mesIDDisplay').text(`#${mesId}`);
2225 tokenCount && mes.find('.tokenCounterDisplay').text(`${tokenCount}t`);2233 tokenCount && mes.find('.tokenCounterDisplay').text(`${tokenCount}t`);
@@ -2234,10 +2242,16 @@ function getMessageFromTemplate({
2234 return mes;2242 return mes;
2235}2243}
22362244
2245/**
2246 * Re-renders a message block with updated content.
2247 * @param {number} messageId Message ID
2248 * @param {object} message Message object
2249 */
2237export function updateMessageBlock(messageId, message) {2250export function updateMessageBlock(messageId, message) {
2238 const messageElement = $(`#chat [mesid="${messageId}"]`);2251 const messageElement = $(`#chat [mesid="${messageId}"]`);
2239 const text = message?.extra?.display_text ?? message.mes;2252 const text = message?.extra?.display_text ?? message.mes;
2240 messageElement.find('.mes_text').html(messageFormatting(text, message.name, message.is_system, message.is_user, messageId));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));
2241 addCopyToCodeBlocks(messageElement);2255 addCopyToCodeBlocks(messageElement);
2242 appendMediaToMessage(message, messageElement);2256 appendMediaToMessage(message, messageElement);
2243}2257}
@@ -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;
@@ -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.
@@ -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();
@@ -11213,14 +11320,15 @@ jQuery(async function () {
1121311320
11214 $(document).keyup(function (e) {11321 $(document).keyup(function (e) {
11215 if (e.key === 'Escape') {11322 if (e.key === 'Escape') {
11216 const isEditVisible = $('#curEditTextarea').is(':visible');11323 const isEditVisible = $('#curEditTextarea').is(':visible') || $('.reasoning_edit_textarea').length > 0;
11217 if (isEditVisible && power_user.auto_save_msg_edits === false) {11324 if (isEditVisible && power_user.auto_save_msg_edits === false) {
11218 closeMessageEditor();11325 closeMessageEditor('all');
11219 $('#send_textarea').focus();11326 $('#send_textarea').focus();
11220 return;11327 return;
11221 }11328 }
11222 if (isEditVisible && power_user.auto_save_msg_edits === true) {11329 if (isEditVisible && power_user.auto_save_msg_edits === true) {
11223 $(`#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');
11224 $('#send_textarea').focus();11332 $('#send_textarea').focus();
11225 return;11333 return;
11226 }11334 }
@@ -11454,8 +11562,8 @@ jQuery(async function () {
11454 $('#avatar-and-name-block').slideToggle();11562 $('#avatar-and-name-block').slideToggle();
11455 });11563 });
1145611564
11457 $(document).on('mouseup touchend', '#show_more_messages', () => {11565 $(document).on('mouseup touchend', '#show_more_messages', async function () {
11458 showMoreMessages();11566 await showMoreMessages();
11459 });11567 });
1146011568
11461 $(document).on('click', '.open_characters_library', async function () {11569 $(document).on('click', '.open_characters_library', async function () {
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
@@ -59,6 +59,16 @@ const hash_derivations = {
59 // Tulu-3-8B59 // Tulu-3-8B
60 // Tulu-3-70B60 // Tulu-3-70B
61 'Tulu'61 'Tulu'
62 ,
63
64 // DeepSeek V2.5
65 '54d400beedcd17f464e10063e0577f6f798fa896266a912d8a366f8a2fcc0bca':
66 'DeepSeek-V2.5'
67 ,
68
69 // DeepSeek R1
70 'b6835114b7303ddd78919a82e4d9f7d8c26ed0d7dfc36beeb12d524f6144eab1':
71 'DeepSeek-V2.5'
62};72};
6373
64const substr_derivations = {74const 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/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/connection-manager/index.js+4 -0
@@ -30,6 +30,7 @@ const CC_COMMANDS = [
30 'api-url',30 'api-url',
31 'model',31 'model',
32 'proxy',32 'proxy',
33 'stop-strings',
33];34];
3435
35const TC_COMMANDS = [36const TC_COMMANDS = [
@@ -43,6 +44,7 @@ const TC_COMMANDS = [
43 'context',44 'context',
44 'instruct-state',45 'instruct-state',
45 'tokenizer',46 'tokenizer',
47 'stop-strings',
46];48];
4749
48const FANCY_NAMES = {50const FANCY_NAMES = {
@@ -57,6 +59,7 @@ const FANCY_NAMES = {
57 'instruct': 'Instruct Template',59 'instruct': 'Instruct Template',
58 'context': 'Context Template',60 'context': 'Context Template',
59 'tokenizer': 'Tokenizer',61 'tokenizer': 'Tokenizer',
62 'stop-strings': 'Custom Stopping Strings',
60};63};
6164
62/**65/**
@@ -138,6 +141,7 @@ const profilesProvider = () => [
138 * @property {string} [context] Context Template141 * @property {string} [context] Context Template
139 * @property {string} [instruct-state] Instruct Mode142 * @property {string} [instruct-state] Instruct Mode
140 * @property {string} [tokenizer] Tokenizer143 * @property {string} [tokenizer] Tokenizer
144 * @property {string} [stop-strings] Custom Stopping Strings
141 * @property {string[]} [exclude] Commands to exclude145 * @property {string[]} [exclude] Commands to exclude
142 */146 */
143147
public/scripts/extensions/tts/index.js+48 -5
@@ -30,6 +30,7 @@ import { GoogleTranslateTtsProvider } from './google-translate.js';
30export { talkingAnimation };30export { talkingAnimation };
3131
32const UPDATE_INTERVAL = 1000;32const UPDATE_INTERVAL = 1000;
33const wrapper = new ModuleWorkerWrapper(moduleWorker);
3334
34let voiceMapEntries = [];35let voiceMapEntries = [];
35let voiceMap = {}; // {charName:voiceid, charName2:voiceid2}36let voiceMap = {}; // {charName:voiceid, charName2:voiceid2}
@@ -120,7 +121,7 @@ async function onNarrateOneMessage() {
120 }121 }
121122
122 resetTtsPlayback();123 resetTtsPlayback();
123 ttsJobQueue.push(message);124 processAndQueueTtsMessage(message);
124 moduleWorker();125 moduleWorker();
125}126}
126127
@@ -147,7 +148,7 @@ async function onNarrateText(args, text) {
147 }148 }
148149
149 resetTtsPlayback();150 resetTtsPlayback();
150 ttsJobQueue.push({ mes: text, name: name });151 processAndQueueTtsMessage({ mes: text, name: name });
151 await moduleWorker();152 await moduleWorker();
152153
153 // Return back to the chat voices154 // Return back to the chat voices
@@ -220,6 +221,36 @@ function isTtsProcessing() {
220 return processing;221 return processing;
221}222}
222223
224/**
225 * Splits a message into lines and adds each non-empty line to the TTS job queue.
226 * @param {Object} message - The message object to be processed.
227 * @param {string} message.mes - The text of the message to be split into lines.
228 * @param {string} message.name - The name associated with the message.
229 * @returns {void}
230 */
231function processAndQueueTtsMessage(message) {
232 if (!extension_settings.tts.narrate_by_paragraphs) {
233 ttsJobQueue.push(message);
234 return;
235 }
236
237 const lines = message.mes.split('\n');
238
239 for (let i = 0; i < lines.length; i++) {
240 const line = lines[i];
241
242 if (line.length === 0) {
243 continue;
244 }
245
246 ttsJobQueue.push(
247 Object.assign({}, message, {
248 mes: line,
249 }),
250 );
251 }
252}
253
223function debugTtsPlayback() {254function debugTtsPlayback() {
224 console.log(JSON.stringify(255 console.log(JSON.stringify(
225 {256 {
@@ -350,7 +381,7 @@ function onAudioControlClicked() {
350 talkingAnimation(false);381 talkingAnimation(false);
351 } else {382 } else {
352 // Default play behavior if not processing or playing is to play the last message.383 // Default play behavior if not processing or playing is to play the last message.
353 ttsJobQueue.push(context.chat[context.chat.length - 1]);384 processAndQueueTtsMessage(context.chat[context.chat.length - 1]);
354 }385 }
355 updateUiAudioPlayState();386 updateUiAudioPlayState();
356}387}
@@ -376,6 +407,7 @@ function completeCurrentAudioJob() {
376 currentAudioJob = null;407 currentAudioJob = null;
377 talkingAnimation(false); //stop lip animation408 talkingAnimation(false); //stop lip animation
378 // updateUiPlayState();409 // updateUiPlayState();
410 wrapper.update();
379}411}
380412
381/**413/**
@@ -466,7 +498,7 @@ async function processTtsQueue() {
466 }498 }
467499
468 if (extension_settings.tts.skip_tags) {500 if (extension_settings.tts.skip_tags) {
469 text = text.replace(/<.*?>.*?<\/.*?>/g, '').trim();501 text = text.replace(/<.*?>[\s\S]*?<\/.*?>/g, '').trim();
470 }502 }
471503
472 if (!extension_settings.tts.pass_asterisks) {504 if (!extension_settings.tts.pass_asterisks) {
@@ -569,6 +601,7 @@ function loadSettings() {
569 $('#tts_narrate_quoted').prop('checked', extension_settings.tts.narrate_quoted_only);601 $('#tts_narrate_quoted').prop('checked', extension_settings.tts.narrate_quoted_only);
570 $('#tts_auto_generation').prop('checked', extension_settings.tts.auto_generation);602 $('#tts_auto_generation').prop('checked', extension_settings.tts.auto_generation);
571 $('#tts_periodic_auto_generation').prop('checked', extension_settings.tts.periodic_auto_generation);603 $('#tts_periodic_auto_generation').prop('checked', extension_settings.tts.periodic_auto_generation);
604 $('#tts_narrate_by_paragraphs').prop('checked', extension_settings.tts.narrate_by_paragraphs);
572 $('#tts_narrate_translated_only').prop('checked', extension_settings.tts.narrate_translated_only);605 $('#tts_narrate_translated_only').prop('checked', extension_settings.tts.narrate_translated_only);
573 $('#tts_narrate_user').prop('checked', extension_settings.tts.narrate_user);606 $('#tts_narrate_user').prop('checked', extension_settings.tts.narrate_user);
574 $('#tts_pass_asterisks').prop('checked', extension_settings.tts.pass_asterisks);607 $('#tts_pass_asterisks').prop('checked', extension_settings.tts.pass_asterisks);
@@ -638,6 +671,11 @@ function onPeriodicAutoGenerationClick() {
638 saveSettingsDebounced();671 saveSettingsDebounced();
639}672}
640673
674function onNarrateByParagraphsClick() {
675 extension_settings.tts.narrate_by_paragraphs = !!$('#tts_narrate_by_paragraphs').prop('checked');
676 saveSettingsDebounced();
677}
678
641679
642function onNarrateDialoguesClick() {680function onNarrateDialoguesClick() {
643 extension_settings.tts.narrate_dialogues_only = !!$('#tts_narrate_dialogues').prop('checked');681 extension_settings.tts.narrate_dialogues_only = !!$('#tts_narrate_dialogues').prop('checked');
@@ -816,7 +854,12 @@ async function onMessageEvent(messageId, lastCharIndex) {
816 lastChatId = context.chatId;854 lastChatId = context.chatId;
817855
818 console.debug(`Adding message from ${message.name} for TTS processing: "${message.mes}"`);856 console.debug(`Adding message from ${message.name} for TTS processing: "${message.mes}"`);
857
858 if (extension_settings.tts.periodic_auto_generation) {
819 ttsJobQueue.push(message);859 ttsJobQueue.push(message);
860 } else {
861 processAndQueueTtsMessage(message);
862 }
820}863}
821864
822async function onMessageDeleted() {865async function onMessageDeleted() {
@@ -1156,6 +1199,7 @@ jQuery(async function () {
1156 $('#tts_pass_asterisks').on('click', onPassAsterisksClick);1199 $('#tts_pass_asterisks').on('click', onPassAsterisksClick);
1157 $('#tts_auto_generation').on('click', onAutoGenerationClick);1200 $('#tts_auto_generation').on('click', onAutoGenerationClick);
1158 $('#tts_periodic_auto_generation').on('click', onPeriodicAutoGenerationClick);1201 $('#tts_periodic_auto_generation').on('click', onPeriodicAutoGenerationClick);
1202 $('#tts_narrate_by_paragraphs').on('click', onNarrateByParagraphsClick);
1159 $('#tts_narrate_user').on('click', onNarrateUserClick);1203 $('#tts_narrate_user').on('click', onNarrateUserClick);
11601204
1161 $('#playback_rate').on('input', function () {1205 $('#playback_rate').on('input', function () {
@@ -1177,7 +1221,6 @@ jQuery(async function () {
1177 loadSettings(); // Depends on Extension Controls and loadTtsProvider1221 loadSettings(); // Depends on Extension Controls and loadTtsProvider
1178 loadTtsProvider(extension_settings.tts.currentProvider); // No dependencies1222 loadTtsProvider(extension_settings.tts.currentProvider); // No dependencies
1179 addAudioControl(); // Depends on Extension Controls1223 addAudioControl(); // Depends on Extension Controls
1180 const wrapper = new ModuleWorkerWrapper(moduleWorker);
1181 setInterval(wrapper.update.bind(wrapper), UPDATE_INTERVAL); // Init depends on all the things1224 setInterval(wrapper.update.bind(wrapper), UPDATE_INTERVAL); // Init depends on all the things
1182 eventSource.on(event_types.MESSAGE_SWIPED, resetTtsPlayback);1225 eventSource.on(event_types.MESSAGE_SWIPED, resetTtsPlayback);
1183 eventSource.on(event_types.CHAT_CHANGED, onChatChanged);1226 eventSource.on(event_types.CHAT_CHANGED, onChatChanged);
public/scripts/extensions/tts/settings.html+4 -0
@@ -30,6 +30,10 @@
30 <input type="checkbox" id="tts_periodic_auto_generation">30 <input type="checkbox" id="tts_periodic_auto_generation">
31 <small data-i18n="Narrate by paragraphs (when streaming)">Narrate by paragraphs (when streaming)</small>31 <small data-i18n="Narrate by paragraphs (when streaming)">Narrate by paragraphs (when streaming)</small>
32 </label>32 </label>
33 <label class="checkbox_label" for="tts_narrate_by_paragraphs">
34 <input type="checkbox" id="tts_narrate_by_paragraphs">
35 <small data-i18n="Narrate by paragraphs (when not streaming)">Narrate by paragraphs (when not streaming)</small>
36 </label>
33 <label class="checkbox_label" for="tts_narrate_quoted">37 <label class="checkbox_label" for="tts_narrate_quoted">
34 <input type="checkbox" id="tts_narrate_quoted">38 <input type="checkbox" id="tts_narrate_quoted">
35 <small data-i18n="Only narrate quotes">Only narrate "quotes"</small>39 <small data-i18n="Only narrate quotes">Only narrate "quotes"</small>
public/scripts/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+50 -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
@@ -4064,4 +4072,45 @@ $(document).ready(() => {
4064 ],4072 ],
4065 helpString: 'activates a movingUI preset by name',4073 helpString: 'activates a movingUI preset by name',
4066 }));4074 }));
4075 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
4076 name: 'stop-strings',
4077 aliases: ['stopping-strings', 'custom-stopping-strings', 'custom-stop-strings'],
4078 helpString: `
4079 <div>
4080 Sets a list of custom stopping strings. Gets the list if no value is provided.
4081 </div>
4082 <div>
4083 <strong>Examples:</strong>
4084 </div>
4085 <ul>
4086 <li>Value must be a JSON-serialized array: <pre><code class="language-stscript">/stop-strings ["goodbye", "farewell"]</code></pre></li>
4087 <li>Pipe characters must be escaped with a backslash: <pre><code class="language-stscript">/stop-strings ["left\\|right"]</code></pre></li>
4088 </ul>
4089 `,
4090 returns: ARGUMENT_TYPE.LIST,
4091 unnamedArgumentList: [
4092 SlashCommandArgument.fromProps({
4093 description: 'list of strings',
4094 typeList: [ARGUMENT_TYPE.LIST],
4095 acceptsMultiple: false,
4096 isRequired: false,
4097 }),
4098 ],
4099 callback: (_, value) => {
4100 if (String(value ?? '').trim()) {
4101 const parsedValue = ((x) => { try { return JSON.parse(x.toString()); } catch { return null; } })(value);
4102 if (!parsedValue || !Array.isArray(parsedValue)) {
4103 throw new Error('Invalid list format. The value must be a JSON-serialized array of strings.');
4104 }
4105 parsedValue.forEach((item, index) => {
4106 parsedValue[index] = String(item);
4107 });
4108 power_user.custom_stopping_strings = JSON.stringify(parsedValue);
4109 $('#custom_stopping_strings').val(power_user.custom_stopping_strings);
4110 saveSettingsDebounced();
4111 }
4112
4113 return power_user.custom_stopping_strings;
4114 },
4115 }));
4067});4116});
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.at)) ? Number(args.at) : 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-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/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,8 +340,62 @@ 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
@@ -342,20 +404,24 @@ input[type='checkbox']:focus-visible {
342 color: inherit;404 color: inherit;
343}405}
344406
345.mes_text u {407.mes_text u,
408.mes_reasoning u {
346 color: var(--SmartThemeUnderlineColor);409 color: var(--SmartThemeUnderlineColor);
347}410}
348411
349.mes_text q {412.mes_text q,
413.mes_reasoning q {
350 color: var(--SmartThemeQuoteColor);414 color: var(--SmartThemeQuoteColor);
351}415}
352416
353.mes_text font[color] em,417.mes_text font[color] em,
354.mes_text font[color] i {418.mes_text font[color] i,
355 color: inherit;419.mes_text font[color] u,
356}420.mes_text font[color] q,
357421.mes_reasoning font[color] em,
358.mes_text font[color] q {422.mes_reasoning font[color] i,
423.mes_reasoning font[color] u,
424.mes_reasoning font[color] q {
359 color: inherit;425 color: inherit;
360}426}
361427
@@ -363,7 +429,8 @@ input[type='checkbox']:focus-visible {
363 display: block;429 display: block;
364}430}
365431
366.mes_text blockquote {432.mes_text blockquote,
433.mes_reasoning blockquote {
367 border-left: 3px solid var(--SmartThemeQuoteColor);434 border-left: 3px solid var(--SmartThemeQuoteColor);
368 padding-left: 10px;435 padding-left: 10px;
369 background-color: var(--black30a);436 background-color: var(--black30a);
@@ -373,18 +440,24 @@ input[type='checkbox']:focus-visible {
373.mes_text strong em,440.mes_text strong em,
374.mes_text strong,441.mes_text strong,
375.mes_text h2,442.mes_text h2,
376.mes_text h1 {443.mes_text h1,
444.mes_reasoning strong em,
445.mes_reasoning strong,
446.mes_reasoning h2,
447.mes_reasoning h1 {
377 font-weight: bold;448 font-weight: bold;
378}449}
379450
380.mes_text pre code {451.mes_text pre code,
452.mes_reasoning pre code {
381 position: relative;453 position: relative;
382 display: block;454 display: block;
383 overflow-x: auto;455 overflow-x: auto;
384 padding: 1em;456 padding: 1em;
385}457}
386458
387.mes_text img:not(.mes_img) {459.mes_text img:not(.mes_img),
460.mes_reasoning img:not(.mes_img) {
388 max-width: 100%;461 max-width: 100%;
389 max-height: var(--doc-height);462 max-height: var(--doc-height);
390}463}
@@ -1027,6 +1100,11 @@ body .panelControlBar {
1027 /*only affects bubblechat to make it sit nicely at the bottom*/1100 /*only affects bubblechat to make it sit nicely at the bottom*/
1028}1101}
10291102
1103.last_mes:has(.mes_text:empty):has(.mes_reasoning_details[open]) .mes_reasoning:not(:empty) {
1104 margin-bottom: 30px;
1105}
1106
1107.last_mes .mes_reasoning,
1030.last_mes .mes_text {1108.last_mes .mes_text {
1031 padding-right: 30px;1109 padding-right: 30px;
1032}1110}
@@ -1240,14 +1318,18 @@ body.swipeAllMessages .mes:not(.last_mes) .swipes-counter {
1240 overflow-y: clip;1318 overflow-y: clip;
1241}1319}
12421320
1243.mes_text {1321.mes_text,
1322.mes_reasoning {
1244 font-weight: 500;1323 font-weight: 500;
1245 line-height: calc(var(--mainFontSize) + .5rem);1324 line-height: calc(var(--mainFontSize) + .5rem);
1325 max-width: 100%;
1326 overflow-wrap: anywhere;
1327}
1328
1329.mes_text {
1246 padding-left: 0;1330 padding-left: 0;
1247 padding-top: 5px;1331 padding-top: 5px;
1248 padding-bottom: 5px;1332 padding-bottom: 5px;
1249 max-width: 100%;
1250 overflow-wrap: anywhere;
1251}1333}
12521334
1253br {1335br {
@@ -2926,7 +3008,7 @@ input[type=search]:focus::-webkit-search-cancel-button {
2926 position: relative;3008 position: relative;
2927}3009}
29283010
2929#rm_print_characters_block .ch_name,3011.character_name_block .ch_name,
2930.avatar-container .ch_name {3012.avatar-container .ch_name {
2931 flex: 1 1 auto;3013 flex: 1 1 auto;
2932 white-space: nowrap;3014 white-space: nowrap;
@@ -2936,6 +3018,13 @@ input[type=search]:focus::-webkit-search-cancel-button {
2936 display: block;3018 display: block;
2937}3019}
29383020
3021.character_name_block .character_version {
3022 text-overflow: ellipsis;
3023 overflow: hidden;
3024 text-wrap: nowrap;
3025 max-width: 50%;
3026}
3027
2939#rm_print_characters_block .character_name_block> :last-child {3028#rm_print_characters_block .character_name_block> :last-child {
2940 flex: 0 100000 auto;3029 flex: 0 100000 auto;
2941 /* Force shrinking first */3030 /* Force shrinking first */
@@ -4148,10 +4237,12 @@ input[type="range"]::-webkit-slider-thumb {
4148 align-items: center;4237 align-items: center;
4149}4238}
41504239
4240.mes_reasoning_edit_cancel,
4151.mes_edit_cancel.menu_button {4241.mes_edit_cancel.menu_button {
4152 background-color: var(--crimson70a);4242 background-color: var(--crimson70a);
4153}4243}
41544244
4245.mes_reasoning_edit_done,
4155.mes_edit_done.menu_button {4246.mes_edit_done.menu_button {
4156 background-color: var(--okGreen70a);4247 background-color: var(--okGreen70a);
4157}4248}
@@ -4160,6 +4251,7 @@ input[type="range"]::-webkit-slider-thumb {
4160 opacity: 1;4251 opacity: 1;
4161}4252}
41624253
4254.reasoning_edit_textarea,
4163.edit_textarea {4255.edit_textarea {
4164 padding: 5px;4256 padding: 5px;
4165 margin: 0;4257 margin: 0;
@@ -5648,6 +5740,7 @@ body:not(.movingUI) .drawer-content.maximized {
56485740
5649.model-card .details-container {5741.model-card .details-container {
5650 text-align: right;5742 text-align: right;
5743 line-height: 0.9;
5651}5744}
56525745
5653.model-card:hover {5746.model-card:hover {
@@ -5670,7 +5763,7 @@ body:not(.movingUI) .drawer-content.maximized {
5670}5763}
56715764
5672.model-title {5765.model-title {
5673 font-size: 13px;5766 font-size: calc(var(--mainFontSize) * 0.95);
5674 font-weight: bold;5767 font-weight: bold;
5675 overflow: hidden;5768 overflow: hidden;
5676}5769}
@@ -5686,7 +5779,7 @@ body:not(.movingUI) .drawer-content.maximized {
5686.model-class,5779.model-class,
5687.model-context-length,5780.model-context-length,
5688.model-date-added {5781.model-date-added {
5689 font-size: 10px;5782 font-size: calc(var(--mainFontSize) * 0.75);
5690}5783}
56915784
5692.model-class,5785.model-class,
@@ -5768,3 +5861,17 @@ body:not(.movingUI) .drawer-content.maximized {
5768.alternate_greetings_list {5861.alternate_greetings_list {
5769 overflow-y: scroll;5862 overflow-y: scroll;
5770}5863}
5864
5865.mes_text div[data-type="assistant_note"]:has(.assistant_note_export) {
5866 display: flex;
5867 flex-direction: row;
5868 flex-wrap: nowrap;
5869 justify-content: space-between;
5870 align-items: center;
5871 gap: 10px;
5872 padding: 0 2px;
5873}
5874
5875.mes_text div[data-type="assistant_note"]:has(.assistant_note_export)>div:not(.assistant_note_export) {
5876 flex: 1;
5877}
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} from './src/util.js';69} from './src/util.js';
71import { UPLOADS_DIRECTORY } from './src/constants.js';70import { UPLOADS_DIRECTORY } from './src/constants.js';
72import { ensureThumbnailCache } from './src/endpoints/thumbnails.js';71import { ensureThumbnailCache } from './src/endpoints/thumbnails.js';
@@ -347,8 +346,8 @@ if (enableCorsProxy) {
347}346}
348347
349function getSessionCookieAge() {348function getSessionCookieAge() {
350 // Defaults to 24 hours in seconds if not set349 // Defaults to "no expiration" if not set
351 const configValue = getConfigValue('sessionTimeout', 24 * 60 * 60);350 const configValue = getConfigValue('sessionTimeout', -1);
352351
353 // Convert to milliseconds352 // Convert to milliseconds
354 if (configValue > 0) {353 if (configValue > 0) {
@@ -377,27 +376,38 @@ app.use(setUserDataMiddleware);
377376
378// CSRF Protection //377// CSRF Protection //
379if (!disableCsrf) {378if (!disableCsrf) {
380 const COOKIES_SECRET = getCookieSecret();379 const csrfSyncProtection = csrfSync({
381380 getTokenFromState: (req) => {
382 const { generateToken, doubleCsrfProtection } = doubleCsrf({381 if (!req.session) {
383 getSecret: getCsrfSecret,382 console.error('(CSRF error) getTokenFromState: Session object not initialized');
384 cookieName: 'X-CSRF-Token',383 return;
385 cookieOptions: {384 }
386 sameSite: 'strict',385 return req.session.csrfToken;
387 secure: false,386 },
387 getTokenFromRequest: (req) => {
388 return req.headers['x-csrf-token']?.toString();
389 },
390 storeTokenInState: (req, token) => {
391 if (!req.session) {
392 console.error('(CSRF error) storeTokenInState: Session object not initialized');
393 return;
394 }
395 req.session.csrfToken = token;
388 },396 },
389 size: 64,397 size: 32,
390 getTokenFromRequest: (req) => req.headers['x-csrf-token'],
391 });398 });
392399
393 app.get('/csrf-token', (req, res) => {400 app.get('/csrf-token', (req, res) => {
394 res.json({401 res.json({
395 'token': generateToken(res, req),402 'token': csrfSyncProtection.generateToken(req),
396 });403 });
397 });404 });
398405
399 app.use(cookieParser(COOKIES_SECRET));406 // Customize the error message
400 app.use(doubleCsrfProtection);407 csrfSyncProtection.invalidCsrfTokenError.message = color.red('Invalid CSRF token. Please refresh the page and try again.');
408 csrfSyncProtection.invalidCsrfTokenError.stack = undefined;
409
410 app.use(csrfSyncProtection.csrfSynchronisedProtection);
401} else {411} else {
402 console.warn('\nCSRF protection is disabled. This will make your server vulnerable to CSRF attacks.\n');412 console.warn('\nCSRF protection is disabled. This will make your server vulnerable to CSRF attacks.\n');
403 app.get('/csrf-token', (req, res) => {413 app.get('/csrf-token', (req, res) => {
@@ -921,6 +931,16 @@ async function verifySecuritySettings() {
921 }931 }
922}932}
923933
934/**
935 * Registers a not-found error response if a not-found error page exists. Should only be called after all other middlewares have been registered.
936 */
937function apply404Middleware() {
938 const notFoundWebpage = safeReadFileSync('./public/error/url-not-found.html') ?? '';
939 app.use((req, res) => {
940 res.status(404).send(notFoundWebpage);
941 });
942}
943
924// User storage module needs to be initialized before starting the server944// User storage module needs to be initialized before starting the server
925initUserStorage(dataRoot)945initUserStorage(dataRoot)
926 .then(ensurePublicDirectoriesExist)946 .then(ensurePublicDirectoriesExist)
@@ -928,4 +948,5 @@ initUserStorage(dataRoot)
928 .then(migrateSystemPrompts)948 .then(migrateSystemPrompts)
929 .then(verifySecuritySettings)949 .then(verifySecuritySettings)
930 .then(preSetupTasks)950 .then(preSetupTasks)
951 .then(apply404Middleware)
931 .finally(startServer);952 .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.log('Google AI Studio response:', responseContent);394 console.log('Google AI Studio response:', responseContent);
384395
385 if (Array.isArray(responseContent?.parts) && isThinking && !showThoughts) {396 const responseText = typeof responseContent === 'string' ? responseContent : responseContent?.parts?.filter(part => !part.thought)?.map(part => part.text)?.join('\n\n');
386 responseContent.parts = responseContent.parts.filter(part => !part.thought);
387 }
388
389 const responseText = typeof responseContent === 'string' ? responseContent : responseContent?.parts?.map(part => part.text)?.join('\n\n');
390 if (!responseText) {397 if (!responseText) {
391 let message = 'Google AI Studio Candidate text empty';398 let message = 'Google AI Studio Candidate text empty';
392 console.log(message, generateResponseJson);399 console.log(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) {
@@ -636,6 +643,89 @@ async function sendCohereRequest(request, response) {
636 }643 }
637}644}
638645
646/**
647 * Sends a request to DeepSeek API.
648 * @param {express.Request} request Express request
649 * @param {express.Response} response Express response
650 */
651async function sendDeepSeekRequest(request, response) {
652 const apiUrl = new URL(request.body.reverse_proxy || API_DEEPSEEK).toString();
653 const apiKey = request.body.reverse_proxy ? request.body.proxy_password : readSecret(request.user.directories, SECRET_KEYS.DEEPSEEK);
654
655 if (!apiKey && !request.body.reverse_proxy) {
656 console.log('DeepSeek API key is missing.');
657 return response.status(400).send({ error: true });
658 }
659
660 const controller = new AbortController();
661 request.socket.removeAllListeners('close');
662 request.socket.on('close', function () {
663 controller.abort();
664 });
665
666 try {
667 let bodyParams = {};
668
669 if (request.body.logprobs > 0) {
670 bodyParams['top_logprobs'] = request.body.logprobs;
671 bodyParams['logprobs'] = true;
672 }
673
674 const postProcessType = String(request.body.model).endsWith('-reasoner') ? 'deepseek-reasoner' : 'deepseek';
675 const processedMessages = postProcessPrompt(request.body.messages, postProcessType, getPromptNames(request));
676
677 const requestBody = {
678 'messages': processedMessages,
679 'model': request.body.model,
680 'temperature': request.body.temperature,
681 'max_tokens': request.body.max_tokens,
682 'stream': request.body.stream,
683 'presence_penalty': request.body.presence_penalty,
684 'frequency_penalty': request.body.frequency_penalty,
685 'top_p': request.body.top_p,
686 'stop': request.body.stop,
687 'seed': request.body.seed,
688 ...bodyParams,
689 };
690
691 const config = {
692 method: 'POST',
693 headers: {
694 'Content-Type': 'application/json',
695 'Authorization': 'Bearer ' + apiKey,
696 },
697 body: JSON.stringify(requestBody),
698 signal: controller.signal,
699 };
700
701 console.log('DeepSeek request:', requestBody);
702
703 const generateResponse = await fetch(apiUrl + '/chat/completions', config);
704
705 if (request.body.stream) {
706 forwardFetchResponse(generateResponse, response);
707 } else {
708 if (!generateResponse.ok) {
709 const errorText = await generateResponse.text();
710 console.log(`DeepSeek API returned error: ${generateResponse.status} ${generateResponse.statusText} ${errorText}`);
711 const errorJson = tryParse(errorText) ?? { error: true };
712 return response.status(500).send(errorJson);
713 }
714 const generateResponseJson = await generateResponse.json();
715 console.log('DeepSeek response:', generateResponseJson);
716 return response.send(generateResponseJson);
717 }
718 } catch (error) {
719 console.log('Error communicating with DeepSeek API: ', error);
720 if (!response.headersSent) {
721 response.send({ error: true });
722 } else {
723 response.end();
724 }
725 }
726}
727
728
639export const router = express.Router();729export const router = express.Router();
640730
641router.post('/status', jsonParser, async function (request, response_getstatus_openai) {731router.post('/status', jsonParser, async function (request, response_getstatus_openai) {
@@ -680,8 +770,8 @@ router.post('/status', jsonParser, async function (request, response_getstatus_o
680 api_key_openai = readSecret(request.user.directories, SECRET_KEYS.NANOGPT);770 api_key_openai = readSecret(request.user.directories, SECRET_KEYS.NANOGPT);
681 headers = {};771 headers = {};
682 } else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.DEEPSEEK) {772 } else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.DEEPSEEK) {
683 api_url = API_DEEPSEEK.replace('/beta', '');773 api_url = new URL(request.body.reverse_proxy || API_DEEPSEEK.replace('/beta', ''));
684 api_key_openai = readSecret(request.user.directories, SECRET_KEYS.DEEPSEEK);774 api_key_openai = request.body.reverse_proxy ? request.body.proxy_password : readSecret(request.user.directories, SECRET_KEYS.DEEPSEEK);
685 headers = {};775 headers = {};
686 } else {776 } else {
687 console.log('This chat completion source is not supported yet.');777 console.log('This chat completion source is not supported yet.');
@@ -777,6 +867,14 @@ router.post('/bias', jsonParser, async function (request, response) {
777 return response.send({});867 return response.send({});
778 }868 }
779 encodeFunction = (text) => new Uint32Array(instance.encodeIds(text));869 encodeFunction = (text) => new Uint32Array(instance.encodeIds(text));
870 } else if (webTokenizers.includes(model)) {
871 const tokenizer = getWebTokenizer(model);
872 const instance = await tokenizer?.get();
873 if (!instance) {
874 console.warn('Tokenizer not initialized:', model);
875 return response.send({});
876 }
877 encodeFunction = (text) => new Uint32Array(instance.encode(text));
780 } else {878 } else {
781 const tokenizer = getTiktokenTokenizer(model);879 const tokenizer = getTiktokenTokenizer(model);
782 encodeFunction = (tokenizer.encode.bind(tokenizer));880 encodeFunction = (tokenizer.encode.bind(tokenizer));
@@ -841,6 +939,7 @@ router.post('/generate', jsonParser, function (request, response) {
841 case CHAT_COMPLETION_SOURCES.MAKERSUITE: return sendMakerSuiteRequest(request, response);939 case CHAT_COMPLETION_SOURCES.MAKERSUITE: return sendMakerSuiteRequest(request, response);
842 case CHAT_COMPLETION_SOURCES.MISTRALAI: return sendMistralAIRequest(request, response);940 case CHAT_COMPLETION_SOURCES.MISTRALAI: return sendMistralAIRequest(request, response);
843 case CHAT_COMPLETION_SOURCES.COHERE: return sendCohereRequest(request, response);941 case CHAT_COMPLETION_SOURCES.COHERE: return sendCohereRequest(request, response);
942 case CHAT_COMPLETION_SOURCES.DEEPSEEK: return sendDeepSeekRequest(request, response);
844 }943 }
845944
846 let apiUrl;945 let apiUrl;
@@ -899,6 +998,10 @@ router.post('/generate', jsonParser, function (request, response) {
899 bodyParams['route'] = 'fallback';998 bodyParams['route'] = 'fallback';
900 }999 }
9011000
1001 if (request.body.show_thoughts) {
1002 bodyParams['include_reasoning'] = true;
1003 }
1004
902 let cachingAtDepth = getConfigValue('claude.cachingAtDepth', -1);1005 let cachingAtDepth = getConfigValue('claude.cachingAtDepth', -1);
903 if (Number.isInteger(cachingAtDepth) && cachingAtDepth >= 0 && request.body.model?.startsWith('anthropic/claude-3')) {1006 if (Number.isInteger(cachingAtDepth) && cachingAtDepth >= 0 && request.body.model?.startsWith('anthropic/claude-3')) {
904 cachingAtDepthForOpenRouterClaude(request.body.messages, cachingAtDepth);1007 cachingAtDepthForOpenRouterClaude(request.body.messages, cachingAtDepth);
@@ -954,18 +1057,6 @@ router.post('/generate', jsonParser, function (request, response) {
954 apiKey = readSecret(request.user.directories, SECRET_KEYS.BLOCKENTROPY);1057 apiKey = readSecret(request.user.directories, SECRET_KEYS.BLOCKENTROPY);
955 headers = {};1058 headers = {};
956 bodyParams = {};1059 bodyParams = {};
957 } else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.DEEPSEEK) {
958 apiUrl = API_DEEPSEEK;
959 apiKey = readSecret(request.user.directories, SECRET_KEYS.DEEPSEEK);
960 headers = {};
961 bodyParams = {};
962
963 if (request.body.logprobs > 0) {
964 bodyParams['top_logprobs'] = request.body.logprobs;
965 bodyParams['logprobs'] = true;
966 }
967
968 request.body.messages = postProcessPrompt(request.body.messages, 'deepseek', getPromptNames(request));
969 } else {1060 } else {
970 console.log('This chat completion source is not supported yet.');1061 console.log('This chat completion source is not supported yet.');
971 return response.status(400).send({ error: true });1062 return response.status(400).send({ error: true });
@@ -1103,4 +1194,3 @@ router.post('/generate', jsonParser, function (request, response) {
1103 }1194 }
1104 }1195 }
1105});1196});
1106
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.log(request.body);863 console.log(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.log('avatar URL not found in request body');1173 console.log('avatar URL not found in request body');
@@ -1207,7 +1214,7 @@ router.post('/duplicate', jsonParser, async function (request, response) {
1207 }1214 }
1208});1215});
12091216
1210router.post('/export', jsonParser, async function (request, response) {1217router.post('/export', jsonParser, validateAvatarUrlMiddleware, async function (request, response) {
1211 try {1218 try {
1212 if (!request.body.format || !request.body.avatar_url) {1219 if (!request.body.format || !request.body.avatar_url) {
1213 return response.sendStatus(400);1220 return response.sendStatus(400);
src/endpoints/chats.js+8 -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,
@@ -294,7 +295,7 @@ function importRisuChat(userName, characterName, jsonData) {
294295
295export const router = express.Router();296export const router = express.Router();
296297
297router.post('/save', jsonParser, function (request, response) {298router.post('/save', jsonParser, validateAvatarUrlMiddleware, function (request, response) {
298 try {299 try {
299 const directoryName = String(request.body.avatar_url).replace('.png', '');300 const directoryName = String(request.body.avatar_url).replace('.png', '');
300 const chatData = request.body.chat;301 const chatData = request.body.chat;
@@ -310,7 +311,7 @@ router.post('/save', jsonParser, function (request, response) {
310 }311 }
311});312});
312313
313router.post('/get', jsonParser, function (request, response) {314router.post('/get', jsonParser, validateAvatarUrlMiddleware, function (request, response) {
314 try {315 try {
315 const dirName = String(request.body.avatar_url).replace('.png', '');316 const dirName = String(request.body.avatar_url).replace('.png', '');
316 const directoryPath = path.join(request.user.directories.chats, dirName);317 const directoryPath = path.join(request.user.directories.chats, dirName);
@@ -347,7 +348,7 @@ router.post('/get', jsonParser, function (request, response) {
347});348});
348349
349350
350router.post('/rename', jsonParser, async function (request, response) {351router.post('/rename', jsonParser, validateAvatarUrlMiddleware, async function (request, response) {
351 if (!request.body || !request.body.original_file || !request.body.renamed_file) {352 if (!request.body || !request.body.original_file || !request.body.renamed_file) {
352 return response.sendStatus(400);353 return response.sendStatus(400);
353 }354 }
@@ -372,7 +373,7 @@ router.post('/rename', jsonParser, async function (request, response) {
372 return response.send({ ok: true, sanitizedFileName });373 return response.send({ ok: true, sanitizedFileName });
373});374});
374375
375router.post('/delete', jsonParser, function (request, response) {376router.post('/delete', jsonParser, validateAvatarUrlMiddleware, function (request, response) {
376 const dirName = String(request.body.avatar_url).replace('.png', '');377 const dirName = String(request.body.avatar_url).replace('.png', '');
377 const fileName = String(request.body.chatfile);378 const fileName = String(request.body.chatfile);
378 const filePath = path.join(request.user.directories.chats, dirName, sanitize(fileName));379 const filePath = path.join(request.user.directories.chats, dirName, sanitize(fileName));
@@ -388,7 +389,7 @@ router.post('/delete', jsonParser, function (request, response) {
388 return response.send('ok');389 return response.send('ok');
389});390});
390391
391router.post('/export', jsonParser, async function (request, response) {392router.post('/export', jsonParser, validateAvatarUrlMiddleware, async function (request, response) {
392 if (!request.body.file || (!request.body.avatar_url && request.body.is_group === false)) {393 if (!request.body.file || (!request.body.avatar_url && request.body.is_group === false)) {
393 return response.sendStatus(400);394 return response.sendStatus(400);
394 }395 }
@@ -478,7 +479,7 @@ router.post('/group/import', urlencodedParser, function (request, response) {
478 }479 }
479});480});
480481
481router.post('/import', urlencodedParser, function (request, response) {482router.post('/import', urlencodedParser, validateAvatarUrlMiddleware, function (request, response) {
482 if (!request.body) return response.sendStatus(400);483 if (!request.body) return response.sendStatus(400);
483484
484 const format = request.body.file_type;485 const format = request.body.file_type;
@@ -626,7 +627,7 @@ router.post('/group/save', jsonParser, (request, response) => {
626 return response.send({ ok: true });627 return response.send({ ok: true });
627});628});
628629
629router.post('/search', jsonParser, function (request, response) {630router.post('/search', jsonParser, validateAvatarUrlMiddleware, function (request, response) {
630 try {631 try {
631 const { query, avatar_url, group_id } = request.body;632 const { query, avatar_url, group_id } = request.body;
632 let chatFiles = [];633 let chatFiles = [];
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
@@ -871,3 +871,14 @@ export class MemoryLimitedMap {
871 return this.map[Symbol.iterator]();871 return this.map[Symbol.iterator]();
872 }872 }
873}873}
874
875/**
876 * A 'safe' version of `fs.readFileSync()`. Returns the contents of a file if it exists, falling back to a default value if not.
877 * @param {string} filePath Path of the file to be read.
878 * @param {Parameters<typeof fs.readFileSync>[1]} options Options object to pass through to `fs.readFileSync()` (default: `{ encoding: 'utf-8' }`).
879 * @returns The contents at `filePath` if it exists, or `null` if not.
880 */
881export function safeReadFileSync(filePath, options = { encoding: 'utf-8' }) {
882 if (fs.existsSync(filePath)) return fs.readFileSync(filePath, options);
883 return null;
884}