Merge branch 'staging' into staging-3071-v2

552a418bae064f6eaad47dcba09bae389f22efa4

Eradev <admin@eradev.com>

74 files changed, +1549 -282Showing whitespace changes
.github/readme.md+1 -1
@@ -274,7 +274,7 @@ You will need two mandatory directory mappings and a port mapping to allow Silly
2742741. Open your Command Line
2752752. Run the following command
276276
277277`docker createrun --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
279279> 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
4545/vectors/
4646/cache/
4747public/css/user.css
48+public/error/
4849/plugins/
4950/data
5051/default/scaffold
@@ -52,3 +53,5 @@ public/scripts/extensions/third-party
5253/certs
5354.aider*
5455.env
56+/StartDev.bat
57+
default/config.yaml+16 -14
@@ -72,7 +72,7 @@ minLogLevel: 0
7272## Set to a positive number to expire session after a certain time of inactivity
7373## Set to 0 to expire session when the browser is closed
7474## Set to a negative number to disable session expiration
7575sessionTimeout: 86400-1
7676# Used to sign session cookies. Will be auto-generated if not set
7777cookieSecret: ''
7878# Disable CSRF protection - NOT RECOMMENDED
@@ -135,24 +135,26 @@ whitelistImportDomains:
135135## headers:
136136## User-Agent: "Googlebot/2.1 (+http://www.google.com/bot.html)"
137137requestOverrides: []
138-# -- EXTENSIONS CONFIGURATION --
138+
139+# EXTENSIONS CONFIGURATION
140+extensions:
139141 # Enable UI extensions
140142enableExtensions enabled: true
141143 # Automatically update extensions when a release version changes
142144enableExtensionsAutoUpdate autoUpdate: true
145+ models:
146+ # Enables automatic model download from HuggingFace
147+ autoDownload: true
148+ # Additional models for extensions. Expects model IDs from HuggingFace model hub in ONNX format
149+ classification: Cohee/distilbert-base-uncased-go-emotions-onnx
150+ captioning: Xenova/vit-gpt2-image-captioning
151+ embedding: Cohee/jina-embeddings-v2-base-en
152+ speechToText: Xenova/whisper-small
153+ textToSpeech: Xenova/speecht5_tts
154+
143155# Additional model tokenizers can be downloaded on demand.
144156# Disabling will fallback to another locally available tokenizer.
145157enableDownloadableTokenizers: true
146-# Extension settings
147-extras:
148- # Disables automatic model download from HuggingFace
149- disableAutoDownload: false
150- # Extra models for plugins. Expects model IDs from HuggingFace model hub in ONNX format
151- classificationModel: Cohee/distilbert-base-uncased-go-emotions-onnx
152- captioningModel: Xenova/vit-gpt2-image-captioning
153- embeddingModel: Cohee/jina-embeddings-v2-base-en
154- speechToTextModel: Xenova/whisper-small
155- textToSpeechModel: Xenova/speecht5_tts
156158# -- OPENAI CONFIGURATION --
157159# A placeholder message to use in strict prompt post-processing mode when the prompt doesn't start with a user message
158160promptPlaceholder: "[Start a new chat]"
default/content/backgrounds/_black.jpg+0 -0

Binary file

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

Binary file

default/content/index.json+8 -0
@@ -782,5 +782,13 @@
782782 {
783783 "filename": "presets/context/Mistral V7.json",
784784 "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"
785793 }
786794]
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 @@
11import { UserDirectoryList, User } from "./src/users";
2+import { CsrfSyncedToken } from "csrf-sync";
23
34declare 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+
422 namespace Express {
523 export interface Request {
624 user: {
@@ -15,11 +33,3 @@ declare global {
1533 */
1634 var DATA_ROOT: string;
1735}
18-
19-declare 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 @@
1515 "**/node_modules/**",
1616 "**/dist/**",
1717 "**/.git/**",
1818 "public/lib/**",
1919 "backups/**",
2020 "data/**",
2121 "cache/**",
package-lock.json+7 -7
@@ -1,12 +1,12 @@
11{
22 "name": "sillytavern",
33 "version": "1.12.1011",
44 "lockfileVersion": 3,
55 "requires": true,
66 "packages": {
77 "": {
88 "name": "sillytavern",
99 "version": "1.12.1011",
1010 "hasInstallScript": true,
1111 "license": "AGPL-3.0",
1212 "dependencies": {
@@ -26,7 +26,7 @@
2626 "cookie-parser": "^1.4.6",
2727 "cookie-session": "^2.1.0",
2828 "cors": "^2.8.5",
2929 "csrf-csrfsync": "^24.20.3",
3030 "diff-match-patch": "^1.0.5",
3131 "dompurify": "^3.1.7",
3232 "droll": "^0.2.1",
@@ -2987,10 +2987,10 @@
29872987 "node": "*"
29882988 }
29892989 },
29902990 "node_modules/csrf-csrfsync": {
29912991 "version": "24.20.43",
29922992 "resolved": "https://registry.npmjs.org/csrf-csrfsync/-/csrf-csrfsync-24.20.43.tgz",
29932993 "integrity": "sha512-LuhBmy5RfRmEfeqeYqgaAuS1eDpVtKZB/Eiec9xiKQLBynJxrGVRdM2yRTwXzltBBzt/YMl1Njo7imzDt6ZT7G/yKh2L9AYsIwSlTPnx2AaxQG7jo4Sm0uXDUzFY8hR59qhDHdjqpW2hojS4oAVIZDzwlMQloIVCTJoDDh0wwA==",
29942994 "license": "ISC",
29952995 "dependencies": {
29962996 "http-errors": "^2.0.0"
package.json+2 -2
@@ -16,7 +16,7 @@
1616 "cookie-parser": "^1.4.6",
1717 "cookie-session": "^2.1.0",
1818 "cors": "^2.8.5",
1919 "csrf-csrfsync": "^24.20.3",
2020 "diff-match-patch": "^1.0.5",
2121 "dompurify": "^3.1.7",
2222 "droll": "^0.2.1",
@@ -86,7 +86,7 @@
8686 "type": "git",
8787 "url": "https://github.com/SillyTavern/SillyTavern.git"
8888 },
8989 "version": "1.12.1011",
9090 "scripts": {
9191 "start": "node server.js",
9292 "start:deno": "deno run --allow-run --allow-net --allow-read --allow-write --allow-sys --allow-env server.js",
post-install.js+91 -11
@@ -64,6 +64,46 @@ const keyMigrationMap = [
6464 newKey: 'backups.chat.throttleInterval',
6565 migrate: (value) => value,
6666 },
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+ },
67107];
68108
69109/**
@@ -73,7 +113,7 @@ const keyMigrationMap = [
73113 * @returns {string[]} Array of all keys in the object
74114 */
75115function getAllKeys(obj, prefix = '') {
76116 if (typeof obj !== 'object' || Array.isArray(obj) || obj === null) {
77117 return [];
78118 }
79119
@@ -173,20 +213,60 @@ function addMissingConfigValues() {
173213 * Creates the default config files if they don't exist yet.
174214 */
175215function 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) {
182239 try {
183240 if (!fsdefaultItem.existsSync(type === 'file)') {
184- const defaultFilePath = path.join('./default', path.parse(file).base);
241+ if (!fs.existsSync(defaultItem.productionPath)) {
185242 fs.copyFileSync(defaultFilePath, file);
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+ );
187262 }
188263 } 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+ );
190270 }
191271 }
192272}
public/img/vllm.svg+1 -0
@@ -0,0 +1 @@
1+<svg version="1.0" width="565.752" height="586.537" viewBox="0 0 424.314 439.903" preserveAspectRatio="xMidYMid" xmlns="http://www.w3.org/2000/svg"><path d="M344.6 41.703c-43.4 23-79.3 42.4-79.7 43.2-.8 1.4-72.3 276.1-84.3 323.5-6.2 24.9-7.1 27.7-8 25.5-.4-.8-.5-77.7-.4-170.8l.3-169.2H86.3c-47.5 0-86.3.2-86.3.5 0 1 162.2 325 166.9 333.3 2.5 4.6 5 9.2 5.6 10.3.9 1.8 2.7 1.9 68.2 1.9 63.1 0 67.2-.1 67.7-1.8.3-.9 24.2-90.8 53-199.7 51-192.7 58.9-222.4 62-233.3.8-2.8 1.1-5.2.8-5.1-.4 0-36.2 18.8-79.6 41.7z"/></svg>
1 \ No newline at end of file
public/index.html+62 -11
@@ -1977,12 +1977,12 @@
19771977 </span>
19781978 </div>
19791979 </div>
19801980 <div class="range-block" data-source="makersuite,deepseek,openrouter">
19811981 <label for="openai_show_thoughts" class="checkbox_label widthFreeExpand">
19821982 <input id="openai_show_thoughts" type="checkbox" />
19831983 <span>
19841984 <span data-i18n="Show model thoughtsreasoning">Show model thoughtsreasoning</span>
19851985 <i class="opacity50p fa-solid fa-circle-info" title="Gemini 2.0 Thinking / DeepSeek Reasoner"></i>
19861986 </span>
19871987 </label>
19881988 <div class="toggle-description justifyLeft marginBot5">
@@ -2692,7 +2692,7 @@
26922692 <option value="windowai">Window AI</option>
26932693 </optgroup>
26942694 </select>
26952695 <div class="inline-drawer wide100p" data-source="openai,claude,mistralai,makersuite,deepseek">
26962696 <div class="inline-drawer-toggle inline-drawer-header">
26972697 <b data-i18n="Reverse Proxy">Reverse Proxy</b>
26982698 <div class="fa-solid fa-circle-chevron-down inline-drawer-icon down"></div>
@@ -2755,7 +2755,7 @@
27552755 </div>
27562756 </div>
27572757 </div>
27582758 <div id="ReverseProxyWarningMessage" data-source="openai,claude,mistralai,makersuite,deepseek">
27592759 <div class="reverse_proxy_warning">
27602760 <b>
27612761 <div data-i18n="Using a proxy that you're not running yourself is a risk to your data privacy.">
@@ -3062,7 +3062,9 @@
30623062 <option value="gemini-1.0-ultra-latest">Gemini 1.0 Ultra</option>
30633063 </optgroup>
30643064 <optgroup label="Subversions">
30653065 <option value="gemini-2.0-flash-thinking-exp-1219">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>
30663068 <option value="gemini-2.0-flash-exp">Gemini 2.0 Flash Experimental</option>
30673069 <option value="gemini-exp-1114">Gemini Experimental 2024-11-14</option>
30683070 <option value="gemini-exp-1121">Gemini Experimental 2024-11-21</option>
@@ -3209,6 +3211,7 @@
32093211 <select id="model_deepseek_select">
32103212 <option value="deepseek-chat">deepseek-chat</option>
32113213 <option value="deepseek-coder">deepseek-coder</option>
3214+ <option value="deepseek-reasoner">deepseek-reasoner</option>
32123215 </select>
32133216 </div>
32143217 </div>
@@ -3797,6 +3800,39 @@
37973800 </div>
37983801 </div>
37993802 <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>
38003836 <h4 class="standoutHeader" data-i18n="Miscellaneous">Miscellaneous</h4>
38013837 <div>
38023838 <small>
@@ -6218,14 +6254,26 @@
62186254 <div class="mes_edit_buttons">
62196255 <div class="mes_edit_done menu_button fa-solid fa-check" title="Confirm" data-i18n="[title]Confirm"></div>
62206256 <div class="mes_edit_copy menu_button fa-solid fa-copy" title="Copy this message" data-i18n="[title]Copy this message"></div>
62216257 <div class="mes_edit_deletemes_edit_add_reasoning menu_button fa-solid fa-trash-canlightbulb" title="DeleteAdd thisa messagereasoning block" data-i18n="[title]DeleteAdd thisa messagereasoning 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>
62236259 <div class="mes_edit_up menu_button fa-solid fa-chevron-up " title="Move message up" data-i18n="[title]Move message up"></div>
62246260 <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>
62266261 <div class="mes_edit_cancel menu_button fa-solid fa-xmark" title="Cancel" data-i18n="[title]Cancel"></div>
62276262 </div>
62286263 </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>
62296277 <div class="mes_text"></div>
62306278 <div class="mes_img_container">
62316279 <div class="mes_img_controls">
@@ -6325,7 +6373,10 @@
63256373 <img alt="Avatar" src="" />
63266374 </div>
63276375 <div class="group_member_name">
63286376 <div class="ch_namecharacter_name_block"></div>
6377+ <span class="ch_name"></span>
6378+ <small class="ch_additional_info character_version"></small>
6379+ </div>
63296380 <div class="tags tags_inline"></div>
63306381 </div>
63316382 <input class="ch_fav" value="" hidden />
public/locales/ar-sa.json+1 -1
@@ -1376,7 +1376,7 @@
13761376 "char_import_2": "Chub Lorebook (رابط مباشر أو معرف)",
13771377 "char_import_3": "حرف JanitorAI (رابط مباشر أو UUID)",
13781378 "char_import_4": "حرف Pygmalion.chat (رابط مباشر أو UUID)",
13791379 "char_import_5": "حرف AICharacterCardAICharacterCards.com (رابط مباشر أو معرف)",
13801380 "char_import_6": "رابط PNG المباشر (راجع",
13811381 "char_import_7": "للمضيفين المسموح بهم)",
13821382 "char_import_8": "شخصية RisuRealm (رابط مباشر)",
public/locales/de-de.json+1 -1
@@ -1376,7 +1376,7 @@
13761376 "char_import_2": "Chub Lorebook (Direktlink oder ID)",
13771377 "char_import_3": "JanitorAI-Charakter (Direktlink oder UUID)",
13781378 "char_import_4": "Pygmalion.chat-Charakter (Direktlink oder UUID)",
13791379 "char_import_5": "AICharacterCardAICharacterCards.com-Charakter (Direktlink oder ID)",
13801380 "char_import_6": "Direkter PNG-Link (siehe",
13811381 "char_import_7": "für erlaubte Hosts)",
13821382 "char_import_8": "RisuRealm-Charakter (Direktlink)",
public/locales/es-es.json+1 -1
@@ -1376,7 +1376,7 @@
13761376 "char_import_2": "Chub Lorebook (enlace directo o ID)",
13771377 "char_import_3": "Carácter de JanitorAI (enlace directo o UUID)",
13781378 "char_import_4": "Carácter Pygmalion.chat (enlace directo o UUID)",
13791379 "char_import_5": "Carácter AICharacterCardAICharacterCards.com (enlace directo o ID)",
13801380 "char_import_6": "Enlace PNG directo (consulte",
13811381 "char_import_7": "para hosts permitidos)",
13821382 "char_import_8": "Personaje RisuRealm (Enlace directo)",
public/locales/fr-fr.json+2 -2
@@ -1297,7 +1297,7 @@
12971297 "char_import_2": "Lorebook de Chub (lien direct ou ID)",
12981298 "char_import_3": "Personnage de JanitorAI (lien direct ou UUID)",
12991299 "char_import_4": "Personnage de Pygmalion.chat (lien direct ou UUID)",
13001300 "char_import_5": "Personnage de AICharacterCardAICharacterCards.com (lien direct ou identifiant)",
13011301 "char_import_6": "Lien PNG direct (voir",
13021302 "char_import_7": "pour les hôtes autorisés)",
13031303 "char_import_8": "Personnage de RisuRealm (lien direct)",
@@ -1385,7 +1385,7 @@
13851385 "enable_functions_desc_1": "Autorise l'utilisation",
13861386 "enable_functions_desc_2": "outils de fonction",
13871387 "enable_functions_desc_3": "Peut être utilisé par diverses extensions pour fournir des fonctionnalités supplémentaires.",
13881388 "Show model thoughtsreasoning": "Afficher les pensées du modèle",
13891389 "Display the model's internal thoughts in the response.": "Afficher les pensées internes du modèle dans la réponse.",
13901390 "Confirm token parsing with": "Confirmer l'analyse des tokens avec",
13911391 "openai_logit_bias_no_items": "Aucun élément",
public/locales/is-is.json+1 -1
@@ -1376,7 +1376,7 @@
13761376 "char_import_2": "Chub Lorebook (beinn hlekkur eða auðkenni)",
13771377 "char_import_3": "JanitorAI karakter (beinn hlekkur eða UUID)",
13781378 "char_import_4": "Pygmalion.chat karakter (beinn hlekkur eða UUID)",
13791379 "char_import_5": "AICharacterCardAICharacterCards.com Karakter (beinn hlekkur eða auðkenni)",
13801380 "char_import_6": "Beinn PNG hlekkur (sjá",
13811381 "char_import_7": "fyrir leyfilega gestgjafa)",
13821382 "char_import_8": "RisuRealm karakter (beinn hlekkur)",
public/locales/it-it.json+1 -1
@@ -1376,7 +1376,7 @@
13761376 "char_import_2": "Lorebook di Chub (collegamento diretto o ID)",
13771377 "char_import_3": "Carattere JanitorAI (collegamento diretto o UUID)",
13781378 "char_import_4": "Carattere Pygmalion.chat (collegamento diretto o UUID)",
13791379 "char_import_5": "Carattere AICharacterCardAICharacterCards.com (Link diretto o ID)",
13801380 "char_import_6": "Collegamento PNG diretto (fare riferimento a",
13811381 "char_import_7": "per gli host consentiti)",
13821382 "char_import_8": "Personaggio RisuRealm (collegamento diretto)",
public/locales/ja-jp.json+1 -1
@@ -1378,7 +1378,7 @@
13781378 "char_import_2": "Chub ロアブック (直接リンクまたは ID)",
13791379 "char_import_3": "JanitorAI キャラクター (直接リンクまたは UUID)",
13801380 "char_import_4": "Pygmalion.chat キャラクター (直接リンクまたは UUID)",
13811381 "char_import_5": "AICharacterCardAICharacterCards.com キャラクター (直接リンクまたは ID)",
13821382 "char_import_6": "直接PNGリンク(参照",
13831383 "char_import_7": "許可されたホストの場合)",
13841384 "char_import_8": "RisuRealm キャラクター (直接リンク)",
public/locales/ko-kr.json+1 -1
@@ -1395,7 +1395,7 @@
13951395 "char_import_2": "Chub Lorebook(직접 링크 또는 ID)",
13961396 "char_import_3": "JanitorAI 캐릭터(직접 링크 또는 UUID)",
13971397 "char_import_4": "Pygmalion.chat 문자(직접 링크 또는 UUID)",
13981398 "char_import_5": "AICharacterCardAICharacterCards.com 캐릭터(직접 링크 또는 ID)",
13991399 "char_import_6": "직접 PNG 링크(참조",
14001400 "char_import_7": "허용된 호스트의 경우)",
14011401 "char_import_8": "RisuRealm 캐릭터 (직접링크)",
public/locales/nl-nl.json+1 -1
@@ -1376,7 +1376,7 @@
13761376 "char_import_2": "Chub Lorebook (directe link of ID)",
13771377 "char_import_3": "JanitorAI-personage (directe link of UUID)",
13781378 "char_import_4": "Pygmalion.chat-teken (directe link of UUID)",
13791379 "char_import_5": "AICharacterCardAICharacterCards.com-teken (directe link of ID)",
13801380 "char_import_6": "Directe PNG-link (zie",
13811381 "char_import_7": "voor toegestane hosts)",
13821382 "char_import_8": "RisuRealm-personage (directe link)",
public/locales/pt-pt.json+1 -1
@@ -1376,7 +1376,7 @@
13761376 "char_import_2": "Chub Lorebook (link direto ou ID)",
13771377 "char_import_3": "Personagem JanitorAI (Link Direto ou UUID)",
13781378 "char_import_4": "Caractere Pygmalion.chat (Link Direto ou UUID)",
13791379 "char_import_5": "Personagem AICharacterCardAICharacterCards.com (link direto ou ID)",
13801380 "char_import_6": "Link PNG direto (consulte",
13811381 "char_import_7": "para hosts permitidos)",
13821382 "char_import_8": "Personagem RisuRealm (link direto)",
public/locales/ru-ru.json+1 -1
@@ -966,7 +966,7 @@
966966 "char_import_2": "Лорбук с Chub (прямая ссылка или ID)",
967967 "char_import_3": "Персонаж с JanitorAI (прямая ссылка или UUID)",
968968 "char_import_4": "Персонаж с Pygmalion.chat (прямая ссылка или UUID)",
969969 "char_import_5": "Персонаж с AICharacterCardAICharacterCards.com (прямая ссылка или ID)",
970970 "char_import_6": "Прямая ссылка на PNG-файл (чтобы узнать список разрешённых хостов, загляните в",
971971 "char_import_7": ")",
972972 "Grammar String": "Грамматика",
public/locales/uk-ua.json+1 -1
@@ -1376,7 +1376,7 @@
13761376 "char_import_2": "Chub Lorebook (пряме посилання або ID)",
13771377 "char_import_3": "Символ JanitorAI (пряме посилання або UUID)",
13781378 "char_import_4": "Символ Pygmalion.chat (пряме посилання або UUID)",
13791379 "char_import_5": "Символ AICharacterCardAICharacterCards.com (пряме посилання або ідентифікатор)",
13801380 "char_import_6": "Пряме посилання на PNG (див",
13811381 "char_import_7": "для дозволених хостів)",
13821382 "char_import_8": "Персонаж RisuRealm (пряме посилання)",
public/locales/vi-vn.json+1 -1
@@ -1376,7 +1376,7 @@
13761376 "char_import_2": "Chub (Nhập URL trực tiếp hoặc ID)",
13771377 "char_import_3": "JanitorAI (Nhập URL trực tiếp hoặc UUID)",
13781378 "char_import_4": "Pygmalion.chat (Nhập URL trực tiếp hoặc UUID)",
13791379 "char_import_5": "AICharacterCardAICharacterCards.com (Nhập URL trực tiếp hoặc ID)",
13801380 "char_import_6": "Nhập PNG trực tiếp (tham khảo",
13811381 "char_import_7": "đối với các máy chủ được phép)",
13821382 "char_import_8": "RisuRealm (URL trực tiếp)",
public/locales/zh-cn.json+10 -10
@@ -266,7 +266,7 @@
266266 "Use system prompt": "使用系统提示词",
267267 "Merges_all_system_messages_desc_1": "合并所有系统消息,直到第一条具有非系统角色的消息,然后通过",
268268 "Merges_all_system_messages_desc_2": "字段发送。",
269269 "Show model thoughtsreasoning": "展示思维链",
270270 "Display the model's internal thoughts in the response.": "展示模型在回复时的内部思维链。",
271271 "Assistant Prefill": "AI预填",
272272 "Expand the editor": "展开编辑器",
@@ -1191,9 +1191,9 @@
11911191 "welcome_message_part_8": "您可随时通过",
11921192 "welcome_message_part_9": "图标来更改此设置。",
11931193 "Persona Name:": "用户角色名称:",
11941194 "Temporarily disable automatic replies from this character": "暂时禁用此角色的自动回复临时禁言此角色",
11951195 "Enable automatic replies from this character": "启用此角色的自动回复解除禁言此角色",
11961196 "Trigger a message from this character": "从此角色触发消息强制触发该角色发言",
11971197 "Move up": "向上移动",
11981198 "Move down": "向下移动",
11991199 "View character card": "查看角色卡片",
@@ -1829,7 +1829,7 @@
18291829 "char_import_2": "Chub 知识书(直链或ID)",
18301830 "char_import_3": "JanitorAI 角色(直链或UUID)",
18311831 "char_import_4": "Pygmalion.chat 角色(直链或UUID)",
18321832 "char_import_5": "AICharacterCardAICharacterCards.com 角色(直链或ID)",
18331833 "char_import_6": "被允许的PNG直链(请参阅",
18341834 "char_import_7": ")",
18351835 "char_import_8": "RisuRealm 角色(直链)",
@@ -1838,7 +1838,7 @@
18381838 "Enter the Git URL of the extension to install": "输入扩展程序的 Git URL 以安装",
18391839 "Disclaimer:": "免责声明:",
18401840 "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.": "使用外部的扩展程序可能存在意料外的副作用和安全隐患。在导入扩展程序前,请一定确认其来源可信。我们不为第三方扩展程序造成的任何损失负责。",
18411841 "Prompt Itemization": "将提示词分条提示词拆分",
18421842 "Show Raw Prompt": "显示原始提示词",
18431843 "Copy Prompt": "复制提示词",
18441844 "Show Prompt Differences": "显示提示词差异",
@@ -2045,8 +2045,8 @@
20452045 "Post a GitHub issue": "在 GitHub 发布问题",
20462046 "Contact the developers": "联系开发者",
20472047 "If you're connected to an API, try asking me something!": "若您已经配置好API,尝试发送些什么吧!",
20482048 "Title/Memo": "标题/备忘录(备忘)",
20492049 "Strategy": "Strategy触发策略",
20502050 "Position": "位置插入位置",
20512051 "Trigger %": "触发率 %触发概率%"
20522052}
public/locales/zh-tw.json+77 -11
@@ -28,14 +28,14 @@
2828 "Prose Augmenter": "散文增強器",
2929 "Text Adventure": "文字冒險",
3030 "response legth(tokens)": "回應長度(符元數)",
3131 "Streaming": "串流即時串流",
3232 "Streaming_desc": "生成時逐位顯示回應逐字顯示生成中的回應內容當此功能關閉時關閉時回應將在完成後一次顯示回應將在生成完成後一次性顯示。",
3333 "context size(tokens)": "上下文長度(符元數)",
3434 "unlocked": "解鎖",
3535 "Only enable this if your model supports context sizes greater than 8192 tokens": "僅在您的模型支援超過 8192 個符元的上下文長度時啟用此功能",
3636 "Max prompt cost:": "最大提示詞費用",
3737 "Display the response bit by bit as it is generated.": "生成時逐位顯示回應逐字顯示生成中的回應內容。",
3838 "When this is off, responses will be displayed all at once when they are complete.": "關閉時,回應將在完成後一次性顯示回應將在生成完成後一次性顯示。",
3939 "Temperature": "溫度",
4040 "rep.pen": "重複懲罰",
4141 "Rep. Pen. Range.": "重複懲罰範圍",
@@ -880,7 +880,7 @@
880880 "popup-button-no": "取消",
881881 "popup-button-cancel": "關閉",
882882 "popup-button-import": "匯入",
883883 "Advanced Definitions": "- 進階定義",
884884 "Prompt Overrides": "提示詞覆寫",
885885 "(For Chat Completion and Instruct Mode)": "(用於聊天補全和指令模式)",
886886 "Insert {{original}} into either box to include the respective default prompt from system settings.": "在任一框中插入 {{original}} 以包含系統設定中的預設提示詞。",
@@ -1381,7 +1381,7 @@
13811381 "char_import_2": "Chub Lorebook(直接連結或 ID)",
13821382 "char_import_3": "JanitorAI 角色(直接連結或 ID)",
13831383 "char_import_4": "Pygmalion.chat 角色(直接連結或 ID)",
13841384 "char_import_5": "AICharacterCardAICharacterCards.com 角色(直接連結或 ID)",
13851385 "char_import_6": "直接 PNG 連結(請參閱",
13861386 "char_import_7": "對於允許的主機)",
13871387 "char_import_8": "RisuRealm角色(直接連結)",
@@ -1745,7 +1745,7 @@
17451745 "Sprite Folder Override": "表情立繪資料夾覆蓋",
17461746 "Sprite set:": "立繪組:",
17471747 "Show Gallery": "查看圖庫",
17481748 "Sticky": "固定黏性",
17491749 "Style Preset": "預設樣式",
17501750 "Summarize chat messages for vector generation": "摘要聊天訊息以進行向量化處理",
17511751 "Summarize chat messages when sending": "傳送時摘要聊天內容",
@@ -2124,7 +2124,7 @@
21242124 "from other websites": "以從其他網站新增。",
21252125 "Go to the": "前往",
21262126 "to install additional features.": "以安裝更多功能。",
21272127 "If you're connected to an API, try asking me something!": "若您已連接若您已連線 API,嘗試問我一些問題吧!",
21282128 "Title/Memo": "標題/備註",
21292129 "Strategy": "插入策略",
21302130 "Position": "位置",
@@ -2357,7 +2357,7 @@
23572357 "Forbid": "禁止",
23582358 "Aphrodite only. Determines the order of samplers. Skew is always applied post-softmax, so it's not included here.": "僅限 Aphrodite 使用。決定採樣器的順序。偏移總是在 softmax 後應用,因此不包括在此。",
23592359 "Aphrodite only. Determines the order of samplers.": "僅限 Aphrodite 使用。決定採樣器的順序。",
23602360 "Show model thoughtsreasoning": "顯示模型思維鏈",
23612361 "Display the model's internal thoughts in the response.": "在回應中顯示模型的思維鏈(內部思考過程)。",
23622362 "Generic (OpenAI-compatible) [LM Studio, LiteLLM, etc.]": "通用(兼容 OpenAI)[LM Studio, LiteLLM 等]",
23632363 "Model ID (optional)": "模型 ID(可選)",
@@ -2386,5 +2386,71 @@
23862386 "Your preset contains proxy and/or custom endpoint settings.": "此預設包含代理和/或自訂端點設定。",
23872387 "Do you want to remove these fields before exporting?": "是否要在匯出前移除這些欄位?",
23882388 "Delete the preset? This action is irreversible and your current settings will be overwritten.": "確定刪除此預設?刪除後無法復原,且此設定將被覆蓋。",
23892389 "Update all": "全部更新",
2390+ "Automatically chooses an alternative provider if chosen providers can't serve your request.": "當所選提供者無法滿足您的請求時,自動選擇替代提供者。",
2391+ "Use extension settings": "使用擴充功能設定",
2392+ "To use instruct formatting, switch to OpenRouter under Text Completion API.": "若要使用指令格式,請在文本補全 API 下切換至 OpenRouter。",
2393+ "Automatically 'continue' a response if the model stopped before reaching a certain amount of tokens.": "如果模型在達到一定數量的符元前停止,則自動繼續生成回應。",
2394+ "Toggle entry's active state.": "切換條目的啟用狀態。",
2395+ "Non-sticky": "無黏性",
2396+ "No cooldown": "無冷卻時間",
2397+ "No delay": "無延遲",
2398+ "Included settings:": "包含設定:",
2399+ "Click on the setting name to omit it from the profile.": "點擊設定名稱以從設定檔中省略。",
2400+ "Tints the chat background and/or character sprites.": "調整聊天背景或角色圖片的色調。",
2401+ "Only chunk on custom boundary": "僅在自定邊界進行分塊(chunk)",
2402+ "help_macros_firstDisplayedMessageId": "載入到可見聊天中的第一則訊息的 ID。",
2403+ "Couldn't import tags:": "無法匯入標籤:",
2404+ "Select providers. No selection = all providers.": "選擇供應商。未選擇=所有供應商。",
2405+ "Select a model": "選擇模型",
2406+ "Search models...": "搜尋模型⋯",
2407+ "[Currently loaded]": "[當前加載]",
2408+ "Search providers...": "搜尋供應商⋯",
2409+ "No-sticky": "無固定",
2410+ "Create a new World Info": "建立新世界資訊",
2411+ "Enter a name for the new file:": "輸入新檔案的名稱:",
2412+ "Valid World Info file name is required": "需要有效的世界資訊檔案名稱",
2413+ "World Info file has an invalid format": "世界資訊檔案格式無效",
2414+ "World Info file has no entries": "世界資訊檔案中無任何條目",
2415+ "Character not found.": "未找到該角色。",
2416+ "Open a chat to get a name of the chat-bound lorebook": "開啟聊天以取得綁定聊天的知識書名稱。",
2417+ "File is not valid: ${0}": "檔案無效或格式不正確:${0}",
2418+ "The world with ${0} is invalid or corrupted.": "世界 ${0} 無效或已損壞。",
2419+ "Deactivated all worlds": "已停用所有世界",
2420+ "No world found named: ${0}": "未找到名為 ${0} 的世界",
2421+ "Activated world: ${0}": "已啟用世界:${0}",
2422+ "Deactivated world: ${0}": "已停用世界:${0}",
2423+ "World was not active: ${0}": "世界 ${0} 未啟用",
2424+ "The world '${0}' has been imported and linked to the character successfully.": "世界「${0}」已成功匯入並綁定至角色。",
2425+ "World/Lorebook imported": "世界/知識書已匯入",
2426+ "Are you sure you want to import '${0}'?": "確定要匯入「${0}」嗎?",
2427+ "Built-in Extensions:": "內建擴充功能:",
2428+ "Installed Extensions:": "已安裝的擴充功能:",
2429+ "Loading third-party extensions... Please wait...": "正在載入第三方擴充功能,請稍候⋯",
2430+ "The page will be reloaded shortly...": "頁面即將重新載入⋯",
2431+ "Extensions state changed": "擴充功能狀態已更改",
2432+ "Error loading extensions. See browser console for details.": "載入擴充功能時出現錯誤。詳情請查看瀏覽器控制台。",
2433+ "You don't have permission to update global extensions.": "您無權更新全域擴充功能。",
2434+ "Extension update failed": "擴充功能更新失敗",
2435+ "Extension ${0} updated to ${1}": "擴充功能 ${0} 已更新至 ${1}",
2436+ "Reload the page to apply updates": "重新加載頁面以使用更新",
2437+ "You don't have permission to delete global extensions.": "您無權刪除全域擴充功能。",
2438+ "Are you sure you want to delete ${0}?": "確定要刪除 ${0} 嗎?",
2439+ "You don't have permission to move extensions.": "您無權移動擴充功能。",
2440+ "Are you sure you want to move ${0} to your local extensions? This will make it available only for you.": "確定要將 ${0} 移至本地擴充功能嗎?此後僅您可使用。",
2441+ "Are you sure you want to move ${0} to the global extensions? This will make it available for all users.": "確定要將 ${0} 移至全域擴充功能嗎?此後所有使用者皆可使用。",
2442+ "Extension ${0} moved.": "擴充功能 ${0} 已移動。",
2443+ "Extension ${0} deleted": "擴充功能 ${0} 已刪除。",
2444+ "Please wait...": "請稍候⋯",
2445+ "Installing extension": "正在安裝擴充功能",
2446+ "Extension installation failed": "擴充功能安裝失敗",
2447+ "Extension '${0}' by ${1} (version ${2}) has been installed successfully!": "擴充功能 '${0}' 由 ${1} 提供(版本 ${2})已成功安裝!",
2448+ "Extension installation successful": "擴充功能安裝成功",
2449+ "Extension updates available": "有可用的擴充功能更新",
2450+ "Auto-updating extensions. This may take several minutes.": "正在自動更新擴充功能。這可能需要幾分鐘時間。",
2451+ "Install": "安裝",
2452+ "Modules provided by your Extras API:": "由您的 Extras API 提供的模組:",
2453+ "Not connected to the API!": "未連線到 API!",
2454+ "ext_type_system": "這是內建的擴充功能,無法刪除,且會跟隨系統更新。",
2455+ "Valid": "已驗證"
23902456}
public/script.js+140 -39
@@ -168,6 +168,7 @@ import {
168168 isTrueBoolean,
169169 toggleDrawer,
170170 isElementInViewport,
171+ copyText,
171172} from './scripts/utils.js';
172173import { debounce_timeout } from './scripts/constants.js';
173174
@@ -266,6 +267,7 @@ import { initSettingsSearch } from './scripts/setting-search.js';
266267import { initBulkEdit } from './scripts/bulk-edit.js';
267268import { deriveTemplatesFromChatTemplate } from './scripts/chat-templates.js';
268269import { getContext } from './scripts/st-context.js';
270+import { initReasoning, PromptReasoning } from './scripts/reasoning.js';
269271
270272// API OBJECT FOR EXTERNAL WIRING
271273globalThis.SillyTavern = {
@@ -442,6 +444,7 @@ export const event_types = {
442444 MESSAGE_DELETED: 'message_deleted',
443445 MESSAGE_UPDATED: 'message_updated',
444446 MESSAGE_FILE_EMBEDDED: 'message_file_embedded',
447+ MORE_MESSAGES_LOADED: 'more_messages_loaded',
445448 IMPERSONATE_READY: 'impersonate_ready',
446449 CHAT_CHANGED: 'chat_id_changed',
447450 GENERATION_AFTER_COMMANDS: 'GENERATION_AFTER_COMMANDS',
@@ -722,6 +725,7 @@ async function getSystemMessages() {
722725 is_user: false,
723726 is_system: true,
724727 mes: await renderTemplateAsync('assistantNote'),
728+ uses_system_ui: true,
725729 extra: {
726730 isSmallSys: true,
727731 },
@@ -979,6 +983,7 @@ async function firstLoadInit() {
979983 initServerHistory();
980984 initSettingsSearch();
981985 initBulkEdit();
986+ initReasoning();
982987 await initScrapers();
983988 doDailyExtensionUpdatesCheck();
984989 await hideLoader();
@@ -1828,7 +1833,7 @@ export async function replaceCurrentChat() {
18281833 }
18291834}
18301835
18311836export async function showMoreMessages(messagesToLoad = null) {
18321837 const firstDisplayedMesId = $('#chat').children('.mes').first().attr('mesid');
18331838 let messageId = Number(firstDisplayedMesId);
18341839 let count = messagesToLoad || power_user.chat_truncation || Number.MAX_SAFE_INTEGER;
@@ -1858,6 +1863,8 @@ export function showMoreMessages(messagesToLoad = null) {
18581863 const newHeight = $('#chat').prop('scrollHeight');
18591864 $('#chat').scrollTop(newHeight - prevHeight);
18601865 }
1866+
1867+ await eventSource.emit(event_types.MORE_MESSAGES_LOADED);
18611868}
18621869
18631870export async function printMessages() {
@@ -2195,6 +2202,7 @@ function getMessageFromTemplate({
21952202 isUser,
21962203 avatarImg,
21972204 bias,
2205+ reasoning,
21982206 isSystem,
21992207 title,
22002208 timerValue,
@@ -2219,6 +2227,7 @@ function getMessageFromTemplate({
22192227 mes.find('.avatar img').attr('src', avatarImg);
22202228 mes.find('.ch_name .name_text').text(characterName);
22212229 mes.find('.mes_bias').html(bias);
2230+ mes.find('.mes_reasoning').html(reasoning);
22222231 mes.find('.timestamp').text(timestamp).attr('title', `${extra?.api ? extra.api + ' - ' : ''}${extra?.model ?? ''}`);
22232232 mes.find('.mesIDDisplay').text(`#${mesId}`);
22242233 tokenCount && mes.find('.tokenCounterDisplay').text(`${tokenCount}t`);
@@ -2233,10 +2242,16 @@ function getMessageFromTemplate({
22332242 return mes;
22342243}
22352244
2245+/**
2246+ * Re-renders a message block with updated content.
2247+ * @param {number} messageId Message ID
2248+ * @param {object} message Message object
2249+ */
22362250export function updateMessageBlock(messageId, message) {
22372251 const messageElement = $(`#chat [mesid="${messageId}"]`);
22382252 const text = message?.extra?.display_text ?? message.mes;
22392253 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));
22402255 addCopyToCodeBlocks(messageElement);
22412256 appendMediaToMessage(message, messageElement);
22422257}
@@ -2315,18 +2330,17 @@ export function addCopyToCodeBlocks(messageElement) {
23152330 const codeBlocks = $(messageElement).find('pre code');
23162331 for (let i = 0; i < codeBlocks.length; i++) {
23172332 hljs.highlightElement(codeBlocks.get(i));
2318- if (navigator.clipboard !== undefined) {
23192333 const copyButton = document.createElement('i');
23202334 copyButton.classList.add('fa-solid', 'fa-copy', 'code-copy', 'interactable');
23212335 copyButton.title = 'Copy code';
23222336 codeBlocks.get(i).appendChild(copyButton);
23232337 copyButton.addEventListener('pointerup', async function (event) {
23242338 navigator.clipboard.writeText( const text = codeBlocks.get(i).innerText);
2339+ await copyText(text);
23252340 toastr.info(t`Copied!`, '', { timeOut: 2000 });
23262341 });
23272342 }
23282343}
2329-}
23302344
23312345
23322346/**
@@ -2396,6 +2410,7 @@ export function addOneMessage(mes, { type = 'normal', insertAfter = null, scroll
23962410 sanitizerOverrides,
23972411 );
23982412 const bias = messageFormatting(mes.extra?.bias ?? '', '', false, false, -1);
2413+ const reasoning = messageFormatting(mes.extra?.reasoning ?? '', '', false, false, -1);
23992414 let bookmarkLink = mes?.extra?.bookmark_link ?? '';
24002415
24012416 let params = {
@@ -2405,6 +2420,7 @@ export function addOneMessage(mes, { type = 'normal', insertAfter = null, scroll
24052420 isUser: mes.is_user,
24062421 avatarImg: avatarImg,
24072422 bias: bias,
2423+ reasoning: reasoning,
24082424 isSystem: isSystem,
24092425 title: title,
24102426 bookmarkLink: bookmarkLink,
@@ -2464,6 +2480,7 @@ export function addOneMessage(mes, { type = 'normal', insertAfter = null, scroll
24642480 const swipeMessage = chatElement.find(`[mesid="${chat.length - 1}"]`);
24652481 swipeMessage.attr('swipeid', params.swipeId);
24662482 swipeMessage.find('.mes_text').html(messageText).attr('title', title);
2483+ swipeMessage.find('.mes_reasoning').html(reasoning);
24672484 swipeMessage.find('.timestamp').text(timestamp).attr('title', `${params.extra.api} - ${params.extra.model}`);
24682485 appendMediaToMessage(mes, swipeMessage);
24692486 if (power_user.timestamp_model_icon && params.extra?.api) {
@@ -3074,6 +3091,7 @@ class StreamingProcessor {
30743091 this.messageTextDom = null;
30753092 this.messageTimerDom = null;
30763093 this.messageTokenCounterDom = null;
3094+ this.messageReasoningDom = null;
30773095 /** @type {HTMLTextAreaElement} */
30783096 this.sendTextarea = document.querySelector('#send_textarea');
30793097 this.type = type;
@@ -3089,6 +3107,7 @@ class StreamingProcessor {
30893107 /** @type {import('./scripts/logprobs.js').TokenLogprobs[]} */
30903108 this.messageLogprobs = [];
30913109 this.toolCalls = [];
3110+ this.reasoning = '';
30923111 }
30933112
30943113 #checkDomElements(messageId) {
@@ -3097,6 +3116,7 @@ class StreamingProcessor {
30973116 this.messageTextDom = this.messageDom?.querySelector('.mes_text');
30983117 this.messageTimerDom = this.messageDom?.querySelector('.mes_timer');
30993118 this.messageTokenCounterDom = this.messageDom?.querySelector('.tokenCounterDisplay');
3119+ this.messageReasoningDom = this.messageDom?.querySelector('.mes_reasoning');
31003120 }
31013121 }
31023122
@@ -3174,18 +3194,27 @@ class StreamingProcessor {
31743194 this.#checkDomElements(messageId);
31753195 this.#updateMessageBlockVisibility();
31763196 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);
31803197 chat[messageId]['mes'] = processedText;
31813198 chat[messageId]['gen_started'] = this.timeStarted;
31823199 chat[messageId]['gen_finished'] = currentTime;
31833200
3184- if (currentTokenCount) {
31853201 if (!chat[messageId]['extra']) {
31863202 chat[messageId]['extra'] = {};
31873203 }
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) {
31893218 chat[messageId]['extra']['token_count'] = currentTokenCount;
31903219 if (this.messageTokenCounterDom instanceof HTMLElement) {
31913220 this.messageTokenCounterDom.textContent = `${currentTokenCount}t`;
@@ -3207,10 +3236,13 @@ class StreamingProcessor {
32073236 if (this.messageTextDom instanceof HTMLElement) {
32083237 this.messageTextDom.innerHTML = formattedText;
32093238 }
3239+
3240+ const timePassed = formatGenerationTimer(this.timeStarted, currentTime, currentTokenCount);
32103241 if (this.messageTimerDom instanceof HTMLElement) {
32113242 this.messageTimerDom.textContent = timePassed.timerValue;
32123243 this.messageTimerDom.title = timePassed.timerTitle;
32133244 }
3245+
32143246 this.setFirstSwipe(messageId);
32153247 }
32163248
@@ -3317,7 +3349,7 @@ class StreamingProcessor {
33173349 }
33183350
33193351 /**
33203352 * @returns {Generator<{ text: string, swipes: string[], logprobs: import('./scripts/logprobs.js').TokenLogprobs, toolCalls: any[], state: any }, void, void>}
33213353 */
33223354 *nullStreamingGeneration() {
33233355 throw new Error('Generation function for streaming is not hooked up');
@@ -3339,7 +3371,7 @@ class StreamingProcessor {
33393371 try {
33403372 const sw = new Stopwatch(1000 / power_user.streaming_fps);
33413373 const timestamps = [];
33423374 for await (const { text, swipes, logprobs, toolCalls, state } of this.generator()) {
33433375 timestamps.push(Date.now());
33443376 if (this.isStopped) {
33453377 return;
@@ -3351,6 +3383,7 @@ class StreamingProcessor {
33513383 if (logprobs) {
33523384 this.messageLogprobs.push(...(Array.isArray(logprobs) ? logprobs : [logprobs]));
33533385 }
3386+ this.reasoning = state?.reasoning ?? '';
33543387 await eventSource.emit(event_types.STREAM_TOKEN_RECEIVED, text);
33553388 await sw.tick(() => this.onProgressStreaming(this.messageId, this.continueMessage + text));
33563389 }
@@ -3817,6 +3850,14 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
38173850 coreChat.pop();
38183851 }
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+
38203861 coreChat = await Promise.all(coreChat.map(async (chatItem, index) => {
38213862 let message = chatItem.mes;
38223863 let regexType = chatItem.is_user ? regex_placement.USER_INPUT : regex_placement.AI_OUTPUT;
@@ -3902,7 +3943,7 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
39023943 * @returns {string[]} Examples array with block heading
39033944 */
39043945 function parseMesExamples(examplesStr) {
39053946 if (!examplesStr || examplesStr.length === 0 || examplesStr === '<START>') {
39063947 return [];
39073948 }
39083949
@@ -4738,6 +4779,7 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
47384779 //const getData = await response.json();
47394780 let getMessage = extractMessageFromData(data);
47404781 let title = extractTitleFromData(data);
4782+ let reasoning = extractReasoningFromData(data);
47414783 kobold_horde_model = title;
47424784
47434785 const swipes = extractMultiSwipes(data, type);
@@ -4764,10 +4806,10 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
47644806 else {
47654807 // Without streaming we'll be having a full message on continuation. Treat it as a last chunk.
47664808 if (originalType !== 'continue') {
47674809 ({ type, getMessage } = await saveReply(type, getMessage, false, title, swipes, reasoning));
47684810 }
47694811 else {
47704812 ({ type, getMessage } = await saveReply('appendFinal', getMessage, false, title, swipes, reasoning));
47714813 }
47724814
47734815 // This relies on `saveReply` having been called to add the message to the chat, so it must be last.
@@ -5469,7 +5511,7 @@ async function promptItemize(itemizedPrompts, requestedMesId) {
54695511 } else {
54705512 diffPrevPrompt.style.display = 'none';
54715513 }
54725514 popup.dlg.querySelector('#copyPromptToClipboard').addEventListener('clickpointerup', async function () {
54735515 let rawPrompt = itemizedPrompts[PromptArrayItemForRawPromptDisplay].rawPrompt;
54745516 let rawPromptValues = rawPrompt;
54755517
@@ -5477,7 +5519,7 @@ async function promptItemize(itemizedPrompts, requestedMesId) {
54775519 rawPromptValues = rawPrompt.map(x => x.content).join('\n');
54785520 }
54795521
54805522 navigator.clipboard.writeTextawait copyText(rawPromptValues);
54815523 toastr.info(t`Copied!`);
54825524 });
54835525
@@ -5672,6 +5714,26 @@ function extractMessageFromData(data) {
56725714}
56735715
56745716/**
5717+ * Extracts the reasoning from the response data.
5718+ * @param {object} data Response data
5719+ * @returns {string} Extracted reasoning
5720+ */
5721+function 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+/**
56755737 * Extracts multiswipe swipes from the response data.
56765738 * @param {Object} data Response data
56775739 * @param {string} type Type of generation
@@ -5851,7 +5913,7 @@ export function cleanUpMessage(getMessage, isImpersonate, isContinue, displayInc
58515913 return getMessage;
58525914}
58535915
58545916export async function saveReply(type, getMessage, fromStreaming, title, swipes, reasoning) {
58555917 if (type != 'append' && type != 'continue' && type != 'appendFinal' && chat.length && (chat[chat.length - 1]['swipe_id'] === undefined ||
58565918 chat[chat.length - 1]['is_user'])) {
58575919 type = 'normal';
@@ -5876,8 +5938,10 @@ export async function saveReply(type, getMessage, fromStreaming, title, swipes)
58765938 chat[chat.length - 1]['send_date'] = getMessageTimeStamp();
58775939 chat[chat.length - 1]['extra']['api'] = getGeneratingApi();
58785940 chat[chat.length - 1]['extra']['model'] = getGeneratingModel();
5941+ chat[chat.length - 1]['extra']['reasoning'] = reasoning;
58795942 if (power_user.message_token_count_enabled) {
58805943 chat[chat.lengthconst -tokenCountText 1]['extra'][= (reasoning || 'token_count'] =) await+ getTokenCountAsync(chat[chat.length - 1]['mes'], 0);
5944+ chat[chat.length - 1]['extra']['token_count'] = await getTokenCountAsync(tokenCountText, 0);
58815945 }
58825946 const chat_id = (chat.length - 1);
58835947 await eventSource.emit(event_types.MESSAGE_RECEIVED, chat_id);
@@ -5896,8 +5960,10 @@ export async function saveReply(type, getMessage, fromStreaming, title, swipes)
58965960 chat[chat.length - 1]['send_date'] = getMessageTimeStamp();
58975961 chat[chat.length - 1]['extra']['api'] = getGeneratingApi();
58985962 chat[chat.length - 1]['extra']['model'] = getGeneratingModel();
5963+ chat[chat.length - 1]['extra']['reasoning'] += reasoning;
58995964 if (power_user.message_token_count_enabled) {
59005965 chat[chat.lengthconst -tokenCountText 1]['extra'][= (reasoning || 'token_count'] =) await+ getTokenCountAsync(chat[chat.length - 1]['mes'], 0);
5966+ chat[chat.length - 1]['extra']['token_count'] = await getTokenCountAsync(tokenCountText, 0);
59015967 }
59025968 const chat_id = (chat.length - 1);
59035969 await eventSource.emit(event_types.MESSAGE_RECEIVED, chat_id);
@@ -5913,8 +5979,10 @@ export async function saveReply(type, getMessage, fromStreaming, title, swipes)
59135979 chat[chat.length - 1]['send_date'] = getMessageTimeStamp();
59145980 chat[chat.length - 1]['extra']['api'] = getGeneratingApi();
59155981 chat[chat.length - 1]['extra']['model'] = getGeneratingModel();
5982+ chat[chat.length - 1]['extra']['reasoning'] += reasoning;
59165983 if (power_user.message_token_count_enabled) {
59175984 chat[chat.lengthconst -tokenCountText 1]['extra'][= (reasoning || 'token_count'] =) await+ getTokenCountAsync(chat[chat.length - 1]['mes'], 0);
5985+ chat[chat.length - 1]['extra']['token_count'] = await getTokenCountAsync(tokenCountText, 0);
59185986 }
59195987 const chat_id = (chat.length - 1);
59205988 await eventSource.emit(event_types.MESSAGE_RECEIVED, chat_id);
@@ -5930,6 +5998,7 @@ export async function saveReply(type, getMessage, fromStreaming, title, swipes)
59305998 chat[chat.length - 1]['send_date'] = getMessageTimeStamp();
59315999 chat[chat.length - 1]['extra']['api'] = getGeneratingApi();
59326000 chat[chat.length - 1]['extra']['model'] = getGeneratingModel();
6001+ chat[chat.length - 1]['extra']['reasoning'] = reasoning;
59336002 if (power_user.trim_spaces) {
59346003 getMessage = getMessage.trim();
59356004 }
@@ -5939,7 +6008,8 @@ export async function saveReply(type, getMessage, fromStreaming, title, swipes)
59396008 chat[chat.length - 1]['gen_finished'] = generationFinished;
59406009
59416010 if (power_user.message_token_count_enabled) {
59426011 chat[chat.lengthconst -tokenCountText 1]['extra'][= (reasoning || 'token_count'] =) await+ getTokenCountAsync(chat[chat.length - 1]['mes'], 0);
6012+ chat[chat.length - 1]['extra']['token_count'] = await getTokenCountAsync(tokenCountText, 0);
59436013 }
59446014
59456015 if (selected_group) {
@@ -6004,6 +6074,19 @@ export async function saveReply(type, getMessage, fromStreaming, title, swipes)
60046074 return { type, getMessage };
60056075}
60066076
6077+export 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+
60076090function saveImageToMessage(img, mes) {
60086091 if (mes && img.image) {
60096092 if (!mes.extra || typeof mes.extra !== 'object') {
@@ -7982,11 +8065,25 @@ function updateEditArrowClasses() {
79828065 }
79838066}
79848067
7985-function closeMessageEditor() {
8068+/**
8069+ * Closes the message editor.
8070+ * @param {'message'|'reasoning'|'all'} what What to close. Default is 'all'.
8071+ */
8072+export function closeMessageEditor(what = 'all') {
8073+ if (what === 'message' || what === 'all') {
79868074 if (this_edit_mes_id) {
79878075 $(`#chat .mes[mesid="${this_edit_mes_id}"] .mes_edit_cancel`).click();
79888076 }
79898077 }
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
79918088export function setGenerationProgress(progress) {
79928089 if (!progress) {
@@ -8417,6 +8514,9 @@ function swipe_left() { // when we swipe left..but no generation.
84178514 streamingProcessor.onStopStreaming();
84188515 }
84198516
8517+ // Make sure ad-hoc changes to extras are saved before swiping away
8518+ syncCurrentSwipeInfoExtras();
8519+
84208520 const swipe_duration = 120;
84218521 const swipe_range = '700px';
84228522 chat[chat.length - 1]['swipe_id']--;
@@ -8468,7 +8568,8 @@ function swipe_left() { // when we swipe left..but no generation.
84688568 }
84698569
84708570 const swipeMessage = $('#chat').find(`[mesid="${chat.length - 1}"]`);
84718571 const tokenCounttokenCountText = await getTokenCountAsync(chat[chat.length - 1]?.mes,extra?.reasoning 0|| '') + chat[chat.length - 1].mes;
8572+ const tokenCount = await getTokenCountAsync(tokenCountText, 0);
84728573 chat[chat.length - 1]['extra']['token_count'] = tokenCount;
84738574 swipeMessage.find('.tokenCounterDisplay').text(`${tokenCount}t`);
84748575 }
@@ -8551,6 +8652,9 @@ const swipe_right = () => {
85518652 return unblockGeneration();
85528653 }
85538654
8655+ // Make sure ad-hoc changes to extras are saved before swiping away
8656+ syncCurrentSwipeInfoExtras();
8657+
85548658 const swipe_duration = 200;
85558659 const swipe_range = 700;
85568660 //console.log(swipe_range);
@@ -8632,6 +8736,7 @@ const swipe_right = () => {
86328736 // resets the timer
86338737 swipeMessage.find('.mes_timer').html('');
86348738 swipeMessage.find('.tokenCounterDisplay').text('');
8739+ swipeMessage.find('.mes_reasoning').html('');
86358740 } else {
86368741 //console.log('showing previously generated swipe candidate, or "..."');
86378742 //console.log('onclick right swipe calling addOneMessage');
@@ -8642,7 +8747,8 @@ const swipe_right = () => {
86428747 chat[chat.length - 1].extra = {};
86438748 }
86448749
86458750 const tokenCounttokenCountText = await getTokenCountAsync(chat[chat.length - 1]?.mes,extra?.reasoning 0|| '') + chat[chat.length - 1].mes;
8751+ const tokenCount = await getTokenCountAsync(tokenCountText, 0);
86468752 chat[chat.length - 1]['extra']['token_count'] = tokenCount;
86478753 swipeMessage.find('.tokenCounterDisplay').text(`${tokenCount}t`);
86488754 }
@@ -9444,7 +9550,8 @@ function addDebugFunctions() {
94449550 message.extra = {};
94459551 }
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);
94489555 }
94499556
94509557 await saveChatConditional();
@@ -9507,7 +9614,7 @@ API Settings: ${JSON.stringify(getSettingsContents[getSettingsContents.main_api
95079614 //console.log(logMessage);
95089615
95099616 try {
95109617 await navigator.clipboard.writeTextcopyText(logMessage);
95119618 toastr.info('Your ST API setup data has been copied to the clipboard.');
95129619 } catch (error) {
95139620 toastr.error('Failed to copy ST Setup to clipboard:', error);
@@ -10572,24 +10679,18 @@ jQuery(async function () {
1057210679 setTimeout(function () { $('#shadow_select_chat_popup').css('display', 'none'); }, animation_duration);
1057310680 });
1057410681
10575- if (navigator.clipboard === undefined) {
10682+ $(document).on('pointerup', '.mes_copy', async function () {
10576- // No clipboard support
10577- $('.mes_copy').remove();
10578- }
10579- else {
10580- $(document).on('pointerup', '.mes_copy', function () {
1058110683 if (this_chid !== undefined || selected_group || name2 === neutralCharacterName) {
1058210684 try {
1058310685 const messageId = $(this).closest('.mes').attr('mesid');
1058410686 const text = chat[messageId]['mes'];
10585- navigator.clipboard.writeText(text);
10687+ await copyText(text);
1058610688 toastr.info('Copied!', '', { timeOut: 2000 });
1058710689 } catch (err) {
1058810690 console.error('Failed to copy: ', err);
1058910691 }
1059010692 }
1059110693 });
10592- }
1059310694
1059410695 $(document).on('pointerup', '.mes_prompt', async function () {
1059510696 let mesIdForItemization = $(this).closest('.mes').attr('mesId');
@@ -11219,14 +11320,15 @@ jQuery(async function () {
1121911320
1122011321 $(document).keyup(function (e) {
1122111322 if (e.key === 'Escape') {
1122211323 const isEditVisible = $('#curEditTextarea').is(':visible') || $('.reasoning_edit_textarea').length > 0;
1122311324 if (isEditVisible && power_user.auto_save_msg_edits === false) {
1122411325 closeMessageEditor('all');
1122511326 $('#send_textarea').focus();
1122611327 return;
1122711328 }
1122811329 if (isEditVisible && power_user.auto_save_msg_edits === true) {
1122911330 $(`#chat .mes[mesid="${this_edit_mes_id}"] .mes_edit_done`).click();
11331+ closeMessageEditor('reasoning');
1123011332 $('#send_textarea').focus();
1123111333 return;
1123211334 }
@@ -11460,8 +11562,8 @@ jQuery(async function () {
1146011562 $('#avatar-and-name-block').slideToggle();
1146111563 });
1146211564
1146311565 $(document).on('mouseup touchend', '#show_more_messages', async function () => {
1146411566 await showMoreMessages();
1146511567 });
1146611568
1146711569 $(document).on('click', '.open_characters_library', async function () {
@@ -11483,4 +11585,3 @@ jQuery(async function () {
1148311585
1148411586 initCustomSelectedSamplers();
1148511587});
11486-
public/scripts/backgrounds.js+4 -4
@@ -482,10 +482,10 @@ function highlightNewBackground(bg) {
482482 */
483483function setFittingClass(fitting) {
484484 const backgrounds = $('#bg1, #bg_custom');
485- backgrounds.toggleClass('cover', fitting === 'cover');
485+ for (const option of ['cover', 'contain', 'stretch', 'center']) {
486486 backgrounds.toggleClass('contain'option, fittingoption === 'contain'fitting);
487- backgrounds.toggleClass('stretch', fitting === 'stretch');
487+ }
488- backgrounds.toggleClass('center', fitting === 'center');
488+ background_settings.fitting = fitting;
489489}
490490
491491function onBackgroundFilterInput() {
public/scripts/chat-templates.js+10 -0
@@ -60,6 +60,16 @@ const hash_derivations = {
6060 // Tulu-3-70B
6161 'Tulu'
6262 ,
63+
64+ // DeepSeek V2.5
65+ '54d400beedcd17f464e10063e0577f6f798fa896266a912d8a366f8a2fcc0bca':
66+ 'DeepSeek-V2.5'
67+ ,
68+
69+ // DeepSeek R1
70+ 'b6835114b7303ddd78919a82e4d9f7d8c26ed0d7dfc36beeb12d524f6144eab1':
71+ 'DeepSeek-V2.5'
72+ ,
6373};
6474
6575const substr_derivations = {
public/scripts/chats.js+17 -0
@@ -11,6 +11,7 @@ import {
1111 getCurrentChatId,
1212 getRequestHeaders,
1313 hideSwipeButtons,
14+ name1,
1415 name2,
1516 reloadCurrentChat,
1617 saveChatDebounced,
@@ -21,6 +22,7 @@ import {
2122 chat_metadata,
2223 neutralCharacterName,
2324 updateChatMetadata,
25+ system_message_types,
2426} from '../script.js';
2527import { selected_group } from './group-chats.js';
2628import { power_user } from './power-user.js';
@@ -34,6 +36,7 @@ import {
3436 humanFileSize,
3537 saveBase64AsFile,
3638 extractTextFromOffice,
39+ download,
3740} from './utils.js';
3841import { extension_settings, renderExtensionTemplateAsync, saveMetadataDebounced } from './extensions.js';
3942import { POPUP_RESULT, POPUP_TYPE, Popup, callGenericPopup } from './popup.js';
@@ -41,6 +44,7 @@ import { ScraperManager } from './scrapers.js';
4144import { DragAndDropHandler } from './dragdrop.js';
4245import { renderTemplateAsync } from './templates.js';
4346import { t } from './i18n.js';
47+import { humanizedDateTime } from './RossAscends-mods.js';
4448
4549/**
4650 * @typedef {Object} FileAttachment
@@ -1437,6 +1441,19 @@ jQuery(function () {
14371441 await viewMessageFile(messageId);
14381442 });
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+
14401457 // Do not change. #attachFile is added by extension.
14411458 $(document).on('click', '#attachFile', function () {
14421459 $('#file_form_input').trigger('click');
public/scripts/extensions.js+23 -9
@@ -38,7 +38,8 @@ export let modules = [];
3838let activeExtensions = new Set();
3939
4040const getApiUrl = () => extension_settings.apiUrl;
4141const sortManifestssortManifestsByOrder = (a, b) => parseInt(a.loading_order) - parseInt(b.loading_order) || String(a.display_name).localeCompare(String(b.display_name));
42+const sortManifestsByName = (a, b) => String(a.display_name).localeCompare(String(b.display_name)) || parseInt(a.loading_order) - parseInt(b.loading_order);
4243let connectedToApi = false;
4344
4445/**
@@ -355,7 +356,7 @@ async function getManifests(names) {
355356 * @returns {Promise<void>}
356357 */
357358async function activateExtensions() {
358359 const extensions = Object.entries(manifests).sort((a, b) => sortManifestssortManifestsByOrder(a[1], b[1]));
359360 const promises = [];
360361
361362 for (let entry of extensions) {
@@ -712,7 +713,10 @@ async function showExtensionsDetails() {
712713
713714 htmlExternal.append(htmlLoading);
714715
715- const extensions = Object.entries(manifests).sort((a, b) => sortManifests(a[1], b[1])).map(getExtensionData);
716+ const sortOrderKey = 'extensions_sortByName';
717+ const sortByName = localStorage.getItem(sortOrderKey) === 'true';
718+ const sortFn = sortByName ? sortManifestsByName : sortManifestsByOrder;
719+ const extensions = Object.entries(manifests).sort((a, b) => sortFn(a[1], b[1])).map(getExtensionData);
716720
717721 extensions.forEach(value => {
718722 const { isExternal, extensionHtml } = value;
@@ -729,7 +733,6 @@ async function showExtensionsDetails() {
729733 /** @type {import('./popup.js').CustomPopupButton} */
730734 const updateAllButton = {
731735 text: t`Update all`,
732- appendAtEnd: true,
733736 action: async () => {
734737 requiresReload = true;
735738 await autoUpdateExtensions(true);
@@ -737,13 +740,23 @@ async function showExtensionsDetails() {
737740 },
738741 };
739742
743+ /** @type {import('./popup.js').CustomPopupButton} */
744+ const sortOrderButton = {
745+ text: sortByName ? t`Sort: Display Name` : t`Sort: Loading Order`,
746+ action: async () => {
747+ abortController.abort();
748+ localStorage.setItem(sortOrderKey, sortByName ? 'false' : 'true');
749+ await showExtensionsDetails();
750+ },
751+ };
752+
740753 let waitingForSave = false;
741754
742755 const popup = new Popup(html, POPUP_TYPE.TEXT, '', {
743756 okButton: t`Close`,
744757 wide: true,
745758 large: true,
746759 customButtons: [sortOrderButton, updateAllButton],
747760 allowVerticalScrolling: true,
748761 onClosing: async () => {
749762 if (waitingForSave) {
@@ -762,7 +775,7 @@ async function showExtensionsDetails() {
762775 });
763776 popupPromise = popup.show();
764777 popup.content.scrollTop = initialScrollTop;
765778 checkForUpdatesManual(sortFn, abortController.signal).finally(() => htmlLoading.remove());
766779 } catch (error) {
767780 toastr.error(t`Error loading extensions. See browser console for details.`);
768781 console.error(error);
@@ -1073,12 +1086,13 @@ function processVersionCheckQueue() {
10731086
10741087/**
10751088 * Performs a manual check for updates on all 3rd-party extensions.
1089+ * @param {function} sortFn Sort function
10761090 * @param {AbortSignal} abortSignal Signal to abort the operation
10771091 * @returns {Promise<any[]>}
10781092 */
10791093async function checkForUpdatesManual(sortFn, abortSignal) {
10801094 const promises = [];
10811095 for (const id of Object.keys(manifests).filter(x => x.startsWith('third-party')).sort((a, b) => sortManifestssortFn(manifests[a], manifests[b]))) {
10821096 const externalId = id.replace('third-party', '');
10831097 const promise = enqueueVersionCheck(async () => {
10841098 try {
@@ -1223,7 +1237,7 @@ export async function runGenerationInterceptors(chat, contextSize, type) {
12231237 exitImmediately = immediately;
12241238 };
12251239
12261240 for (const manifest of Object.values(manifests).filter(x => x.generate_interceptor).sort((a, b) => sortManifestssortManifestsByOrder(a, b))) {
12271241 const interceptorKey = manifest.generate_interceptor;
12281242 if (typeof globalThis[interceptorKey] === 'function') {
12291243 try {
public/scripts/extensions/caption/settings.html+2 -0
@@ -54,6 +54,8 @@
5454 <option data-type="anthropic" value="claude-3-sonnet-20240229">claude-3-sonnet-20240229</option>
5555 <option data-type="anthropic" value="claude-3-haiku-20240307">claude-3-haiku-20240307</option>
5656 <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>
5759 <option data-type="google" value="gemini-2.0-flash-thinking-exp-1219">gemini-2.0-flash-thinking-exp-1219</option>
5860 <option data-type="google" value="gemini-1.5-flash">gemini-1.5-flash</option>
5961 <option data-type="google" value="gemini-1.5-flash-latest">gemini-1.5-flash-latest</option>
public/scripts/extensions/tts/index.js+1 -1
@@ -466,7 +466,7 @@ async function processTtsQueue() {
466466 }
467467
468468 if (extension_settings.tts.skip_tags) {
469469 text = text.replace(/<.*?>.[\s\S]*?<\/.*?>/g, '').trim();
470470 }
471471
472472 if (!extension_settings.tts.pass_asterisks) {
public/scripts/group-chats.js+10 -0
@@ -81,6 +81,7 @@ import { t } from './i18n.js';
8181
8282export {
8383 selected_group,
84+ openGroupId,
8485 is_group_automode_enabled,
8586 hideMutedSprites,
8687 is_group_generating,
@@ -1367,6 +1368,15 @@ function getGroupCharacterBlock(character) {
13671368 template.find('.ch_fav').val(isFav);
13681369 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+
13701380 let queuePosition = groupChatQueueOrder.get(character.avatar);
13711381 if (queuePosition) {
13721382 template.find('.queue_position').text(queuePosition);
public/scripts/kai-settings.js+1 -1
@@ -188,7 +188,7 @@ export async function generateKoboldWithStreaming(generate_data, signal) {
188188 if (data?.token) {
189189 text += data.token;
190190 }
191191 yield { text, swipes: [], toolCalls: [], state: {} };
192192 }
193193 };
194194}
public/scripts/nai-settings.js+1 -1
@@ -746,7 +746,7 @@ export async function generateNovelWithStreaming(generate_data, signal) {
746746 text += data.token;
747747 }
748748
749749 yield { text, swipes: [], logprobs: parseNovelAILogprobs(data.logprobs), toolCalls: [], state: {} };
750750 }
751751 };
752752}
public/scripts/openai.js+43 -11
@@ -1096,8 +1096,8 @@ async function preparePromptsForChatCompletion({ Scenario, charPersonality, name
10961096 // Unordered prompts without marker
10971097 { role: 'system', content: impersonationPrompt, identifier: 'impersonate' },
10981098 { role: 'system', content: quietPrompt, identifier: 'quietPrompt' },
1099- { role: 'system', content: bias, identifier: 'bias' },
11001099 { role: 'system', content: groupNudge, identifier: 'groupNudge' },
1100+ { role: 'assistant', content: bias, identifier: 'bias' },
11011101 ];
11021102
11031103 // Tavern Extras - Summary
@@ -1922,7 +1922,7 @@ async function sendOpenAIRequest(type, messages, signal) {
19221922 }
19231923
19241924 // Proxy is only supported for Claude, OpenAI, Mistral, and Google MakerSuite
19251925 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)) {
19261926 await validateReverseProxy();
19271927 generate_data['reverse_proxy'] = oai_settings.reverse_proxy;
19281928 generate_data['proxy_password'] = oai_settings.proxy_password;
@@ -2030,6 +2030,16 @@ async function sendOpenAIRequest(type, messages, signal) {
20302030 // https://api-docs.deepseek.com/api/create-chat-completion
20312031 if (isDeepSeek) {
20322032 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+ }
20332043 }
20342044
20352045 if ((isOAI || isOpenRouter || isMistral || isCustom || isCohere || isNano) && oai_settings.seed >= 0) {
@@ -2085,6 +2095,7 @@ async function sendOpenAIRequest(type, messages, signal) {
20852095 let text = '';
20862096 const swipes = [];
20872097 const toolCalls = [];
2098+ const state = { reasoning: '' };
20882099 while (true) {
20892100 const { done, value } = await reader.read();
20902101 if (done) return;
@@ -2095,14 +2106,14 @@ async function sendOpenAIRequest(type, messages, signal) {
20952106
20962107 if (Array.isArray(parsed?.choices) && parsed?.choices?.[0]?.index > 0) {
20972108 const swipeIndex = parsed.choices[0].index - 1;
20982109 swipes[swipeIndex] = (swipes[swipeIndex] || '') + getStreamingReply(parsed, state);
20992110 } else {
21002111 text += getStreamingReply(parsed, state);
21012112 }
21022113
21032114 ToolManager.parseToolCalls(toolCalls, parsed);
21042115
21052116 yield { text, swipes: swipes, logprobs: parseChatCompletionLogprobs(parsed), toolCalls: toolCalls, state: state };
21062117 }
21072118 };
21082119 }
@@ -2129,13 +2140,32 @@ async function sendOpenAIRequest(type, messages, signal) {
21292140 }
21302141}
21312142
2132-function 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+ */
2149+function getStreamingReply(data, state) {
21332150 if (oai_settings.chat_completion_source === chat_completion_sources.CLAUDE) {
21342151 return data?.delta?.text || '';
21352152 } 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] || '';
21372157 } else if (oai_settings.chat_completion_source === chat_completion_sources.COHERE) {
21382158 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 ?? '';
21392169 } else {
21402170 return data.choices?.[0]?.delta?.content ?? data.choices?.[0]?.message?.content ?? data.choices?.[0]?.text ?? '';
21412171 }
@@ -3346,7 +3376,7 @@ async function getStatusOpen() {
33463376 chat_completion_source: oai_settings.chat_completion_source,
33473377 };
33483378
33493379 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)) {
33503380 await validateReverseProxy();
33513381 }
33523382
@@ -4204,7 +4234,7 @@ async function onModelChange() {
42044234 $('#openai_max_context').attr('max', max_32k);
42054235 } else if (value.includes('gemini-1.5-pro') || value.includes('gemini-exp-1206')) {
42064236 $('#openai_max_context').attr('max', max_2mil);
42074237 } else if (value.includes('gemini-1.5-flash') || value.includes('gemini-2.0-flash-exp') || value.includes('gemini-2.0-flash-thinking-exp')) {
42084238 $('#openai_max_context').attr('max', max_1mil);
42094239 } else if (value.includes('gemini-1.0-pro') || value === 'gemini-pro') {
42104240 $('#openai_max_context').attr('max', max_32k);
@@ -4488,7 +4518,7 @@ async function onModelChange() {
44884518 if (oai_settings.chat_completion_source === chat_completion_sources.DEEPSEEK) {
44894519 if (oai_settings.max_context_unlocked) {
44904520 $('#openai_max_context').attr('max', unlocked_max);
44914521 } else if (oai_settings.deepseek_model ==['deepseek-reasoner', 'deepseek-chat'].includes(oai_settings.deepseek_model)) {
44924522 $('#openai_max_context').attr('max', max_64k);
44934523 } else if (oai_settings.deepseek_model == 'deepseek-coder') {
44944524 $('#openai_max_context').attr('max', max_16k);
@@ -4725,7 +4755,7 @@ async function onConnectButtonClick(e) {
47254755 await writeSecret(SECRET_KEYS.DEEPSEEK, api_key_deepseek);
47264756 }
47274757
47284758 if (!secret_state[SECRET_KEYS.DEEPSEEK] && !oai_settings.reverse_proxy) {
47294759 console.log('No secret key saved for DeepSeek');
47304760 return;
47314761 }
@@ -4901,6 +4931,8 @@ export function isImageInliningSupported() {
49014931 const visionSupportedModels = [
49024932 'gpt-4-vision',
49034933 'gemini-2.0-flash-thinking-exp-1219',
4934+ 'gemini-2.0-flash-thinking-exp-01-21',
4935+ 'gemini-2.0-flash-thinking-exp',
49044936 'gemini-2.0-flash-exp',
49054937 'gemini-1.5-flash',
49064938 'gemini-1.5-flash-latest',
public/scripts/power-user.js+9 -1
@@ -253,6 +253,14 @@ let power_user = {
253253 content: 'Write {{char}}\'s next reply in a fictional chat between {{char}} and {{user}}.',
254254 },
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+
256264 personas: {},
257265 default_persona: null,
258266 persona_descriptions: {},
@@ -2534,7 +2542,7 @@ async function loadUntilMesId(mesId) {
25342542 let target;
25352543
25362544 while (getFirstDisplayedMessageId() > mesId && getFirstDisplayedMessageId() !== 0) {
25372545 await showMoreMessages();
25382546 await delay(1);
25392547 target = $('#chat').find(`.mes[mesid=${mesId}]`);
25402548
public/scripts/reasoning.js+297 -0
@@ -0,0 +1,297 @@
1+import { chat, closeMessageEditor, saveChatConditional, saveSettingsDebounced, substituteParams, updateMessageBlock } from '../script.js';
2+import { t } from './i18n.js';
3+import { MacrosParser } from './macros.js';
4+import { Popup } from './popup.js';
5+import { power_user } from './power-user.js';
6+import { SlashCommand } from './slash-commands/SlashCommand.js';
7+import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from './slash-commands/SlashCommandArgument.js';
8+import { commonEnumProviders } from './slash-commands/SlashCommandCommonEnumsProvider.js';
9+import { SlashCommandParser } from './slash-commands/SlashCommandParser.js';
10+import { 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+ */
17+function 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+ */
28+export 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+
78+function 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+
110+function registerReasoningSlashCommands() {
111+ SlashCommandParser.addCommandObject(SlashCommand.fromProps({
112+ name: 'reasoning-get',
113+ returns: ARGUMENT_TYPE.STRING,
114+ helpString: t`Get the contents of a reasoning block of a message. Returns an empty string if the message does not have a reasoning block.`,
115+ unnamedArgumentList: [
116+ SlashCommandArgument.fromProps({
117+ description: 'Message ID. If not provided, the message ID of the last message is used.',
118+ typeList: ARGUMENT_TYPE.NUMBER,
119+ enumProvider: commonEnumProviders.messages(),
120+ }),
121+ ],
122+ callback: (_args, value) => {
123+ const messageId = !isNaN(Number(value)) ? Number(value) : chat.length - 1;
124+ const message = chat[messageId];
125+ const reasoning = String(message?.extra?.reasoning ?? '');
126+ return reasoning.replace(PromptReasoning.REASONING_PLACEHOLDER_REGEX, '');
127+ },
128+ }));
129+
130+ SlashCommandParser.addCommandObject(SlashCommand.fromProps({
131+ name: 'reasoning-set',
132+ returns: ARGUMENT_TYPE.STRING,
133+ helpString: t`Set the reasoning block of a message. Returns the reasoning block content.`,
134+ namedArgumentList: [
135+ SlashCommandNamedArgument.fromProps({
136+ name: 'at',
137+ description: 'Message ID. If not provided, the message ID of the last message is used.',
138+ typeList: ARGUMENT_TYPE.NUMBER,
139+ enumProvider: commonEnumProviders.messages(),
140+ }),
141+ ],
142+ unnamedArgumentList: [
143+ SlashCommandArgument.fromProps({
144+ description: 'Reasoning block content.',
145+ typeList: ARGUMENT_TYPE.STRING,
146+ }),
147+ ],
148+ callback: async (args, value) => {
149+ const messageId = !isNaN(Number(args[0])) ? Number(args[0]) : chat.length - 1;
150+ const message = chat[messageId];
151+ if (!message?.extra) {
152+ return '';
153+ }
154+
155+ message.extra.reasoning = String(value ?? '');
156+ await saveChatConditional();
157+
158+ closeMessageEditor('reasoning');
159+ updateMessageBlock(messageId, message);
160+ return message.extra.reasoning;
161+ },
162+ }));
163+}
164+
165+function 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+
171+function 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+
292+export function initReasoning() {
293+ loadReasoningSettings();
294+ setReasoningEventHandlers();
295+ registerReasoningSlashCommands();
296+ registerReasoningMacros();
297+}
public/scripts/slash-commands.js+6 -2
@@ -42,6 +42,7 @@ import {
4242 showMoreMessages,
4343 stopGeneration,
4444 substituteParams,
45+ syncCurrentSwipeInfoExtras,
4546 system_avatar,
4647 system_message_types,
4748 this_chid,
@@ -1968,8 +1969,8 @@ export function initDefaultSlashCommands() {
19681969 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
19691970 name: 'chat-render',
19701971 helpString: 'Renders a specified number of messages into the chat window. Displays all messages if no argument is provided.',
19711972 callback: async (args, number) => {
19721973 await showMoreMessages(number && !isNaN(Number(number)) ? Number(number) : Number.MAX_SAFE_INTEGER);
19731974 if (isTrueBoolean(String(args?.scroll ?? ''))) {
19741975 $('#chat').scrollTop(0);
19751976 }
@@ -2814,8 +2815,11 @@ async function addSwipeCallback(args, value) {
28142815 const newSwipeId = lastMessage.swipes.length - 1;
28152816
28162817 if (isTrueBoolean(args.switch)) {
2818+ // Make sure ad-hoc changes to extras are saved before swiping away
2819+ syncCurrentSwipeInfoExtras();
28172820 lastMessage.swipe_id = newSwipeId;
28182821 lastMessage.mes = lastMessage.swipes[newSwipeId];
2822+ lastMessage.extra = structuredClone(lastMessage.swipe_info?.[newSwipeId]?.extra ?? lastMessage.extra ?? {});
28192823 }
28202824
28212825 await saveChatConditional();
public/scripts/sse-stream.js+30 -0
@@ -220,6 +220,36 @@ async function* parseStreamData(json) {
220220 }
221221 return;
222222 }
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+ }
223253 else if (typeof json.choices[0].delta.content === 'string' && json.choices[0].delta.content.length > 0) {
224254 for (let j = 0; j < json.choices[0].delta.content.length; j++) {
225255 const str = json.choices[0].delta.content[j];
public/scripts/st-context.js+10 -2
@@ -1,6 +1,7 @@
11import {
22 activateSendButtons,
33 addOneMessage,
4+ appendMediaToMessage,
45 callPopup,
56 characters,
67 chat,
@@ -12,6 +13,7 @@ import {
1213 extension_prompts,
1314 Generate,
1415 generateQuietPrompt,
16+ getCharacters,
1517 getCurrentChatId,
1618 getRequestHeaders,
1719 getThumbnailUrl,
@@ -40,6 +42,7 @@ import {
4042 substituteParamsExtended,
4143 this_chid,
4244 updateChatMetadata,
45+ updateMessageBlock,
4346} from '../script.js';
4447import {
4548 extension_settings,
@@ -55,7 +58,7 @@ import { MacrosParser } from './macros.js';
5558import { oai_settings } from './openai.js';
5659import { callGenericPopup, Popup, POPUP_RESULT, POPUP_TYPE } from './popup.js';
5760import { power_user, registerDebugFunction } from './power-user.js';
5861import { humanizedDateTime, isMobile, shouldSendOnEnter } from './RossAscends-mods.js';
5962import { ScraperManager } from './scrapers.js';
6063import { executeSlashCommands, executeSlashCommandsWithOptions, registerSlashCommand } from './slash-commands.js';
6164import { SlashCommand } from './slash-commands/SlashCommand.js';
@@ -65,7 +68,7 @@ import { tag_map, tags } from './tags.js';
6568import { textgenerationwebui_settings } from './textgen-settings.js';
6669import { tokenizers, getTextTokens, getTokenCount, getTokenCountAsync, getTokenizerModel } from './tokenizers.js';
6770import { ToolManager } from './tool-calling.js';
6871import { timestampToMoment, uuidv4 } from './utils.js';
6972
7073export function getContext() {
7174 return {
@@ -167,6 +170,11 @@ export function getContext() {
167170 chatCompletionSettings: oai_settings,
168171 textCompletionSettings: textgenerationwebui_settings,
169172 powerUserSettings: power_user,
173+ getCharacters,
174+ uuidv4,
175+ humanizedDateTime,
176+ updateMessageBlock,
177+ appendMediaToMessage,
170178 };
171179}
172180
public/scripts/templates/assistantNote.html+6 -0
@@ -1,3 +1,9 @@
1+<div data-type="assistant_note">
12 <div>
23 <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>
39</div>
public/scripts/templates/importCharacters.html+1 -1
@@ -7,7 +7,7 @@
77 <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>
88 <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>
99 <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>
1010 <li><span data-i18n="char_import_5">AICharacterCardAICharacterCards.com Character (Direct Link or ID)</span><br><span data-i18n="char_import_example">Example:</span> <tt>AICC/aicharcards/the-game-master</tt></li>
1111 <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>
1212 <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>
1313 </ul>
public/scripts/textgen-models.js+11 -11
@@ -319,8 +319,6 @@ export async function loadFeatherlessModels(data) {
319319 return;
320320 }
321321
322- // Sort the data by model id (default A-Z)
323- data.sort((a, b) => a.id.localeCompare(b.id));
324322 originalModels = data; // Store the original data for search
325323 featherlessModels = data;
326324
@@ -334,10 +332,8 @@ export async function loadFeatherlessModels(data) {
334332 // Retrieve the stored number of items per page or default to 10
335333 const perPage = Number(localStorage.getItem(storageKey)) || 10;
336334
337335 // Initialize pagination with the full set of models
338- const currentModelIndex = data.findIndex(x => x.id === textgen_settings.featherless_model);
336+ applyFiltersAndSort();
339- featherlessCurrentPage = currentModelIndex >= 0 ? (currentModelIndex / perPage) + 1 : 1;
340- setupPagination(originalModels, perPage);
341337
342338 // Function to set up pagination (also used for filtered results)
343339 function setupPagination(models, perPage, pageNumber = featherlessCurrentPage) {
@@ -383,7 +379,7 @@ export async function loadFeatherlessModels(data) {
383379
384380 const dateAddedDiv = document.createElement('div');
385381 dateAddedDiv.classList.add('model-date-added');
386382 dateAddedDiv.textContent = `Added On: ${new Date(model.updated_atcreated * 1000).toLocaleDateString()}`;
387383
388384 detailsContainer.appendChild(modelClassDiv);
389385 detailsContainer.appendChild(contextLengthDiv);
@@ -472,6 +468,7 @@ export async function loadFeatherlessModels(data) {
472468 featherlessTop = await fetchFeatherlessStats();
473469 }
474470 const featherlessIds = featherlessTop.map(stat => stat.id);
471+
475472 if (selectedCategory === 'New') {
476473 featherlessNew = await fetchFeatherlessNew();
477474 }
@@ -493,7 +490,7 @@ export async function loadFeatherlessModels(data) {
493490 return matchesSearch && matchesClass && matchesNew;
494491 }
495492 else {
496493 return matchesSearch && matchesClass;
497494 }
498495 });
499496
@@ -502,11 +499,14 @@ export async function loadFeatherlessModels(data) {
502499 } else if (selectedSortOrder === 'desc') {
503500 filteredModels.sort((a, b) => b.id.localeCompare(a.id));
504501 } else if (selectedSortOrder === 'date_asc') {
505502 filteredModels.sort((a, b) => a.updated_at.localeCompare(created - b.updated_at)created);
506503 } else if (selectedSortOrder === 'date_desc') {
507504 filteredModels.sort((a, b) => b.updated_at.localeCompare(created - a.updated_at)created);
508505 }
509506
507+ const currentModelIndex = filteredModels.findIndex(x => x.id === textgen_settings.featherless_model);
508+ featherlessCurrentPage = currentModelIndex >= 0 ? (currentModelIndex / perPage) + 1 : 1;
509+
510510 setupPagination(filteredModels, Number(localStorage.getItem(storageKey)) || perPage, featherlessCurrentPage);
511511 }
512512
@@ -528,7 +528,7 @@ async function fetchFeatherlessStats() {
528528}
529529
530530async function fetchFeatherlessNew() {
531531 const response = await fetch('https://api.featherless.ai/feather/models?sort=-created_at&perPage=1020');
532532 const data = await response.json();
533533 return data.items;
534534}
public/scripts/textgen-settings.js+4 -3
@@ -986,6 +986,7 @@ export async function generateTextGenWithStreaming(generate_data, signal) {
986986 let logprobs = null;
987987 const swipes = [];
988988 const toolCalls = [];
989+ const state = {};
989990 while (true) {
990991 const { done, value } = await reader.read();
991992 if (done) return;
@@ -1004,7 +1005,7 @@ export async function generateTextGenWithStreaming(generate_data, signal) {
10041005 logprobs = parseTextgenLogprobs(newText, data.choices?.[0]?.logprobs || data?.completion_probabilities);
10051006 }
10061007
10071008 yield { text, swipes, logprobs, toolCalls, state };
10081009 }
10091010 };
10101011}
@@ -1231,7 +1232,7 @@ export function getTextGenGenerationData(finalPrompt, maxTokens, isImpersonate,
12311232 'top_p': settings.top_p,
12321233 'typical_p': settings.typical_p,
12331234 'typical': settings.typical_p,
12341235 'sampler_seed': settings.seed >= 0 ? settings.seed : undefined,
12351236 'min_p': settings.min_p,
12361237 'repetition_penalty': settings.rep_pen,
12371238 'frequency_penalty': settings.freq_pen,
@@ -1294,7 +1295,7 @@ export function getTextGenGenerationData(finalPrompt, maxTokens, isImpersonate,
12941295 'temperature_last': (settings.type === OOBA || settings.type === APHRODITE || settings.type == TABBY) ? settings.temperature_last : undefined,
12951296 'speculative_ngram': settings.type === TABBY ? settings.speculative_ngram : undefined,
12961297 'do_sample': settings.type === OOBA ? settings.do_sample : undefined,
12971298 'seed': settings.seed >= 0 ? settings.seed : undefined,
12981299 'guidance_scale': cfgValues?.guidanceScale?.value ?? settings.guidance_scale ?? 1,
12991300 'negative_prompt': cfgValues?.negativePrompt ?? substituteParams(settings.negative_prompt) ?? '',
13001301 'grammar_string': settings.grammar_string,
public/scripts/utils.js+20 -0
@@ -392,6 +392,26 @@ export function getStringHash(str, seed = 0) {
392392}
393393
394394/**
395+ * Copy text to clipboard. Use navigator.clipboard.writeText if available, otherwise use document.execCommand.
396+ * @param {string} text - The text to copy to the clipboard.
397+ * @returns {Promise<void>} A promise that resolves when the text has been copied to the clipboard.
398+ */
399+export function copyText(text) {
400+ if (navigator.clipboard) {
401+ return navigator.clipboard.writeText(text);
402+ }
403+
404+ const parent = document.querySelector('dialog[open]:last-of-type') ?? document.body;
405+ const textArea = document.createElement('textarea');
406+ textArea.value = text;
407+ parent.appendChild(textArea);
408+ textArea.focus();
409+ textArea.select();
410+ document.execCommand('copy');
411+ parent.removeChild(textArea);
412+}
413+
414+/**
395415 * Map of debounced functions to their timers.
396416 * Weak map is used to avoid memory leaks.
397417 * @type {WeakMap<function, any>}
public/style.css+131 -24
@@ -292,36 +292,44 @@ input[type='checkbox']:focus-visible {
292292 filter: grayscale(25%);
293293}
294294
295295.mes_text table {,
296+.mes_reasoning table {
296297 border-spacing: 0;
297298 border-collapse: collapse;
298299 margin-bottom: 10px;
299300}
300301
301302.mes_text td,
302303.mes_text th {,
304+.mes_reasoning td,
305+.mes_reasoning th {
303306 border: 1px solid;
304307 border-collapse: collapse;
305308 padding: 0.25em;
306309}
307310
308311.mes_text p {,
312+.mes_reasoning p {
309313 margin-top: 0;
310314 margin-bottom: 10px;
311315}
312316
313317.mes_text li tt {,
318+.mes_reasoning li tt {
314319 display: inline-block;
315320}
316321
317322.mes_text ol,
318323.mes_text ul {,
324+.mes_reasoning ol,
325+.mes_reasoning ul {
319326 margin-top: 5px;
320327 margin-bottom: 5px;
321328}
322329
323330.mes_text br,
324331.mes_bias br {,
332+.mes_reasoning br {
325333 content: ' ';
326334}
327335
@@ -332,25 +340,83 @@ input[type='checkbox']:focus-visible {
332340 color: var(--SmartThemeQuoteColor);
333341}
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+
335395.mes_text i,
336396.mes_text em {,
397+.mes_reasoning i,
398+.mes_reasoning em {
337399 color: var(--SmartThemeEmColor);
338400}
339401
340402.mes_text u {,
403+.mes_reasoning u {
341404 color: var(--SmartThemeUnderlineColor);
342405}
343406
344407.mes_text q {,
408+.mes_reasoning q {
345409 color: var(--SmartThemeQuoteColor);
346410}
347411
348412.mes_text font[color] em,
349413.mes_text font[color] i {,
350- color: inherit;
414+.mes_text font[color] u,
351-}
415+.mes_text font[color] q,
352-
416+.mes_reasoning font[color] em,
353417.mes_textmes_reasoning font[color] q {i,
418+.mes_reasoning font[color] u,
419+.mes_reasoning font[color] q {
354420 color: inherit;
355421}
356422
@@ -358,7 +424,8 @@ input[type='checkbox']:focus-visible {
358424 display: block;
359425}
360426
361427.mes_text blockquote {,
428+.mes_reasoning blockquote {
362429 border-left: 3px solid var(--SmartThemeQuoteColor);
363430 padding-left: 10px;
364431 background-color: var(--black30a);
@@ -368,18 +435,24 @@ input[type='checkbox']:focus-visible {
368435.mes_text strong em,
369436.mes_text strong,
370437.mes_text h2,
371438.mes_text h1 {,
439+.mes_reasoning strong em,
440+.mes_reasoning strong,
441+.mes_reasoning h2,
442+.mes_reasoning h1 {
372443 font-weight: bold;
373444}
374445
375446.mes_text pre code {,
447+.mes_reasoning pre code {
376448 position: relative;
377449 display: block;
378450 overflow-x: auto;
379451 padding: 1em;
380452}
381453
382454.mes_text img:not(.mes_img) {,
455+.mes_reasoning img:not(.mes_img) {
383456 max-width: 100%;
384457 max-height: var(--doc-height);
385458}
@@ -1022,6 +1095,11 @@ body .panelControlBar {
10221095 /*only affects bubblechat to make it sit nicely at the bottom*/
10231096}
10241097
1098+.last_mes:has(.mes_text:empty):has(.mes_reasoning_details[open]) .mes_reasoning:not(:empty) {
1099+ margin-bottom: 30px;
1100+}
1101+
1102+.last_mes .mes_reasoning,
10251103.last_mes .mes_text {
10261104 padding-right: 30px;
10271105}
@@ -1235,14 +1313,18 @@ body.swipeAllMessages .mes:not(.last_mes) .swipes-counter {
12351313 overflow-y: clip;
12361314}
12371315
12381316.mes_text {,
1317+.mes_reasoning {
12391318 font-weight: 500;
12401319 line-height: calc(var(--mainFontSize) + .5rem);
1320+ max-width: 100%;
1321+ overflow-wrap: anywhere;
1322+}
1323+
1324+.mes_text {
12411325 padding-left: 0;
12421326 padding-top: 5px;
12431327 padding-bottom: 5px;
1244- max-width: 100%;
1245- overflow-wrap: anywhere;
12461328}
12471329
12481330br {
@@ -2921,7 +3003,7 @@ input[type=search]:focus::-webkit-search-cancel-button {
29213003 position: relative;
29223004}
29233005
29243006#rm_print_characters_block.character_name_block .ch_name,
29253007.avatar-container .ch_name {
29263008 flex: 1 1 auto;
29273009 white-space: nowrap;
@@ -2931,6 +3013,13 @@ input[type=search]:focus::-webkit-search-cancel-button {
29313013 display: block;
29323014}
29333015
3016+.character_name_block .character_version {
3017+ text-overflow: ellipsis;
3018+ overflow: hidden;
3019+ text-wrap: nowrap;
3020+ max-width: 50%;
3021+}
3022+
29343023#rm_print_characters_block .character_name_block> :last-child {
29353024 flex: 0 100000 auto;
29363025 /* Force shrinking first */
@@ -4143,10 +4232,12 @@ input[type="range"]::-webkit-slider-thumb {
41434232 align-items: center;
41444233}
41454234
4235+.mes_reasoning_edit_cancel,
41464236.mes_edit_cancel.menu_button {
41474237 background-color: var(--crimson70a);
41484238}
41494239
4240+.mes_reasoning_edit_done,
41504241.mes_edit_done.menu_button {
41514242 background-color: var(--okGreen70a);
41524243}
@@ -4155,6 +4246,7 @@ input[type="range"]::-webkit-slider-thumb {
41554246 opacity: 1;
41564247}
41574248
4249+.reasoning_edit_textarea,
41584250.edit_textarea {
41594251 padding: 5px;
41604252 margin: 0;
@@ -5643,6 +5735,7 @@ body:not(.movingUI) .drawer-content.maximized {
56435735
56445736.model-card .details-container {
56455737 text-align: right;
5738+ line-height: 0.9;
56465739}
56475740
56485741.model-card:hover {
@@ -5665,7 +5758,7 @@ body:not(.movingUI) .drawer-content.maximized {
56655758}
56665759
56675760.model-title {
5668- font-size: 13px;
5761+ font-size: calc(var(--mainFontSize) * 0.95);
56695762 font-weight: bold;
56705763 overflow: hidden;
56715764}
@@ -5681,7 +5774,7 @@ body:not(.movingUI) .drawer-content.maximized {
56815774.model-class,
56825775.model-context-length,
56835776.model-date-added {
5684- font-size: 10px;
5777+ font-size: calc(var(--mainFontSize) * 0.75);
56855778}
56865779
56875780.model-class,
@@ -5763,3 +5856,17 @@ body:not(.movingUI) .drawer-content.maximized {
57635856.alternate_greetings_list {
57645857 overflow-y: scroll;
57655858}
5859+
5860+.mes_text div[data-type="assistant_note"]:has(.assistant_note_export) {
5861+ display: flex;
5862+ flex-direction: row;
5863+ flex-wrap: nowrap;
5864+ justify-content: space-between;
5865+ align-items: center;
5866+ gap: 10px;
5867+ padding: 0 2px;
5868+}
5869+
5870+.mes_text div[data-type="assistant_note"]:has(.assistant_note_export)>div:not(.assistant_note_export) {
5871+ flex: 1;
5872+}
server.js+39 -18
@@ -18,10 +18,9 @@ import { hideBin } from 'yargs/helpers';
1818
1919// express/server related library imports
2020import cors from 'cors';
2121import { doubleCsrfcsrfSync } from 'csrf-csrfsync';
2222import express from 'express';
2323import compression from 'compression';
24-import cookieParser from 'cookie-parser';
2524import cookieSession from 'cookie-session';
2625import multer from 'multer';
2726import responseTime from 'response-time';
@@ -40,7 +39,6 @@ util.inspect.defaultOptions.depth = 4;
4039import { loadPlugins } from './src/plugin-loader.js';
4140import {
4241 initUserStorage,
43- getCsrfSecret,
4442 getCookieSecret,
4543 getCookieSessionName,
4644 getAllEnabledUsers,
@@ -67,6 +65,7 @@ import {
6765 forwardFetchResponse,
6866 removeColorFormatting,
6967 getSeparator,
68+ safeReadFileSync,
7069 setupLogLevel,
7170} from './src/util.js';
7271import { UPLOADS_DIRECTORY } from './src/constants.js';
@@ -348,8 +347,8 @@ if (enableCorsProxy) {
348347}
349348
350349function getSessionCookieAge() {
351350 // Defaults to 24 hours in"no secondsexpiration" if not set
352351 const configValue = getConfigValue('sessionTimeout', 24 * 60 * 60-1);
353352
354353 // Convert to milliseconds
355354 if (configValue > 0) {
@@ -378,27 +377,38 @@ app.use(setUserDataMiddleware);
378377
379378// CSRF Protection //
380379if (!disableCsrf) {
381380 const COOKIES_SECRETcsrfSyncProtection = getCookieSecretcsrfSync();{
382-
381+ getTokenFromState: (req) => {
383- const { generateToken, doubleCsrfProtection } = doubleCsrf({
382+ if (!req.session) {
384- getSecret: getCsrfSecret,
383+ console.error('(CSRF error) getTokenFromState: Session object not initialized');
385- cookieName: 'X-CSRF-Token',
384+ return;
386- cookieOptions: {
385+ }
387- sameSite: 'strict',
386+ return req.session.csrfToken;
388- secure: false,
387+ },
388+ getTokenFromRequest: (req) => {
389+ return req.headers['x-csrf-token']?.toString();
390+ },
391+ storeTokenInState: (req, token) => {
392+ if (!req.session) {
393+ console.error('(CSRF error) storeTokenInState: Session object not initialized');
394+ return;
395+ }
396+ req.session.csrfToken = token;
389397 },
390398 size: 6432,
391- getTokenFromRequest: (req) => req.headers['x-csrf-token'],
392399 });
393400
394401 app.get('/csrf-token', (req, res) => {
395402 res.json({
396403 'token': csrfSyncProtection.generateToken(res, req),
397404 });
398405 });
399406
400- app.use(cookieParser(COOKIES_SECRET));
407+ // Customize the error message
401- app.use(doubleCsrfProtection);
408+ csrfSyncProtection.invalidCsrfTokenError.message = color.red('Invalid CSRF token. Please refresh the page and try again.');
409+ csrfSyncProtection.invalidCsrfTokenError.stack = undefined;
410+
411+ app.use(csrfSyncProtection.csrfSynchronisedProtection);
402412} else {
403413 console.warn('\nCSRF protection is disabled. This will make your server vulnerable to CSRF attacks.\n');
404414 app.get('/csrf-token', (req, res) => {
@@ -925,6 +935,16 @@ async function verifySecuritySettings() {
925935 }
926936}
927937
938+/**
939+ * Registers a not-found error response if a not-found error page exists. Should only be called after all other middlewares have been registered.
940+ */
941+function apply404Middleware() {
942+ const notFoundWebpage = safeReadFileSync('./public/error/url-not-found.html') ?? '';
943+ app.use((req, res) => {
944+ res.status(404).send(notFoundWebpage);
945+ });
946+}
947+
928948// User storage module needs to be initialized before starting the server
929949initUserStorage(dataRoot)
930950 .then(ensurePublicDirectoriesExist)
@@ -932,4 +952,5 @@ initUserStorage(dataRoot)
932952 .then(migrateSystemPrompts)
933953 .then(verifySecuritySettings)
934954 .then(preSetupTasks)
955+ .then(apply404Middleware)
935956 .finally(startServer);
src/endpoints/avatars.js+2 -1
@@ -9,6 +9,7 @@ import { sync as writeFileAtomicSync } from 'write-file-atomic';
99import { jsonParser, urlencodedParser } from '../express-common.js';
1010import { AVATAR_WIDTH, AVATAR_HEIGHT } from '../constants.js';
1111import { getImages, tryParse } from '../util.js';
12+import { getFileNameValidationFunction } from '../middleware/validateFileName.js';
1213
1314export const router = express.Router();
1415
@@ -17,7 +18,7 @@ router.post('/get', jsonParser, function (request, response) {
1718 response.send(JSON.stringify(images));
1819});
1920
2021router.post('/delete', jsonParser, getFileNameValidationFunction('avatar'), function (request, response) {
2122 if (!request.body) return response.sendStatus(400);
2223
2324 if (request.body.avatar !== sanitize(request.body.avatar)) {
src/endpoints/backends/chat-completions.js+113 -23
@@ -37,6 +37,8 @@ import {
3737 getTiktokenTokenizer,
3838 sentencepieceTokenizers,
3939 TEXT_COMPLETION_MODELS,
40+ webTokenizers,
41+ getWebTokenizer,
4042} from '../tokenizers.js';
4143
4244const API_OPENAI = 'https://api.openai.com/v1';
@@ -61,6 +63,7 @@ const API_DEEPSEEK = 'https://api.deepseek.com/beta';
6163 * @returns
6264 */
6365function postProcessPrompt(messages, type, names) {
66+ const addAssistantPrefix = x => x.length && (x[x.length - 1].role !== 'assistant' || (x[x.length - 1].prefix = true)) ? x : x;
6467 switch (type) {
6568 case 'merge':
6669 case 'claude':
@@ -70,7 +73,9 @@ function postProcessPrompt(messages, type, names) {
7073 case 'strict':
7174 return mergeMessages(messages, names, true, true);
7275 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));
7479 default:
7580 return messages;
7681 }
@@ -284,6 +289,7 @@ async function sendMakerSuiteRequest(request, response) {
284289 const model = String(request.body.model);
285290 const stream = Boolean(request.body.stream);
286291 const showThoughts = Boolean(request.body.show_thoughts);
292+ const isThinking = model.includes('thinking');
287293
288294 const generationConfig = {
289295 stopSequences: request.body.stop,
@@ -324,6 +330,12 @@ async function sendMakerSuiteRequest(request, response) {
324330 body.systemInstruction = prompt.system_instruction;
325331 }
326332
333+ if (isThinking && showThoughts) {
334+ generationConfig.thinkingConfig = {
335+ includeThoughts: true,
336+ };
337+ }
338+
327339 return body;
328340 }
329341
@@ -337,7 +349,6 @@ async function sendMakerSuiteRequest(request, response) {
337349 controller.abort();
338350 });
339351
340- const isThinking = model.includes('thinking');
341352 const apiVersion = isThinking ? 'v1alpha' : 'v1beta';
342353 const responseType = (stream ? 'streamGenerateContent' : 'generateContent');
343354
@@ -382,11 +393,7 @@ async function sendMakerSuiteRequest(request, response) {
382393 const responseContent = candidates[0].content ?? candidates[0].output;
383394 console.error('Google AI Studio response:', responseContent);
384395
385- if (Array.isArray(responseContent?.parts) && isThinking && !showThoughts) {
396+ const responseText = typeof responseContent === 'string' ? responseContent : responseContent?.parts?.filter(part => !part.thought)?.map(part => part.text)?.join('\n\n');
386- responseContent.parts = responseContent.parts.filter(part => !part.thought);
387- }
388-
389- const responseText = typeof responseContent === 'string' ? responseContent : responseContent?.parts?.map(part => part.text)?.join('\n\n');
390397 if (!responseText) {
391398 let message = 'Google AI Studio Candidate text empty';
392399 console.error(message, generateResponseJson);
@@ -394,7 +401,7 @@ async function sendMakerSuiteRequest(request, response) {
394401 }
395402
396403 // Wrap it back to OAI format
397404 const reply = { choices: [{ 'message': { 'content': responseText } }], responseContent };
398405 return response.send(reply);
399406 }
400407 } catch (error) {
@@ -639,6 +646,89 @@ async function sendCohereRequest(request, response) {
639646 }
640647}
641648
649+/**
650+ * Sends a request to DeepSeek API.
651+ * @param {express.Request} request Express request
652+ * @param {express.Response} response Express response
653+ */
654+async function sendDeepSeekRequest(request, response) {
655+ const apiUrl = new URL(request.body.reverse_proxy || API_DEEPSEEK).toString();
656+ const apiKey = request.body.reverse_proxy ? request.body.proxy_password : readSecret(request.user.directories, SECRET_KEYS.DEEPSEEK);
657+
658+ if (!apiKey && !request.body.reverse_proxy) {
659+ console.log('DeepSeek API key is missing.');
660+ return response.status(400).send({ error: true });
661+ }
662+
663+ const controller = new AbortController();
664+ request.socket.removeAllListeners('close');
665+ request.socket.on('close', function () {
666+ controller.abort();
667+ });
668+
669+ try {
670+ let bodyParams = {};
671+
672+ if (request.body.logprobs > 0) {
673+ bodyParams['top_logprobs'] = request.body.logprobs;
674+ bodyParams['logprobs'] = true;
675+ }
676+
677+ const postProcessType = String(request.body.model).endsWith('-reasoner') ? 'deepseek-reasoner' : 'deepseek';
678+ const processedMessages = postProcessPrompt(request.body.messages, postProcessType, getPromptNames(request));
679+
680+ const requestBody = {
681+ 'messages': processedMessages,
682+ 'model': request.body.model,
683+ 'temperature': request.body.temperature,
684+ 'max_tokens': request.body.max_tokens,
685+ 'stream': request.body.stream,
686+ 'presence_penalty': request.body.presence_penalty,
687+ 'frequency_penalty': request.body.frequency_penalty,
688+ 'top_p': request.body.top_p,
689+ 'stop': request.body.stop,
690+ 'seed': request.body.seed,
691+ ...bodyParams,
692+ };
693+
694+ const config = {
695+ method: 'POST',
696+ headers: {
697+ 'Content-Type': 'application/json',
698+ 'Authorization': 'Bearer ' + apiKey,
699+ },
700+ body: JSON.stringify(requestBody),
701+ signal: controller.signal,
702+ };
703+
704+ console.log('DeepSeek request:', requestBody);
705+
706+ const generateResponse = await fetch(apiUrl + '/chat/completions', config);
707+
708+ if (request.body.stream) {
709+ forwardFetchResponse(generateResponse, response);
710+ } else {
711+ if (!generateResponse.ok) {
712+ const errorText = await generateResponse.text();
713+ console.log(`DeepSeek API returned error: ${generateResponse.status} ${generateResponse.statusText} ${errorText}`);
714+ const errorJson = tryParse(errorText) ?? { error: true };
715+ return response.status(500).send(errorJson);
716+ }
717+ const generateResponseJson = await generateResponse.json();
718+ console.log('DeepSeek response:', generateResponseJson);
719+ return response.send(generateResponseJson);
720+ }
721+ } catch (error) {
722+ console.log('Error communicating with DeepSeek API: ', error);
723+ if (!response.headersSent) {
724+ response.send({ error: true });
725+ } else {
726+ response.end();
727+ }
728+ }
729+}
730+
731+
642732export const router = express.Router();
643733
644734router.post('/status', jsonParser, async function (request, response_getstatus_openai) {
@@ -683,8 +773,8 @@ router.post('/status', jsonParser, async function (request, response_getstatus_o
683773 api_key_openai = readSecret(request.user.directories, SECRET_KEYS.NANOGPT);
684774 headers = {};
685775 } else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.DEEPSEEK) {
686776 api_url = new URL(request.body.reverse_proxy || API_DEEPSEEK.replace('/beta', ''));
687777 api_key_openai = request.body.reverse_proxy ? request.body.proxy_password : readSecret(request.user.directories, SECRET_KEYS.DEEPSEEK);
688778 headers = {};
689779 } else {
690780 console.error('This chat completion source is not supported yet.');
@@ -779,6 +869,14 @@ router.post('/bias', jsonParser, async function (request, response) {
779869 return response.send({});
780870 }
781871 encodeFunction = (text) => new Uint32Array(instance.encodeIds(text));
872+ } else if (webTokenizers.includes(model)) {
873+ const tokenizer = getWebTokenizer(model);
874+ const instance = await tokenizer?.get();
875+ if (!instance) {
876+ console.warn('Tokenizer not initialized:', model);
877+ return response.send({});
878+ }
879+ encodeFunction = (text) => new Uint32Array(instance.encode(text));
782880 } else {
783881 const tokenizer = getTiktokenTokenizer(model);
784882 encodeFunction = (tokenizer.encode.bind(tokenizer));
@@ -843,6 +941,7 @@ router.post('/generate', jsonParser, function (request, response) {
843941 case CHAT_COMPLETION_SOURCES.MAKERSUITE: return sendMakerSuiteRequest(request, response);
844942 case CHAT_COMPLETION_SOURCES.MISTRALAI: return sendMistralAIRequest(request, response);
845943 case CHAT_COMPLETION_SOURCES.COHERE: return sendCohereRequest(request, response);
944+ case CHAT_COMPLETION_SOURCES.DEEPSEEK: return sendDeepSeekRequest(request, response);
846945 }
847946
848947 let apiUrl;
@@ -901,6 +1000,10 @@ router.post('/generate', jsonParser, function (request, response) {
9011000 bodyParams['route'] = 'fallback';
9021001 }
9031002
1003+ if (request.body.show_thoughts) {
1004+ bodyParams['include_reasoning'] = true;
1005+ }
1006+
9041007 let cachingAtDepth = getConfigValue('claude.cachingAtDepth', -1);
9051008 if (Number.isInteger(cachingAtDepth) && cachingAtDepth >= 0 && request.body.model?.startsWith('anthropic/claude-3')) {
9061009 cachingAtDepthForOpenRouterClaude(request.body.messages, cachingAtDepth);
@@ -956,18 +1059,6 @@ router.post('/generate', jsonParser, function (request, response) {
9561059 apiKey = readSecret(request.user.directories, SECRET_KEYS.BLOCKENTROPY);
9571060 headers = {};
9581061 bodyParams = {};
959- } else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.DEEPSEEK) {
960- apiUrl = API_DEEPSEEK;
961- apiKey = readSecret(request.user.directories, SECRET_KEYS.DEEPSEEK);
962- headers = {};
963- bodyParams = {};
964-
965- if (request.body.logprobs > 0) {
966- bodyParams['top_logprobs'] = request.body.logprobs;
967- bodyParams['logprobs'] = true;
968- }
969-
970- request.body.messages = postProcessPrompt(request.body.messages, 'deepseek', getPromptNames(request));
9711062 } else {
9721063 console.error('This chat completion source is not supported yet.');
9731064 return response.status(400).send({ error: true });
@@ -1105,4 +1196,3 @@ router.post('/generate', jsonParser, function (request, response) {
11051196 }
11061197 }
11071198});
1108-
src/endpoints/backgrounds.js+2 -1
@@ -7,6 +7,7 @@ import sanitize from 'sanitize-filename';
77import { jsonParser, urlencodedParser } from '../express-common.js';
88import { invalidateThumbnail } from './thumbnails.js';
99import { getImages } from '../util.js';
10+import { getFileNameValidationFunction } from '../middleware/validateFileName.js';
1011
1112export const router = express.Router();
1213
@@ -15,7 +16,7 @@ router.post('/all', jsonParser, function (request, response) {
1516 response.send(JSON.stringify(images));
1617});
1718
1819router.post('/delete', jsonParser, getFileNameValidationFunction('bg'), function (request, response) {
1920 if (!request.body) return response.sendStatus(400);
2021
2122 if (request.body.bg !== sanitize(request.body.bg)) {
src/endpoints/characters.js+19 -12
@@ -14,6 +14,7 @@ import jimp from 'jimp';
1414
1515import { AVATAR_WIDTH, AVATAR_HEIGHT } from '../constants.js';
1616import { jsonParser, urlencodedParser } from '../express-common.js';
17+import { default as validateAvatarUrlMiddleware, getFileNameValidationFunction } from '../middleware/validateFileName.js';
1718import { deepMerge, humanizedISO8601DateTime, tryParse, extractFileFromZipBuffer, MemoryLimitedMap, getConfigValue } from '../util.js';
1819import { TavernCardValidator } from '../validator/TavernCardValidator.js';
1920import { parse, write } from '../character-card-parser.js';
@@ -73,12 +74,18 @@ async function writeCharacterData(inputFile, data, outputFile, request, crop = u
7374 * Read the image, resize, and save it as a PNG into the buffer.
7475 * @returns {Promise<Buffer>} Image buffer
7576 */
7677 async function getInputImage() {
78+ try {
7779 if (Buffer.isBuffer(inputFile)) {
7880 return await parseImageBuffer(inputFile, crop);
7981 }
8082
8183 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+ }
8289 }
8390
8491 const inputImage = await getInputImage();
@@ -756,7 +763,7 @@ router.post('/create', urlencodedParser, async function (request, response) {
756763 }
757764});
758765
759766router.post('/rename', jsonParser, validateAvatarUrlMiddleware, async function (request, response) {
760767 if (!request.body.avatar_url || !request.body.new_name) {
761768 return response.sendStatus(400);
762769 }
@@ -803,7 +810,7 @@ router.post('/rename', jsonParser, async function (request, response) {
803810 }
804811});
805812
806813router.post('/edit', urlencodedParser, validateAvatarUrlMiddleware, async function (request, response) {
807814 if (!request.body) {
808815 console.error('Error: no response body detected');
809816 response.status(400).send('Error: no response body detected');
@@ -852,7 +859,7 @@ router.post('/edit', urlencodedParser, async function (request, response) {
852859 * @param {Object} response - The HTTP response object.
853860 * @returns {void}
854861 */
855862router.post('/edit-attribute', jsonParser, validateAvatarUrlMiddleware, async function (request, response) {
856863 console.debug(request.body);
857864 if (!request.body) {
858865 console.error('Error: no response body detected');
@@ -898,7 +905,7 @@ router.post('/edit-attribute', jsonParser, async function (request, response) {
898905 *
899906 * @returns {void}
900907 * */
901908router.post('/merge-attributes', jsonParser, getFileNameValidationFunction('avatar'), async function (request, response) {
902909 try {
903910 const update = request.body;
904911 const avatarPath = path.join(request.user.directories.characters, update.avatar);
@@ -929,7 +936,7 @@ router.post('/merge-attributes', jsonParser, async function (request, response)
929936 }
930937});
931938
932939router.post('/delete', jsonParser, validateAvatarUrlMiddleware, async function (request, response) {
933940 if (!request.body || !request.body.avatar_url) {
934941 return response.sendStatus(400);
935942 }
@@ -992,7 +999,7 @@ router.post('/all', jsonParser, async function (request, response) {
992999 }
9931000});
9941001
9951002router.post('/get', jsonParser, validateAvatarUrlMiddleware, async function (request, response) {
9961003 try {
9971004 if (!request.body) return response.sendStatus(400);
9981005 const item = request.body.avatar_url;
@@ -1011,7 +1018,7 @@ router.post('/get', jsonParser, async function (request, response) {
10111018 }
10121019});
10131020
10141021router.post('/chats', jsonParser, validateAvatarUrlMiddleware, async function (request, response) {
10151022 if (!request.body) return response.sendStatus(400);
10161023
10171024 const characterDirectory = (request.body.avatar_url).replace('.png', '');
@@ -1160,7 +1167,7 @@ router.post('/import', urlencodedParser, async function (request, response) {
11601167 }
11611168});
11621169
11631170router.post('/duplicate', jsonParser, validateAvatarUrlMiddleware, async function (request, response) {
11641171 try {
11651172 if (!request.body.avatar_url) {
11661173 console.error('avatar URL not found in request body');
@@ -1206,7 +1213,7 @@ router.post('/duplicate', jsonParser, async function (request, response) {
12061213 }
12071214});
12081215
12091216router.post('/export', jsonParser, validateAvatarUrlMiddleware, async function (request, response) {
12101217 try {
12111218 if (!request.body.format || !request.body.avatar_url) {
12121219 return response.sendStatus(400);
src/endpoints/chats.js+38 -7
@@ -9,6 +9,7 @@ import { sync as writeFileAtomicSync } from 'write-file-atomic';
99import _ from 'lodash';
1010
1111import { jsonParser, urlencodedParser } from '../express-common.js';
12+import validateAvatarUrlMiddleware from '../middleware/validateFileName.js';
1213import {
1314 getConfigValue,
1415 humanizedISO8601DateTime,
@@ -264,9 +265,37 @@ function flattenChubChat(userName, characterName, lines) {
264265 return (lines ?? []).map(convert).join('\n');
265266}
266267
268+/**
269+ * Imports a chat from RisuAI format.
270+ * @param {string} userName User name
271+ * @param {string} characterName Character name
272+ * @param {object} jsonData Imported chat data
273+ * @returns {string} Chat data
274+ */
275+function importRisuChat(userName, characterName, jsonData) {
276+ /** @type {object[]} */
277+ const chat = [{
278+ user_name: userName,
279+ character_name: characterName,
280+ create_date: humanizedISO8601DateTime(),
281+ }];
282+
283+ for (const message of jsonData.data.message) {
284+ const isUser = message.role === 'user';
285+ chat.push({
286+ name: message.name ?? (isUser ? userName : characterName),
287+ is_user: isUser,
288+ send_date: Number(message.time ?? Date.now()),
289+ mes: message.data ?? '',
290+ });
291+ }
292+
293+ return chat.map(obj => JSON.stringify(obj)).join('\n');
294+}
295+
267296export const router = express.Router();
268297
269298router.post('/save', jsonParser, validateAvatarUrlMiddleware, function (request, response) {
270299 try {
271300 const directoryName = String(request.body.avatar_url).replace('.png', '');
272301 const chatData = request.body.chat;
@@ -282,7 +311,7 @@ router.post('/save', jsonParser, function (request, response) {
282311 }
283312});
284313
285314router.post('/get', jsonParser, validateAvatarUrlMiddleware, function (request, response) {
286315 try {
287316 const dirName = String(request.body.avatar_url).replace('.png', '');
288317 const directoryPath = path.join(request.user.directories.chats, dirName);
@@ -319,7 +348,7 @@ router.post('/get', jsonParser, function (request, response) {
319348});
320349
321350
322351router.post('/rename', jsonParser, validateAvatarUrlMiddleware, async function (request, response) {
323352 if (!request.body || !request.body.original_file || !request.body.renamed_file) {
324353 return response.sendStatus(400);
325354 }
@@ -344,7 +373,7 @@ router.post('/rename', jsonParser, async function (request, response) {
344373 return response.send({ ok: true, sanitizedFileName });
345374});
346375
347376router.post('/delete', jsonParser, validateAvatarUrlMiddleware, function (request, response) {
348377 const dirName = String(request.body.avatar_url).replace('.png', '');
349378 const fileName = String(request.body.chatfile);
350379 const filePath = path.join(request.user.directories.chats, dirName, sanitize(fileName));
@@ -360,7 +389,7 @@ router.post('/delete', jsonParser, function (request, response) {
360389 return response.send('ok');
361390});
362391
363392router.post('/export', jsonParser, validateAvatarUrlMiddleware, async function (request, response) {
364393 if (!request.body.file || (!request.body.avatar_url && request.body.is_group === false)) {
365394 return response.sendStatus(400);
366395 }
@@ -449,7 +478,7 @@ router.post('/group/import', urlencodedParser, function (request, response) {
449478 }
450479});
451480
452481router.post('/import', urlencodedParser, validateAvatarUrlMiddleware, function (request, response) {
453482 if (!request.body) return response.sendStatus(400);
454483
455484 const format = request.body.file_type;
@@ -480,6 +509,8 @@ router.post('/import', urlencodedParser, function (request, response) {
480509 importFunc = importOobaChat;
481510 } else if (Array.isArray(jsonData.messages)) { // Agnai's format
482511 importFunc = importAgnaiChat;
512+ } else if (jsonData.type === 'risuChat') { // RisuAI format
513+ importFunc = importRisuChat;
483514 } else { // Unknown format
484515 console.error('Incorrect chat format .json');
485516 return response.send({ error: true });
@@ -595,7 +626,7 @@ router.post('/group/save', jsonParser, (request, response) => {
595626 return response.send({ ok: true });
596627});
597628
598629router.post('/search', jsonParser, validateAvatarUrlMiddleware, function (request, response) {
599630 try {
600631 const { query, avatar_url, group_id } = request.body;
601632 let chatFiles = [];
src/endpoints/search.js+37 -0
@@ -4,6 +4,8 @@ import express from 'express';
44import { decode } from 'html-entities';
55import { readSecret, SECRET_KEYS } from './secrets.js';
66import { jsonParser } from '../express-common.js';
7+import { trimV1 } from '../util.js';
8+import { setAdditionalHeaders } from '../additional-headers.js';
79
810export const router = express.Router();
911
@@ -257,6 +259,41 @@ router.post('/tavily', jsonParser, async (request, response) => {
257259 }
258260});
259261
262+router.post('/koboldcpp', jsonParser, async (request, response) => {
263+ try {
264+ const { query, url } = request.body;
265+
266+ if (!url) {
267+ console.error('No URL provided for KoboldCpp search');
268+ return response.sendStatus(400);
269+ }
270+
271+ console.debug('KoboldCpp search query', query);
272+
273+ const baseUrl = trimV1(url);
274+ const args = {
275+ method: 'POST',
276+ headers: {},
277+ body: JSON.stringify({ q: query }),
278+ };
279+
280+ setAdditionalHeaders(request, args, baseUrl);
281+ const result = await fetch(`${baseUrl}/api/extra/websearch`, args);
282+
283+ if (!result.ok) {
284+ const text = await result.text();
285+ console.error('KoboldCpp request failed', result.statusText, text);
286+ return response.status(500).send(text);
287+ }
288+
289+ const data = await result.json();
290+ return response.json(data);
291+ } catch (error) {
292+ console.error(error);
293+ return response.sendStatus(500);
294+ }
295+});
296+
260297router.post('/visit', jsonParser, async (request, response) => {
261298 try {
262299 const url = request.body.url;
src/endpoints/settings.js+5 -4
@@ -9,9 +9,10 @@ import { SETTINGS_FILE } from '../constants.js';
99import { getConfigValue, generateTimestamp, removeOldBackups } from '../util.js';
1010import { jsonParser } from '../express-common.js';
1111import { getAllUserHandles, getUserDirectories } from '../users.js';
12+import { getFileNameValidationFunction } from '../middleware/validateFileName.js';
1213
1314const ENABLE_EXTENSIONS = !!getConfigValue('enableExtensionsextensions.enabled', true);
1415const ENABLE_EXTENSIONS_AUTO_UPDATE = !!getConfigValue('enableExtensionsAutoUpdateextensions.autoUpdate', true);
1516const ENABLE_ACCOUNTS = getConfigValue('enableUserAccounts', false);
1617
1718// 10 minutes
@@ -296,7 +297,7 @@ router.post('/get-snapshots', jsonParser, async (request, response) => {
296297 }
297298});
298299
299300router.post('/load-snapshot', jsonParser, getFileNameValidationFunction('name'), async (request, response) => {
300301 try {
301302 const userFilesPattern = getFilePrefix(request.user.profile.handle);
302303
@@ -330,7 +331,7 @@ router.post('/make-snapshot', jsonParser, async (request, response) => {
330331 }
331332});
332333
333334router.post('/restore-snapshot', jsonParser, getFileNameValidationFunction('name'), async (request, response) => {
334335 try {
335336 const userFilesPattern = getFilePrefix(request.user.profile.handle);
336337
src/endpoints/tokenizers.js+42 -0
@@ -238,6 +238,15 @@ export const sentencepieceTokenizers = [
238238 'jamba',
239239];
240240
241+export const webTokenizers = [
242+ 'claude',
243+ 'llama3',
244+ 'command-r',
245+ 'qwen2',
246+ 'nemo',
247+ 'deepseek',
248+];
249+
241250/**
242251 * Gets the Sentencepiece tokenizer by the model name.
243252 * @param {string} model Sentencepiece model name
@@ -276,6 +285,39 @@ export function getSentencepiceTokenizer(model) {
276285}
277286
278287/**
288+ * Gets the Web tokenizer by the model name.
289+ * @param {string} model Web tokenizer model name
290+ * @returns {WebTokenizer|null} Web tokenizer
291+ */
292+export 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+/**
279321 * Counts the token ids for the given text using the Sentencepiece tokenizer.
280322 * @param {SentencePieceTokenizer} tokenizer Sentencepiece tokenizer
281323 * @param {string} text Text to tokenize
src/endpoints/users-private.js+1 -0
@@ -23,6 +23,7 @@ router.post('/logout', async (request, response) => {
2323 }
2424
2525 request.session.handle = null;
26+ request.session.csrfToken = null;
2627 request.session = null;
2728 return response.sendStatus(204);
2829 } catch (error) {
src/endpoints/vectors.js+1 -1
@@ -164,7 +164,7 @@ function getSourceSettings(source, request) {
164164 };
165165 case 'transformers':
166166 return {
167167 model: getConfigValue('extrasextensions.embeddingModelmodels.embedding', ''),
168168 };
169169 case 'palm':
170170 return {
src/middleware/basicAuth.js+4 -3
@@ -5,17 +5,18 @@
55import { Buffer } from 'node:buffer';
66import storage from 'node-persist';
77import { getAllUserHandles, toKey, getPasswordHash } from '../users.js';
88import { getConfig, getConfigValue, safeReadFileSync } from '../util.js';
99
1010const PER_USER_BASIC_AUTH = getConfigValue('perUserBasicAuth', false);
1111const ENABLE_ACCOUNTS = getConfigValue('enableUserAccounts', false);
1212
13+const basicAuthMiddleware = async function (request, response, callback) {
14+ const unauthorizedWebpage = safeReadFileSync('./public/error/unauthorized.html') ?? '';
1315 const unauthorizedResponse = (res) => {
1416 res.set('WWW-Authenticate', 'Basic realm="SillyTavern", charset="UTF-8"');
1517 return res.status(401).send('Authentication required'unauthorizedWebpage);
1618 };
1719
18-const basicAuthMiddleware = async function (request, response, callback) {
1920 const config = getConfig();
2021 const authHeader = request.headers.authorization;
2122
src/middleware/validateFileName.js+34 -0
@@ -0,0 +1,34 @@
1+import 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+ */
8+export 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+
33+const avatarUrlValidationFunction = getFileNameValidationFunction('avatar_url');
34+export default avatarUrlValidationFunction;
src/middleware/whitelist.js+16 -5
@@ -1,10 +1,11 @@
11import path from 'node:path';
22import fs from 'node:fs';
33import process from 'node:process';
4+import Handlebars from 'handlebars';
45import ipMatching from 'ip-matching';
56
67import { getIpFromRequest } from '../express-common.js';
78import { color, getConfigValue, safeReadFileSync } from '../util.js';
89
910const whitelistPath = path.join(process.cwd(), './whitelist.txt');
1011const enableForwardedWhitelist = getConfigValue('enableForwardedWhitelist', false);
@@ -52,12 +53,16 @@ function getForwardedIp(req) {
5253 * @returns {import('express').RequestHandler} The middleware function
5354 */
5455export default function whitelistMiddleware(whitelistMode, listen) {
56+ const forbiddenWebpage = Handlebars.compile(
57+ safeReadFileSync('./public/error/forbidden-by-whitelist.html') ?? '',
58+ );
59+
5560 return function (req, res, next) {
5661 const clientIp = getIpFromRequest(req);
5762 const forwardedIp = getForwardedIp(req);
63+ const userAgent = req.headers['user-agent'];
5864
5965 if (listen && !knownIPs.has(clientIp)) {
60- const userAgent = req.headers['user-agent'];
6166 console.log(color.yellow(`New connection from ${clientIp}; User Agent: ${userAgent}\n`));
6267 knownIPs.add(clientIp);
6368
@@ -76,9 +81,15 @@ export default function whitelistMiddleware(whitelistMode, listen) {
7681 || forwardedIp && whitelistMode === true && !whitelist.some(x => ipMatching.matches(forwardedIp, ipMatching.getMatch(x)))
7782 ) {
7883 // 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 }));
8293 }
8394 next();
8495 };
src/prompt-converters.js+2 -0
@@ -360,6 +360,8 @@ export function convertCohereMessages(messages, names) {
360360 */
361361export function convertGooglePrompt(messages, model, useSysPrompt, names) {
362362 const visionSupportedModels = [
363+ 'gemini-2.0-flash-thinking-exp',
364+ 'gemini-2.0-flash-thinking-exp-01-21',
363365 'gemini-2.0-flash-thinking-exp-1219',
364366 'gemini-2.0-flash-exp',
365367 'gemini-1.5-flash',
src/transformers.js+6 -6
@@ -19,31 +19,31 @@ const tasks = {
1919 'text-classification': {
2020 defaultModel: 'Cohee/distilbert-base-uncased-go-emotions-onnx',
2121 pipeline: null,
2222 configField: 'extrasextensions.classificationModelmodels.classification',
2323 quantized: true,
2424 },
2525 'image-to-text': {
2626 defaultModel: 'Xenova/vit-gpt2-image-captioning',
2727 pipeline: null,
2828 configField: 'extrasextensions.captioningModelmodels.captioning',
2929 quantized: true,
3030 },
3131 'feature-extraction': {
3232 defaultModel: 'Xenova/all-mpnet-base-v2',
3333 pipeline: null,
3434 configField: 'extrasextensions.embeddingModelmodels.embedding',
3535 quantized: true,
3636 },
3737 'automatic-speech-recognition': {
3838 defaultModel: 'Xenova/whisper-small',
3939 pipeline: null,
4040 configField: 'extrasextensions.speechToTextModelmodels.speechToText',
4141 quantized: true,
4242 },
4343 'text-to-speech': {
4444 defaultModel: 'Xenova/speecht5_tts',
4545 pipeline: null,
4646 configField: 'extrasextensions.textToSpeechModelmodels.textToSpeech',
4747 quantized: false,
4848 },
4949};
@@ -132,7 +132,7 @@ export async function getPipeline(task, forceModel = '') {
132132
133133 const cacheDir = path.join(globalThis.DATA_ROOT, '_cache');
134134 const model = forceModel || getModelForTask(task);
135135 const localOnly = !getConfigValue('extrasextensions.disableAutoDownloadmodels.autoDownload', falsetrue);
136136 console.log('Initializing transformers.js pipeline for task', task, 'with model', model);
137137 const instance = await pipeline(task, model, { cache_dir: cacheDir, quantized: tasks[task].quantized ?? true, local_files_only: localOnly });
138138 tasks[task].pipeline = instance;
src/users.js+2 -1
@@ -458,7 +458,8 @@ export function getPasswordSalt() {
458458 */
459459export function getCookieSessionName() {
460460 // 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);
462463 return `session-${suffix}`;
463464}
464465
src/util.js+11 -0
@@ -889,3 +889,14 @@ export class MemoryLimitedMap {
889889 return this.map[Symbol.iterator]();
890890 }
891891}
892+
893+/**
894+ * A 'safe' version of `fs.readFileSync()`. Returns the contents of a file if it exists, falling back to a default value if not.
895+ * @param {string} filePath Path of the file to be read.
896+ * @param {Parameters<typeof fs.readFileSync>[1]} options Options object to pass through to `fs.readFileSync()` (default: `{ encoding: 'utf-8' }`).
897+ * @returns The contents at `filePath` if it exists, or `null` if not.
898+ */
899+export function safeReadFileSync(filePath, options = { encoding: 'utf-8' }) {
900+ if (fs.existsSync(filePath)) return fs.readFileSync(filePath, options);
901+ return null;
902+}