Merge branch 'staging' into support-multiple-expressions

c12f26441e65082c2b63a60c139e750c53ea938d

Wolfsblvt <wolfsblvt@gmail.com>

124 files changed, +3038 -1309Showing whitespace changes
.github/ISSUE_TEMPLATE/bug-report.yml+2 -0
@@ -80,6 +80,8 @@ body:
80 required: true80 required: true
81 - label: I have checked the [docs](https://docs.sillytavern.app/) ![important](https://img.shields.io/badge/Important!-F6094E)81 - label: I have checked the [docs](https://docs.sillytavern.app/) ![important](https://img.shields.io/badge/Important!-F6094E)
82 required: true82 required: true
83 - label: I confirm that my issue is not related to third-party content, unofficial extension or patch. If in doubt, check with a new [user account](https://docs.sillytavern.app/administration/multi-user/) and with extensions disabled
84 required: true
8385
84 - type: markdown86 - type: markdown
85 attributes:87 attributes:
default/config.yaml+14 -0
@@ -6,7 +6,13 @@ cardsCacheCapacity: 100
6# -- SERVER CONFIGURATION --6# -- SERVER CONFIGURATION --
7# Listen for incoming connections7# Listen for incoming connections
8listen: false8listen: false
9# Listen on a specific address, supports IPv4 and IPv6
10listenAddress:
11 ipv4: 0.0.0.0
12 ipv6: '[::]'
9# Enables IPv6 and/or IPv4 protocols. Need to have at least one enabled!13# Enables IPv6 and/or IPv4 protocols. Need to have at least one enabled!
14# - Use option "auto" to automatically detect support
15# - Use true or false (no qoutes) to enable or disable each protocol
10protocol:16protocol:
11 ipv4: true17 ipv4: true
12 ipv6: false18 ipv6: false
@@ -65,6 +71,8 @@ autheliaAuth: false
65# the username and passwords for basic auth are the same as those71# the username and passwords for basic auth are the same as those
66# for the individual accounts72# for the individual accounts
67perUserBasicAuth: false73perUserBasicAuth: false
74# Minimum log level to display in the terminal (DEBUG = 0, INFO = 1, WARN = 2, ERROR = 3)
75minLogLevel: 0
6876
69# User session timeout *in seconds* (defaults to 24 hours).77# User session timeout *in seconds* (defaults to 24 hours).
70## Set to a positive number to expire session after a certain time of inactivity78## Set to a positive number to expire session after a certain time of inactivity
@@ -179,6 +187,10 @@ ollama:
179 # * 0: Unload the model immediately after the request187 # * 0: Unload the model immediately after the request
180 # * N (any positive number): Keep the model loaded for N seconds after the request.188 # * N (any positive number): Keep the model loaded for N seconds after the request.
181 keepAlive: -1189 keepAlive: -1
190 # Controls the "num_batch" (batch size) parameter of the generation request
191 # * -1: Use the default value of the model
192 # * N (positive number): Use the specified value. Must be a power of 2, e.g. 128, 256, 512, etc.
193 batchSize: -1
182# -- ANTHROPIC CLAUDE API CONFIGURATION --194# -- ANTHROPIC CLAUDE API CONFIGURATION --
183claude:195claude:
184 # Enables caching of the system prompt (if supported).196 # Enables caching of the system prompt (if supported).
@@ -198,3 +210,5 @@ claude:
198 cachingAtDepth: -1210 cachingAtDepth: -1
199# -- SERVER PLUGIN CONFIGURATION --211# -- SERVER PLUGIN CONFIGURATION --
200enableServerPlugins: false212enableServerPlugins: false
213# Attempt to automatically update server plugins on startup
214enableServerPluginsAutoUpdate: true
default/content/index.json+0 -4
@@ -672,10 +672,6 @@
672 "type": "moving_ui"672 "type": "moving_ui"
673 },673 },
674 {674 {
675 "filename": "presets/moving-ui/Black Magic Time.json",
676 "type": "moving_ui"
677 },
678 {
679 "filename": "presets/quick-replies/Default.json",675 "filename": "presets/quick-replies/Default.json",
680 "type": "quick_replies"676 "type": "quick_replies"
681 },677 },
default/content/presets/moving-ui/Black Magic Time.json+0 -45
@@ -1,45 +0,0 @@
1{
2 "name": "Black Magic Time",
3 "movingUIState": {
4 "sheld": {
5 "top": 488,
6 "left": 1407,
7 "right": 1,
8 "bottom": 4,
9 "margin": "unset",
10 "width": 471,
11 "height": 439
12 },
13 "floatingPrompt": {
14 "width": 369,
15 "height": 441
16 },
17 "right-nav-panel": {
18 "top": 0,
19 "left": 1400,
20 "right": 111,
21 "bottom": 446,
22 "margin": "unset",
23 "width": 479,
24 "height": 487
25 },
26 "WorldInfo": {
27 "top": 41,
28 "left": 369,
29 "right": 642,
30 "bottom": 51,
31 "margin": "unset",
32 "width": 1034,
33 "height": 858
34 },
35 "left-nav-panel": {
36 "top": 442,
37 "left": 0,
38 "right": 1546,
39 "bottom": 25,
40 "margin": "unset",
41 "width": 368,
42 "height": 483
43 }
44 }
45}
45 \ No newline at end of file \ No newline at end of file
package-lock.json+30 -7
@@ -1,12 +1,12 @@
1{1{
2 "name": "sillytavern",2 "name": "sillytavern",
3 "version": "1.12.11",3 "version": "1.12.12",
4 "lockfileVersion": 3,4 "lockfileVersion": 3,
5 "requires": true,5 "requires": true,
6 "packages": {6 "packages": {
7 "": {7 "": {
8 "name": "sillytavern",8 "name": "sillytavern",
9 "version": "1.12.11",9 "version": "1.12.12",
10 "hasInstallScript": true,10 "hasInstallScript": true,
11 "license": "AGPL-3.0",11 "license": "AGPL-3.0",
12 "dependencies": {12 "dependencies": {
@@ -28,7 +28,7 @@
28 "cors": "^2.8.5",28 "cors": "^2.8.5",
29 "csrf-sync": "^4.0.3",29 "csrf-sync": "^4.0.3",
30 "diff-match-patch": "^1.0.5",30 "diff-match-patch": "^1.0.5",
31 "dompurify": "^3.1.7",31 "dompurify": "^3.2.4",
32 "droll": "^0.2.1",32 "droll": "^0.2.1",
33 "express": "^4.21.0",33 "express": "^4.21.0",
34 "form-data": "^4.0.0",34 "form-data": "^4.0.0",
@@ -41,6 +41,7 @@
41 "html-entities": "^2.5.2",41 "html-entities": "^2.5.2",
42 "iconv-lite": "^0.6.3",42 "iconv-lite": "^0.6.3",
43 "ip-matching": "^2.1.2",43 "ip-matching": "^2.1.2",
44 "ip-regex": "^5.0.0",
44 "ipaddr.js": "^2.0.1",45 "ipaddr.js": "^2.0.1",
45 "jimp": "^0.22.10",46 "jimp": "^0.22.10",
46 "localforage": "^1.10.0",47 "localforage": "^1.10.0",
@@ -1462,6 +1463,13 @@
1462 "@types/jquery": "*"1463 "@types/jquery": "*"
1463 }1464 }
1464 },1465 },
1466 "node_modules/@types/trusted-types": {
1467 "version": "2.0.7",
1468 "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
1469 "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
1470 "license": "MIT",
1471 "optional": true
1472 },
1465 "node_modules/@types/write-file-atomic": {1473 "node_modules/@types/write-file-atomic": {
1466 "version": "4.0.3",1474 "version": "4.0.3",
1467 "resolved": "https://registry.npmjs.org/@types/write-file-atomic/-/write-file-atomic-4.0.3.tgz",1475 "resolved": "https://registry.npmjs.org/@types/write-file-atomic/-/write-file-atomic-4.0.3.tgz",
@@ -3217,10 +3225,13 @@
3217 }3225 }
3218 },3226 },
3219 "node_modules/dompurify": {3227 "node_modules/dompurify": {
3220 "version": "3.1.7",3228 "version": "3.2.4",
3221 "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.1.7.tgz",3229 "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.4.tgz",
3222 "integrity": "sha512-VaTstWtsneJY8xzy7DekmYWEOZcmzIe3Qb3zPd4STve1OBTa+e+WmS1ITQec1fZYXI3HCsOZZiSMpG6oxoWMWQ==",3230 "integrity": "sha512-ysFSFEDVduQpyhzAob/kkuJjf5zWkZD8/A9ywSp1byueyuCfHamrCBa14/Oc2iiB0e51B+NpxSl5gmzn+Ms/mg==",
3223 "license": "(MPL-2.0 OR Apache-2.0)"3231 "license": "(MPL-2.0 OR Apache-2.0)",
3232 "optionalDependencies": {
3233 "@types/trusted-types": "^2.0.7"
3234 }
3224 },3235 },
3225 "node_modules/domutils": {3236 "node_modules/domutils": {
3226 "version": "3.1.0",3237 "version": "3.1.0",
@@ -4610,6 +4621,18 @@
4610 "integrity": "sha512-/ok+VhKMasgR5gvTRViwRFQfc0qYt9Vdowg6TO4/pFlDCob5ZjGPkwuOoQVCd5OrMm20zqh+1vA8KLJZTeWudg==",4621 "integrity": "sha512-/ok+VhKMasgR5gvTRViwRFQfc0qYt9Vdowg6TO4/pFlDCob5ZjGPkwuOoQVCd5OrMm20zqh+1vA8KLJZTeWudg==",
4611 "license": "LGPL-3.0-only"4622 "license": "LGPL-3.0-only"
4612 },4623 },
4624 "node_modules/ip-regex": {
4625 "version": "5.0.0",
4626 "resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-5.0.0.tgz",
4627 "integrity": "sha512-fOCG6lhoKKakwv+C6KdsOnGvgXnmgfmp0myi3bcNwj3qfwPAxRKWEuFhvEFF7ceYIz6+1jRZ+yguLFAmUNPEfw==",
4628 "license": "MIT",
4629 "engines": {
4630 "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
4631 },
4632 "funding": {
4633 "url": "https://github.com/sponsors/sindresorhus"
4634 }
4635 },
4613 "node_modules/ipaddr.js": {4636 "node_modules/ipaddr.js": {
4614 "version": "2.1.0",4637 "version": "2.1.0",
4615 "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.1.0.tgz",4638 "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.1.0.tgz",
package.json+4 -2
@@ -18,7 +18,7 @@
18 "cors": "^2.8.5",18 "cors": "^2.8.5",
19 "csrf-sync": "^4.0.3",19 "csrf-sync": "^4.0.3",
20 "diff-match-patch": "^1.0.5",20 "diff-match-patch": "^1.0.5",
21 "dompurify": "^3.1.7",21 "dompurify": "^3.2.4",
22 "droll": "^0.2.1",22 "droll": "^0.2.1",
23 "express": "^4.21.0",23 "express": "^4.21.0",
24 "form-data": "^4.0.0",24 "form-data": "^4.0.0",
@@ -31,6 +31,7 @@
31 "html-entities": "^2.5.2",31 "html-entities": "^2.5.2",
32 "iconv-lite": "^0.6.3",32 "iconv-lite": "^0.6.3",
33 "ip-matching": "^2.1.2",33 "ip-matching": "^2.1.2",
34 "ip-regex": "^5.0.0",
34 "ipaddr.js": "^2.0.1",35 "ipaddr.js": "^2.0.1",
35 "jimp": "^0.22.10",36 "jimp": "^0.22.10",
36 "localforage": "^1.10.0",37 "localforage": "^1.10.0",
@@ -86,9 +87,10 @@
86 "type": "git",87 "type": "git",
87 "url": "https://github.com/SillyTavern/SillyTavern.git"88 "url": "https://github.com/SillyTavern/SillyTavern.git"
88 },89 },
89 "version": "1.12.11",90 "version": "1.12.12",
90 "scripts": {91 "scripts": {
91 "start": "node server.js",92 "start": "node server.js",
93 "debug": "node server.js --inspect",
92 "start:deno": "deno run --allow-run --allow-net --allow-read --allow-write --allow-sys --allow-env server.js",94 "start:deno": "deno run --allow-run --allow-net --allow-read --allow-write --allow-sys --allow-env server.js",
93 "start:bun": "bun server.js",95 "start:bun": "bun server.js",
94 "start:no-csrf": "node server.js --disableCsrf",96 "start:no-csrf": "node server.js --disableCsrf",
plugins.js+7 -0
@@ -48,6 +48,13 @@ async function updatePlugins() {
48 console.log(`Updating plugin ${color.green(directory)}...`);48 console.log(`Updating plugin ${color.green(directory)}...`);
49 const pluginPath = path.join(pluginsPath, directory);49 const pluginPath = path.join(pluginsPath, directory);
50 const pluginRepo = git(pluginPath);50 const pluginRepo = git(pluginPath);
51
52 const isRepo = await pluginRepo.checkIsRepo();
53 if (!isRepo) {
54 console.log(`Directory ${color.yellow(directory)} is not a Git repository`);
55 continue;
56 }
57
51 await pluginRepo.fetch();58 await pluginRepo.fetch();
52 const commitHash = await pluginRepo.revparse(['HEAD']);59 const commitHash = await pluginRepo.revparse(['HEAD']);
53 const trackingBranch = await pluginRepo.revparse(['--abbrev-ref', '@{u}']);60 const trackingBranch = await pluginRepo.revparse(['--abbrev-ref', '@{u}']);
public/css/mobile-styles.css+0 -2
@@ -216,8 +216,6 @@
216216
217 }217 }
218218
219 #showRawPrompt,
220 #copyPromptToClipboard,
221 #groupCurrentMemberPopoutButton,219 #groupCurrentMemberPopoutButton,
222 #summaryExtensionPopoutButton {220 #summaryExtensionPopoutButton {
223 display: none;221 display: none;
public/css/popup.css+4 -0
@@ -72,6 +72,10 @@ dialog {
72 overflow-x: auto;72 overflow-x: auto;
73}73}
7474
75.popup.left_aligned_dialogue_popup .popup-content {
76 text-align: start;
77}
78
75/* Opening animation */79/* Opening animation */
76.popup[opening] {80.popup[opening] {
77 animation: pop-in var(--popup-animation-speed) ease-in-out;81 animation: pop-in var(--popup-animation-speed) ease-in-out;
public/css/select2-overrides.css+7 -0
@@ -100,6 +100,13 @@
100 border: 1px solid var(--SmartThemeBorderColor);100 border: 1px solid var(--SmartThemeBorderColor);
101}101}
102102
103.select2-container .select2-results .select2-results__option--disabled {
104 color: inherit;
105 background-color: inherit;
106 cursor: not-allowed;
107 filter: brightness(0.5);
108}
109
103.select2-container .select2-selection--multiple .select2-selection__choice,110.select2-container .select2-selection--multiple .select2-selection__choice,
104.select2-container .select2-selection--single .select2-selection__choice {111.select2-container .select2-selection--single .select2-selection__choice {
105 border-radius: 5px;112 border-radius: 5px;
public/css/toggle-dependent.css+4 -0
@@ -493,3 +493,7 @@ label[for="trim_spaces"]:not(:has(input:checked)) small {
493#mistralai_other_models:empty {493#mistralai_other_models:empty {
494 display: none;494 display: none;
495}495}
496
497#banned_tokens_block_ooba:not(:has(#send_banned_tokens_textgenerationwebui:checked)) #banned_tokens_controls_ooba {
498 filter: brightness(0.5);
499}
public/index.html+134 -91
@@ -730,7 +730,7 @@
730 <input type="range" id="top_k_openai" name="volume" min="0" max="500" step="1">730 <input type="range" id="top_k_openai" name="volume" min="0" max="500" step="1">
731 </div>731 </div>
732 <div class="range-block-counter">732 <div class="range-block-counter">
733 <input type="number" min="0" max="200" step="1" data-for="top_k_openai" id="top_k_counter_openai">733 <input type="number" min="0" max="500" step="1" data-for="top_k_openai" id="top_k_counter_openai">
734 </div>734 </div>
735 </div>735 </div>
736 </div>736 </div>
@@ -1621,17 +1621,34 @@
1621 </div>1621 </div>
1622 <div data-tg-type-mode="except" data-tg-type="generic" id="banned_tokens_block_ooba" class="wide100p">1622 <div data-tg-type-mode="except" data-tg-type="generic" id="banned_tokens_block_ooba" class="wide100p">
1623 <hr class="width100p">1623 <hr class="width100p">
1624 <h4 class="range-block-title justifyCenter">1624 <div class="range-block-title title_restorable">
1625 <span data-i18n="Banned Tokens">Banned Tokens/Strings</span>1625 <div>
1626 <strong data-i18n="Banned Tokens">Banned Tokens/Strings</strong>
1626 <div class="margin5 fa-solid fa-circle-info opacity50p " data-i18n="[title]LLaMA / Mistral / Yi models only" title="Enter sequences you don't want to appear in the output.&#13;Unquoted text will be tokenized in the back end and banned as tokens.&#13;[token ids] will be banned as-is.&#13;Most tokens have a leading space. Use token counter (with the correct tokenizer selected first!) if you are unsure.&#13;Enclose text in double quotes to ban the entire string as a set.&#13;Quoted Strings and [Token ids] must be on their own line."></div>1627 <div class="margin5 fa-solid fa-circle-info opacity50p " data-i18n="[title]LLaMA / Mistral / Yi models only" title="Enter sequences you don't want to appear in the output.&#13;Unquoted text will be tokenized in the back end and banned as tokens.&#13;[token ids] will be banned as-is.&#13;Most tokens have a leading space. Use token counter (with the correct tokenizer selected first!) if you are unsure.&#13;Enclose text in double quotes to ban the entire string as a set.&#13;Quoted Strings and [Token ids] must be on their own line."></div>
1627 </h4>1628 </div>
1629 <label id="send_banned_tokens_label" for="send_banned_tokens_textgenerationwebui" class="checkbox_label">
1630 <input id="send_banned_tokens_textgenerationwebui" type="checkbox" style="display:none;" />
1631 <small><i class="fa-solid fa-power-off menu_button togglable margin0"></i></small>
1632 </label>
1633 </div>
1634 <div id="banned_tokens_controls_ooba">
1635 <div class="textAlignCenter">
1636 <small data-i18n="Global list">Global list</small>
1637 </div>
1638 <div class="wide100p marginBot10">
1639 <textarea id="global_banned_tokens_textgenerationwebui" class="text_pole textarea_compact" name="global_banned_tokens_textgenerationwebui" rows="3" data-i18n="[placeholder]Example: some text [42, 69, 1337]" placeholder='some text as tokens&#10;[420, 69, 1337]&#10;"Some verbatim string"'></textarea>
1640 </div>
1641 <div class="textAlignCenter">
1642 <small data-i18n="Preset-specific list">Preset-specific list</small>
1643 </div>
1628 <div class="wide100p">1644 <div class="wide100p">
1629 <textarea id="banned_tokens_textgenerationwebui" class="text_pole textarea_compact" name="banned_tokens_textgenerationwebui" rows="3" data-i18n="[placeholder]Example: some text [42, 69, 1337]" placeholder='some text as tokens&#10;[420, 69, 1337]&#10;"Some verbatim string"'></textarea>1645 <textarea id="banned_tokens_textgenerationwebui" class="text_pole textarea_compact" name="banned_tokens_textgenerationwebui" rows="3" data-i18n="[placeholder]Example: some text [42, 69, 1337]" placeholder='some text as tokens&#10;[420, 69, 1337]&#10;"Some verbatim string"'></textarea>
1630 </div>1646 </div>
1631 </div>1647 </div>
1648 </div>
1632 <div class="range-block wide100p">1649 <div class="range-block wide100p">
1633 <div id="logit_bias_textgenerationwebui" class="range-block-title title_restorable">1650 <div id="logit_bias_textgenerationwebui" class="range-block-title title_restorable">
1634 <span data-i18n="Logit Bias">Logit Bias</span>1651 <strong data-i18n="Logit Bias">Logit Bias</strong>
1635 <div id="textgen_logit_bias_new_entry" class="menu_button menu_button_icon">1652 <div id="textgen_logit_bias_new_entry" class="menu_button menu_button_icon">
1636 <i class="fa-xs fa-solid fa-plus"></i>1653 <i class="fa-xs fa-solid fa-plus"></i>
1637 <small data-i18n="Add">Add</small>1654 <small data-i18n="Add">Add</small>
@@ -1934,7 +1951,7 @@
1934 </span>1951 </span>
1935 </div>1952 </div>
1936 </div>1953 </div>
1937 <div class="range-block" data-source="openai,cohere,mistralai,custom,claude,openrouter,groq">1954 <div class="range-block" data-source="openai,cohere,mistralai,custom,claude,openrouter,groq,deepseek">
1938 <label for="openai_function_calling" class="checkbox_label flexWrap widthFreeExpand">1955 <label for="openai_function_calling" class="checkbox_label flexWrap widthFreeExpand">
1939 <input id="openai_function_calling" type="checkbox" />1956 <input id="openai_function_calling" type="checkbox" />
1940 <span data-i18n="Enable function calling">Enable function calling</span>1957 <span data-i18n="Enable function calling">Enable function calling</span>
@@ -1957,6 +1974,7 @@
1957 <span data-i18n="image_inlining_hint_3">menu to attach an image file to the chat.</span>1974 <span data-i18n="image_inlining_hint_3">menu to attach an image file to the chat.</span>
1958 </div>1975 </div>
1959 <div class="flex-container flexFlowColumn wide100p textAlignCenter marginTop10" data-source="openai,custom">1976 <div class="flex-container flexFlowColumn wide100p textAlignCenter marginTop10" data-source="openai,custom">
1977 <div class="flex-container oneline-dropdown">
1960 <label for="openai_inline_image_quality" data-i18n="Inline Image Quality">1978 <label for="openai_inline_image_quality" data-i18n="Inline Image Quality">
1961 Inline Image Quality1979 Inline Image Quality
1962 </label>1980 </label>
@@ -1967,6 +1985,7 @@
1967 </select>1985 </select>
1968 </div>1986 </div>
1969 </div>1987 </div>
1988 </div>
1970 <div class="range-block" data-source="makersuite">1989 <div class="range-block" data-source="makersuite">
1971 <label for="use_makersuite_sysprompt" class="checkbox_label widthFreeExpand">1990 <label for="use_makersuite_sysprompt" class="checkbox_label widthFreeExpand">
1972 <input id="use_makersuite_sysprompt" type="checkbox" />1991 <input id="use_makersuite_sysprompt" type="checkbox" />
@@ -1981,12 +2000,12 @@
1981 </span>2000 </span>
1982 </div>2001 </div>
1983 </div>2002 </div>
1984 <div class="range-block" data-source="makersuite,deepseek,openrouter">2003 <div class="range-block" data-source="deepseek,openrouter">
1985 <label for="openai_show_thoughts" class="checkbox_label widthFreeExpand">2004 <label for="openai_show_thoughts" class="checkbox_label widthFreeExpand">
1986 <input id="openai_show_thoughts" type="checkbox" />2005 <input id="openai_show_thoughts" type="checkbox" />
1987 <span>2006 <span>
1988 <span data-i18n="Request model reasoning">Request model reasoning</span>2007 <span data-i18n="Request model reasoning">Request model reasoning</span>
1989 <i class="opacity50p fa-solid fa-circle-info" title="Gemini 2.0 Thinking / DeepSeek Reasoner"></i>2008 <i class="opacity50p fa-solid fa-circle-info" title="DeepSeek Reasoner"></i>
1990 </span>2009 </span>
1991 </label>2010 </label>
1992 <div class="toggle-description justifyLeft marginBot5">2011 <div class="toggle-description justifyLeft marginBot5">
@@ -1995,6 +2014,18 @@
1995 </span>2014 </span>
1996 </div>2015 </div>
1997 </div>2016 </div>
2017 <div class="flex-container flexFlowColumn wide100p textAlignCenter marginTop10" data-source="openai,custom">
2018 <div class="flex-container oneline-dropdown" title="Constrains effort on reasoning for reasoning models.&#10;Currently supported values are low, medium, and high.&#10;Reducing reasoning effort can result in faster responses and fewer tokens used on reasoning in a response." data-i18n="[title]Constrains effort on reasoning for reasoning models.">
2019 <label for="openai_reasoning_effort" data-i18n="Reasoning Effort">
2020 Reasoning Effort
2021 </label>
2022 <select id="openai_reasoning_effort">
2023 <option data-i18n="openai_reasoning_effort_low" value="low">Low</option>
2024 <option data-i18n="openai_reasoning_effort_medium" value="medium">Medium</option>
2025 <option data-i18n="openai_reasoning_effort_high" value="high">High</option>
2026 </select>
2027 </div>
2028 </div>
1998 <div class="range-block" data-source="claude">2029 <div class="range-block" data-source="claude">
1999 <div class="wide100p">2030 <div class="wide100p">
2000 <div class="flex-container alignItemsCenter">2031 <div class="flex-container alignItemsCenter">
@@ -2809,27 +2840,6 @@
2809 <div>2840 <div>
2810 <h4 data-i18n="OpenAI Model">OpenAI Model</h4>2841 <h4 data-i18n="OpenAI Model">OpenAI Model</h4>
2811 <select id="model_openai_select">2842 <select id="model_openai_select">
2812 <optgroup label="GPT-3.5 Turbo">
2813 <option value="gpt-3.5-turbo">gpt-3.5-turbo</option>
2814 <option value="gpt-3.5-turbo-0125">gpt-3.5-turbo-0125 (2024)</option>
2815 <option value="gpt-3.5-turbo-1106">gpt-3.5-turbo-1106 (2023)</option>
2816 <option value="gpt-3.5-turbo-0613">gpt-3.5-turbo-0613 (2023)</option>
2817 <option value="gpt-3.5-turbo-0301">gpt-3.5-turbo-0301 (2023)</option>
2818 <option value="gpt-3.5-turbo-16k">gpt-3.5-turbo-16k</option>
2819 <option value="gpt-3.5-turbo-16k-0613">gpt-3.5-turbo-16k-0613 (2023)</option>
2820 </optgroup>
2821 <optgroup label="GPT-3.5 Turbo Instruct">
2822 <option value="gpt-3.5-turbo-instruct">gpt-3.5-turbo-instruct</option>
2823 <option value="gpt-3.5-turbo-instruct-0914">gpt-3.5-turbo-instruct-0914</option>
2824 </optgroup>
2825 <optgroup label="GPT-4">
2826 <option value="gpt-4">gpt-4</option>
2827 <option value="gpt-4-0613">gpt-4-0613 (2023)</option>
2828 <option value="gpt-4-0314">gpt-4-0314 (2023)</option>
2829 <option value="gpt-4-32k">gpt-4-32k</option>
2830 <option value="gpt-4-32k-0613">gpt-4-32k-0613 (2023)</option>
2831 <option value="gpt-4-32k-0314">gpt-4-32k-0314 (2023)</option>
2832 </optgroup>
2833 <optgroup label="GPT-4o">2843 <optgroup label="GPT-4o">
2834 <option value="gpt-4o">gpt-4o</option>2844 <option value="gpt-4o">gpt-4o</option>
2835 <option value="gpt-4o-2024-11-20">gpt-4o-2024-11-20</option>2845 <option value="gpt-4o-2024-11-20">gpt-4o-2024-11-20</option>
@@ -2837,29 +2847,44 @@
2837 <option value="gpt-4o-2024-05-13">gpt-4o-2024-05-13</option>2847 <option value="gpt-4o-2024-05-13">gpt-4o-2024-05-13</option>
2838 <option value="chatgpt-4o-latest">chatgpt-4o-latest</option>2848 <option value="chatgpt-4o-latest">chatgpt-4o-latest</option>
2839 </optgroup>2849 </optgroup>
2840 <optgroup label="gpt-4o-mini">2850 <optgroup label="GPT-4o mini">
2841 <option value="gpt-4o-mini">gpt-4o-mini</option>2851 <option value="gpt-4o-mini">gpt-4o-mini</option>
2842 <option value="gpt-4o-mini-2024-07-18">gpt-4o-mini-2024-07-18</option>2852 <option value="gpt-4o-2024-11-20">gpt-4o-2024-11-20</option>
2853 <option value="gpt-4o-2024-08-06">gpt-4o-2024-08-06</option>
2854 <option value="gpt-4o-2024-05-13">gpt-4o-2024-05-13</option>
2855 <option value="chatgpt-4o-latest">chatgpt-4o-latest</option>
2856 </optgroup>
2857 <optgroup label="o1 and o1-mini">
2858 <option value="o1">o1</option>
2859 <option value="o1-2024-12-17">o1-2024-12-17</option>
2860 <option value="o1-mini">o1-mini</option>
2861 <option value="o1-mini-2024-09-12">o1-mini-2024-09-12</option>
2862 <option value="o1-preview">o1-preview</option>
2863 <option value="o1-preview-2024-09-12">o1-preview-2024-09-12</option>
2843 </optgroup>2864 </optgroup>
2844 <optgroup label="GPT-4 Turbo">2865 <optgroup label="o3">
2866 <option value="o3-mini">o3-mini</option>
2867 <option value="o3-mini-2025-01-31">o3-mini-2025-01-31</option>
2868 </optgroup>
2869 <optgroup label="GPT-4 Turbo and GPT-4">
2845 <option value="gpt-4-turbo">gpt-4-turbo</option>2870 <option value="gpt-4-turbo">gpt-4-turbo</option>
2846 <option value="gpt-4-turbo-2024-04-09">gpt-4-turbo-2024-04-09</option>2871 <option value="gpt-4-turbo-2024-04-09">gpt-4-turbo-2024-04-09</option>
2847 <option value="gpt-4-turbo-preview">gpt-4-turbo-preview</option>2872 <option value="gpt-4-turbo-preview">gpt-4-turbo-preview</option>
2848 <option value="gpt-4-vision-preview">gpt-4-vision-preview</option>
2849 <option value="gpt-4-0125-preview">gpt-4-0125-preview (2024)</option>2873 <option value="gpt-4-0125-preview">gpt-4-0125-preview (2024)</option>
2850 <option value="gpt-4-1106-preview">gpt-4-1106-preview (2023)</option>2874 <option value="gpt-4-1106-preview">gpt-4-1106-preview (2023)</option>
2875 <option value="gpt-4">gpt-4</option>
2876 <option value="gpt-4-0613">gpt-4-0613 (2023)</option>
2877 <option value="gpt-4-0314">gpt-4-0314 (2023)</option>
2851 </optgroup>2878 </optgroup>
2852 <optgroup label="o1">2879 <optgroup label="GPT-3.5 Turbo">
2853 <option value="o1-preview">o1-preview</option>2880 <option value="gpt-3.5-turbo">gpt-3.5-turbo</option>
2854 <option value="o1-mini">o1-mini</option>2881 <option value="gpt-3.5-turbo-0125">gpt-3.5-turbo-0125 (2024)</option>
2882 <option value="gpt-3.5-turbo-1106">gpt-3.5-turbo-1106 (2023)</option>
2883 <option value="gpt-3.5-turbo-instruct">gpt-3.5-turbo-instruct</option>
2855 </optgroup>2884 </optgroup>
2856 <optgroup label="Other">2885 <optgroup label="Other">
2857 <option value="text-davinci-003">text-davinci-003</option>2886 <option value="babbage-002">babbage-002</option>
2858 <option value="text-davinci-002">text-davinci-002</option>2887 <option value="davinci-002">davinci-002</option>
2859 <option value="text-curie-001">text-curie-001</option>
2860 <option value="text-babbage-001">text-babbage-001</option>
2861 <option value="text-ada-001">text-ada-001</option>
2862 <option value="code-davinci-002">code-davinci-002</option>
2863 </optgroup>2888 </optgroup>
2864 <optgroup id="openai_external_category" label="External">2889 <optgroup id="openai_external_category" label="External">
2865 </optgroup>2890 </optgroup>
@@ -3058,6 +3083,7 @@
3058 <h4 data-i18n="Google Model">Google Model</h4>3083 <h4 data-i18n="Google Model">Google Model</h4>
3059 <select id="model_google_select">3084 <select id="model_google_select">
3060 <optgroup label="Primary">3085 <optgroup label="Primary">
3086 <option value="gemini-2.0-flash">Gemini 2.0 Flash</option>
3061 <option value="gemini-1.5-pro">Gemini 1.5 Pro</option>3087 <option value="gemini-1.5-pro">Gemini 1.5 Pro</option>
3062 <option value="gemini-1.5-flash">Gemini 1.5 Flash</option>3088 <option value="gemini-1.5-flash">Gemini 1.5 Flash</option>
3063 <option value="gemini-1.0-pro">Gemini 1.0 Pro (Deprecated)</option>3089 <option value="gemini-1.0-pro">Gemini 1.0 Pro (Deprecated)</option>
@@ -3066,6 +3092,11 @@
3066 <option value="gemini-1.0-ultra-latest">Gemini 1.0 Ultra</option>3092 <option value="gemini-1.0-ultra-latest">Gemini 1.0 Ultra</option>
3067 </optgroup>3093 </optgroup>
3068 <optgroup label="Subversions">3094 <optgroup label="Subversions">
3095 <option value="gemini-2.0-pro-exp">Gemini 2.0 Pro Experimental</option>
3096 <option value="gemini-2.0-pro-exp-02-05">Gemini 2.0 Pro Experimental 2025-02-05</option>
3097 <option value="gemini-2.0-flash-lite-preview">Gemini 2.0 Flash-Lite Preview</option>
3098 <option value="gemini-2.0-flash-lite-preview-02-05">Gemini 2.0 Flash-Lite Preview 2025-02-05</option>
3099 <option value="gemini-2.0-flash-001">Gemini 2.0 Flash [001]</option>
3069 <option value="gemini-2.0-flash-thinking-exp">Gemini 2.0 Flash Thinking Experimental</option>3100 <option value="gemini-2.0-flash-thinking-exp">Gemini 2.0 Flash Thinking Experimental</option>
3070 <option value="gemini-2.0-flash-thinking-exp-01-21">Gemini 2.0 Flash Thinking Experimental 2025-01-21</option>3101 <option value="gemini-2.0-flash-thinking-exp-01-21">Gemini 2.0 Flash Thinking Experimental 2025-01-21</option>
3071 <option value="gemini-2.0-flash-thinking-exp-1219">Gemini 2.0 Flash Thinking Experimental 2024-12-19</option>3102 <option value="gemini-2.0-flash-thinking-exp-1219">Gemini 2.0 Flash Thinking Experimental 2024-12-19</option>
@@ -3155,34 +3186,22 @@
3155 </div>3186 </div>
3156 <h4 data-i18n="Groq Model">Groq Model</h4>3187 <h4 data-i18n="Groq Model">Groq Model</h4>
3157 <select id="model_groq_select">3188 <select id="model_groq_select">
3158 <optgroup label="Llama 3.3">3189 <optgroup label="Production Models">
3190 <option value="gemma2-9b-it">gemma2-9b-it</option>
3159 <option value="llama-3.3-70b-versatile">llama-3.3-70b-versatile</option>3191 <option value="llama-3.3-70b-versatile">llama-3.3-70b-versatile</option>
3192 <option value="llama-3.1-8b-instant">llama-3.1-8b-instant</option>
3193 <option value="llama3-70b-8192">llama3-70b-8192</option>
3194 <option value="llama3-8b-8192">llama3-8b-8192</option>
3195 <option value="mixtral-8x7b-32768">mixtral-8x7b-32768</option>
3160 </optgroup>3196 </optgroup>
3161 <optgroup label="Llama 3.2">3197 <optgroup label="Preview Models">
3198 <option value="deepseek-r1-distill-llama-70b">deepseek-r1-distill-llama-70b</option>
3199 <option value="llama-3.3-70b-specdec">llama-3.3-70b-specdec</option>
3162 <option value="llama-3.2-1b-preview">llama-3.2-1b-preview</option>3200 <option value="llama-3.2-1b-preview">llama-3.2-1b-preview</option>
3163 <option value="llama-3.2-3b-preview">llama-3.2-3b-preview</option>3201 <option value="llama-3.2-3b-preview">llama-3.2-3b-preview</option>
3164 <option value="llama-3.2-11b-vision-preview">llama-3.2-11b-vision-preview</option>3202 <option value="llama-3.2-11b-vision-preview">llama-3.2-11b-vision-preview</option>
3165 <option value="llama-3.2-90b-vision-preview">llama-3.2-90b-vision-preview</option>3203 <option value="llama-3.2-90b-vision-preview">llama-3.2-90b-vision-preview</option>
3166 </optgroup>3204 </optgroup>
3167 <optgroup label="Llama 3.1">
3168 <option value="llama-3.1-8b-instant">llama-3.1-8b-instant</option>
3169 <option value="llama-3.1-70b-versatile">llama-3.1-70b-versatile</option>
3170 <option value="llama-3.1-405b-reasoning">llama-3.1-405b-reasoning</option>
3171 </optgroup>
3172 <optgroup label="Llama 3">
3173 <option value="llama3-groq-8b-8192-tool-use-preview">llama3-groq-8b-8192-tool-use-preview</option>
3174 <option value="llama3-groq-70b-8192-tool-use-preview">llama3-groq-70b-8192-tool-use-preview</option>
3175 <option value="llama3-8b-8192">llama3-8b-8192</option>
3176 <option value="llama3-70b-8192">llama3-70b-8192</option>
3177 </optgroup>
3178 <optgroup label="Gemma">
3179 <option value="gemma-7b-it">gemma-7b-it</option>
3180 <option value="gemma2-9b-it">gemma2-9b-it</option>
3181 </optgroup>
3182 <optgroup label="Other">
3183 <option value="mixtral-8x7b-32768">mixtral-8x7b-32768</option>
3184 <option value="llava-v1.5-7b-4096-preview">llava-v1.5-7b-4096-preview</option>
3185 </optgroup>
3186 </select>3205 </select>
3187 </div>3206 </div>
3188 <div id="nanogpt_form" data-source="nanogpt">3207 <div id="nanogpt_form" data-source="nanogpt">
@@ -3515,7 +3534,7 @@
3515 </label>3534 </label>
3516 <label id="instruct_enabled_label"for="instruct_enabled" class="checkbox_label flex1" title="Enable Instruct Mode" data-i18n="[title]instruct_enabled">3535 <label id="instruct_enabled_label"for="instruct_enabled" class="checkbox_label flex1" title="Enable Instruct Mode" data-i18n="[title]instruct_enabled">
3517 <input id="instruct_enabled" type="checkbox" style="display:none;" />3536 <input id="instruct_enabled" type="checkbox" style="display:none;" />
3518 <small><i class="fa-solid fa-power-off menu_button margin0"></i></small>3537 <small><i class="fa-solid fa-power-off menu_button togglable margin0"></i></small>
3519 </label>3538 </label>
3520 </div>3539 </div>
3521 </h4>3540 </h4>
@@ -3693,7 +3712,7 @@
3693 <div class="flex-container">3712 <div class="flex-container">
3694 <label id="sysprompt_enabled_label" for="sysprompt_enabled" class="checkbox_label flex1" title="Enable System Prompt" data-i18n="[title]sysprompt_enabled">3713 <label id="sysprompt_enabled_label" for="sysprompt_enabled" class="checkbox_label flex1" title="Enable System Prompt" data-i18n="[title]sysprompt_enabled">
3695 <input id="sysprompt_enabled" type="checkbox" style="display:none;" />3714 <input id="sysprompt_enabled" type="checkbox" style="display:none;" />
3696 <small><i class="fa-solid fa-power-off menu_button margin0"></i></small>3715 <small><i class="fa-solid fa-power-off menu_button togglable margin0"></i></small>
3697 </label>3716 </label>
3698 </div>3717 </div>
3699 </h4>3718 </h4>
@@ -3747,8 +3766,8 @@
3747 </div>3766 </div>
3748 <label class="checkbox_label" for="custom_stopping_strings_macro">3767 <label class="checkbox_label" for="custom_stopping_strings_macro">
3749 <input id="custom_stopping_strings_macro" type="checkbox" checked>3768 <input id="custom_stopping_strings_macro" type="checkbox" checked>
3750 <small data-i18n="Replace Macro in Custom Stopping Strings">3769 <small data-i18n="Replace Macro in Stop Strings">
3751 Replace Macro in Custom Stopping Strings3770 Replace Macro in Stop Strings
3752 </small>3771 </small>
3753 </label>3772 </label>
3754 </div>3773 </div>
@@ -3795,18 +3814,42 @@
3795 <span data-i18n="Reasoning">Reasoning</span>3814 <span data-i18n="Reasoning">Reasoning</span>
3796 </h4>3815 </h4>
3797 <div>3816 <div>
3798 <label class="checkbox_label" for="reasoning_auto_parse" title="Automatically parse reasoning blocks from main content between the reasoning prefix/suffix. Both fields must be defined and non-empty." data-i18n="[title]reasoning_auto_parse">3817 <div class="flex-container alignItemsBaseline">
3818 <label class="checkbox_label flex1" for="reasoning_auto_parse" title="Automatically parse reasoning blocks from main content between the reasoning prefix/suffix. Both fields must be defined and non-empty." data-i18n="[title]reasoning_auto_parse">
3799 <input id="reasoning_auto_parse" type="checkbox" />3819 <input id="reasoning_auto_parse" type="checkbox" />
3800 <small data-i18n="Auto-Parse Reasoning">3820 <small data-i18n="Auto-Parse">
3801 Auto-Parse Reasoning3821 Auto-Parse
3822 </small>
3823 </label>
3824 <label class="checkbox_label flex1" for="reasoning_auto_expand" title="Automatically expand reasoning blocks." data-i18n="[title]reasoning_auto_expand">
3825 <input id="reasoning_auto_expand" type="checkbox" />
3826 <small data-i18n="Auto-Expand">
3827 Auto-Expand
3828 </small>
3829 </label>
3830 <label class="checkbox_label flex1" for="reasoning_show_hidden" title="Show reasoning time for models with hidden reasoning." data-i18n="[title]reasoning_show_hidden">
3831 <input id="reasoning_show_hidden" type="checkbox" />
3832 <small data-i18n="Show Hidden">
3833 Show Hidden
3802 </small>3834 </small>
3803 </label>3835 </label>
3804 <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">3836 </div>
3837 <div class="flex-container alignItemsBaseline">
3838 <label class="checkbox_label flex1" for="reasoning_add_to_prompts" title="Add existing reasoning blocks to prompts. To add a new reasoning block, use the message edit menu." data-i18n="[title]reasoning_add_to_prompts">
3805 <input id="reasoning_add_to_prompts" type="checkbox" />3839 <input id="reasoning_add_to_prompts" type="checkbox" />
3806 <small data-i18n="Add Reasoning to Prompts">3840 <small data-i18n="Add to Prompts">
3807 Add Reasoning to Prompts3841 Add to Prompts
3808 </small>3842 </small>
3809 </label>3843 </label>
3844 <div class="flex1 flex-container alignItemsBaseline" title="Maximum number of reasoning blocks to be added per prompt, counting from the last message." data-i18n="[title]reasoning_max_additions">
3845 <input id="reasoning_max_additions" class="text_pole textarea_compact widthUnset" type="number" min="0" max="999"></textarea>
3846 <small data-i18n="Max">Max</small>
3847 </div>
3848 </div>
3849 <details>
3850 <summary data-i18n="Reasoning Formatting">
3851 Reasoning Formatting
3852 </summary>
3810 <div class="flex-container">3853 <div class="flex-container">
3811 <div class="flex1" title="Inserted before the reasoning content." data-i18n="[title]reasoning_prefix">3854 <div class="flex1" title="Inserted before the reasoning content." data-i18n="[title]reasoning_prefix">
3812 <small data-i18n="Prefix">Prefix</small>3855 <small data-i18n="Prefix">Prefix</small>
@@ -3822,11 +3865,8 @@
3822 <small data-i18n="Separator">Separator</small>3865 <small data-i18n="Separator">Separator</small>
3823 <textarea id="reasoning_separator" class="text_pole textarea_compact autoSetHeight"></textarea>3866 <textarea id="reasoning_separator" class="text_pole textarea_compact autoSetHeight"></textarea>
3824 </div>3867 </div>
3825 <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">
3826 <small data-i18n="Max Additions">Max Additions</small>
3827 <input id="reasoning_max_additions" class="text_pole textarea_compact" type="number" min="0" max="999"></textarea>
3828 </div>
3829 </div>3868 </div>
3869 </details>
3830 </div>3870 </div>
3831 </div>3871 </div>
3832 <div>3872 <div>
@@ -3961,7 +4001,7 @@
3961 <div class="alignitemscenter flex-container flexFlowColumn flexGrow flexShrink gap0 flexBasis48p" title="Cap the number of entry activation recursions" data-i18n="[title]Cap the number of entry activation recursions">4001 <div class="alignitemscenter flex-container flexFlowColumn flexGrow flexShrink gap0 flexBasis48p" title="Cap the number of entry activation recursions" data-i18n="[title]Cap the number of entry activation recursions">
3962 <small>4002 <small>
3963 <span data-i18n="Max Recursion Steps">Max Recursion Steps</span>4003 <span data-i18n="Max Recursion Steps">Max Recursion Steps</span>
3964 <div class="fa-solid fa-triangle-exclamation opacity50p" data-i18n="[title]0 = unlimited, 1 = scans once and doesn't recurse, 2 = scans once and recurses once, etc\n(disabled when min activations are used)" title="0 = unlimited, 1 = scans once and doesn't recurse, 2 = scans once and recurses once, etc&#10;(disabled when min activations are used)"></div>4004 <div class="fa-solid fa-triangle-exclamation opacity50p" data-i18n="[title]0 = unlimited, 1 = scans once and doesn't recurse, 2 = scans once and recurses once, etc" title="0 = unlimited, 1 = scans once and doesn't recurse, 2 = scans once and recurses once, etc&#10;(disabled when min activations are used)"></div>
3965 </small>4005 </small>
3966 <input class="neo-range-slider" type="range" id="world_info_max_recursion_steps" name="world_info_max_recursion_steps" min="0" max="10" step="1">4006 <input class="neo-range-slider" type="range" id="world_info_max_recursion_steps" name="world_info_max_recursion_steps" min="0" max="10" step="1">
3967 <input class="neo-range-input" type="number" min="0" max="10" step="1" data-for="world_info_max_recursion_steps" id="world_info_max_recursion_steps_counter">4007 <input class="neo-range-input" type="number" min="0" max="10" step="1" data-for="world_info_max_recursion_steps" id="world_info_max_recursion_steps_counter">
@@ -4842,6 +4882,7 @@
4842 </div>4882 </div>
4843 <div id="extensions_settings" class="flex1 wide50p">4883 <div id="extensions_settings" class="flex1 wide50p">
4844 <div id="assets_container" class="extension_container"></div>4884 <div id="assets_container" class="extension_container"></div>
4885 <div id="typing_indicator_container" class="extension_container"></div>
4845 <div id="expressions_container" class="extension_container"></div>4886 <div id="expressions_container" class="extension_container"></div>
4846 <div id="sd_container" class="extension_container"></div>4887 <div id="sd_container" class="extension_container"></div>
4847 <div id="tts_container" class="extension_container"></div>4888 <div id="tts_container" class="extension_container"></div>
@@ -5837,7 +5878,7 @@
5837 <div class="inline-drawer-content flex-container paddingBottom5px wide100p">5878 <div class="inline-drawer-content flex-container paddingBottom5px wide100p">
5838 <div class="flex-container wide100p alignitemscenter">5879 <div class="flex-container wide100p alignitemscenter">
5839 <div name="keywordsAndLogicBlock" class="flex-container wide100p alignitemscenter">5880 <div name="keywordsAndLogicBlock" class="flex-container wide100p alignitemscenter">
5840 <div class="world_entry_form_control flex1">5881 <div class="world_entry_form_control keyprimary flex1">
5841 <small class="displayNone">5882 <small class="displayNone">
5842 <span data-i18n="Comma separated (required)">5883 <span data-i18n="Comma separated (required)">
5843 Comma separated (required)5884 Comma separated (required)
@@ -6259,14 +6300,19 @@
6259 </div>6300 </div>
6260 </div>6301 </div>
6261 <details class="mes_reasoning_details">6302 <details class="mes_reasoning_details">
6262 <summary class="mes_reasoning_summary">6303 <summary class="mes_reasoning_summary flex-container">
6263 <span data-i18n="Reasoning">Reasoning</span>6304 <div class="mes_reasoning_header_block flex-container">
6264 <div class="mes_reasoning_actions">6305 <div class="mes_reasoning_header flex-container">
6265 <div class="mes_reasoning_edit_done mes_button fa-solid fa-check" title="Confirm" data-i18n="[title]Confirmedit"></div>6306 <span class="mes_reasoning_header_title" data-i18n="Thought for some time">Thought for some time</span>
6266 <div class="mes_reasoning_edit_cancel mes_button fa-solid fa-xmark" title="Cancel edit" data-i18n="[title]Cancel edit"></div>6307 <div class="mes_reasoning_arrow fa-solid fa-chevron-up"></div>
6267 <div class="mes_reasoning_edit mes_button fa-solid fa-pencil" title="Edit reasoning" data-i18n="[title]Edit reasoning"></div>6308 </div>
6309 </div>
6310 <div class="mes_reasoning_actions flex-container">
6311 <div class="mes_reasoning_edit_done menu_button edit_button fa-solid fa-check" title="Confirm" data-i18n="[title]Confirmedit"></div>
6312 <div class="mes_reasoning_delete menu_button edit_button fa-solid fa-trash-can" title="Remove reasoning" data-i18n="[title]Remove reasoning"></div>
6313 <div class="mes_reasoning_edit_cancel menu_button edit_button fa-solid fa-xmark" title="Cancel edit" data-i18n="[title]Cancel edit"></div>
6268 <div class="mes_reasoning_copy mes_button fa-solid fa-copy" title="Copy reasoning" data-i18n="[title]Copy reasoning"></div>6314 <div class="mes_reasoning_copy mes_button fa-solid fa-copy" title="Copy reasoning" data-i18n="[title]Copy reasoning"></div>
6269 <div class="mes_reasoning_delete mes_button fa-solid fa-trash-can" title="Remove reasoning" data-i18n="[title]Remove reasoning"></div>6315 <div class="mes_reasoning_edit mes_button fa-solid fa-pencil" title="Edit reasoning" data-i18n="[title]Edit reasoning"></div>
6270 </div>6316 </div>
6271 </summary>6317 </summary>
6272 <div class="mes_reasoning"></div>6318 <div class="mes_reasoning"></div>
@@ -6485,9 +6531,6 @@
6485 </div>6531 </div>
64866532
6487 <!-- chat and input bar -->6533 <!-- chat and input bar -->
6488 <div id="typing_indicator_template" class="template_element">
6489 <div class="typing_indicator"><span class="typing_indicator_name">CHAR</span> is typing</div>
6490 </div>
6491 <div id="message_file_template" class="template_element">6534 <div id="message_file_template" class="template_element">
6492 <div class="mes_file_container">6535 <div class="mes_file_container">
6493 <div class="fa-lg fa-solid fa-file-alt mes_file_icon"></div>6536 <div class="fa-lg fa-solid fa-file-alt mes_file_icon"></div>
@@ -6847,8 +6890,8 @@
6847 </div>6890 </div>
6848 <div id="form_sheld">6891 <div id="form_sheld">
6849 <div id="dialogue_del_mes">6892 <div id="dialogue_del_mes">
6850 <div id="dialogue_del_mes_ok" class="menu_button">Delete</div>6893 <div id="dialogue_del_mes_ok" data-i18n="Delete" class="menu_button">Delete</div>
6851 <div id="dialogue_del_mes_cancel" class="menu_button">Cancel</div>6894 <div id="dialogue_del_mes_cancel" data-i18n="Cancel" class="menu_button">Cancel</div>
6852 </div>6895 </div>
6853 <div id="send_form" class="no-connection">6896 <div id="send_form" class="no-connection">
6854 <form id="file_form" class="wide100p displayNone">6897 <form id="file_form" class="wide100p displayNone">
public/locales/ar-sa.json+1 -1
@@ -482,7 +482,7 @@
482 "separate with commas w/o space between": "فصل بفواصل دون مسافة بينها",482 "separate with commas w/o space between": "فصل بفواصل دون مسافة بينها",
483 "Custom Stopping Strings": "سلاسل توقف مخصصة",483 "Custom Stopping Strings": "سلاسل توقف مخصصة",
484 "JSON serialized array of strings": "مصفوفة سلسلة JSON متسلسلة",484 "JSON serialized array of strings": "مصفوفة سلسلة JSON متسلسلة",
485 "Replace Macro in Custom Stopping Strings": "استبدال الماكرو في سلاسل التوقف المخصصة",485 "Replace Macro in Stop Strings": "استبدال الماكرو في سلاسل التوقف المخصصة",
486 "Auto-Continue": "المتابعة التلقائية",486 "Auto-Continue": "المتابعة التلقائية",
487 "Allow for Chat Completion APIs": "السماح بواجهات برمجة التطبيقات لإكمال الدردشة",487 "Allow for Chat Completion APIs": "السماح بواجهات برمجة التطبيقات لإكمال الدردشة",
488 "Target length (tokens)": "الطول المستهدف (رموز)",488 "Target length (tokens)": "الطول المستهدف (رموز)",
public/locales/de-de.json+1 -1
@@ -482,7 +482,7 @@
482 "separate with commas w/o space between": "getrennt durch Kommas ohne Leerzeichen dazwischen",482 "separate with commas w/o space between": "getrennt durch Kommas ohne Leerzeichen dazwischen",
483 "Custom Stopping Strings": "Benutzerdefinierte Stoppzeichenfolgen",483 "Custom Stopping Strings": "Benutzerdefinierte Stoppzeichenfolgen",
484 "JSON serialized array of strings": "JSON serialisierte Reihe von Zeichenfolgen",484 "JSON serialized array of strings": "JSON serialisierte Reihe von Zeichenfolgen",
485 "Replace Macro in Custom Stopping Strings": "Makro in benutzerdefinierten Stoppzeichenfolgen ersetzen",485 "Replace Macro in Stop Strings": "Makro in benutzerdefinierten Stoppzeichenfolgen ersetzen",
486 "Auto-Continue": "Automatisch fortsetzen",486 "Auto-Continue": "Automatisch fortsetzen",
487 "Allow for Chat Completion APIs": "Erlaube Chat-Vervollständigungs-APIs",487 "Allow for Chat Completion APIs": "Erlaube Chat-Vervollständigungs-APIs",
488 "Target length (tokens)": "Ziel-Länge (Tokens)",488 "Target length (tokens)": "Ziel-Länge (Tokens)",
public/locales/es-es.json+1 -1
@@ -482,7 +482,7 @@
482 "separate with commas w/o space between": "separe con comas sin espacio entre ellas",482 "separate with commas w/o space between": "separe con comas sin espacio entre ellas",
483 "Custom Stopping Strings": "Cadenas de Detención Personalizadas",483 "Custom Stopping Strings": "Cadenas de Detención Personalizadas",
484 "JSON serialized array of strings": "Arreglo de cadenas serializado en JSON",484 "JSON serialized array of strings": "Arreglo de cadenas serializado en JSON",
485 "Replace Macro in Custom Stopping Strings": "Reemplazar macro en Cadenas de Detención Personalizadas",485 "Replace Macro in Stop Strings": "Reemplazar macro en Cadenas de Detención Personalizadas",
486 "Auto-Continue": "Autocontinuar",486 "Auto-Continue": "Autocontinuar",
487 "Allow for Chat Completion APIs": "Permitir para APIs de Completado de Chat",487 "Allow for Chat Completion APIs": "Permitir para APIs de Completado de Chat",
488 "Target length (tokens)": "Longitud objetivo (tokens)",488 "Target length (tokens)": "Longitud objetivo (tokens)",
public/locales/fr-fr.json+2 -2
@@ -434,7 +434,7 @@
434 "Non-markdown strings": "Chaînes non Markdown",434 "Non-markdown strings": "Chaînes non Markdown",
435 "Custom Stopping Strings": "Chaînes d'arrêt personnalisées",435 "Custom Stopping Strings": "Chaînes d'arrêt personnalisées",
436 "JSON serialized array of strings": "Tableau de chaînes sérialisé JSON",436 "JSON serialized array of strings": "Tableau de chaînes sérialisé JSON",
437 "Replace Macro in Custom Stopping Strings": "Remplacer les macro dans les chaînes d'arrêt personnalisées",437 "Replace Macro in Stop Strings": "Remplacer les macro dans les chaînes d'arrêt personnalisées",
438 "Auto-Continue": "Auto-Continue",438 "Auto-Continue": "Auto-Continue",
439 "Allow for Chat Completion APIs": "Autoriser les APIs de complétion de chat",439 "Allow for Chat Completion APIs": "Autoriser les APIs de complétion de chat",
440 "Target length (tokens)": "Longueur cible (tokens)",440 "Target length (tokens)": "Longueur cible (tokens)",
@@ -1485,7 +1485,7 @@
1485 "(disabled when max recursion steps are used)": "(désactivé lorsque le nombre maximum de pas de récursivité est utilisé)",1485 "(disabled when max recursion steps are used)": "(désactivé lorsque le nombre maximum de pas de récursivité est utilisé)",
1486 "Cap the number of entry activation recursions": "Plafonner le nombre de récursions d'activation d'entrée",1486 "Cap the number of entry activation recursions": "Plafonner le nombre de récursions d'activation d'entrée",
1487 "Max Recursion Steps": "Nombre maximal d'étapes de récursivité",1487 "Max Recursion Steps": "Nombre maximal d'étapes de récursivité",
1488 "0 = unlimited, 1 = scans once and doesn't recurse, 2 = scans once and recurses once, etc\\n(disabled when min activations are used)": "0 = illimité, 1 = scanne une fois et ne récure pas, 2 = scanne une fois et récure une fois, etc.\n(désactivé lorsque des activations minimales sont utilisées)",1488 "0 = unlimited, 1 = scans once and doesn't recurse, 2 = scans once and recurses once, etc": "0 = illimité, 1 = scanne une fois et ne récure pas, 2 = scanne une fois et récure une fois, etc.\n(désactivé lorsque des activations minimales sont utilisées)",
1489 "Include names with each message into the context for scanning": "Inclure les noms dans chaque message dans le contexte pour l'analyse.",1489 "Include names with each message into the context for scanning": "Inclure les noms dans chaque message dans le contexte pour l'analyse.",
1490 "Apply current sorting as Order": "Appliquer le tri actuel comme ordre",1490 "Apply current sorting as Order": "Appliquer le tri actuel comme ordre",
1491 "Display swipe numbers for all messages, not just the last.": "Afficher le nombre de balayage sur tous les messages, et pas seulement le dernier.",1491 "Display swipe numbers for all messages, not just the last.": "Afficher le nombre de balayage sur tous les messages, et pas seulement le dernier.",
public/locales/is-is.json+1 -1
@@ -482,7 +482,7 @@
482 "separate with commas w/o space between": "aðskilið með kommum án bila milli",482 "separate with commas w/o space between": "aðskilið með kommum án bila milli",
483 "Custom Stopping Strings": "Eigin stopp-strengir",483 "Custom Stopping Strings": "Eigin stopp-strengir",
484 "JSON serialized array of strings": "JSON raðað fylki af strengjum",484 "JSON serialized array of strings": "JSON raðað fylki af strengjum",
485 "Replace Macro in Custom Stopping Strings": "Skiptu út í macro í sérsniðnum stoppa strengjum",485 "Replace Macro in Stop Strings": "Skiptu út í macro í sérsniðnum stoppa strengjum",
486 "Auto-Continue": "Sjálfvirk Forná",486 "Auto-Continue": "Sjálfvirk Forná",
487 "Allow for Chat Completion APIs": "Leyfa fyrir spjall Loka APIs",487 "Allow for Chat Completion APIs": "Leyfa fyrir spjall Loka APIs",
488 "Target length (tokens)": "Markaðarlengd (texti)",488 "Target length (tokens)": "Markaðarlengd (texti)",
public/locales/it-it.json+1 -1
@@ -482,7 +482,7 @@
482 "separate with commas w/o space between": "separati con virgole senza spazio tra loro",482 "separate with commas w/o space between": "separati con virgole senza spazio tra loro",
483 "Custom Stopping Strings": "Stringhe di Stop Personalizzate",483 "Custom Stopping Strings": "Stringhe di Stop Personalizzate",
484 "JSON serialized array of strings": "Matrice serializzata JSON di stringhe",484 "JSON serialized array of strings": "Matrice serializzata JSON di stringhe",
485 "Replace Macro in Custom Stopping Strings": "Sostituisci Macro in Stringhe di Arresto Personalizzate",485 "Replace Macro in Stop Strings": "Sostituisci Macro in Stringhe di Arresto Personalizzate",
486 "Auto-Continue": "Auto-continua",486 "Auto-Continue": "Auto-continua",
487 "Allow for Chat Completion APIs": "Consenti per API di completamento chat",487 "Allow for Chat Completion APIs": "Consenti per API di completamento chat",
488 "Target length (tokens)": "Lunghezza obiettivo (token)",488 "Target length (tokens)": "Lunghezza obiettivo (token)",
public/locales/ja-jp.json+1 -1
@@ -482,7 +482,7 @@
482 "separate with commas w/o space between": "間にスペースのないカンマで区切ります",482 "separate with commas w/o space between": "間にスペースのないカンマで区切ります",
483 "Custom Stopping Strings": "カスタム停止文字列",483 "Custom Stopping Strings": "カスタム停止文字列",
484 "JSON serialized array of strings": "文字列のJSONシリアル化配列",484 "JSON serialized array of strings": "文字列のJSONシリアル化配列",
485 "Replace Macro in Custom Stopping Strings": "カスタム停止文字列内のマクロを置換する",485 "Replace Macro in Stop Strings": "カスタム停止文字列内のマクロを置換する",
486 "Auto-Continue": "自動継続",486 "Auto-Continue": "自動継続",
487 "Allow for Chat Completion APIs": "チャット補完APIを許可",487 "Allow for Chat Completion APIs": "チャット補完APIを許可",
488 "Target length (tokens)": "ターゲット長さ(トークン)",488 "Target length (tokens)": "ターゲット長さ(トークン)",
public/locales/ko-kr.json+10 -10
@@ -211,7 +211,7 @@
211 "Sampler Priority": "샘플러 우선 순위",211 "Sampler Priority": "샘플러 우선 순위",
212 "Ooba only. Determines the order of samplers.": "Ooba 전용. 샘플러의 순서를 결정합니다.",212 "Ooba only. Determines the order of samplers.": "Ooba 전용. 샘플러의 순서를 결정합니다.",
213 "Character Names Behavior": "캐릭터 이름 동작",213 "Character Names Behavior": "캐릭터 이름 동작",
214 "[title]character_names_none": "캐릭터 이름 접두사를 추가하지 않습니다. 그룹 채팅에서는 좋지 않을 수 있으므로, 이 설정을 선택할 때는 주의해야 합니다.",214 "character_names_none": "캐릭터 이름 접두사를 추가하지 않습니다. 그룹 채팅에서는 좋지 않을 수 있으므로, 이 설정을 선택할 때는 주의해야 합니다.",
215 "Helps the model to associate messages with characters.": "모델이 메시지를 캐릭터와 연관시키는 데 도움이 됩니다.",215 "Helps the model to associate messages with characters.": "모델이 메시지를 캐릭터와 연관시키는 데 도움이 됩니다.",
216 "None": "없음",216 "None": "없음",
217 "None (not injected)": "없음 (삽입되지 않음)",217 "None (not injected)": "없음 (삽입되지 않음)",
@@ -404,7 +404,7 @@
404 "Custom API Key": "커스텀 API 키",404 "Custom API Key": "커스텀 API 키",
405 "Available Models": "사용 가능한 모델",405 "Available Models": "사용 가능한 모델",
406 "Prompt Post-Processing": "신속한 후처리",406 "Prompt Post-Processing": "신속한 후처리",
407 "[title]API Connections;[no_connection_text]api_no_connection": "연결이 되지 않았습니다...",407 "api_no_connection": "연결이 되지 않았습니다...",
408 "Applies additional processing to the prompt before sending it to the API.": "API로 보내기 전에 프롬프트에 추가 처리를 적용합니다.",408 "Applies additional processing to the prompt before sending it to the API.": "API로 보내기 전에 프롬프트에 추가 처리를 적용합니다.",
409 "Verifies your API connection by sending a short test message. Be aware that you'll be credited for it!": "짧은 테스트 메시지를 보내어 API 연결을 확인합니다. 이에 대해 유료 크레딧이 지불될 수 있음을 인식하세요!",409 "Verifies your API connection by sending a short test message. Be aware that you'll be credited for it!": "짧은 테스트 메시지를 보내어 API 연결을 확인합니다. 이에 대해 유료 크레딧이 지불될 수 있음을 인식하세요!",
410 "Test Message": "테스트 메시지",410 "Test Message": "테스트 메시지",
@@ -492,7 +492,7 @@
492 "separate with commas w/o space between": "쉼표로 구분 (공백 없이)",492 "separate with commas w/o space between": "쉼표로 구분 (공백 없이)",
493 "Custom Stopping Strings": "사용자 정의 중지 문자열",493 "Custom Stopping Strings": "사용자 정의 중지 문자열",
494 "JSON serialized array of strings": "문자열의 JSON 직렬화된 배열",494 "JSON serialized array of strings": "문자열의 JSON 직렬화된 배열",
495 "Replace Macro in Custom Stopping Strings": "사용자 정의 중단 문자열에서 매크로 교체",495 "Replace Macro in Stop Strings": "사용자 정의 중단 문자열에서 매크로 교체",
496 "Auto-Continue": "자동 계속하기",496 "Auto-Continue": "자동 계속하기",
497 "Allow for Chat Completion APIs": "채팅 완성 API 허용",497 "Allow for Chat Completion APIs": "채팅 완성 API 허용",
498 "Target length (tokens)": "대상 길이 (토큰)",498 "Target length (tokens)": "대상 길이 (토큰)",
@@ -625,7 +625,7 @@
625 "Single-row message input area. Mobile only, no effect on PC": "한 줄짜리 메시지 입력 영역. 모바일 전용, PC에는 영향 없음",625 "Single-row message input area. Mobile only, no effect on PC": "한 줄짜리 메시지 입력 영역. 모바일 전용, PC에는 영향 없음",
626 "Compact Input Area (Mobile)": "조그마한 입력 영역 (모바일)",626 "Compact Input Area (Mobile)": "조그마한 입력 영역 (모바일)",
627 "Swipe # for All Messages": "모든 스와이프 메시지에 대해 번호 매기기",627 "Swipe # for All Messages": "모든 스와이프 메시지에 대해 번호 매기기",
628 "[title]Display swipe numbers for all messages, not just the last.": "마지막 메시지만이 아니라 모든 메시지에 대한 스와이프 번호를 표시합니다.",628 "Display swipe numbers for all messages, not just the last.": "마지막 메시지만이 아니라 모든 메시지에 대한 스와이프 번호를 표시합니다.",
629 "In the Character Management panel, show quick selection buttons for favorited characters": "캐릭터 관리 패널에서 즐겨찾는 캐릭터에 대한 빠른 선택 버튼을 표시합니다",629 "In the Character Management panel, show quick selection buttons for favorited characters": "캐릭터 관리 패널에서 즐겨찾는 캐릭터에 대한 빠른 선택 버튼을 표시합니다",
630 "Characters Hotswap": "캐릭터 핫스왑",630 "Characters Hotswap": "캐릭터 핫스왑",
631 "Enable magnification for zoomed avatar display.": "마우스 포인터를 아바타 위에 올려두면 아바타가 확대 됩니다.",631 "Enable magnification for zoomed avatar display.": "마우스 포인터를 아바타 위에 올려두면 아바타가 확대 됩니다.",
@@ -1537,7 +1537,7 @@
1537 "Only apply color as accent": "색상은 오직 강조로써만 적용됩니다",1537 "Only apply color as accent": "색상은 오직 강조로써만 적용됩니다",
1538 "qr--colorClear": "색상 지우기",1538 "qr--colorClear": "색상 지우기",
1539 "Color": "색상",1539 "Color": "색상",
1540 "[title]world_button_title": "캐릭터 로어. 클릭하여 로드하세요. Shift를 클릭하면 '월드 인포 링크' 팝업이 열립니다.",1540 "world_button_title": "캐릭터 로어. 클릭하여 로드하세요. Shift를 클릭하면 '월드 인포 링크' 팝업이 열립니다.",
1541 "Select TTS Provider": "TTS 공급자 선택",1541 "Select TTS Provider": "TTS 공급자 선택",
1542 "tts_enabled": "활성화",1542 "tts_enabled": "활성화",
1543 "Narrate user messages": "사용자 메시지 나레이션",1543 "Narrate user messages": "사용자 메시지 나레이션",
@@ -1582,15 +1582,15 @@
1582 "Prompt Content": "프롬프트 내용",1582 "Prompt Content": "프롬프트 내용",
1583 "Instruct Sequences": "지시 시퀀스",1583 "Instruct Sequences": "지시 시퀀스",
1584 "Prefer Character Card Instructions": "캐릭터 카드의 지시사항을 선호",1584 "Prefer Character Card Instructions": "캐릭터 카드의 지시사항을 선호",
1585 "[title]If checked and the character card contains a Post-History Instructions override, use that instead": "활성화 된 경우, 캐릭터 카드에 Post-History 지시 무시 항목이 포함되어 있으면, 카드 지시사항의 내용으로 대신 사용합니다.",1585 "If checked and the character card contains a Post-History Instructions override, use that instead": "활성화 된 경우, 캐릭터 카드에 Post-History 지시 무시 항목이 포함되어 있으면, 카드 지시사항의 내용으로 대신 사용합니다.",
1586 "Auto-select Input Text": "입력 텍스트 자동 선택",1586 "Auto-select Input Text": "입력 텍스트 자동 선택",
1587 "[title]Enable auto-select of input text in some text fields when clicking/selecting them. Applies to popup input textboxes, and possible other custom input fields.": "일부 텍스트 필드를 클릭하거나 선택할 때 자동으로 입력된 텍스트가 선택되도록 설정합니다. 팝업 입력창과 기타 커스텀 입력 필드에 적용됩니다.",1587 "Enable auto-select of input text in some text fields when clicking/selecting them. Applies to popup input textboxes, and possible other custom input fields.": "일부 텍스트 필드를 클릭하거나 선택할 때 자동으로 입력된 텍스트가 선택되도록 설정합니다. 팝업 입력창과 기타 커스텀 입력 필드에 적용됩니다.",
1588 "Markdown Hotkeys": "마크다운 입력 단축키",1588 "Markdown Hotkeys": "마크다운 입력 단축키",
1589 "[title]markdown_hotkeys_desc": "특정 텍스트 입력창에서 마크다운 형식 문자를 입력하기 위한 단축키를 활성화합니다. '/help hotkeys'를 참고하세요.",1589 "markdown_hotkeys_desc": "특정 텍스트 입력창에서 마크다운 형식 문자를 입력하기 위한 단축키를 활성화합니다. '/help hotkeys'를 참고하세요.",
1590 "Show group chat queue": "그룹 채팅 대기열 표시",1590 "Show group chat queue": "그룹 채팅 대기열 표시",
1591 "[title]In group chat, highlight the character(s) that are currently queued to generate responses and the order in which they will respond.": "그룹 채팅에서 응답을 생성하기 위해 현재 대기 중인 캐릭터와 응답할 순서를 강조 표시합니다.",1591 "In group chat, highlight the character(s) that are currently queued to generate responses and the order in which they will respond.": "그룹 채팅에서 응답을 생성하기 위해 현재 대기 중인 캐릭터와 응답할 순서를 강조 표시합니다.",
1592 "Quick 'Impersonate' button": "빠른 '사칭' 버튼",1592 "Quick 'Impersonate' button": "빠른 '사칭' 버튼",
1593 "[title]Show a button in the input area to ask the AI to impersonate your character for a single message": "입력 영역에 AI에게 한 메시지 동안 당신의 캐릭터 연기를 사칭하도록 요청하는 버튼을 표시합니다.",1593 "Show a button in the input area to ask the AI to impersonate your character for a single message": "입력 영역에 AI에게 한 메시지 동안 당신의 캐릭터 연기를 사칭하도록 요청하는 버튼을 표시합니다.",
1594 "Injection Template": "삽입 템플릿",1594 "Injection Template": "삽입 템플릿",
1595 "Query messages": "쿼리 메시지 수",1595 "Query messages": "쿼리 메시지 수",
1596 "Score threshold": "점수 임계값",1596 "Score threshold": "점수 임계값",
public/locales/nl-nl.json+1 -1
@@ -482,7 +482,7 @@
482 "separate with commas w/o space between": "gescheiden met komma's zonder spatie ertussen",482 "separate with commas w/o space between": "gescheiden met komma's zonder spatie ertussen",
483 "Custom Stopping Strings": "Aangepaste Stopreeksen",483 "Custom Stopping Strings": "Aangepaste Stopreeksen",
484 "JSON serialized array of strings": "JSON geserialiseerde reeks van strings",484 "JSON serialized array of strings": "JSON geserialiseerde reeks van strings",
485 "Replace Macro in Custom Stopping Strings": "Macro vervangen in aangepaste stopreeksen",485 "Replace Macro in Stop Strings": "Macro vervangen in aangepaste stopreeksen",
486 "Auto-Continue": "Automatisch doorgaan",486 "Auto-Continue": "Automatisch doorgaan",
487 "Allow for Chat Completion APIs": "Chatvervolledigings-API's toestaan",487 "Allow for Chat Completion APIs": "Chatvervolledigings-API's toestaan",
488 "Target length (tokens)": "Doellengte (tokens)",488 "Target length (tokens)": "Doellengte (tokens)",
public/locales/pt-pt.json+1 -1
@@ -482,7 +482,7 @@
482 "separate with commas w/o space between": "separe com vírgulas sem espaço entre",482 "separate with commas w/o space between": "separe com vírgulas sem espaço entre",
483 "Custom Stopping Strings": "Cadeias de parada personalizadas",483 "Custom Stopping Strings": "Cadeias de parada personalizadas",
484 "JSON serialized array of strings": "Matriz de strings serializada em JSON",484 "JSON serialized array of strings": "Matriz de strings serializada em JSON",
485 "Replace Macro in Custom Stopping Strings": "Substituir Macro em Strings de Parada Personalizadas",485 "Replace Macro in Stop Strings": "Substituir Macro em Strings de Parada Personalizadas",
486 "Auto-Continue": "Auto-Continuar",486 "Auto-Continue": "Auto-Continuar",
487 "Allow for Chat Completion APIs": "Permitir APIs de Completar Chat",487 "Allow for Chat Completion APIs": "Permitir APIs de Completar Chat",
488 "Target length (tokens)": "Comprimento alvo (tokens)",488 "Target length (tokens)": "Comprimento alvo (tokens)",
public/locales/ru-ru.json+73 -13
@@ -161,7 +161,7 @@
161 "View hidden API keys": "Посмотреть скрытые API-ключи",161 "View hidden API keys": "Посмотреть скрытые API-ключи",
162 "Advanced Formatting": "Расширенное форматирование",162 "Advanced Formatting": "Расширенное форматирование",
163 "Context Template": "Шаблон контекста",163 "Context Template": "Шаблон контекста",
164 "Replace Macro in Custom Stopping Strings": "Заменять макросы в пользовательских стоп-строках",164 "Replace Macro in Stop Strings": "Заменять макросы в пользовательских стоп-строках",
165 "Story String": "Строка истории",165 "Story String": "Строка истории",
166 "Example Separator": "Разделитель примеров сообщений",166 "Example Separator": "Разделитель примеров сообщений",
167 "Chat Start": "Начало чата",167 "Chat Start": "Начало чата",
@@ -195,7 +195,7 @@
195 "Yes": "Да",195 "Yes": "Да",
196 "No": "Нет",196 "No": "Нет",
197 "Context %": "Процент контекста",197 "Context %": "Процент контекста",
198 "Budget Cap": "Бюджетный лимит",198 "Budget Cap": "Лимит бюджета",
199 "(0 = disabled)": "(0 = отключено)",199 "(0 = disabled)": "(0 = отключено)",
200 "None": "Отсутствует",200 "None": "Отсутствует",
201 "User Settings": "Настройки пользователя",201 "User Settings": "Настройки пользователя",
@@ -575,10 +575,10 @@
575 "Characters sorting order": "Порядок сортировки персонажей",575 "Characters sorting order": "Порядок сортировки персонажей",
576 "Remove": "Убрать",576 "Remove": "Убрать",
577 "Select a World Info file for": "Выбрать файл с миром для",577 "Select a World Info file for": "Выбрать файл с миром для",
578 "Primary Lorebook": "Основного лорбука",578 "Primary Lorebook": "Основной лорбук",
579 "A selected World Info will be bound to this character as its own Lorebook.": "Информация о мире будет привязана к персонажу как его собственный лорбук",579 "A selected World Info will be bound to this character as its own Lorebook.": "Информация о мире будет привязана к персонажу как его собственный лорбук.",
580 "When generating an AI reply, it will be combined with the entries from a global World Info selector.": "Когда ИИ генерирует ответ, он будет совмещён с записями из глобально выбранного мира",580 "When generating an AI reply, it will be combined with the entries from a global World Info selector.": "Когда ИИ генерирует ответ, он будет совмещён с записями из глобально выбранного мира.",
581 "Exporting a character would also export the selected Lorebook file embedded in the JSON data.": "При экспорте персонажа вместе с ним также выгрузится выбранный лорбук в виде JSON",581 "Exporting a character would also export the selected Lorebook file embedded in the JSON data.": "При экспорте персонажа вместе с ним также выгрузится выбранный лорбук в виде JSON.",
582 "Additional Lorebooks": "Вспомогательные лорбуки",582 "Additional Lorebooks": "Вспомогательные лорбуки",
583 "Associate one or more auxillary Lorebooks with this character.": "Привязать к этому персонажу один или больше вспомогательных лорбуков",583 "Associate one or more auxillary Lorebooks with this character.": "Привязать к этому персонажу один или больше вспомогательных лорбуков",
584 "NOTE: These choices are optional and won't be preserved on character export!": "ВНИМАНИЕ: эти выборы необязательные и не будут сохранены при экспорте персонажа!",584 "NOTE: These choices are optional and won't be preserved on character export!": "ВНИМАНИЕ: эти выборы необязательные и не будут сохранены при экспорте персонажа!",
@@ -593,7 +593,7 @@
593 "Prompt": "Промпт",593 "Prompt": "Промпт",
594 "Copy": "Скопировать",594 "Copy": "Скопировать",
595 "Confirm": "Подтвердить",595 "Confirm": "Подтвердить",
596 "Copy this message": "Скопировать сообщение",596 "Copy this message": "Продублировать сообщение",
597 "Delete this message": "Удалить сообщение",597 "Delete this message": "Удалить сообщение",
598 "Move message up": "Переместить сообщение вверх",598 "Move message up": "Переместить сообщение вверх",
599 "Move message down": "Переместить сообщение вниз",599 "Move message down": "Переместить сообщение вниз",
@@ -612,7 +612,7 @@
612 "Ask AI to write your message for you": "Попросить ИИ написать сообщение за вас",612 "Ask AI to write your message for you": "Попросить ИИ написать сообщение за вас",
613 "Continue the last message": "Продолжить текущее сообщение",613 "Continue the last message": "Продолжить текущее сообщение",
614 "Bind user name to that avatar": "Закрепить имя за этим аватаром",614 "Bind user name to that avatar": "Закрепить имя за этим аватаром",
615 "Select this as default persona for the new chats.": "Выберать эту Персону в качестве персоны по умолчанию для новых чатов.",615 "Select this as default persona for the new chats.": "Выбирать эту персону по умолчанию для всех новых чатов.",
616 "Change persona image": "Сменить аватар персоны",616 "Change persona image": "Сменить аватар персоны",
617 "Delete persona": "Удалить персону",617 "Delete persona": "Удалить персону",
618 "Reduced Motion": "Сокращение анимаций",618 "Reduced Motion": "Сокращение анимаций",
@@ -640,7 +640,7 @@
640 "Token Probabilities": "Вероятности токенов",640 "Token Probabilities": "Вероятности токенов",
641 "Close chat": "Закрыть чат",641 "Close chat": "Закрыть чат",
642 "Manage chat files": "Все чаты",642 "Manage chat files": "Все чаты",
643 "Import Extension From Git Repo": "Импортировать расширение из Git Repository",643 "Import Extension From Git Repo": "Импортировать расширение из Git-репозитория.",
644 "Install extension": "Установить расширение",644 "Install extension": "Установить расширение",
645 "Manage extensions": "Управление расширениями",645 "Manage extensions": "Управление расширениями",
646 "Tokens persona description": "Токенов",646 "Tokens persona description": "Токенов",
@@ -1122,7 +1122,7 @@
1122 "help_hotkeys_0": "Горячие клавиши",1122 "help_hotkeys_0": "Горячие клавиши",
1123 "You can browse a list of bundled characters in the": "Комплектных персонажей можно найти в меню",1123 "You can browse a list of bundled characters in the": "Комплектных персонажей можно найти в меню",
1124 "Download Extensions & Assets": "Загрузить расширения и ресурсы",1124 "Download Extensions & Assets": "Загрузить расширения и ресурсы",
1125 "menu within": "внутри этих кубиков",1125 "menu within": "в меню",
1126 "Assets URL": "URL с описанием ресурсов",1126 "Assets URL": "URL с описанием ресурсов",
1127 "Custom (OpenAI-compatible)": "Кастомный (совместимый с OpenAI)",1127 "Custom (OpenAI-compatible)": "Кастомный (совместимый с OpenAI)",
1128 "Custom Endpoint (Base URL)": "Кастомный эндпоинт (базовый URL)",1128 "Custom Endpoint (Base URL)": "Кастомный эндпоинт (базовый URL)",
@@ -1943,7 +1943,7 @@
1943 "and connect to an": "и подключитесь к",1943 "and connect to an": "и подключитесь к",
1944 "You can add more": "Можете добавить больше",1944 "You can add more": "Можете добавить больше",
1945 "from other websites": "с других сайтов.",1945 "from other websites": "с других сайтов.",
1946 "Go to the": "Загляните в",1946 "Go to the": "Заходите в",
1947 "to install additional features.": ", чтобы установить разные дополнительные ресурсы.",1947 "to install additional features.": ", чтобы установить разные дополнительные ресурсы.",
1948 "or_welcome": "; также доступен",1948 "or_welcome": "; также доступен",
1949 "Claude API Key": "Ключ от API Claude",1949 "Claude API Key": "Ключ от API Claude",
@@ -1958,7 +1958,7 @@
1958 "Save": "Сохранить",1958 "Save": "Сохранить",
1959 "Chat Lorebook": "Лорбук для чата",1959 "Chat Lorebook": "Лорбук для чата",
1960 "chat_world_template_txt": "Выбранный мир будет привязан к этому чату. Будет добавляться в промпт наряду с глобальным лорбуком и лором персонажа.",1960 "chat_world_template_txt": "Выбранный мир будет привязан к этому чату. Будет добавляться в промпт наряду с глобальным лорбуком и лором персонажа.",
1961 "world_button_title": "Лор персонажа\n\nНажмите, чтобы загрузить\nShift + клик, чтобы открыть диалог привязки мира",1961 "world_button_title": "Лор персонажа\n\nНажмите, чтобы загрузить\nShift + ЛКМ, чтобы открыть диалог привязки мира",
1962 "No auxillary Lorebooks set. Click here to select.": "Вспомогательный лорбук не выбран. Нажмите, чтобы выбрать.",1962 "No auxillary Lorebooks set. Click here to select.": "Вспомогательный лорбук не выбран. Нажмите, чтобы выбрать.",
1963 "ext_regex_user_input_desc": "Отправленные вами сообщения.",1963 "ext_regex_user_input_desc": "Отправленные вами сообщения.",
1964 "ext_regex_ai_input_desc": "Полученные от API ответы.",1964 "ext_regex_ai_input_desc": "Полученные от API ответы.",
@@ -2144,5 +2144,65 @@
2144 "Not connected to the API!": "Нет соединения с API!",2144 "Not connected to the API!": "Нет соединения с API!",
2145 "ext_type_system": "Это комплектное расширение. Его нельзя удалить, а обновляется оно вместе со всей системой.",2145 "ext_type_system": "Это комплектное расширение. Его нельзя удалить, а обновляется оно вместе со всей системой.",
2146 "Update all": "Обновить все",2146 "Update all": "Обновить все",
2147 "Close": "Закрыть"2147 "Close": "Закрыть",
2148 "Optional modules:": "Необязательные модули:",
2149 "Sort: Display Name": "Сортировать: по названию",
2150 "Sort: Loading Order": "Сортировать: в порядке загрузки",
2151 "Click to toggle": "Нажмите, чтобы включить или выключить",
2152 "Loading Asset List": "Загрузить список ресурсов",
2153 "Don't ask again for this URL": "Запомнить выбор для этого адреса",
2154 "Are you sure you want to connect to the following url?": "Вы точно хотите подключиться к этому адресу?",
2155 "All": "Всё",
2156 "Characters": "Персонажи",
2157 "Ambient sounds": "Звуковой эмбиент",
2158 "Blip sounds": "Звуки уведомлений",
2159 "Background music": "Фоновая музыка",
2160 "Search": "Поиск",
2161 "extension_install_1": "Чтобы загружать расширения из этого списка, у вас должен быть установлен ",
2162 "extension_install_2": ".",
2163 "extension_install_3": "Нажмите на иконку ",
2164 "extension_install_4": ", чтобы перейти в репозиторий расширения и получить более подробную информацию о нём.",
2165 "Extension repo/guide:": "Репозиторий расширения:",
2166 "Preview in browser": "Предпросмотр",
2167 "Adds a function tool": "Частично или полностью работает через вызов функций",
2168 "Tool": "Функции",
2169 "Move extension": "Переместить расширение",
2170 "ext_type_local": "Это локальное расширение, доступно только вам",
2171 "ext_type_global": "Это глобальное расширение, доступно всем пользователям",
2172 "Move": "Переместить",
2173 "Enter the Git URL of the extension to install": "Введите Git-адрес расширения",
2174 "Please be aware that using external extensions can have unintended side effects and may pose security risks. Always make sure you trust the source before importing an extension. We are not responsible for any damage caused by third-party extensions.": "помните, что используя расширения от сторонних авторов, вы можете подвергать систему опасности. Устанавливайте расширения только от проверенных разработчиков. Мы не несём ответственности за любой ущерб, причинённый сторонними расширениями.",
2175 "Disclaimer:": "Внимание:",
2176 "Example:": "Пример:",
2177 "context_derived": "Считывать из метаданных модели (по возможности)",
2178 "instruct_derived": "Считывать из метаданных модели (по возможности)",
2179 "Confirm token parsing with": "Чтобы убедиться в правильности выделения токенов, используйте",
2180 "Reasoning Effort": "Рассуждения",
2181 "Constrains effort on reasoning for reasoning models.": "Регулирует объём внутренних рассуждений модели (reasoning), для моделей которые поддерживают эту возможность.\nНа данный момент поддерживаются три значения: Подробные, Обычные, Поверхностные.\nПри менее подробном рассуждении ответ получается быстрее, а также экономятся токены, уходящие на рассуждения.",
2182 "openai_reasoning_effort_low": "Поверхностные",
2183 "openai_reasoning_effort_medium": "Обычные",
2184 "openai_reasoning_effort_high": "Подробные",
2185 "Persona Lore Alt+Click to open the lorebook": "Лорбук данной персоны\nAlt + ЛКМ чтобы открыть лорбук",
2186 "Persona Lorebook for": "Лорбук для персоны",
2187 "persona_world_template_txt": "Выбранная Информация о мире будет привязана к этой персоне. Информация будет добавляться в каждом промпте вместе с глобальным лорбуком и лорбуками персонажа и чата.",
2188 "Global list": "Глобальный список",
2189 "Preset-specific list": "Список для данного пресета",
2190 "Banned tokens/strings are being sent in the request.": "Запрещённые токены и строки отсылаются в запросе.",
2191 "Banned tokens/strings are NOT being sent in the request.": "Запрещённые токены и строки НЕ отсылаются в запросе.",
2192 "Add a reasoning block": "Добавить блок рассуждений",
2193 "Create a copy of this message?": "Продублировать это сообщение?",
2194 "Max Recursion Steps": "Макс. глубина рекурсии",
2195 "0 = unlimited, 1 = scans once and doesn't recurse, 2 = scans once and recurses once, etc": "0 = неограничено, 1 = сканировать единожды, 2 = сканировать единожды и сделать один повторный проход, и т.д.\n(неактивно при указанном мин. числе активаций)",
2196 "(disabled when max recursion steps are used)": "(неактивно при указанной макс. глубине рекурсии)",
2197 "Enter a valid API URL": "Введите корректный адрес API",
2198 "No Ollama model selected.": "Не выбрана модель Ollama",
2199 "Background Fitting": "Способ подгонки фона под разрешение",
2200 "Chat Lore Alt+Click to open the lorebook": "Лорбук данного чата\nAlt + ЛКМ чтобы открыть лорбук",
2201 "Token Counter": "Подсчитать токены",
2202 "Type / paste in the box below to see the number of tokens in the text.": "Введите или вставьте текст в окошко ниже, чтобы подсчитать количество токенов в нём.",
2203 "Selected tokenizer:": "Выбранный токенайзер:",
2204 "Input:": "Входные данные:",
2205 "Tokenized text:": "Токенизированный текст:",
2206 "Token IDs:": "Идентификаторы токенов:",
2207 "Tokens:": "Токенов:"
2148}2208}
2148 \ No newline at end of file2208 \ No newline at end of file
public/locales/uk-ua.json+1 -1
@@ -482,7 +482,7 @@
482 "separate with commas w/o space between": "розділяйте комами без пропусків між ними",482 "separate with commas w/o space between": "розділяйте комами без пропусків між ними",
483 "Custom Stopping Strings": "Власні рядки зупинки",483 "Custom Stopping Strings": "Власні рядки зупинки",
484 "JSON serialized array of strings": "JSON-серіалізований масив рядків",484 "JSON serialized array of strings": "JSON-серіалізований масив рядків",
485 "Replace Macro in Custom Stopping Strings": "Замінювати макроси у власних рядках зупинки",485 "Replace Macro in Stop Strings": "Замінювати макроси у власних рядках зупинки",
486 "Auto-Continue": "Автоматичне продовження",486 "Auto-Continue": "Автоматичне продовження",
487 "Allow for Chat Completion APIs": "Дозволити для Chat Completion API",487 "Allow for Chat Completion APIs": "Дозволити для Chat Completion API",
488 "Target length (tokens)": "Цільова довжина (токени)",488 "Target length (tokens)": "Цільова довжина (токени)",
public/locales/vi-vn.json+1 -1
@@ -482,7 +482,7 @@
482 "separate with commas w/o space between": "phân tách bằng dấu phẩy không có khoảng trắng giữa",482 "separate with commas w/o space between": "phân tách bằng dấu phẩy không có khoảng trắng giữa",
483 "Custom Stopping Strings": "Chuỗi dừng tùy chỉnh",483 "Custom Stopping Strings": "Chuỗi dừng tùy chỉnh",
484 "JSON serialized array of strings": "Mảng chuỗi được tuần tự hóa JSON",484 "JSON serialized array of strings": "Mảng chuỗi được tuần tự hóa JSON",
485 "Replace Macro in Custom Stopping Strings": "Thay thế Macro trong Chuỗi Dừng Tùy chỉnh",485 "Replace Macro in Stop Strings": "Thay thế Macro trong Chuỗi Dừng Tùy chỉnh",
486 "Auto-Continue": "Tự động Tiếp tục",486 "Auto-Continue": "Tự động Tiếp tục",
487 "Allow for Chat Completion APIs": "Cho phép các API hoàn thành Trò chuyện",487 "Allow for Chat Completion APIs": "Cho phép các API hoàn thành Trò chuyện",
488 "Target length (tokens)": "Độ dài mục tiêu (token)",488 "Target length (tokens)": "Độ dài mục tiêu (token)",
public/locales/zh-cn.json+12 -12
@@ -215,7 +215,7 @@
215 "Classifier Free Guidance. More helpful tip coming soon": "无分类器指导(CFG)。更多有用的提示敬请期待。",215 "Classifier Free Guidance. More helpful tip coming soon": "无分类器指导(CFG)。更多有用的提示敬请期待。",
216 "Scale": "缩放比例",216 "Scale": "缩放比例",
217 "Negative Prompt": "负面提示词",217 "Negative Prompt": "负面提示词",
218 "Used if CFG Scale is unset globally, per chat or character": "如果无分类器指导(CFG)缩放比例未在全局设置,它将作用于每个聊天或每个角色",218 "Used if CFG Scale is unset globally, per chat or character": "如果CFG缩放比例未被全局设置,它将作用于所有聊天或角色",
219 "Add text here that would make the AI generate things you don't want in your outputs.": "请在此处添加文本,以避免生成您不希望出现在输出中的内容。",219 "Add text here that would make the AI generate things you don't want in your outputs.": "请在此处添加文本,以避免生成您不希望出现在输出中的内容。",
220 "Grammar String": "语法字符串",220 "Grammar String": "语法字符串",
221 "GBNF or EBNF, depends on the backend in use. If you're using this you should know which.": "GBNF 或 EBNF,取决于使用的后端。如果您使用这个,您应该知道该用哪一个。",221 "GBNF or EBNF, depends on the backend in use. If you're using this you should know which.": "GBNF 或 EBNF,取决于使用的后端。如果您使用这个,您应该知道该用哪一个。",
@@ -559,7 +559,7 @@
559 "Prompt Content": "提示词内容",559 "Prompt Content": "提示词内容",
560 "Custom Stopping Strings": "自定义停止字符串",560 "Custom Stopping Strings": "自定义停止字符串",
561 "JSON serialized array of strings": "JSON序列化的字符串数组",561 "JSON serialized array of strings": "JSON序列化的字符串数组",
562 "Replace Macro in Custom Stopping Strings": "替换自定义停止字符串中的宏",562 "Replace Macro in Stop Strings": "替换自定义停止字符串中的宏",
563 "Token Padding": "词符填充",563 "Token Padding": "词符填充",
564 "Miscellaneous": "杂项",564 "Miscellaneous": "杂项",
565 "Non-markdown strings": "非 Markdown 字符串",565 "Non-markdown strings": "非 Markdown 字符串",
@@ -584,7 +584,7 @@
584 "(0 = unlimited, use budget)": "(“0”为无限制,使用预算)",584 "(0 = unlimited, use budget)": "(“0”为无限制,使用预算)",
585 "Cap the number of entry activation recursions": "限制条目激活递归的次数",585 "Cap the number of entry activation recursions": "限制条目激活递归的次数",
586 "Max Recursion Steps": "最大递归深度",586 "Max Recursion Steps": "最大递归深度",
587 "0 = unlimited, 1 = scans once and doesn't recurse, 2 = scans once and recurses once, etc\\n(disabled when min activations are used)": "“0”为无限制,“1”为扫描一次且不递归,“2”为扫描一次且递归一次,依此类推\n(当使用最小激活次数时,此功能被禁用)",587 "0 = unlimited, 1 = scans once and doesn't recurse, 2 = scans once and recurses once, etc": "“0”为无限制,“1”为扫描一次且不递归,“2”为扫描一次且递归一次,依此类推\n(当使用最小激活次数时,此功能被禁用)",
588 "Insertion Strategy": "插入策略",588 "Insertion Strategy": "插入策略",
589 "Sorted Evenly": "均匀排序",589 "Sorted Evenly": "均匀排序",
590 "Character Lore First": "角色世界书优先",590 "Character Lore First": "角色世界书优先",
@@ -1208,7 +1208,7 @@
1208 "View contents": "查看内容",1208 "View contents": "查看内容",
1209 "Remove the file": "删除文件",1209 "Remove the file": "删除文件",
1210 "Author's Note": "作者注释",1210 "Author's Note": "作者注释",
1211 "Unique to this chat": "此聊天独有",1211 "Unique to this chat": "仅对此聊天生效",
1212 "Checkpoints inherit the Note from their parent, and can be changed individually after that.": "检查点从其父级继承注释,之后可以单独更改。",1212 "Checkpoints inherit the Note from their parent, and can be changed individually after that.": "检查点从其父级继承注释,之后可以单独更改。",
1213 "Include in World Info Scanning": "纳入世界信息扫描",1213 "Include in World Info Scanning": "纳入世界信息扫描",
1214 "Before Main Prompt / Story String": "主提示词/故事线之前",1214 "Before Main Prompt / Story String": "主提示词/故事线之前",
@@ -1224,13 +1224,13 @@
1224 "Replace Author's Note": "替换作者注",1224 "Replace Author's Note": "替换作者注",
1225 "Default Author's Note": "默认作者注",1225 "Default Author's Note": "默认作者注",
1226 "Will be automatically added as the Author's Note for all new chats.": "将自动添加为所有新聊天的作者注释。",1226 "Will be automatically added as the Author's Note for all new chats.": "将自动添加为所有新聊天的作者注释。",
1227 "Chat CFG": "聊天CFG",1227 "Chat CFG": "本聊天的CFG缩放",
1228 "1 = disabled": "“1”为已禁用",1228 "1 = disabled": "“1”为禁用",
1229 "write short replies, write replies using past tense": "写简短的回复,用过去时写回复",1229 "write short replies, write replies using past tense": "写简短的回复,用过去时写回复",
1230 "Positive Prompt": "正面提示词",1230 "Positive Prompt": "正面提示词",
1231 "Use character CFG scales": "单独为各个角色设置CFG缩放",1231 "Use character CFG scales": "单独为各个角色设置CFG缩放",
1232 "Character CFG": "角色CFG配置",1232 "Character CFG": "角色CFG配置",
1233 "Will be automatically added as the CFG for this character.": "将自动添加为该角色的 CFG。",1233 "Will be automatically added as the CFG for this character.": "将自动添加到该角色的CFG设置中。",
1234 "Global CFG": "全局CFG",1234 "Global CFG": "全局CFG",
1235 "Will be used as the default CFG options for every chat unless overridden.": "除非被覆盖,否则将用作每次聊天的默认 CFG 选项。",1235 "Will be used as the default CFG options for every chat unless overridden.": "除非被覆盖,否则将用作每次聊天的默认 CFG 选项。",
1236 "CFG Prompt Cascading": "CFG 提示词级联",1236 "CFG Prompt Cascading": "CFG 提示词级联",
@@ -1485,7 +1485,7 @@
1485 "ext_regex_replace_string_placeholder": "使用 {{match}} 包含来自“查找正则表达式”或“$1”、“$2”等的匹配文本作为捕获组。",1485 "ext_regex_replace_string_placeholder": "使用 {{match}} 包含来自“查找正则表达式”或“$1”、“$2”等的匹配文本作为捕获组。",
1486 "Trim Out": "修剪掉",1486 "Trim Out": "修剪掉",
1487 "ext_regex_trim_placeholder": "在替换之前全局修剪正则表达式匹配中任何不需要的部分。用回车键分隔每个元素。",1487 "ext_regex_trim_placeholder": "在替换之前全局修剪正则表达式匹配中任何不需要的部分。用回车键分隔每个元素。",
1488 "ext_regex_affects": "影响",1488 "ext_regex_affects": "作用范围",
1489 "ext_regex_user_input_desc": "用户发送的消息",1489 "ext_regex_user_input_desc": "用户发送的消息",
1490 "ext_regex_user_input": "用户输入",1490 "ext_regex_user_input": "用户输入",
1491 "ext_regex_ai_input_desc": "从生成式API中获取的信息。",1491 "ext_regex_ai_input_desc": "从生成式API中获取的信息。",
@@ -1719,9 +1719,9 @@
1719 "Chat Lorebook for": "聊天知识书",1719 "Chat Lorebook for": "聊天知识书",
1720 "chat_world_template_txt": "选定的世界信息将绑定到此聊天。生成 AI 回复时,\n它将与全球和角色传说书中的条目相结合。",1720 "chat_world_template_txt": "选定的世界信息将绑定到此聊天。生成 AI 回复时,\n它将与全球和角色传说书中的条目相结合。",
1721 "chat_rename_1": "输入聊天的新名称:",1721 "chat_rename_1": "输入聊天的新名称:",
1722 "chat_rename_2": "注意!!使用已有文件名会导致错误!!",1722 "chat_rename_2": "注意!!与其他文件重名会导致错误!!",
1723 "chat_rename_3": "此举会将次聊天与标记为“检查点”的聊天解绑。",1723 "chat_rename_3": "此举会将此聊天与标记为“检查点”的聊天解绑。",
1724 "chat_rename_4": "不需要在结尾添加 '.JSONL'",1724 "chat_rename_4": "(不需要在结尾添加 '.JSONL' 后缀)",
1725 "Enter Checkpoint Name:": "输入检查点名称:",1725 "Enter Checkpoint Name:": "输入检查点名称:",
1726 "(Leave empty to auto-generate)": "(留空以自动生成)",1726 "(Leave empty to auto-generate)": "(留空以自动生成)",
1727 "The currently existing checkpoint will be unlinked and replaced with the new checkpoint, but can still be found in the Chat Management.": "当前检查点将会被解绑并替换为新的检查点,但仍可在聊天管理中找到。",1727 "The currently existing checkpoint will be unlinked and replaced with the new checkpoint, but can still be found in the Chat Management.": "当前检查点将会被解绑并替换为新的检查点,但仍可在聊天管理中找到。",
@@ -1974,7 +1974,7 @@
1974 "Enter your password below to confirm:": "输入您的密码以确认:",1974 "Enter your password below to confirm:": "输入您的密码以确认:",
1975 "Chat Scenario Override": "聊天场景覆盖",1975 "Chat Scenario Override": "聊天场景覆盖",
1976 "Remove": "移除",1976 "Remove": "移除",
1977 "Unique to this chat.": "Unique to this chat.",1977 "Unique to this chat.": "仅对此聊天生效。",
1978 "All group members will use the following scenario text instead of what is specified in their character cards.": "All group members will use the following scenario text instead of what is specified in their character cards.",1978 "All group members will use the following scenario text instead of what is specified in their character cards.": "All group members will use the following scenario text instead of what is specified in their character cards.",
1979 "The following scenario text will be used instead of the value set in the character card.": "The following scenario text will be used instead of the value set in the character card.",1979 "The following scenario text will be used instead of the value set in the character card.": "The following scenario text will be used instead of the value set in the character card.",
1980 "Checkpoints inherit the scenario override from their parent, and can be changed individually after that.": "Checkpoints inherit the scenario override from their parent, and can be changed individually after that.",1980 "Checkpoints inherit the scenario override from their parent, and can be changed individually after that.": "Checkpoints inherit the scenario override from their parent, and can be changed individually after that.",
public/locales/zh-tw.json+3 -3
@@ -483,7 +483,7 @@
483 "separate with commas w/o space between": "用逗號分隔,之間無空格",483 "separate with commas w/o space between": "用逗號分隔,之間無空格",
484 "Custom Stopping Strings": "自訂停止字串",484 "Custom Stopping Strings": "自訂停止字串",
485 "JSON serialized array of strings": "JSON 序列化字串數組",485 "JSON serialized array of strings": "JSON 序列化字串數組",
486 "Replace Macro in Custom Stopping Strings": "取代自訂停止字串中的巨集",486 "Replace Macro in Stop Strings": "取代自訂停止字串中的巨集",
487 "Auto-Continue": "自動繼續",487 "Auto-Continue": "自動繼續",
488 "Allow for Chat Completion APIs": "允許聊天補全 API",488 "Allow for Chat Completion APIs": "允許聊天補全 API",
489 "Target length (tokens)": "目標長度(符元)",489 "Target length (tokens)": "目標長度(符元)",
@@ -1458,7 +1458,7 @@
1458 "Example: http://localhost:1234/v1": "例如:http://localhost:1234/v1",1458 "Example: http://localhost:1234/v1": "例如:http://localhost:1234/v1",
1459 "popup-button-crop": "裁剪",1459 "popup-button-crop": "裁剪",
1460 "(disabled when max recursion steps are used)": "(當最大遞歸步驟數使用時將停用)",1460 "(disabled when max recursion steps are used)": "(當最大遞歸步驟數使用時將停用)",
1461 "0 = unlimited, 1 = scans once and doesn't recurse, 2 = scans once and recurses once, etc\n(disabled when min activations are used)": "0 = 無限制,1 = 掃描一次且不遞歸,2 = 掃描一次並遞歸一次,以此類推\n(使用最小啟動設定時將停用)",1461 "0 = unlimited, 1 = scans once and doesn't recurse, 2 = scans once and recurses once, etc": "0 = 無限制,1 = 掃描一次且不遞歸,2 = 掃描一次並遞歸一次,以此類推\n(使用最小啟動設定時將停用)",
1462 "A greedy, brute-force algorithm used in LLM sampling to find the most likely sequence of words or tokens. It expands multiple candidate sequences at once, maintaining a fixed number (beam width) of top sequences at each step.": "一種用於 LLM 抽樣的貪婪演算法,用於尋找最可能的單詞或標記序列。該方法會同時展開多個候選序列,並在每一步中保持固定數量的頂級序列(beam width)。",1462 "A greedy, brute-force algorithm used in LLM sampling to find the most likely sequence of words or tokens. It expands multiple candidate sequences at once, maintaining a fixed number (beam width) of top sequences at each step.": "一種用於 LLM 抽樣的貪婪演算法,用於尋找最可能的單詞或標記序列。該方法會同時展開多個候選序列,並在每一步中保持固定數量的頂級序列(beam width)。",
1463 "A multiplicative factor to expand the overall area that the nodes take up.": "節點佔用該擴充功能區域的倍數。",1463 "A multiplicative factor to expand the overall area that the nodes take up.": "節點佔用該擴充功能區域的倍數。",
1464 "Abort current image generation task": "終止目前的圖片生成任務",1464 "Abort current image generation task": "終止目前的圖片生成任務",
@@ -1805,7 +1805,7 @@
1805 "context_derived": "若可能,根據模型元數據推導。",1805 "context_derived": "若可能,根據模型元數據推導。",
1806 "instruct_derived": "若可能,根據模型元數據推導。",1806 "instruct_derived": "若可能,根據模型元數據推導。",
1807 "Inserted before the first User's message.": "插入於第一則使用者訊息之前。",1807 "Inserted before the first User's message.": "插入於第一則使用者訊息之前。",
1808 "0 = unlimited, 1 = scans once and doesn't recurse, 2 = scans once and recurses once, etc\\n(disabled when min activations are used)": "0 = 無限制,1 = 掃描一次不遞歸,2 = 掃描一次後遞歸一次 ⋯以此類推\n(啟用最小啟動次數時無效)",1808 "0 = unlimited, 1 = scans once and doesn't recurse, 2 = scans once and recurses once, etc": "0 = 無限制,1 = 掃描一次不遞歸,2 = 掃描一次後遞歸一次 ⋯以此類推\n(啟用最小啟動次數時無效)",
1809 "Quick 'Impersonate' button": "快速「AI 扮演使用者」按鈕",1809 "Quick 'Impersonate' button": "快速「AI 扮演使用者」按鈕",
1810 "Manual": "手動",1810 "Manual": "手動",
1811 "Any contents here will replace the default Post-History Instructions used for this character. (v2 spec: post_history_instructions)": "此處填入的內容將取代該角色的默認聊天歷史後指示(Post-History Instructions)。\n(v2 格式:specpost_history_instructions)",1811 "Any contents here will replace the default Post-History Instructions used for this character. (v2 spec: post_history_instructions)": "此處填入的內容將取代該角色的默認聊天歷史後指示(Post-History Instructions)。\n(v2 格式:specpost_history_instructions)",
public/script.js+202 -144
@@ -269,7 +269,8 @@ import { initSettingsSearch } from './scripts/setting-search.js';
269import { initBulkEdit } from './scripts/bulk-edit.js';269import { initBulkEdit } from './scripts/bulk-edit.js';
270import { deriveTemplatesFromChatTemplate } from './scripts/chat-templates.js';270import { deriveTemplatesFromChatTemplate } from './scripts/chat-templates.js';
271import { getContext } from './scripts/st-context.js';271import { getContext } from './scripts/st-context.js';
272import { initReasoning, PromptReasoning } from './scripts/reasoning.js';272import { extractReasoningFromData, initReasoning, PromptReasoning, ReasoningHandler, removeReasoningFromString, updateReasoningUI } from './scripts/reasoning.js';
273import { accountStorage } from './scripts/util/AccountStorage.js';
273274
274// API OBJECT FOR EXTERNAL WIRING275// API OBJECT FOR EXTERNAL WIRING
275globalThis.SillyTavern = {276globalThis.SillyTavern = {
@@ -365,6 +366,10 @@ DOMPurify.addHook('uponSanitizeElement', (node, _, config) => {
365 return;366 return;
366 }367 }
367368
369 if (!(node instanceof Element)) {
370 return;
371 }
372
368 let mediaBlocked = false;373 let mediaBlocked = false;
369374
370 switch (node.tagName) {375 switch (node.tagName) {
@@ -419,7 +424,7 @@ DOMPurify.addHook('uponSanitizeElement', (node, _, config) => {
419 const entityId = getCurrentEntityId();424 const entityId = getCurrentEntityId();
420 const warningShownKey = `mediaWarningShown:${entityId}`;425 const warningShownKey = `mediaWarningShown:${entityId}`;
421426
422 if (localStorage.getItem(warningShownKey) === null) {427 if (accountStorage.getItem(warningShownKey) === null) {
423 const warningToast = toastr.warning(428 const warningToast = toastr.warning(
424 t`Use the 'Ext. Media' button to allow it. Click on this message to dismiss.`,429 t`Use the 'Ext. Media' button to allow it. Click on this message to dismiss.`,
425 t`External media has been blocked`,430 t`External media has been blocked`,
@@ -430,7 +435,7 @@ DOMPurify.addHook('uponSanitizeElement', (node, _, config) => {
430 },435 },
431 );436 );
432437
433 localStorage.setItem(warningShownKey, 'true');438 accountStorage.setItem(warningShownKey, 'true');
434 }439 }
435 }440 }
436});441});
@@ -492,9 +497,11 @@ export const event_types = {
492 // TODO: Naming convention is inconsistent with other events497 // TODO: Naming convention is inconsistent with other events
493 CHARACTER_DELETED: 'characterDeleted',498 CHARACTER_DELETED: 'characterDeleted',
494 CHARACTER_DUPLICATED: 'character_duplicated',499 CHARACTER_DUPLICATED: 'character_duplicated',
500 CHARACTER_RENAMED: 'character_renamed',
495 /** @deprecated The event is aliased to STREAM_TOKEN_RECEIVED. */501 /** @deprecated The event is aliased to STREAM_TOKEN_RECEIVED. */
496 SMOOTH_STREAM_TOKEN_RECEIVED: 'stream_token_received',502 SMOOTH_STREAM_TOKEN_RECEIVED: 'stream_token_received',
497 STREAM_TOKEN_RECEIVED: 'stream_token_received',503 STREAM_TOKEN_RECEIVED: 'stream_token_received',
504 STREAM_REASONING_DONE: 'stream_reasoning_done',
498 FILE_ATTACHMENT_DELETED: 'file_attachment_deleted',505 FILE_ATTACHMENT_DELETED: 'file_attachment_deleted',
499 WORLDINFO_FORCE_ACTIVATE: 'worldinfo_force_activate',506 WORLDINFO_FORCE_ACTIVATE: 'worldinfo_force_activate',
500 OPEN_CHARACTER_LIBRARY: 'open_character_library',507 OPEN_CHARACTER_LIBRARY: 'open_character_library',
@@ -1025,12 +1032,22 @@ export function setAnimationDuration(ms = null) {
1025 document.documentElement.style.setProperty('--animation-duration', `${animation_duration}ms`);1032 document.documentElement.style.setProperty('--animation-duration', `${animation_duration}ms`);
1026}1033}
10271034
1035/**
1036 * Sets the currently active character
1037 * @param {object|number|string} [entityOrKey] - An entity with id property (character, group, tag), or directly an id or tag key. If not provided, the active character is reset to `null`.
1038 */
1028export function setActiveCharacter(entityOrKey) {1039export function setActiveCharacter(entityOrKey) {
1029 active_character = getTagKeyForEntity(entityOrKey);1040 active_character = entityOrKey ? getTagKeyForEntity(entityOrKey) : null;
1041 if (active_character) active_group = null;
1030}1042}
10311043
1044/**
1045 * Sets the currently active group.
1046 * @param {object|number|string} [entityOrKey] - An entity with id property (character, group, tag), or directly an id or tag key. If not provided, the active group is reset to `null`.
1047 */
1032export function setActiveGroup(entityOrKey) {1048export function setActiveGroup(entityOrKey) {
1033 active_group = getTagKeyForEntity(entityOrKey);1049 active_group = entityOrKey ? getTagKeyForEntity(entityOrKey) : null;
1050 if (active_group) active_character = null;
1034}1051}
10351052
1036/**1053/**
@@ -1489,7 +1506,7 @@ export async function printCharacters(fullRefresh = false) {
14891506
1490 $('#rm_print_characters_pagination').pagination({1507 $('#rm_print_characters_pagination').pagination({
1491 dataSource: entities,1508 dataSource: entities,
1492 pageSize: Number(localStorage.getItem(storageKey)) || per_page_default,1509 pageSize: Number(accountStorage.getItem(storageKey)) || per_page_default,
1493 sizeChangerOptions: [10, 25, 50, 100, 250, 500, 1000],1510 sizeChangerOptions: [10, 25, 50, 100, 250, 500, 1000],
1494 pageRange: 1,1511 pageRange: 1,
1495 pageNumber: saveCharactersPage || 1,1512 pageNumber: saveCharactersPage || 1,
@@ -1533,7 +1550,7 @@ export async function printCharacters(fullRefresh = false) {
1533 eventSource.emit(event_types.CHARACTER_PAGE_LOADED);1550 eventSource.emit(event_types.CHARACTER_PAGE_LOADED);
1534 },1551 },
1535 afterSizeSelectorChange: function (e) {1552 afterSizeSelectorChange: function (e) {
1536 localStorage.setItem(storageKey, e.target.value);1553 accountStorage.setItem(storageKey, e.target.value);
1537 },1554 },
1538 afterPaging: function (e) {1555 afterPaging: function (e) {
1539 saveCharactersPage = e;1556 saveCharactersPage = e;
@@ -2189,26 +2206,29 @@ function insertSVGIcon(mes, extra) {
2189 modelName = extra.api;2206 modelName = extra.api;
2190 }2207 }
21912208
2192 const image = new Image();2209 const insertOrReplaceSVG = (image, className, targetSelector, insertBefore) => {
2193 // Add classes for styling and identification
2194 image.classList.add('icon-svg', 'timestamp-icon');
2195 image.src = `/img/${modelName}.svg`;
2196 image.title = `${extra?.api ? extra.api + ' - ' : ''}${extra?.model ?? ''}`;
2197
2198 image.onload = async function () {2210 image.onload = async function () {
2199 // Check if an SVG already exists adjacent to the timestamp2211 let existingSVG = insertBefore ? mes.find(targetSelector).prev(`.${className}`) : mes.find(targetSelector).next(`.${className}`);
2200 let existingSVG = mes.find('.timestamp').next('.timestamp-icon');
2201
2202 if (existingSVG.length) {2212 if (existingSVG.length) {
2203 // Replace existing SVG
2204 existingSVG.replaceWith(image);2213 existingSVG.replaceWith(image);
2205 } else {2214 } else {
2206 // Append the new SVG if none exists2215 if (insertBefore) mes.find(targetSelector).before(image);
2207 mes.find('.timestamp').after(image);2216 else mes.find(targetSelector).after(image);
2208 }2217 }
2209
2210 await SVGInject(image);2218 await SVGInject(image);
2211 };2219 };
2220 };
2221
2222 const createModelImage = (className, targetSelector, insertBefore) => {
2223 const image = new Image();
2224 image.classList.add('icon-svg', className);
2225 image.src = `/img/${modelName}.svg`;
2226 image.title = `${extra?.api ? extra.api + ' - ' : ''}${extra?.model ?? ''}`;
2227 insertOrReplaceSVG(image, className, targetSelector, insertBefore);
2228 };
2229
2230 createModelImage('timestamp-icon', '.timestamp');
2231 createModelImage('thinking-icon', '.mes_reasoning_header_title', true);
2212}2232}
22132233
22142234
@@ -2219,7 +2239,6 @@ function getMessageFromTemplate({
2219 isUser,2239 isUser,
2220 avatarImg,2240 avatarImg,
2221 bias,2241 bias,
2222 reasoning,
2223 isSystem,2242 isSystem,
2224 title,2243 title,
2225 timerValue,2244 timerValue,
@@ -2244,7 +2263,6 @@ function getMessageFromTemplate({
2244 mes.find('.avatar img').attr('src', avatarImg);2263 mes.find('.avatar img').attr('src', avatarImg);
2245 mes.find('.ch_name .name_text').text(characterName);2264 mes.find('.ch_name .name_text').text(characterName);
2246 mes.find('.mes_bias').html(bias);2265 mes.find('.mes_bias').html(bias);
2247 mes.find('.mes_reasoning').html(reasoning);
2248 mes.find('.timestamp').text(timestamp).attr('title', `${extra?.api ? extra.api + ' - ' : ''}${extra?.model ?? ''}`);2266 mes.find('.timestamp').text(timestamp).attr('title', `${extra?.api ? extra.api + ' - ' : ''}${extra?.model ?? ''}`);
2249 mes.find('.mesIDDisplay').text(`#${mesId}`);2267 mes.find('.mesIDDisplay').text(`#${mesId}`);
2250 tokenCount && mes.find('.tokenCounterDisplay').text(`${tokenCount}t`);2268 tokenCount && mes.find('.tokenCounterDisplay').text(`${tokenCount}t`);
@@ -2252,6 +2270,8 @@ function getMessageFromTemplate({
2252 timerValue && mes.find('.mes_timer').attr('title', timerTitle).text(timerValue);2270 timerValue && mes.find('.mes_timer').attr('title', timerTitle).text(timerValue);
2253 bookmarkLink && updateBookmarkDisplay(mes);2271 bookmarkLink && updateBookmarkDisplay(mes);
22542272
2273 updateReasoningUI(mes);
2274
2255 if (power_user.timestamp_model_icon && extra?.api) {2275 if (power_user.timestamp_model_icon && extra?.api) {
2256 insertSVGIcon(mes, extra);2276 insertSVGIcon(mes, extra);
2257 }2277 }
@@ -2263,12 +2283,18 @@ function getMessageFromTemplate({
2263 * Re-renders a message block with updated content.2283 * Re-renders a message block with updated content.
2264 * @param {number} messageId Message ID2284 * @param {number} messageId Message ID
2265 * @param {object} message Message object2285 * @param {object} message Message object
2286 * @param {object} [options={}] Optional arguments
2287 * @param {boolean} [options.rerenderMessage=true] Whether to re-render the message content (inside <c>.mes_text</c>)
2266 */2288 */
2267export function updateMessageBlock(messageId, message) {2289export function updateMessageBlock(messageId, message, { rerenderMessage = true } = {}) {
2268 const messageElement = $(`#chat [mesid="${messageId}"]`);2290 const messageElement = $(`#chat [mesid="${messageId}"]`);
2291 if (rerenderMessage) {
2269 const text = message?.extra?.display_text ?? message.mes;2292 const text = message?.extra?.display_text ?? message.mes;
2270 messageElement.find('.mes_text').html(messageFormatting(text, message.name, message.is_system, message.is_user, messageId, {}, false));2293 messageElement.find('.mes_text').html(messageFormatting(text, message.name, message.is_system, message.is_user, messageId, {}, false));
2271 messageElement.find('.mes_reasoning').html(messageFormatting(message.extra?.reasoning ?? '', '', false, false, messageId, {}, true));2294 }
2295
2296 updateReasoningUI(messageElement);
2297
2272 addCopyToCodeBlocks(messageElement);2298 addCopyToCodeBlocks(messageElement);
2273 appendMediaToMessage(message, messageElement);2299 appendMediaToMessage(message, messageElement);
2274}2300}
@@ -2428,7 +2454,6 @@ export function addOneMessage(mes, { type = 'normal', insertAfter = null, scroll
2428 false,2454 false,
2429 );2455 );
2430 const bias = messageFormatting(mes.extra?.bias ?? '', '', false, false, -1, {}, false);2456 const bias = messageFormatting(mes.extra?.bias ?? '', '', false, false, -1, {}, false);
2431 const reasoning = messageFormatting(mes.extra?.reasoning ?? '', '', false, false, chat.indexOf(mes), {}, true);
2432 let bookmarkLink = mes?.extra?.bookmark_link ?? '';2457 let bookmarkLink = mes?.extra?.bookmark_link ?? '';
24332458
2434 let params = {2459 let params = {
@@ -2438,7 +2463,6 @@ export function addOneMessage(mes, { type = 'normal', insertAfter = null, scroll
2438 isUser: mes.is_user,2463 isUser: mes.is_user,
2439 avatarImg: avatarImg,2464 avatarImg: avatarImg,
2440 bias: bias,2465 bias: bias,
2441 reasoning: reasoning,
2442 isSystem: isSystem,2466 isSystem: isSystem,
2443 title: title,2467 title: title,
2444 bookmarkLink: bookmarkLink,2468 bookmarkLink: bookmarkLink,
@@ -2446,7 +2470,7 @@ export function addOneMessage(mes, { type = 'normal', insertAfter = null, scroll
2446 timestamp: timestamp,2470 timestamp: timestamp,
2447 extra: mes.extra,2471 extra: mes.extra,
2448 tokenCount: mes.extra?.token_count ?? 0,2472 tokenCount: mes.extra?.token_count ?? 0,
2449 ...formatGenerationTimer(mes.gen_started, mes.gen_finished, mes.extra?.token_count),2473 ...formatGenerationTimer(mes.gen_started, mes.gen_finished, mes.extra?.token_count, mes.extra?.reasoning_duration),
2450 };2474 };
24512475
2452 const renderedMessage = getMessageFromTemplate(params);2476 const renderedMessage = getMessageFromTemplate(params);
@@ -2498,8 +2522,8 @@ export function addOneMessage(mes, { type = 'normal', insertAfter = null, scroll
2498 const swipeMessage = chatElement.find(`[mesid="${chat.length - 1}"]`);2522 const swipeMessage = chatElement.find(`[mesid="${chat.length - 1}"]`);
2499 swipeMessage.attr('swipeid', params.swipeId);2523 swipeMessage.attr('swipeid', params.swipeId);
2500 swipeMessage.find('.mes_text').html(messageText).attr('title', title);2524 swipeMessage.find('.mes_text').html(messageText).attr('title', title);
2501 swipeMessage.find('.mes_reasoning').html(reasoning);
2502 swipeMessage.find('.timestamp').text(timestamp).attr('title', `${params.extra.api} - ${params.extra.model}`);2525 swipeMessage.find('.timestamp').text(timestamp).attr('title', `${params.extra.api} - ${params.extra.model}`);
2526 updateReasoningUI(swipeMessage);
2503 appendMediaToMessage(mes, swipeMessage);2527 appendMediaToMessage(mes, swipeMessage);
2504 if (power_user.timestamp_model_icon && params.extra?.api) {2528 if (power_user.timestamp_model_icon && params.extra?.api) {
2505 insertSVGIcon(swipeMessage, params.extra);2529 insertSVGIcon(swipeMessage, params.extra);
@@ -2566,13 +2590,14 @@ export function formatCharacterAvatar(characterAvatar) {
2566 * @param {Date} gen_started Date when generation was started2590 * @param {Date} gen_started Date when generation was started
2567 * @param {Date} gen_finished Date when generation was finished2591 * @param {Date} gen_finished Date when generation was finished
2568 * @param {number} tokenCount Number of tokens generated (0 if not available)2592 * @param {number} tokenCount Number of tokens generated (0 if not available)
2593 * @param {number?} [reasoningDuration=null] Reasoning duration (null if no reasoning was done)
2569 * @returns {Object} Object containing the formatted timer value and title2594 * @returns {Object} Object containing the formatted timer value and title
2570 * @example2595 * @example
2571 * const { timerValue, timerTitle } = formatGenerationTimer(gen_started, gen_finished, tokenCount);2596 * const { timerValue, timerTitle } = formatGenerationTimer(gen_started, gen_finished, tokenCount);
2572 * console.log(timerValue); // 1.2s2597 * console.log(timerValue); // 1.2s
2573 * console.log(timerTitle); // Generation queued: 12:34:56 7 Jan 2021\nReply received: 12:34:57 7 Jan 2021\nTime to generate: 1.2 seconds\nToken rate: 5 t/s2598 * console.log(timerTitle); // Generation queued: 12:34:56 7 Jan 2021\nReply received: 12:34:57 7 Jan 2021\nTime to generate: 1.2 seconds\nToken rate: 5 t/s
2574 */2599 */
2575function formatGenerationTimer(gen_started, gen_finished, tokenCount) {2600function formatGenerationTimer(gen_started, gen_finished, tokenCount, reasoningDuration = null) {
2576 if (!gen_started || !gen_finished) {2601 if (!gen_started || !gen_finished) {
2577 return {};2602 return {};
2578 }2603 }
@@ -2586,8 +2611,9 @@ function formatGenerationTimer(gen_started, gen_finished, tokenCount) {
2586 `Generation queued: ${start.format(dateFormat)}`,2611 `Generation queued: ${start.format(dateFormat)}`,
2587 `Reply received: ${finish.format(dateFormat)}`,2612 `Reply received: ${finish.format(dateFormat)}`,
2588 `Time to generate: ${seconds} seconds`,2613 `Time to generate: ${seconds} seconds`,
2614 reasoningDuration > 0 ? `Time to think: ${reasoningDuration / 1000} seconds` : '',
2589 tokenCount > 0 ? `Token rate: ${Number(tokenCount / seconds).toFixed(1)} t/s` : '',2615 tokenCount > 0 ? `Token rate: ${Number(tokenCount / seconds).toFixed(1)} t/s` : '',
2590 ].join('\n');2616 ].filter(x => x).join('\n').trim();
25912617
2592 if (isNaN(seconds) || seconds < 0) {2618 if (isNaN(seconds) || seconds < 0) {
2593 return { timerValue: '', timerTitle };2619 return { timerValue: '', timerTitle };
@@ -2775,7 +2801,8 @@ export async function generateQuietPrompt(quiet_prompt, quietToLoud, skipWIAN, q
2775 TempResponseLength.save(main_api, responseLength);2801 TempResponseLength.save(main_api, responseLength);
2776 eventHook = TempResponseLength.setupEventHook(main_api);2802 eventHook = TempResponseLength.setupEventHook(main_api);
2777 }2803 }
2778 return await Generate('quiet', options);2804 const result = await Generate('quiet', options);
2805 return removeReasoningFromString(result);
2779 } finally {2806 } finally {
2780 if (responseLengthCustomized && TempResponseLength.isCustomized()) {2807 if (responseLengthCustomized && TempResponseLength.isCustomized()) {
2781 TempResponseLength.restore(main_api);2808 TempResponseLength.restore(main_api);
@@ -3075,8 +3102,8 @@ export function isStreamingEnabled() {
3075 (main_api == 'openai' &&3102 (main_api == 'openai' &&
3076 oai_settings.stream_openai &&3103 oai_settings.stream_openai &&
3077 !noStreamSources.includes(oai_settings.chat_completion_source) &&3104 !noStreamSources.includes(oai_settings.chat_completion_source) &&
3078 !(oai_settings.chat_completion_source == chat_completion_sources.OPENAI && (oai_settings.openai_model.startsWith('o1') || oai_settings.openai_model.startsWith('o3'))) &&3105 !(oai_settings.chat_completion_source == chat_completion_sources.OPENAI && ['o1-2024-12-17', 'o1'].includes(oai_settings.openai_model))
3079 !(oai_settings.chat_completion_source == chat_completion_sources.MAKERSUITE && oai_settings.google_model.includes('bison')))3106 )
3080 || (main_api == 'kobold' && kai_settings.streaming_kobold && kai_flags.can_use_streaming)3107 || (main_api == 'kobold' && kai_settings.streaming_kobold && kai_flags.can_use_streaming)
3081 || (main_api == 'novel' && nai_settings.streaming_novel)3108 || (main_api == 'novel' && nai_settings.streaming_novel)
3082 || (main_api == 'textgenerationwebui' && textgen_settings.streaming));3109 || (main_api == 'textgenerationwebui' && textgen_settings.streaming));
@@ -3105,11 +3132,14 @@ class StreamingProcessor {
3105 constructor(type, forceName2, timeStarted, continueMessage) {3132 constructor(type, forceName2, timeStarted, continueMessage) {
3106 this.result = '';3133 this.result = '';
3107 this.messageId = -1;3134 this.messageId = -1;
3135 /** @type {HTMLElement} */
3108 this.messageDom = null;3136 this.messageDom = null;
3137 /** @type {HTMLElement} */
3109 this.messageTextDom = null;3138 this.messageTextDom = null;
3139 /** @type {HTMLElement} */
3110 this.messageTimerDom = null;3140 this.messageTimerDom = null;
3141 /** @type {HTMLElement} */
3111 this.messageTokenCounterDom = null;3142 this.messageTokenCounterDom = null;
3112 this.messageReasoningDom = null;
3113 /** @type {HTMLTextAreaElement} */3143 /** @type {HTMLTextAreaElement} */
3114 this.sendTextarea = document.querySelector('#send_textarea');3144 this.sendTextarea = document.querySelector('#send_textarea');
3115 this.type = type;3145 this.type = type;
@@ -3125,7 +3155,8 @@ class StreamingProcessor {
3125 /** @type {import('./scripts/logprobs.js').TokenLogprobs[]} */3155 /** @type {import('./scripts/logprobs.js').TokenLogprobs[]} */
3126 this.messageLogprobs = [];3156 this.messageLogprobs = [];
3127 this.toolCalls = [];3157 this.toolCalls = [];
3128 this.reasoning = '';3158 // Initialize reasoning in its own handler
3159 this.reasoningHandler = new ReasoningHandler(timeStarted);
3129 }3160 }
31303161
3131 #checkDomElements(messageId) {3162 #checkDomElements(messageId) {
@@ -3134,8 +3165,8 @@ class StreamingProcessor {
3134 this.messageTextDom = this.messageDom?.querySelector('.mes_text');3165 this.messageTextDom = this.messageDom?.querySelector('.mes_text');
3135 this.messageTimerDom = this.messageDom?.querySelector('.mes_timer');3166 this.messageTimerDom = this.messageDom?.querySelector('.mes_timer');
3136 this.messageTokenCounterDom = this.messageDom?.querySelector('.tokenCounterDisplay');3167 this.messageTokenCounterDom = this.messageDom?.querySelector('.tokenCounterDisplay');
3137 this.messageReasoningDom = this.messageDom?.querySelector('.mes_reasoning');
3138 }3168 }
3169 this.reasoningHandler.updateDom(messageId);
3139 }3170 }
31403171
3141 #updateMessageBlockVisibility() {3172 #updateMessageBlockVisibility() {
@@ -3145,22 +3176,12 @@ class StreamingProcessor {
3145 }3176 }
3146 }3177 }
31473178
3148 showMessageButtons(messageId) {3179 markUIGenStarted() {
3149 if (messageId == -1) {3180 deactivateSendButtons();
3150 return;
3151 }
3152
3153 showStopButton();
3154 $(`#chat .mes[mesid="${messageId}"] .mes_buttons`).css({ 'display': 'none' });
3155 }
3156
3157 hideMessageButtons(messageId) {
3158 if (messageId == -1) {
3159 return;
3160 }3181 }
31613182
3162 hideStopButton();3183 markUIGenStopped() {
3163 $(`#chat .mes[mesid="${messageId}"] .mes_buttons`).css({ 'display': 'flex' });3184 activateSendButtons();
3164 }3185 }
31653186
3166 async onStartStreaming(text) {3187 async onStartStreaming(text) {
@@ -3169,20 +3190,18 @@ class StreamingProcessor {
3169 if (this.type == 'impersonate') {3190 if (this.type == 'impersonate') {
3170 this.sendTextarea.value = '';3191 this.sendTextarea.value = '';
3171 this.sendTextarea.dispatchEvent(new Event('input', { bubbles: true }));3192 this.sendTextarea.dispatchEvent(new Event('input', { bubbles: true }));
3172 }3193 } else {
3173 else {
3174 await saveReply(this.type, text, true, '', [], '');3194 await saveReply(this.type, text, true, '', [], '');
3175 messageId = chat.length - 1;3195 messageId = chat.length - 1;
3176 this.#checkDomElements(messageId);3196 this.#checkDomElements(messageId);
3177 this.showMessageButtons(messageId);3197 this.markUIGenStarted();
3178 }3198 }
3179
3180 hideSwipeButtons();3199 hideSwipeButtons();
3181 scrollChatToBottom();3200 scrollChatToBottom();
3182 return messageId;3201 return messageId;
3183 }3202 }
31843203
3185 onProgressStreaming(messageId, text, isFinal) {3204 async onProgressStreaming(messageId, text, isFinal) {
3186 const isImpersonate = this.type == 'impersonate';3205 const isImpersonate = this.type == 'impersonate';
3187 const isContinue = this.type == 'continue';3206 const isContinue = this.type == 'continue';
31883207
@@ -3194,11 +3213,9 @@ class StreamingProcessor {
31943213
3195 let processedText = cleanUpMessage(text, isImpersonate, isContinue, !isFinal, this.stoppingStrings);3214 let processedText = cleanUpMessage(text, isImpersonate, isContinue, !isFinal, this.stoppingStrings);
31963215
3197 // Predict unbalanced asterisks / quotes during streaming
3198 const charsToBalance = ['*', '"', '```'];3216 const charsToBalance = ['*', '"', '```'];
3199 for (const char of charsToBalance) {3217 for (const char of charsToBalance) {
3200 if (!isFinal && isOdd(countOccurrences(processedText, char))) {3218 if (!isFinal && isOdd(countOccurrences(processedText, char))) {
3201 // Add character at the end to balance it
3202 const separator = char.length > 1 ? '\n' : '';3219 const separator = char.length > 1 ? '\n' : '';
3203 processedText = processedText.trimEnd() + separator + char;3220 processedText = processedText.trimEnd() + separator + char;
3204 }3221 }
@@ -3207,31 +3224,25 @@ class StreamingProcessor {
3207 if (isImpersonate) {3224 if (isImpersonate) {
3208 this.sendTextarea.value = processedText;3225 this.sendTextarea.value = processedText;
3209 this.sendTextarea.dispatchEvent(new Event('input', { bubbles: true }));3226 this.sendTextarea.dispatchEvent(new Event('input', { bubbles: true }));
3210 }3227 } else {
3211 else {3228 const mesChanged = chat[messageId]['mes'] !== processedText;
3212 this.#checkDomElements(messageId);3229 this.#checkDomElements(messageId);
3213 this.#updateMessageBlockVisibility();3230 this.#updateMessageBlockVisibility();
3214 const currentTime = new Date();3231 const currentTime = new Date();
3215 chat[messageId]['mes'] = processedText;3232 chat[messageId]['mes'] = processedText;
3216 chat[messageId]['gen_started'] = this.timeStarted;3233 chat[messageId]['gen_started'] = this.timeStarted;
3217 chat[messageId]['gen_finished'] = currentTime;3234 chat[messageId]['gen_finished'] = currentTime;
3218
3219 if (!chat[messageId]['extra']) {3235 if (!chat[messageId]['extra']) {
3220 chat[messageId]['extra'] = {};3236 chat[messageId]['extra'] = {};
3221 }3237 }
32223238
3223 if (this.reasoning) {3239 // Update reasoning
3224 chat[messageId]['extra']['reasoning'] = power_user.trim_spaces ? this.reasoning.trim() : this.reasoning;3240 await this.reasoningHandler.process(messageId, mesChanged);
3225 if (this.messageReasoningDom instanceof HTMLElement) {3241 processedText = chat[messageId]['mes'];
3226 const formattedReasoning = messageFormatting(this.reasoning, '', false, false, messageId, {}, true);
3227 this.messageReasoningDom.innerHTML = formattedReasoning;
3228 }
3229 }
32303242
3231 // Don't waste time calculating token count for streaming3243 // Token count update.
3232 const tokenCountText = (this.reasoning || '') + processedText;3244 const tokenCountText = this.reasoningHandler.reasoning + processedText;
3233 const currentTokenCount = isFinal && power_user.message_token_count_enabled ? getTokenCount(tokenCountText, 0) : 0;3245 const currentTokenCount = isFinal && power_user.message_token_count_enabled ? getTokenCount(tokenCountText, 0) : 0;
3234
3235 if (currentTokenCount) {3246 if (currentTokenCount) {
3236 chat[messageId]['extra']['token_count'] = currentTokenCount;3247 chat[messageId]['extra']['token_count'] = currentTokenCount;
3237 if (this.messageTokenCounterDom instanceof HTMLElement) {3248 if (this.messageTokenCounterDom instanceof HTMLElement) {
@@ -3257,7 +3268,7 @@ class StreamingProcessor {
3257 this.messageTextDom.innerHTML = formattedText;3268 this.messageTextDom.innerHTML = formattedText;
3258 }3269 }
32593270
3260 const timePassed = formatGenerationTimer(this.timeStarted, currentTime, currentTokenCount);3271 const timePassed = formatGenerationTimer(this.timeStarted, currentTime, currentTokenCount, this.reasoningHandler.getDuration());
3261 if (this.messageTimerDom instanceof HTMLElement) {3272 if (this.messageTimerDom instanceof HTMLElement) {
3262 this.messageTimerDom.textContent = timePassed.timerValue;3273 this.messageTimerDom.textContent = timePassed.timerValue;
3263 this.messageTimerDom.title = timePassed.timerTitle;3274 this.messageTimerDom.title = timePassed.timerTitle;
@@ -3272,10 +3283,12 @@ class StreamingProcessor {
3272 }3283 }
32733284
3274 async onFinishStreaming(messageId, text) {3285 async onFinishStreaming(messageId, text) {
3275 this.hideMessageButtons(this.messageId);3286 this.markUIGenStopped();
3276 this.onProgressStreaming(messageId, text, true);3287 await this.onProgressStreaming(messageId, text, true);
3277 addCopyToCodeBlocks($(`#chat .mes[mesid="${messageId}"]`));3288 addCopyToCodeBlocks($(`#chat .mes[mesid="${messageId}"]`));
32783289
3290 await this.reasoningHandler.finish(messageId);
3291
3279 if (Array.isArray(this.swipes) && this.swipes.length > 0) {3292 if (Array.isArray(this.swipes) && this.swipes.length > 0) {
3280 const message = chat[messageId];3293 const message = chat[messageId];
3281 const swipeInfo = {3294 const swipeInfo = {
@@ -3315,7 +3328,7 @@ class StreamingProcessor {
3315 this.abortController.abort();3328 this.abortController.abort();
3316 this.isStopped = true;3329 this.isStopped = true;
33173330
3318 this.hideMessageButtons(this.messageId);3331 this.markUIGenStopped();
3319 generatedPromptCache = '';3332 generatedPromptCache = '';
3320 unblockGeneration();3333 unblockGeneration();
33213334
@@ -3366,7 +3379,7 @@ class StreamingProcessor {
3366 for await (const { text, swipes, logprobs, toolCalls, state } of this.generator()) {3379 for await (const { text, swipes, logprobs, toolCalls, state } of this.generator()) {
3367 timestamps.push(Date.now());3380 timestamps.push(Date.now());
3368 if (this.isStopped || this.abortController.signal.aborted) {3381 if (this.isStopped || this.abortController.signal.aborted) {
3369 return;3382 return this.result;
3370 }3383 }
33713384
3372 this.toolCalls = toolCalls;3385 this.toolCalls = toolCalls;
@@ -3375,9 +3388,10 @@ class StreamingProcessor {
3375 if (logprobs) {3388 if (logprobs) {
3376 this.messageLogprobs.push(...(Array.isArray(logprobs) ? logprobs : [logprobs]));3389 this.messageLogprobs.push(...(Array.isArray(logprobs) ? logprobs : [logprobs]));
3377 }3390 }
3378 this.reasoning = getRegexedString(state?.reasoning ?? '', regex_placement.REASONING);3391 // Get the updated reasoning string into the handler
3392 this.reasoningHandler.updateReasoning(this.messageId, state?.reasoning);
3379 await eventSource.emit(event_types.STREAM_TOKEN_RECEIVED, text);3393 await eventSource.emit(event_types.STREAM_TOKEN_RECEIVED, text);
3380 await sw.tick(() => this.onProgressStreaming(this.messageId, this.continueMessage + text));3394 await sw.tick(async () => await this.onProgressStreaming(this.messageId, this.continueMessage + text));
3381 }3395 }
3382 const seconds = (timestamps[timestamps.length - 1] - timestamps[0]) / 1000;3396 const seconds = (timestamps[timestamps.length - 1] - timestamps[0]) / 1000;
3383 console.warn(`Stream stats: ${timestamps.length} tokens, ${seconds.toFixed(2)} seconds, rate: ${Number(timestamps.length / seconds).toFixed(2)} TPS`);3397 console.warn(`Stream stats: ${timestamps.length} tokens, ${seconds.toFixed(2)} seconds, rate: ${Number(timestamps.length / seconds).toFixed(2)} TPS`);
@@ -3453,7 +3467,7 @@ export async function generateRaw(prompt, api, instructOverride, quietToLoud, sy
3453 break;3467 break;
3454 }3468 }
3455 case 'textgenerationwebui':3469 case 'textgenerationwebui':
3456 generateData = getTextGenGenerationData(prompt, amount_gen, false, false, null, 'quiet');3470 generateData = await getTextGenGenerationData(prompt, amount_gen, false, false, null, 'quiet');
3457 TempResponseLength.restore(api);3471 TempResponseLength.restore(api);
3458 break;3472 break;
3459 case 'openai': {3473 case 'openai': {
@@ -4440,7 +4454,7 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
4440 // For prompt bit itemization4454 // For prompt bit itemization
4441 let mesSendString = '';4455 let mesSendString = '';
44424456
4443 function getCombinedPrompt(isNegative) {4457 async function getCombinedPrompt(isNegative) {
4444 // Only return if the guidance scale doesn't exist or the value is 14458 // Only return if the guidance scale doesn't exist or the value is 1
4445 // Also don't return if constructing the neutral prompt4459 // Also don't return if constructing the neutral prompt
4446 if (isNegative && !useCfgPrompt) {4460 if (isNegative && !useCfgPrompt) {
@@ -4467,10 +4481,16 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
4467 // TODO: Make all extension prompts use an array/splice method4481 // TODO: Make all extension prompts use an array/splice method
4468 const lengthDiff = mesSend.length - cfgPrompt.depth;4482 const lengthDiff = mesSend.length - cfgPrompt.depth;
4469 const cfgDepth = lengthDiff >= 0 ? lengthDiff : 0;4483 const cfgDepth = lengthDiff >= 0 ? lengthDiff : 0;
4484 const cfgMessage = finalMesSend[cfgDepth];
4485 if (cfgMessage) {
4486 if (!Array.isArray(finalMesSend[cfgDepth].extensionPrompts)) {
4487 finalMesSend[cfgDepth].extensionPrompts = [];
4488 }
4470 finalMesSend[cfgDepth].extensionPrompts.push(`${cfgPrompt.value}\n`);4489 finalMesSend[cfgDepth].extensionPrompts.push(`${cfgPrompt.value}\n`);
4471 }4490 }
4472 }4491 }
4473 }4492 }
4493 }
44744494
4475 // Add prompt bias after everything else4495 // Add prompt bias after everything else
4476 // Always run with continue4496 // Always run with continue
@@ -4543,13 +4563,13 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
4543 };4563 };
45444564
4545 // Before returning the combined prompt, give available context related information to all subscribers.4565 // Before returning the combined prompt, give available context related information to all subscribers.
4546 eventSource.emitAndWait(event_types.GENERATE_BEFORE_COMBINE_PROMPTS, data);4566 await eventSource.emit(event_types.GENERATE_BEFORE_COMBINE_PROMPTS, data);
45474567
4548 // If one or multiple subscribers return a value, forfeit the responsibillity of flattening the context.4568 // If one or multiple subscribers return a value, forfeit the responsibillity of flattening the context.
4549 return !data.combinedPrompt ? combine() : data.combinedPrompt;4569 return !data.combinedPrompt ? combine() : data.combinedPrompt;
4550 }4570 }
45514571
4552 let finalPrompt = getCombinedPrompt(false);4572 let finalPrompt = await getCombinedPrompt(false);
45534573
4554 const eventData = { prompt: finalPrompt, dryRun: dryRun };4574 const eventData = { prompt: finalPrompt, dryRun: dryRun };
4555 await eventSource.emit(event_types.GENERATE_AFTER_COMBINE_PROMPTS, eventData);4575 await eventSource.emit(event_types.GENERATE_AFTER_COMBINE_PROMPTS, eventData);
@@ -4583,8 +4603,8 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
4583 }4603 }
4584 break;4604 break;
4585 case 'textgenerationwebui': {4605 case 'textgenerationwebui': {
4586 const cfgValues = useCfgPrompt ? { guidanceScale: cfgGuidanceScale, negativePrompt: getCombinedPrompt(true) } : null;4606 const cfgValues = useCfgPrompt ? { guidanceScale: cfgGuidanceScale, negativePrompt: await getCombinedPrompt(true) } : null;
4587 generate_data = getTextGenGenerationData(finalPrompt, maxLength, isImpersonate, isContinue, cfgValues, type);4607 generate_data = await getTextGenGenerationData(finalPrompt, maxLength, isImpersonate, isContinue, cfgValues, type);
4588 break;4608 break;
4589 }4609 }
4590 case 'novel': {4610 case 'novel': {
@@ -5510,7 +5530,7 @@ async function promptItemize(itemizedPrompts, requestedMesId) {
5510 toastr.info(t`Copied!`);5530 toastr.info(t`Copied!`);
5511 });5531 });
55125532
5513 popup.dlg.querySelector('#showRawPrompt').addEventListener('click', function () {5533 popup.dlg.querySelector('#showRawPrompt').addEventListener('click', async function () {
5514 //console.log(itemizedPrompts[PromptArrayItemForRawPromptDisplay].rawPrompt);5534 //console.log(itemizedPrompts[PromptArrayItemForRawPromptDisplay].rawPrompt);
5515 console.log(PromptArrayItemForRawPromptDisplay);5535 console.log(PromptArrayItemForRawPromptDisplay);
5516 console.log(itemizedPrompts);5536 console.log(itemizedPrompts);
@@ -5518,6 +5538,17 @@ async function promptItemize(itemizedPrompts, requestedMesId) {
55185538
5519 const rawPrompt = flatten(itemizedPrompts[PromptArrayItemForRawPromptDisplay].rawPrompt);5539 const rawPrompt = flatten(itemizedPrompts[PromptArrayItemForRawPromptDisplay].rawPrompt);
55205540
5541 // Mobile needs special handholding. The side-view on the popup wouldn't work,
5542 // so we just show an additional popup for this.
5543 if (isMobile()) {
5544 const content = document.createElement('div');
5545 content.classList.add('tokenItemizingMaintext');
5546 content.innerText = rawPrompt;
5547 const popup = new Popup(content, POPUP_TYPE.TEXT, null, { allowVerticalScrolling: true, leftAlign: true });
5548 await popup.show();
5549 return;
5550 }
5551
5521 //let DisplayStringifiedPrompt = JSON.stringify(itemizedPrompts[PromptArrayItemForRawPromptDisplay].rawPrompt).replace(/\n+/g, '<br>');5552 //let DisplayStringifiedPrompt = JSON.stringify(itemizedPrompts[PromptArrayItemForRawPromptDisplay].rawPrompt).replace(/\n+/g, '<br>');
5522 const rawPromptWrapper = document.getElementById('rawPromptWrapper');5553 const rawPromptWrapper = document.getElementById('rawPromptWrapper');
5523 rawPromptWrapper.innerText = rawPrompt;5554 rawPromptWrapper.innerText = rawPrompt;
@@ -5701,37 +5732,6 @@ function extractMessageFromData(data) {
5701}5732}
57025733
5703/**5734/**
5704 * Extracts the reasoning from the response data.
5705 * @param {object} data Response data
5706 * @returns {string} Extracted reasoning
5707 */
5708function extractReasoningFromData(data) {
5709 switch (main_api) {
5710 case 'textgenerationwebui':
5711 switch (textgen_settings.type) {
5712 case textgen_types.OPENROUTER:
5713 return data?.choices?.[0]?.reasoning ?? '';
5714 }
5715 break;
5716
5717 case 'openai':
5718 if (!oai_settings.show_thoughts) break;
5719
5720 switch (oai_settings.chat_completion_source) {
5721 case chat_completion_sources.DEEPSEEK:
5722 return data?.choices?.[0]?.message?.reasoning_content ?? '';
5723 case chat_completion_sources.OPENROUTER:
5724 return data?.choices?.[0]?.message?.reasoning ?? '';
5725 case chat_completion_sources.MAKERSUITE:
5726 return data?.responseContent?.parts?.filter(part => part.thought)?.map(part => part.text)?.join('\n\n') ?? '';
5727 }
5728 break;
5729 }
5730
5731 return '';
5732}
5733
5734/**
5735 * Extracts multiswipe swipes from the response data.5735 * Extracts multiswipe swipes from the response data.
5736 * @param {Object} data Response data5736 * @param {Object} data Response data
5737 * @param {string} type Type of generation5737 * @param {string} type Type of generation
@@ -5921,6 +5921,15 @@ export async function saveReply(type, getMessage, fromStreaming, title, swipes,
5921 chat[chat.length - 1]['extra'] = {};5921 chat[chat.length - 1]['extra'] = {};
5922 }5922 }
59235923
5924 // Coerce null/undefined to empty string
5925 if (chat.length && !chat[chat.length - 1]['extra']['reasoning']) {
5926 chat[chat.length - 1]['extra']['reasoning'] = '';
5927 }
5928
5929 if (!reasoning) {
5930 reasoning = '';
5931 }
5932
5924 let oldMessage = '';5933 let oldMessage = '';
5925 const generationFinished = new Date();5934 const generationFinished = new Date();
5926 const img = extractImageFromMessage(getMessage);5935 const img = extractImageFromMessage(getMessage);
@@ -5937,6 +5946,7 @@ export async function saveReply(type, getMessage, fromStreaming, title, swipes,
5937 chat[chat.length - 1]['extra']['api'] = getGeneratingApi();5946 chat[chat.length - 1]['extra']['api'] = getGeneratingApi();
5938 chat[chat.length - 1]['extra']['model'] = getGeneratingModel();5947 chat[chat.length - 1]['extra']['model'] = getGeneratingModel();
5939 chat[chat.length - 1]['extra']['reasoning'] = reasoning;5948 chat[chat.length - 1]['extra']['reasoning'] = reasoning;
5949 chat[chat.length - 1]['extra']['reasoning_duration'] = null;
5940 if (power_user.message_token_count_enabled) {5950 if (power_user.message_token_count_enabled) {
5941 const tokenCountText = (reasoning || '') + chat[chat.length - 1]['mes'];5951 const tokenCountText = (reasoning || '') + chat[chat.length - 1]['mes'];
5942 chat[chat.length - 1]['extra']['token_count'] = await getTokenCountAsync(tokenCountText, 0);5952 chat[chat.length - 1]['extra']['token_count'] = await getTokenCountAsync(tokenCountText, 0);
@@ -5958,7 +5968,8 @@ export async function saveReply(type, getMessage, fromStreaming, title, swipes,
5958 chat[chat.length - 1]['send_date'] = getMessageTimeStamp();5968 chat[chat.length - 1]['send_date'] = getMessageTimeStamp();
5959 chat[chat.length - 1]['extra']['api'] = getGeneratingApi();5969 chat[chat.length - 1]['extra']['api'] = getGeneratingApi();
5960 chat[chat.length - 1]['extra']['model'] = getGeneratingModel();5970 chat[chat.length - 1]['extra']['model'] = getGeneratingModel();
5961 chat[chat.length - 1]['extra']['reasoning'] += reasoning;5971 chat[chat.length - 1]['extra']['reasoning'] = reasoning;
5972 chat[chat.length - 1]['extra']['reasoning_duration'] = null;
5962 if (power_user.message_token_count_enabled) {5973 if (power_user.message_token_count_enabled) {
5963 const tokenCountText = (reasoning || '') + chat[chat.length - 1]['mes'];5974 const tokenCountText = (reasoning || '') + chat[chat.length - 1]['mes'];
5964 chat[chat.length - 1]['extra']['token_count'] = await getTokenCountAsync(tokenCountText, 0);5975 chat[chat.length - 1]['extra']['token_count'] = await getTokenCountAsync(tokenCountText, 0);
@@ -5978,6 +5989,7 @@ export async function saveReply(type, getMessage, fromStreaming, title, swipes,
5978 chat[chat.length - 1]['extra']['api'] = getGeneratingApi();5989 chat[chat.length - 1]['extra']['api'] = getGeneratingApi();
5979 chat[chat.length - 1]['extra']['model'] = getGeneratingModel();5990 chat[chat.length - 1]['extra']['model'] = getGeneratingModel();
5980 chat[chat.length - 1]['extra']['reasoning'] += reasoning;5991 chat[chat.length - 1]['extra']['reasoning'] += reasoning;
5992 // We don't know if the reasoning duration extended, so we don't update it here on purpose.
5981 if (power_user.message_token_count_enabled) {5993 if (power_user.message_token_count_enabled) {
5982 const tokenCountText = (reasoning || '') + chat[chat.length - 1]['mes'];5994 const tokenCountText = (reasoning || '') + chat[chat.length - 1]['mes'];
5983 chat[chat.length - 1]['extra']['token_count'] = await getTokenCountAsync(tokenCountText, 0);5995 chat[chat.length - 1]['extra']['token_count'] = await getTokenCountAsync(tokenCountText, 0);
@@ -5997,6 +6009,7 @@ export async function saveReply(type, getMessage, fromStreaming, title, swipes,
5997 chat[chat.length - 1]['extra']['api'] = getGeneratingApi();6009 chat[chat.length - 1]['extra']['api'] = getGeneratingApi();
5998 chat[chat.length - 1]['extra']['model'] = getGeneratingModel();6010 chat[chat.length - 1]['extra']['model'] = getGeneratingModel();
5999 chat[chat.length - 1]['extra']['reasoning'] = reasoning;6011 chat[chat.length - 1]['extra']['reasoning'] = reasoning;
6012 chat[chat.length - 1]['extra']['reasoning_duration'] = null;
6000 if (power_user.trim_spaces) {6013 if (power_user.trim_spaces) {
6001 getMessage = getMessage.trim();6014 getMessage = getMessage.trim();
6002 }6015 }
@@ -6137,20 +6150,21 @@ function extractImageFromMessage(getMessage) {
6137 return { getMessage, image, title };6150 return { getMessage, image, title };
6138}6151}
61396152
6153/**
6154 * A function mainly used to switch 'generating' state - setting it to false and activating the buttons again
6155 */
6140export function activateSendButtons() {6156export function activateSendButtons() {
6141 is_send_press = false;6157 is_send_press = false;
6142 $('#send_but').removeClass('displayNone');
6143 $('#mes_continue').removeClass('displayNone');
6144 $('#mes_impersonate').removeClass('displayNone');
6145 $('.mes_buttons:last').show();
6146 hideStopButton();6158 hideStopButton();
6159 delete document.body.dataset.generating;
6147}6160}
61486161
6162/**
6163 * A function mainly used to switch 'generating' state - setting it to true and deactivating the buttons
6164 */
6149export function deactivateSendButtons() {6165export function deactivateSendButtons() {
6150 $('#send_but').addClass('displayNone');
6151 $('#mes_continue').addClass('displayNone');
6152 $('#mes_impersonate').addClass('displayNone');
6153 showStopButton();6166 showStopButton();
6167 document.body.dataset.generating = 'true';
6154}6168}
61556169
6156export function resetChatState() {6170export function resetChatState() {
@@ -6243,9 +6257,35 @@ export async function renameCharacter(name = null, { silent = false, renameChats
6243 const data = await response.json();6257 const data = await response.json();
6244 const newAvatar = data.avatar;6258 const newAvatar = data.avatar;
62456259
6246 // Replace tags list6260 const oldName = getCharaFilename(null, { manualAvatarKey: oldAvatar });
6261 const newName = getCharaFilename(null, { manualAvatarKey: newAvatar });
6262
6263 // Replace other auxillery fields where was referenced by avatar key
6264 // Tag List
6247 renameTagKey(oldAvatar, newAvatar);6265 renameTagKey(oldAvatar, newAvatar);
62486266
6267 // Addtional lore books
6268 const charLore = world_info.charLore?.find(x => x.name == oldName);
6269 if (charLore) {
6270 charLore.name = newName;
6271 saveSettingsDebounced();
6272 }
6273
6274 // Char-bound Author's Notes
6275 const charNote = extension_settings.note.chara?.find(x => x.name == oldName);
6276 if (charNote) {
6277 charNote.name = newName;
6278 saveSettingsDebounced();
6279 }
6280
6281 // Update active character, if the current one was the currently active one
6282 if (active_character === oldAvatar) {
6283 active_character = newAvatar;
6284 saveSettingsDebounced();
6285 }
6286
6287 await eventSource.emit(event_types.CHARACTER_RENAMED, oldAvatar, newAvatar);
6288
6249 // Reload characters list6289 // Reload characters list
6250 await getCharacters();6290 await getCharacters();
62516291
@@ -6846,10 +6886,11 @@ export async function getSettings() {
6846 $('#your_name').val(name1);6886 $('#your_name').val(name1);
6847 }6887 }
68486888
6889 accountStorage.init(settings?.accountStorage);
6849 await setUserControls(data.enable_accounts);6890 await setUserControls(data.enable_accounts);
68506891
6851 // Allow subscribers to mutate settings6892 // Allow subscribers to mutate settings
6852 eventSource.emit(event_types.SETTINGS_LOADED_BEFORE, settings);6893 await eventSource.emit(event_types.SETTINGS_LOADED_BEFORE, settings);
68536894
6854 //Load KoboldAI settings6895 //Load KoboldAI settings
6855 koboldai_setting_names = data.koboldai_setting_names;6896 koboldai_setting_names = data.koboldai_setting_names;
@@ -6946,7 +6987,7 @@ export async function getSettings() {
6946 loadProxyPresets(settings);6987 loadProxyPresets(settings);
69476988
6948 // Allow subscribers to mutate settings6989 // Allow subscribers to mutate settings
6949 eventSource.emit(event_types.SETTINGS_LOADED_AFTER, settings);6990 await eventSource.emit(event_types.SETTINGS_LOADED_AFTER, settings);
69506991
6951 // Set context size after loading power user (may override the max value)6992 // Set context size after loading power user (may override the max value)
6952 $('#max_context').val(max_context);6993 $('#max_context').val(max_context);
@@ -7006,7 +7047,7 @@ export async function getSettings() {
7006 }7047 }
7007 await validateDisabledSamplers();7048 await validateDisabledSamplers();
7008 settingsReady = true;7049 settingsReady = true;
7009 eventSource.emit(event_types.SETTINGS_LOADED);7050 await eventSource.emit(event_types.SETTINGS_LOADED);
7010}7051}
70117052
7012function selectKoboldGuiPreset() {7053function selectKoboldGuiPreset() {
@@ -7017,7 +7058,8 @@ function selectKoboldGuiPreset() {
70177058
7018export async function saveSettings(loopCounter = 0) {7059export async function saveSettings(loopCounter = 0) {
7019 if (!settingsReady) {7060 if (!settingsReady) {
7020 console.warn('Settings not ready, aborting save');7061 console.warn('Settings not ready, scheduling another save');
7062 saveSettingsDebounced();
7021 return;7063 return;
7022 }7064 }
70237065
@@ -7038,6 +7080,7 @@ export async function saveSettings(loopCounter = 0) {
7038 url: '/api/settings/save',7080 url: '/api/settings/save',
7039 data: JSON.stringify({7081 data: JSON.stringify({
7040 firstRun: firstRun,7082 firstRun: firstRun,
7083 accountStorage: accountStorage.getState(),
7041 currentVersion: currentVersion,7084 currentVersion: currentVersion,
7042 username: name1,7085 username: name1,
7043 active_character: active_character,7086 active_character: active_character,
@@ -7103,8 +7146,10 @@ export function setGenerationParamsFromPreset(preset) {
7103// Common code for message editor done and auto-save7146// Common code for message editor done and auto-save
7104function updateMessage(div) {7147function updateMessage(div) {
7105 const mesBlock = div.closest('.mes_block');7148 const mesBlock = div.closest('.mes_block');
7106 let text = mesBlock.find('.edit_textarea').val();7149 let text = mesBlock.find('.edit_textarea').val()
7107 const mes = chat[this_edit_mes_id];7150 ?? mesBlock.find('.mes_text').text();
7151 const mesElement = div.closest('.mes');
7152 const mes = chat[mesElement.attr('mesid')];
71087153
7109 let regexPlacement;7154 let regexPlacement;
7110 if (mes.is_user) {7155 if (mes.is_user) {
@@ -7223,6 +7268,11 @@ async function messageEditDone(div) {
7223 appendMediaToMessage(mes, div.closest('.mes'));7268 appendMediaToMessage(mes, div.closest('.mes'));
7224 addCopyToCodeBlocks(div.closest('.mes'));7269 addCopyToCodeBlocks(div.closest('.mes'));
72257270
7271 const reasoningEditDone = mesBlock.find('.mes_reasoning_edit_done:visible');
7272 if (reasoningEditDone.length > 0) {
7273 reasoningEditDone.trigger('click');
7274 }
7275
7226 await eventSource.emit(event_types.MESSAGE_UPDATED, this_edit_mes_id);7276 await eventSource.emit(event_types.MESSAGE_UPDATED, this_edit_mes_id);
7227 this_edit_mes_id = undefined;7277 this_edit_mes_id = undefined;
7228 await saveChatConditional();7278 await saveChatConditional();
@@ -7486,7 +7536,7 @@ export function select_rm_info(type, charId, previousCharId = null) {
7486 }7536 }
74877537
7488 try {7538 try {
7489 const perPage = Number(localStorage.getItem('Characters_PerPage')) || per_page_default;7539 const perPage = Number(accountStorage.getItem('Characters_PerPage')) || per_page_default;
7490 const page = Math.floor(charIndex / perPage) + 1;7540 const page = Math.floor(charIndex / perPage) + 1;
7491 const selector = `#rm_print_characters_block [title*="${avatarFileName}"]`;7541 const selector = `#rm_print_characters_block [title*="${avatarFileName}"]`;
7492 $('#rm_print_characters_pagination').pagination('go', page);7542 $('#rm_print_characters_pagination').pagination('go', page);
@@ -7518,7 +7568,7 @@ export function select_rm_info(type, charId, previousCharId = null) {
7518 return;7568 return;
7519 }7569 }
75207570
7521 const perPage = Number(localStorage.getItem('Characters_PerPage')) || per_page_default;7571 const perPage = Number(accountStorage.getItem('Characters_PerPage')) || per_page_default;
7522 const page = Math.floor(charIndex / perPage) + 1;7572 const page = Math.floor(charIndex / perPage) + 1;
7523 $('#rm_print_characters_pagination').pagination('go', page);7573 $('#rm_print_characters_pagination').pagination('go', page);
7524 const selector = `#rm_print_characters_block [grid="${charId}"]`;7574 const selector = `#rm_print_characters_block [grid="${charId}"]`;
@@ -8723,11 +8773,6 @@ const swipe_right = () => {
8723 easing: animation_easing,8773 easing: animation_easing,
8724 queue: false,8774 queue: false,
8725 complete: async function () {8775 complete: async function () {
8726 /*if (!selected_group) {
8727 var typingIndicator = $("#typing_indicator_template .typing_indicator").clone();
8728 typingIndicator.find(".typing_indicator_name").text(characters[this_chid].name);
8729 } */
8730 /* $("#chat").append(typingIndicator); */
8731 const is_animation_scroll = ($('#chat').scrollTop() >= ($('#chat').prop('scrollHeight') - $('#chat').outerHeight()) - 10);8776 const is_animation_scroll = ($('#chat').scrollTop() >= ($('#chat').prop('scrollHeight') - $('#chat').outerHeight()) - 10);
8732 //console.log(parseInt(chat[chat.length-1]['swipe_id']));8777 //console.log(parseInt(chat[chat.length-1]['swipe_id']));
8733 //console.log(chat[chat.length-1]['swipes'].length);8778 //console.log(chat[chat.length-1]['swipes'].length);
@@ -8738,7 +8783,7 @@ const swipe_right = () => {
8738 // resets the timer8783 // resets the timer
8739 swipeMessage.find('.mes_timer').html('');8784 swipeMessage.find('.mes_timer').html('');
8740 swipeMessage.find('.tokenCounterDisplay').text('');8785 swipeMessage.find('.tokenCounterDisplay').text('');
8741 swipeMessage.find('.mes_reasoning').html('');8786 updateReasoningUI(swipeMessage, { reset: true });
8742 } else {8787 } else {
8743 //console.log('showing previously generated swipe candidate, or "..."');8788 //console.log('showing previously generated swipe candidate, or "..."');
8744 //console.log('onclick right swipe calling addOneMessage');8789 //console.log('onclick right swipe calling addOneMessage');
@@ -8788,7 +8833,6 @@ const swipe_right = () => {
8788 if (run_generate && !is_send_press && parseInt(chat[chat.length - 1]['swipe_id']) === chat[chat.length - 1]['swipes'].length) {8833 if (run_generate && !is_send_press && parseInt(chat[chat.length - 1]['swipe_id']) === chat[chat.length - 1]['swipes'].length) {
8789 console.debug('caught here 2');8834 console.debug('caught here 2');
8790 is_send_press = true;8835 is_send_press = true;
8791 $('.mes_buttons:last').hide();
8792 await Generate('swipe');8836 await Generate('swipe');
8793 } else {8837 } else {
8794 if (parseInt(chat[chat.length - 1]['swipe_id']) !== chat[chat.length - 1]['swipes'].length) {8838 if (parseInt(chat[chat.length - 1]['swipe_id']) !== chat[chat.length - 1]['swipes'].length) {
@@ -9390,6 +9434,9 @@ export async function deleteCharacter(characterKey, { deleteChats = true } = {})
9390 continue;9434 continue;
9391 }9435 }
93929436
9437 accountStorage.removeItem(`AlertWI_${character.avatar}`);
9438 accountStorage.removeItem(`AlertRegex_${character.avatar}`);
9439 accountStorage.removeItem(`mediaWarningShown:${character.avatar}`);
9393 delete tag_map[character.avatar];9440 delete tag_map[character.avatar];
9394 select_rm_info('char_delete', character.name);9441 select_rm_info('char_delete', character.name);
93959442
@@ -9592,8 +9639,8 @@ function addDebugFunctions() {
9592 });9639 });
95939640
9594 registerDebugFunction('toggleRegenerateWarning', 'Toggle Ctrl+Enter regeneration confirmation', 'Toggle the warning when regenerating a message with a Ctrl+Enter hotkey.', () => {9641 registerDebugFunction('toggleRegenerateWarning', 'Toggle Ctrl+Enter regeneration confirmation', 'Toggle the warning when regenerating a message with a Ctrl+Enter hotkey.', () => {
9595 localStorage.setItem('RegenerateWithCtrlEnter', localStorage.getItem('RegenerateWithCtrlEnter') === 'true' ? 'false' : 'true');9642 accountStorage.setItem('RegenerateWithCtrlEnter', accountStorage.getItem('RegenerateWithCtrlEnter') === 'true' ? 'false' : 'true');
9596 toastr.info('Regenerate warning is now ' + (localStorage.getItem('RegenerateWithCtrlEnter') === 'true' ? 'disabled' : 'enabled'));9643 toastr.info('Regenerate warning is now ' + (accountStorage.getItem('RegenerateWithCtrlEnter') === 'true' ? 'disabled' : 'enabled'));
9597 });9644 });
95989645
9599 registerDebugFunction('copySetup', 'Copy ST setup to clipboard [WIP]', 'Useful data when reporting bugs', async () => {9646 registerDebugFunction('copySetup', 'Copy ST setup to clipboard [WIP]', 'Useful data when reporting bugs', async () => {
@@ -10735,6 +10782,12 @@ jQuery(async function () {
10735 var edit_mes_id = $(this).closest('.mes').attr('mesid');10782 var edit_mes_id = $(this).closest('.mes').attr('mesid');
10736 this_edit_mes_id = edit_mes_id;10783 this_edit_mes_id = edit_mes_id;
1073710784
10785 // Also edit reasoning, if it exists
10786 const reasoningEdit = $(this).closest('.mes_block').find('.mes_reasoning_edit:visible');
10787 if (reasoningEdit.length > 0) {
10788 reasoningEdit.trigger('click');
10789 }
10790
10738 var text = chat[edit_mes_id]['mes'];10791 var text = chat[edit_mes_id]['mes'];
10739 if (chat[edit_mes_id]['is_user']) {10792 if (chat[edit_mes_id]['is_user']) {
10740 this_edit_mes_chname = name1;10793 this_edit_mes_chname = name1;
@@ -10868,6 +10921,11 @@ jQuery(async function () {
10868 appendMediaToMessage(chat[this_edit_mes_id], $(this).closest('.mes'));10921 appendMediaToMessage(chat[this_edit_mes_id], $(this).closest('.mes'));
10869 addCopyToCodeBlocks($(this).closest('.mes'));10922 addCopyToCodeBlocks($(this).closest('.mes'));
1087010923
10924 const reasoningEditDone = $(this).closest('.mes_block').find('.mes_reasoning_edit_cancel:visible');
10925 if (reasoningEditDone.length > 0) {
10926 reasoningEditDone.trigger('click');
10927 }
10928
10871 await eventSource.emit(event_types.MESSAGE_UPDATED, this_edit_mes_id);10929 await eventSource.emit(event_types.MESSAGE_UPDATED, this_edit_mes_id);
10872 this_edit_mes_id = undefined;10930 this_edit_mes_id = undefined;
10873 });10931 });
@@ -10931,7 +10989,7 @@ jQuery(async function () {
10931 });10989 });
1093210990
10933 $(document).on('click', '.mes_edit_copy', async function () {10991 $(document).on('click', '.mes_edit_copy', async function () {
10934 const confirmation = await callGenericPopup('Create a copy of this message?', POPUP_TYPE.CONFIRM);10992 const confirmation = await callGenericPopup(t`Create a copy of this message?`, POPUP_TYPE.CONFIRM);
10935 if (!confirmation) {10993 if (!confirmation) {
10936 return;10994 return;
10937 }10995 }
@@ -11410,7 +11468,7 @@ jQuery(async function () {
11410 );11468 );
11411 break;*/11469 break;*/
11412 default:11470 default:
11413 eventSource.emit('charManagementDropdown', target);11471 await eventSource.emit('charManagementDropdown', target);
11414 }11472 }
11415 $('#char-management-dropdown').prop('selectedIndex', 0);11473 $('#char-management-dropdown').prop('selectedIndex', 0);
11416 });11474 });
@@ -11572,7 +11630,7 @@ jQuery(async function () {
1157211630
11573 $(document).on('click', '.open_characters_library', async function () {11631 $(document).on('click', '.open_characters_library', async function () {
11574 await getCharacters();11632 await getCharacters();
11575 eventSource.emit(event_types.OPEN_CHARACTER_LIBRARY);11633 await eventSource.emit(event_types.OPEN_CHARACTER_LIBRARY);
11576 });11634 });
1157711635
11578 // Added here to prevent execution before script.js is loaded and get rid of quirky timeouts11636 // Added here to prevent execution before script.js is loaded and get rid of quirky timeouts
public/scripts/RossAscends-mods.js+58 -30
@@ -27,7 +27,6 @@ import {
27 send_on_enter_options,27 send_on_enter_options,
28} from './power-user.js';28} from './power-user.js';
2929
30import { LoadLocal, SaveLocal, LoadLocalBool } from './f-localStorage.js';
31import { selected_group, is_group_generating, openGroupById } from './group-chats.js';30import { selected_group, is_group_generating, openGroupById } from './group-chats.js';
32import { getTagKeyForEntity, applyTagsOnCharacterSelect } from './tags.js';31import { getTagKeyForEntity, applyTagsOnCharacterSelect } from './tags.js';
33import {32import {
@@ -41,6 +40,8 @@ import { textgen_types, textgenerationwebui_settings as textgen_settings, getTex
41import { debounce_timeout } from './constants.js';40import { debounce_timeout } from './constants.js';
4241
43import { Popup } from './popup.js';42import { Popup } from './popup.js';
43import { accountStorage } from './util/AccountStorage.js';
44import { getCurrentUserHandle } from './user.js';
4445
45var RPanelPin = document.getElementById('rm_button_panel_pin');46var RPanelPin = document.getElementById('rm_button_panel_pin');
46var LPanelPin = document.getElementById('lm_button_panel_pin');47var LPanelPin = document.getElementById('lm_button_panel_pin');
@@ -279,17 +280,32 @@ async function RA_autoloadchat() {
279 // active character is the name, we should look it up in the character list and get the id280 // active character is the name, we should look it up in the character list and get the id
280 if (active_character !== null && active_character !== undefined) {281 if (active_character !== null && active_character !== undefined) {
281 const active_character_id = characters.findIndex(x => getTagKeyForEntity(x) === active_character);282 const active_character_id = characters.findIndex(x => getTagKeyForEntity(x) === active_character);
282 if (active_character_id !== null) {283 if (active_character_id !== -1) {
283 await selectCharacterById(String(active_character_id));284 await selectCharacterById(String(active_character_id));
284285
285 // Do a little tomfoolery to spoof the tag selector286 // Do a little tomfoolery to spoof the tag selector
286 const selectedCharElement = $(`#rm_print_characters_block .character_select[chid="${active_character_id}"]`);287 const selectedCharElement = $(`#rm_print_characters_block .character_select[chid="${active_character_id}"]`);
287 applyTagsOnCharacterSelect.call(selectedCharElement);288 applyTagsOnCharacterSelect.call(selectedCharElement);
289 } else {
290 setActiveCharacter(null);
291 saveSettingsDebounced();
292 console.warn(`Currently active character with ID ${active_character} not found. Resetting to no active character.`);
288 }293 }
289 }294 }
290295
291 if (active_group !== null && active_group !== undefined) {296 if (active_group !== null && active_group !== undefined) {
292 await openGroupById(String(active_group));297 if (active_character) {
298 console.warn('Active character and active group are both set. Only active character will be loaded. Resetting active group.');
299 setActiveGroup(null);
300 saveSettingsDebounced();
301 } else {
302 const result = await openGroupById(String(active_group));
303 if (!result) {
304 setActiveGroup(null);
305 saveSettingsDebounced();
306 console.warn(`Currently active group with ID ${active_group} not found. Resetting to no active group.`);
307 }
308 }
293 }309 }
294310
295 // if the character list hadn't been loaded yet, try again.311 // if the character list hadn't been loaded yet, try again.
@@ -409,32 +425,34 @@ function RA_autoconnect(PrevApi) {
409function OpenNavPanels() {425function OpenNavPanels() {
410 if (!isMobile()) {426 if (!isMobile()) {
411 //auto-open R nav if locked and previously open427 //auto-open R nav if locked and previously open
412 if (LoadLocalBool('NavLockOn') == true && LoadLocalBool('NavOpened') == true) {428 if (accountStorage.getItem('NavLockOn') == 'true' && accountStorage.getItem('NavOpened') == 'true') {
413 //console.log("RA -- clicking right nav to open");429 //console.log("RA -- clicking right nav to open");
414 $('#rightNavDrawerIcon').click();430 $('#rightNavDrawerIcon').click();
415 }431 }
416432
417 //auto-open L nav if locked and previously open433 //auto-open L nav if locked and previously open
418 if (LoadLocalBool('LNavLockOn') == true && LoadLocalBool('LNavOpened') == true) {434 if (accountStorage.getItem('LNavLockOn') == 'true' && accountStorage.getItem('LNavOpened') == 'true') {
419 console.debug('RA -- clicking left nav to open');435 console.debug('RA -- clicking left nav to open');
420 $('#leftNavDrawerIcon').click();436 $('#leftNavDrawerIcon').click();
421 }437 }
422438
423 //auto-open WI if locked and previously open439 //auto-open WI if locked and previously open
424 if (LoadLocalBool('WINavLockOn') == true && LoadLocalBool('WINavOpened') == true) {440 if (accountStorage.getItem('WINavLockOn') == 'true' && accountStorage.getItem('WINavOpened') == 'true') {
425 console.debug('RA -- clicking WI to open');441 console.debug('RA -- clicking WI to open');
426 $('#WIDrawerIcon').click();442 $('#WIDrawerIcon').click();
427 }443 }
428 }444 }
429}445}
430446
447const getUserInputKey = () => getCurrentUserHandle() + '_userInput';
448
431function restoreUserInput() {449function restoreUserInput() {
432 if (!power_user.restore_user_input) {450 if (!power_user.restore_user_input) {
433 console.debug('restoreUserInput disabled');451 console.debug('restoreUserInput disabled');
434 return;452 return;
435 }453 }
436454
437 const userInput = LoadLocal('userInput');455 const userInput = localStorage.getItem(getUserInputKey());
438 if (userInput) {456 if (userInput) {
439 $('#send_textarea').val(userInput)[0].dispatchEvent(new Event('input', { bubbles: true }));457 $('#send_textarea').val(userInput)[0].dispatchEvent(new Event('input', { bubbles: true }));
440 }458 }
@@ -442,7 +460,8 @@ function restoreUserInput() {
442460
443function saveUserInput() {461function saveUserInput() {
444 const userInput = String($('#send_textarea').val());462 const userInput = String($('#send_textarea').val());
445 SaveLocal('userInput', userInput);463 localStorage.setItem(getUserInputKey(), userInput);
464 console.debug('User Input -- ', userInput);
446}465}
447const saveUserInputDebounced = debounce(saveUserInput);466const saveUserInputDebounced = debounce(saveUserInput);
448467
@@ -739,7 +758,7 @@ export function initRossMods() {
739758
740 //toggle pin class when lock toggle clicked759 //toggle pin class when lock toggle clicked
741 $(RPanelPin).on('click', function () {760 $(RPanelPin).on('click', function () {
742 SaveLocal('NavLockOn', $(RPanelPin).prop('checked'));761 accountStorage.setItem('NavLockOn', $(RPanelPin).prop('checked'));
743 if ($(RPanelPin).prop('checked') == true) {762 if ($(RPanelPin).prop('checked') == true) {
744 //console.log('adding pin class to right nav');763 //console.log('adding pin class to right nav');
745 $(RightNavPanel).addClass('pinnedOpen');764 $(RightNavPanel).addClass('pinnedOpen');
@@ -757,7 +776,7 @@ export function initRossMods() {
757 }776 }
758 });777 });
759 $(LPanelPin).on('click', function () {778 $(LPanelPin).on('click', function () {
760 SaveLocal('LNavLockOn', $(LPanelPin).prop('checked'));779 accountStorage.setItem('LNavLockOn', $(LPanelPin).prop('checked'));
761 if ($(LPanelPin).prop('checked') == true) {780 if ($(LPanelPin).prop('checked') == true) {
762 //console.log('adding pin class to Left nav');781 //console.log('adding pin class to Left nav');
763 $(LeftNavPanel).addClass('pinnedOpen');782 $(LeftNavPanel).addClass('pinnedOpen');
@@ -776,7 +795,7 @@ export function initRossMods() {
776 });795 });
777796
778 $(WIPanelPin).on('click', function () {797 $(WIPanelPin).on('click', function () {
779 SaveLocal('WINavLockOn', $(WIPanelPin).prop('checked'));798 accountStorage.setItem('WINavLockOn', $(WIPanelPin).prop('checked'));
780 if ($(WIPanelPin).prop('checked') == true) {799 if ($(WIPanelPin).prop('checked') == true) {
781 console.debug('adding pin class to WI');800 console.debug('adding pin class to WI');
782 $(WorldInfo).addClass('pinnedOpen');801 $(WorldInfo).addClass('pinnedOpen');
@@ -796,8 +815,8 @@ export function initRossMods() {
796 });815 });
797816
798 // read the state of right Nav Lock and apply to rightnav classlist817 // read the state of right Nav Lock and apply to rightnav classlist
799 $(RPanelPin).prop('checked', LoadLocalBool('NavLockOn'));818 $(RPanelPin).prop('checked', accountStorage.getItem('NavLockOn') == 'true');
800 if (LoadLocalBool('NavLockOn') == true) {819 if (accountStorage.getItem('NavLockOn') == 'true') {
801 //console.log('setting pin class via local var');820 //console.log('setting pin class via local var');
802 $(RightNavPanel).addClass('pinnedOpen');821 $(RightNavPanel).addClass('pinnedOpen');
803 $(RightNavDrawerIcon).addClass('drawerPinnedOpen');822 $(RightNavDrawerIcon).addClass('drawerPinnedOpen');
@@ -808,8 +827,8 @@ export function initRossMods() {
808 $(RightNavDrawerIcon).addClass('drawerPinnedOpen');827 $(RightNavDrawerIcon).addClass('drawerPinnedOpen');
809 }828 }
810 // read the state of left Nav Lock and apply to leftnav classlist829 // read the state of left Nav Lock and apply to leftnav classlist
811 $(LPanelPin).prop('checked', LoadLocalBool('LNavLockOn'));830 $(LPanelPin).prop('checked', accountStorage.getItem('LNavLockOn') === 'true');
812 if (LoadLocalBool('LNavLockOn') == true) {831 if (accountStorage.getItem('LNavLockOn') == 'true') {
813 //console.log('setting pin class via local var');832 //console.log('setting pin class via local var');
814 $(LeftNavPanel).addClass('pinnedOpen');833 $(LeftNavPanel).addClass('pinnedOpen');
815 $(LeftNavDrawerIcon).addClass('drawerPinnedOpen');834 $(LeftNavDrawerIcon).addClass('drawerPinnedOpen');
@@ -821,8 +840,8 @@ export function initRossMods() {
821 }840 }
822841
823 // read the state of left Nav Lock and apply to leftnav classlist842 // read the state of left Nav Lock and apply to leftnav classlist
824 $(WIPanelPin).prop('checked', LoadLocalBool('WINavLockOn'));843 $(WIPanelPin).prop('checked', accountStorage.getItem('WINavLockOn') === 'true');
825 if (LoadLocalBool('WINavLockOn') == true) {844 if (accountStorage.getItem('WINavLockOn') == 'true') {
826 //console.log('setting pin class via local var');845 //console.log('setting pin class via local var');
827 $(WorldInfo).addClass('pinnedOpen');846 $(WorldInfo).addClass('pinnedOpen');
828 $(WIDrawerIcon).addClass('drawerPinnedOpen');847 $(WIDrawerIcon).addClass('drawerPinnedOpen');
@@ -837,22 +856,22 @@ export function initRossMods() {
837 //save state of Right nav being open or closed856 //save state of Right nav being open or closed
838 $('#rightNavDrawerIcon').on('click', function () {857 $('#rightNavDrawerIcon').on('click', function () {
839 if (!$('#rightNavDrawerIcon').hasClass('openIcon')) {858 if (!$('#rightNavDrawerIcon').hasClass('openIcon')) {
840 SaveLocal('NavOpened', 'true');859 accountStorage.setItem('NavOpened', 'true');
841 } else { SaveLocal('NavOpened', 'false'); }860 } else { accountStorage.setItem('NavOpened', 'false'); }
842 });861 });
843862
844 //save state of Left nav being open or closed863 //save state of Left nav being open or closed
845 $('#leftNavDrawerIcon').on('click', function () {864 $('#leftNavDrawerIcon').on('click', function () {
846 if (!$('#leftNavDrawerIcon').hasClass('openIcon')) {865 if (!$('#leftNavDrawerIcon').hasClass('openIcon')) {
847 SaveLocal('LNavOpened', 'true');866 accountStorage.setItem('LNavOpened', 'true');
848 } else { SaveLocal('LNavOpened', 'false'); }867 } else { accountStorage.setItem('LNavOpened', 'false'); }
849 });868 });
850869
851 //save state of Left nav being open or closed870 //save state of Left nav being open or closed
852 $('#WorldInfo').on('click', function () {871 $('#WorldInfo').on('click', function () {
853 if (!$('#WorldInfo').hasClass('openIcon')) {872 if (!$('#WorldInfo').hasClass('openIcon')) {
854 SaveLocal('WINavOpened', 'true');873 accountStorage.setItem('WINavOpened', 'true');
855 } else { SaveLocal('WINavOpened', 'false'); }874 } else { accountStorage.setItem('WINavOpened', 'false'); }
856 });875 });
857876
858 var chatbarInFocus = false;877 var chatbarInFocus = false;
@@ -868,8 +887,8 @@ export function initRossMods() {
868 OpenNavPanels();887 OpenNavPanels();
869 }, 300);888 }, 300);
870889
871 $(SelectedCharacterTab).click(function () { SaveLocal('SelectedNavTab', 'rm_button_selected_ch'); });890 $(SelectedCharacterTab).click(function () { accountStorage.setItem('SelectedNavTab', 'rm_button_selected_ch'); });
872 $('#rm_button_characters').click(function () { SaveLocal('SelectedNavTab', 'rm_button_characters'); });891 $('#rm_button_characters').click(function () { accountStorage.setItem('SelectedNavTab', 'rm_button_characters'); });
873892
874 // when a char is selected from the list, save them as the auto-load character for next page load893 // when a char is selected from the list, save them as the auto-load character for next page load
875894
@@ -1063,14 +1082,21 @@ export function initRossMods() {
1063 // Ctrl+Enter for Regeneration Last Response. If editing, accept the edits instead1082 // Ctrl+Enter for Regeneration Last Response. If editing, accept the edits instead
1064 if (event.ctrlKey && event.key == 'Enter') {1083 if (event.ctrlKey && event.key == 'Enter') {
1065 const editMesDone = $('.mes_edit_done:visible');1084 const editMesDone = $('.mes_edit_done:visible');
1085 const reasoningMesDone = $('.mes_reasoning_edit_done:visible');
1066 if (editMesDone.length > 0) {1086 if (editMesDone.length > 0) {
1067 console.debug('Accepting edits with Ctrl+Enter');1087 console.debug('Accepting edits with Ctrl+Enter');
1068 $('#send_textarea').focus();1088 $('#send_textarea').trigger('focus');
1069 editMesDone.trigger('click');1089 editMesDone.trigger('click');
1070 return;1090 return;
1071 } else if (is_send_press == false) {1091 } else if (reasoningMesDone.length > 0) {
1092 console.debug('Accepting edits with Ctrl+Enter');
1093 $('#send_textarea').trigger('focus');
1094 reasoningMesDone.trigger('click');
1095 return;
1096 }
1097 else if (is_send_press == false) {
1072 const skipConfirmKey = 'RegenerateWithCtrlEnter';1098 const skipConfirmKey = 'RegenerateWithCtrlEnter';
1073 const skipConfirm = LoadLocalBool(skipConfirmKey);1099 const skipConfirm = accountStorage.getItem(skipConfirmKey) === 'true';
1074 function doRegenerate() {1100 function doRegenerate() {
1075 console.debug('Regenerating with Ctrl+Enter');1101 console.debug('Regenerating with Ctrl+Enter');
1076 $('#option_regenerate').trigger('click');1102 $('#option_regenerate').trigger('click');
@@ -1082,13 +1108,15 @@ export function initRossMods() {
1082 let regenerateWithCtrlEnter = false;1108 let regenerateWithCtrlEnter = false;
1083 const result = await Popup.show.confirm('Regenerate Message', 'Are you sure you want to regenerate the latest message?', {1109 const result = await Popup.show.confirm('Regenerate Message', 'Are you sure you want to regenerate the latest message?', {
1084 customInputs: [{ id: 'regenerateWithCtrlEnter', label: 'Don\'t ask again' }],1110 customInputs: [{ id: 'regenerateWithCtrlEnter', label: 'Don\'t ask again' }],
1085 onClose: (popup) => regenerateWithCtrlEnter = popup.inputResults.get('regenerateWithCtrlEnter') ?? false,1111 onClose: (popup) => {
1112 regenerateWithCtrlEnter = popup.inputResults.get('regenerateWithCtrlEnter') ?? false;
1113 },
1086 });1114 });
1087 if (!result) {1115 if (!result) {
1088 return;1116 return;
1089 }1117 }
10901118
1091 SaveLocal(skipConfirmKey, regenerateWithCtrlEnter);1119 accountStorage.setItem(skipConfirmKey, String(regenerateWithCtrlEnter));
1092 doRegenerate();1120 doRegenerate();
1093 }1121 }
1094 return;1122 return;
public/scripts/chat-templates.js+2 -1
@@ -69,6 +69,7 @@ const hash_derivations = {
69 // DeepSeek R169 // DeepSeek R1
70 'b6835114b7303ddd78919a82e4d9f7d8c26ed0d7dfc36beeb12d524f6144eab1':70 'b6835114b7303ddd78919a82e4d9f7d8c26ed0d7dfc36beeb12d524f6144eab1':
71 'DeepSeek-V2.5'71 'DeepSeek-V2.5'
72 ,
72};73};
7374
74const substr_derivations = {75const substr_derivations = {
@@ -97,6 +98,6 @@ export async function deriveTemplatesFromChatTemplate(chat_template, hash) {
97 }98 }
98 }99 }
99100
100 console.log(`Unknown chat template hash: ${hash} for [${chat_template}]`);101 console.warn(`Unknown chat template hash: ${hash} for [${chat_template}]`);
101 return null;102 return null;
102}103}
public/scripts/chats.js+42 -6
@@ -45,6 +45,7 @@ import { DragAndDropHandler } from './dragdrop.js';
45import { renderTemplateAsync } from './templates.js';45import { renderTemplateAsync } from './templates.js';
46import { t } from './i18n.js';46import { t } from './i18n.js';
47import { humanizedDateTime } from './RossAscends-mods.js';47import { humanizedDateTime } from './RossAscends-mods.js';
48import { accountStorage } from './util/AccountStorage.js';
4849
49/**50/**
50 * @typedef {Object} FileAttachment51 * @typedef {Object} FileAttachment
@@ -621,21 +622,56 @@ async function enlargeMessageImage() {
621}622}
622623
623async function deleteMessageImage() {624async function deleteMessageImage() {
624 const value = await callGenericPopup('<h3>Delete image from message?<br>This action can\'t be undone.</h3>', POPUP_TYPE.CONFIRM);625 const value = await callGenericPopup('<h3>Delete image from message?<br>This action can\'t be undone.</h3>', POPUP_TYPE.TEXT, '', {
626 okButton: t`Delete one`,
627 customButtons: [
628 {
629 text: t`Delete all`,
630 appendAtEnd: true,
631 result: POPUP_RESULT.CUSTOM1,
632 },
633 {
634 text: t`Cancel`,
635 appendAtEnd: true,
636 result: POPUP_RESULT.CANCELLED,
637 },
638 ],
639 });
625640
626 if (value !== POPUP_RESULT.AFFIRMATIVE) {641 if (!value) {
627 return;642 return;
628 }643 }
629644
630 const mesBlock = $(this).closest('.mes');645 const mesBlock = $(this).closest('.mes');
631 const mesId = mesBlock.attr('mesid');646 const mesId = mesBlock.attr('mesid');
632 const message = chat[mesId];647 const message = chat[mesId];
648
649 let isLastImage = true;
650
651 if (Array.isArray(message.extra.image_swipes)) {
652 const indexOf = message.extra.image_swipes.indexOf(message.extra.image);
653 if (indexOf > -1) {
654 message.extra.image_swipes.splice(indexOf, 1);
655 isLastImage = message.extra.image_swipes.length === 0;
656 if (!isLastImage) {
657 const newIndex = Math.min(indexOf, message.extra.image_swipes.length - 1);
658 message.extra.image = message.extra.image_swipes[newIndex];
659 }
660 }
661 }
662
663 if (isLastImage || value === POPUP_RESULT.CUSTOM1) {
633 delete message.extra.image;664 delete message.extra.image;
634 delete message.extra.inline_image;665 delete message.extra.inline_image;
635 delete message.extra.title;666 delete message.extra.title;
636 delete message.extra.append_title;667 delete message.extra.append_title;
668 delete message.extra.image_swipes;
637 mesBlock.find('.mes_img_container').removeClass('img_extra');669 mesBlock.find('.mes_img_container').removeClass('img_extra');
638 mesBlock.find('.mes_img').attr('src', '');670 mesBlock.find('.mes_img').attr('src', '');
671 } else {
672 appendMediaToMessage(message, mesBlock);
673 }
674
639 await saveChatConditional();675 await saveChatConditional();
640}676}
641677
@@ -1043,8 +1079,8 @@ async function openAttachmentManager() {
1043 renderAttachments();1079 renderAttachments();
1044 });1080 });
10451081
1046 let sortField = localStorage.getItem('DataBank_sortField') || 'created';1082 let sortField = accountStorage.getItem('DataBank_sortField') || 'created';
1047 let sortOrder = localStorage.getItem('DataBank_sortOrder') || 'desc';1083 let sortOrder = accountStorage.getItem('DataBank_sortOrder') || 'desc';
1048 let filterString = '';1084 let filterString = '';
10491085
1050 const template = $(await renderExtensionTemplateAsync('attachments', 'manager', {}));1086 const template = $(await renderExtensionTemplateAsync('attachments', 'manager', {}));
@@ -1060,8 +1096,8 @@ async function openAttachmentManager() {
10601096
1061 sortField = this.selectedOptions[0].dataset.sortField;1097 sortField = this.selectedOptions[0].dataset.sortField;
1062 sortOrder = this.selectedOptions[0].dataset.sortOrder;1098 sortOrder = this.selectedOptions[0].dataset.sortOrder;
1063 localStorage.setItem('DataBank_sortField', sortField);1099 accountStorage.setItem('DataBank_sortField', sortField);
1064 localStorage.setItem('DataBank_sortOrder', sortOrder);1100 accountStorage.setItem('DataBank_sortOrder', sortOrder);
1065 renderAttachments();1101 renderAttachments();
1066 });1102 });
1067 function handleBulkAction(action) {1103 function handleBulkAction(action) {
public/scripts/extensions.js+9 -8
@@ -9,6 +9,7 @@ import { getContext } from './st-context.js';
9import { isAdmin } from './user.js';9import { isAdmin } from './user.js';
10import { t } from './i18n.js';10import { t } from './i18n.js';
11import { debounce_timeout } from './constants.js';11import { debounce_timeout } from './constants.js';
12import { accountStorage } from './util/AccountStorage.js';
1213
13export {14export {
14 getContext,15 getContext,
@@ -612,12 +613,12 @@ function generateExtensionHtml(name, manifest, isActive, isDisabled, isExternal,
612 }613 }
613614
614 let toggleElement = isActive || isDisabled ?615 let toggleElement = isActive || isDisabled ?
615 `<input type="checkbox" title="Click to toggle" data-name="${name}" class="${isActive ? 'toggle_disable' : 'toggle_enable'} ${checkboxClass}" ${isActive ? 'checked' : ''}>` :616 '<input type="checkbox" title="' + t`Click to toggle` + `" data-name="${name}" class="${isActive ? 'toggle_disable' : 'toggle_enable'} ${checkboxClass}" ${isActive ? 'checked' : ''}>` :
616 `<input type="checkbox" title="Cannot enable extension" data-name="${name}" class="extension_missing ${checkboxClass}" disabled>`;617 `<input type="checkbox" title="Cannot enable extension" data-name="${name}" class="extension_missing ${checkboxClass}" disabled>`;
617618
618 let deleteButton = isExternal ? `<button class="btn_delete menu_button" data-name="${externalId}" title="Delete"><i class="fa-fw fa-solid fa-trash-can"></i></button>` : '';619 let deleteButton = isExternal ? `<button class="btn_delete menu_button" data-name="${externalId}" data-i18n="[title]Delete" title="Delete"><i class="fa-fw fa-solid fa-trash-can"></i></button>` : '';
619 let updateButton = isExternal ? `<button class="btn_update menu_button displayNone" data-name="${externalId}" title="Update available"><i class="fa-solid fa-download fa-fw"></i></button>` : '';620 let updateButton = isExternal ? `<button class="btn_update menu_button displayNone" data-name="${externalId}" title="Update available"><i class="fa-solid fa-download fa-fw"></i></button>` : '';
620 let moveButton = isExternal && isUserAdmin ? `<button class="btn_move menu_button" data-name="${externalId}" title="Move"><i class="fa-solid fa-folder-tree fa-fw"></i></button>` : '';621 let moveButton = isExternal && isUserAdmin ? `<button class="btn_move menu_button" data-name="${externalId}" data-i18n="[title]Move" title="Move"><i class="fa-solid fa-folder-tree fa-fw"></i></button>` : '';
621 let modulesInfo = '';622 let modulesInfo = '';
622623
623 if (isActive && Array.isArray(manifest.optional)) {624 if (isActive && Array.isArray(manifest.optional)) {
@@ -625,7 +626,7 @@ function generateExtensionHtml(name, manifest, isActive, isDisabled, isExternal,
625 modules.forEach(x => optional.delete(x));626 modules.forEach(x => optional.delete(x));
626 if (optional.size > 0) {627 if (optional.size > 0) {
627 const optionalString = DOMPurify.sanitize([...optional].join(', '));628 const optionalString = DOMPurify.sanitize([...optional].join(', '));
628 modulesInfo = `<div class="extension_modules">Optional modules: <span class="optional">${optionalString}</span></div>`;629 modulesInfo = '<div class="extension_modules">' + t`Optional modules:` + ` <span class="optional">${optionalString}</span></div>`;
629 }630 }
630 } else if (!isDisabled) { // Neither active nor disabled631 } else if (!isDisabled) { // Neither active nor disabled
631 const requirements = new Set(manifest.requires);632 const requirements = new Set(manifest.requires);
@@ -724,7 +725,7 @@ async function showExtensionsDetails() {
724 htmlExternal.append(htmlLoading);725 htmlExternal.append(htmlLoading);
725726
726 const sortOrderKey = 'extensions_sortByName';727 const sortOrderKey = 'extensions_sortByName';
727 const sortByName = localStorage.getItem(sortOrderKey) === 'true';728 const sortByName = accountStorage.getItem(sortOrderKey) === 'true';
728 const sortFn = sortByName ? sortManifestsByName : sortManifestsByOrder;729 const sortFn = sortByName ? sortManifestsByName : sortManifestsByOrder;
729 const extensions = Object.entries(manifests).sort((a, b) => sortFn(a[1], b[1])).map(getExtensionData);730 const extensions = Object.entries(manifests).sort((a, b) => sortFn(a[1], b[1])).map(getExtensionData);
730731
@@ -755,7 +756,7 @@ async function showExtensionsDetails() {
755 text: sortByName ? t`Sort: Display Name` : t`Sort: Loading Order`,756 text: sortByName ? t`Sort: Display Name` : t`Sort: Loading Order`,
756 action: async () => {757 action: async () => {
757 abortController.abort();758 abortController.abort();
758 localStorage.setItem(sortOrderKey, sortByName ? 'false' : 'true');759 accountStorage.setItem(sortOrderKey, sortByName ? 'false' : 'true');
759 await showExtensionsDetails();760 await showExtensionsDetails();
760 },761 },
761 };762 };
@@ -1163,11 +1164,11 @@ async function checkForExtensionUpdates(force) {
1163 const currentDate = new Date().toDateString();1164 const currentDate = new Date().toDateString();
11641165
1165 // Don't nag more than once a day1166 // Don't nag more than once a day
1166 if (localStorage.getItem(STORAGE_NAG_KEY) === currentDate) {1167 if (accountStorage.getItem(STORAGE_NAG_KEY) === currentDate) {
1167 return;1168 return;
1168 }1169 }
11691170
1170 localStorage.setItem(STORAGE_NAG_KEY, currentDate);1171 accountStorage.setItem(STORAGE_NAG_KEY, currentDate);
1171 }1172 }
11721173
1173 const isCurrentUserAdmin = isAdmin();1174 const isCurrentUserAdmin = isAdmin();
public/scripts/extensions/assets/index.js+16 -17
@@ -8,7 +8,9 @@ import { getRequestHeaders, processDroppedFiles, eventSource, event_types } from
8import { deleteExtension, extensionNames, getContext, installExtension, renderExtensionTemplateAsync } from '../../extensions.js';8import { deleteExtension, extensionNames, getContext, installExtension, renderExtensionTemplateAsync } from '../../extensions.js';
9import { POPUP_TYPE, Popup, callGenericPopup } from '../../popup.js';9import { POPUP_TYPE, Popup, callGenericPopup } from '../../popup.js';
10import { executeSlashCommands } from '../../slash-commands.js';10import { executeSlashCommands } from '../../slash-commands.js';
11import { accountStorage } from '../../util/AccountStorage.js';
11import { flashHighlight, getStringHash, isValidUrl } from '../../utils.js';12import { flashHighlight, getStringHash, isValidUrl } from '../../utils.js';
13import { t } from '../../i18n.js';
12export { MODULE_NAME };14export { MODULE_NAME };
1315
14const MODULE_NAME = 'assets';16const MODULE_NAME = 'assets';
@@ -58,11 +60,11 @@ const KNOWN_TYPES = {
58 'blip': 'Blip sounds',60 'blip': 'Blip sounds',
59};61};
6062
61function downloadAssetsList(url) {63async function downloadAssetsList(url) {
62 updateCurrentAssets().then(function () {64 updateCurrentAssets().then(async function () {
63 fetch(url, { cache: 'no-cache' })65 fetch(url, { cache: 'no-cache' })
64 .then(response => response.json())66 .then(response => response.json())
65 .then(json => {67 .then(async function(json) {
6668
67 availableAssets = {};69 availableAssets = {};
68 $('#assets_menu').empty();70 $('#assets_menu').empty();
@@ -83,10 +85,10 @@ function downloadAssetsList(url) {
8385
84 $('#assets_type_select').empty();86 $('#assets_type_select').empty();
85 $('#assets_search').val('');87 $('#assets_search').val('');
86 $('#assets_type_select').append($('<option />', { value: '', text: 'All' }));88 $('#assets_type_select').append($('<option />', { value: '', text: t`All` }));
8789
88 for (const type of assetTypes) {90 for (const type of assetTypes) {
89 const option = $('<option />', { value: type, text: KNOWN_TYPES[type] || type });91 const option = $('<option />', { value: type, text: t([KNOWN_TYPES[type] || type]) });
90 $('#assets_type_select').append(option);92 $('#assets_type_select').append(option);
91 }93 }
9294
@@ -103,11 +105,7 @@ function downloadAssetsList(url) {
103 assetTypeMenu.append(`<h3>${KNOWN_TYPES[assetType] || assetType}</h3>`).hide();105 assetTypeMenu.append(`<h3>${KNOWN_TYPES[assetType] || assetType}</h3>`).hide();
104106
105 if (assetType == 'extension') {107 if (assetType == 'extension') {
106 assetTypeMenu.append(`108 assetTypeMenu.append(await renderExtensionTemplateAsync('assets', 'installation'));
107 <div class="assets-list-git">
108 To download extensions from this page, you need to have <a href="https://git-scm.com/downloads" target="_blank">Git</a> installed.<br>
109 Click the <i class="fa-solid fa-sm fa-arrow-up-right-from-square"></i> icon to visit the Extension's repo for tips on how to use it.
110 </div>`);
111 }109 }
112110
113 for (const i in availableAssets[assetType].sort((a, b) => a?.name && b?.name && a['name'].localeCompare(b['name']))) {111 for (const i in availableAssets[assetType].sort((a, b) => a?.name && b?.name && a['name'].localeCompare(b['name']))) {
@@ -183,7 +181,7 @@ function downloadAssetsList(url) {
183 const displayName = DOMPurify.sanitize(asset['name'] || asset['id']);181 const displayName = DOMPurify.sanitize(asset['name'] || asset['id']);
184 const description = DOMPurify.sanitize(asset['description'] || '');182 const description = DOMPurify.sanitize(asset['description'] || '');
185 const url = isValidUrl(asset['url']) ? asset['url'] : '';183 const url = isValidUrl(asset['url']) ? asset['url'] : '';
186 const title = assetType === 'extension' ? `Extension repo/guide: ${url}` : 'Preview in browser';184 const title = assetType === 'extension' ? t`Extension repo/guide:` + ` ${url}` : t`Preview in browser`;
187 const previewIcon = (assetType === 'extension' || assetType === 'character') ? 'fa-arrow-up-right-from-square' : 'fa-headphones-simple';185 const previewIcon = (assetType === 'extension' || assetType === 'character') ? 'fa-arrow-up-right-from-square' : 'fa-headphones-simple';
188 const toolTag = assetType === 'extension' && asset['tool'];186 const toolTag = assetType === 'extension' && asset['tool'];
189187
@@ -194,9 +192,10 @@ function downloadAssetsList(url) {
194 <b>${displayName}</b>192 <b>${displayName}</b>
195 <a class="asset_preview" href="${url}" target="_blank" title="${title}">193 <a class="asset_preview" href="${url}" target="_blank" title="${title}">
196 <i class="fa-solid fa-sm ${previewIcon}"></i>194 <i class="fa-solid fa-sm ${previewIcon}"></i>
197 </a>195 </a>` +
198 ${toolTag ? '<span class="tag" title="Adds a function tool"><i class="fa-solid fa-sm fa-wrench"></i> Tool</span>' : ''}196 (toolTag ? '<span class="tag" title="' + t`Adds a function tool` + '"><i class="fa-solid fa-sm fa-wrench"></i> ' +
199 </span>197 t`Tool` + '</span>' : '') +
198 `</span>
200 <small class="asset-description">199 <small class="asset-description">
201 ${description}200 ${description}
202 </small>201 </small>
@@ -432,14 +431,14 @@ jQuery(async () => {
432 connectButton.on('click', async function () {431 connectButton.on('click', async function () {
433 const url = DOMPurify.sanitize(String(assetsJsonUrl.val()));432 const url = DOMPurify.sanitize(String(assetsJsonUrl.val()));
434 const rememberKey = `Assets_SkipConfirm_${getStringHash(url)}`;433 const rememberKey = `Assets_SkipConfirm_${getStringHash(url)}`;
435 const skipConfirm = localStorage.getItem(rememberKey) === 'true';434 const skipConfirm = accountStorage.getItem(rememberKey) === 'true';
436435
437 const confirmation = skipConfirm || await Popup.show.confirm('Loading Asset List', `<span>Are you sure you want to connect to the following url?</span><var>${url}</var>`, {436 const confirmation = skipConfirm || await Popup.show.confirm(t`Loading Asset List`, '<span>' + t`Are you sure you want to connect to the following url?` + `</span><var>${url}</var>`, {
438 customInputs: [{ id: 'assets-remember', label: 'Don\'t ask again for this URL' }],437 customInputs: [{ id: 'assets-remember', label: 'Don\'t ask again for this URL' }],
439 onClose: popup => {438 onClose: popup => {
440 if (popup.result) {439 if (popup.result) {
441 const rememberValue = popup.inputResults.get('assets-remember');440 const rememberValue = popup.inputResults.get('assets-remember');
442 localStorage.setItem(rememberKey, String(rememberValue));441 accountStorage.setItem(rememberKey, String(rememberValue));
443 }442 }
444 },443 },
445 });444 });
public/scripts/extensions/assets/installation.html+4 -0
@@ -0,0 +1,4 @@
1<div class="assets-list-git">
2 <span data-i18n="extension_install_1">To download extensions from this page, you need to have </span><a href="https://git-scm.com/downloads" target="_blank">Git</a><span data-i18n="extension_install_2"> installed.</span><br>
3 <span data-i18n="extension_install_3">Click the </span><i class="fa-solid fa-sm fa-arrow-up-right-from-square"></i><span data-i18n="extension_install_4"> icon to visit the Extension's repo for tips on how to use it.</span>
4</div>
\ No newline at end of file4 \ No newline at end of file
public/scripts/extensions/assets/window.html+1 -1
@@ -33,7 +33,7 @@ To install a single 3rd party extension, use the &quot;Install Extensions&quot;
33 <div id="assets_filters" class="flex-container">33 <div id="assets_filters" class="flex-container">
34 <select id="assets_type_select" class="text_pole flex1">34 <select id="assets_type_select" class="text_pole flex1">
35 </select>35 </select>
36 <input id="assets_search" class="text_pole flex1" placeholder="Search" type="search">36 <input id="assets_search" class="text_pole flex1" data-i18n="[placeholder]Search" placeholder="Search" type="search">
37 <div id="assets-characters-button" class="menu_button menu_button_icon">37 <div id="assets-characters-button" class="menu_button menu_button_icon">
38 <i class="fa-solid fa-image-portrait"></i>38 <i class="fa-solid fa-image-portrait"></i>
39 <span data-i18n="Characters">Characters</span>39 <span data-i18n="Characters">Characters</span>
public/scripts/extensions/caption/settings.html+7 -1
@@ -10,7 +10,7 @@
10 <select id="caption_source" class="text_pole">10 <select id="caption_source" class="text_pole">
11 <option value="local" data-i18n="Local">Local</option>11 <option value="local" data-i18n="Local">Local</option>
12 <option value="multimodal" data-i18n="Multimodal (OpenAI / Anthropic / llama / Google)">Multimodal (OpenAI / Anthropic / llama / Google)</option>12 <option value="multimodal" data-i18n="Multimodal (OpenAI / Anthropic / llama / Google)">Multimodal (OpenAI / Anthropic / llama / Google)</option>
13 <option value="extras" data-i18n="Extras">Extras</option>13 <option value="extras" data-i18n="Extras">Extras (deprecated)</option>
14 <option value="horde" data-i18n="Horde">Horde</option>14 <option value="horde" data-i18n="Horde">Horde</option>
15 </select>15 </select>
16 <div id="caption_multimodal_block" class="flex-container wide100p">16 <div id="caption_multimodal_block" class="flex-container wide100p">
@@ -53,6 +53,12 @@
53 <option data-type="anthropic" value="claude-3-opus-20240229">claude-3-opus-20240229</option>53 <option data-type="anthropic" value="claude-3-opus-20240229">claude-3-opus-20240229</option>
54 <option data-type="anthropic" value="claude-3-sonnet-20240229">claude-3-sonnet-20240229</option>54 <option data-type="anthropic" value="claude-3-sonnet-20240229">claude-3-sonnet-20240229</option>
55 <option data-type="anthropic" value="claude-3-haiku-20240307">claude-3-haiku-20240307</option>55 <option data-type="anthropic" value="claude-3-haiku-20240307">claude-3-haiku-20240307</option>
56 <option data-type="google" value="gemini-2.0-pro-exp">gemini-2.0-pro-exp</option>
57 <option data-type="google" value="gemini-2.0-pro-exp-02-05">gemini-2.0-pro-exp-02-05</option>
58 <option data-type="google" value="gemini-2.0-flash-lite-preview">gemini-2.0-flash-lite-preview</option>
59 <option data-type="google" value="gemini-2.0-flash-lite-preview-02-05">gemini-2.0-flash-lite-preview-02-05</option>
60 <option data-type="google" value="gemini-2.0-flash">gemini-2.0-flash</option>
61 <option data-type="google" value="gemini-2.0-flash-001">gemini-2.0-flash-001</option>
56 <option data-type="google" value="gemini-2.0-flash-exp">gemini-2.0-flash-exp</option>62 <option data-type="google" value="gemini-2.0-flash-exp">gemini-2.0-flash-exp</option>
57 <option data-type="google" value="gemini-2.0-flash-thinking-exp">gemini-2.0-flash-thinking-exp</option>63 <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>64 <option data-type="google" value="gemini-2.0-flash-thinking-exp-01-21">gemini-2.0-flash-thinking-exp-01-21</option>
public/scripts/extensions/expressions/index.js+0 -1
@@ -2190,7 +2190,6 @@ function migrateSettings() {
2190 description: 'Character name - or unique character identifier (avatar key). If not provided, the current character for this chat will be used (does not work in group chats)',2190 description: 'Character name - or unique character identifier (avatar key). If not provided, the current character for this chat will be used (does not work in group chats)',
2191 typeList: [ARGUMENT_TYPE.STRING],2191 typeList: [ARGUMENT_TYPE.STRING],
2192 enumProvider: commonEnumProviders.characters('character'),2192 enumProvider: commonEnumProviders.characters('character'),
2193 forceEnum: true,
2194 }),2193 }),
2195 ],2194 ],
2196 helpString: 'Returns the last set expression for the named character.',2195 helpString: 'Returns the last set expression for the named character.',
public/scripts/extensions/expressions/settings.html+1 -1
@@ -23,7 +23,7 @@
23 <small data-i18n="Select the API for classifying expressions.">Select the API for classifying expressions.</small>23 <small data-i18n="Select the API for classifying expressions.">Select the API for classifying expressions.</small>
24 <select id="expression_api" class="flex1 margin0">24 <select id="expression_api" class="flex1 margin0">
25 <option value="0" data-i18n="Local">Local</option>25 <option value="0" data-i18n="Local">Local</option>
26 <option value="1" data-i18n="Extras">Extras</option>26 <option value="1" data-i18n="Extras">Extras (deprecated)</option>
27 <option value="2" data-i18n="Main API">Main API</option>27 <option value="2" data-i18n="Main API">Main API</option>
28 <option value="3" data-i18n="WebLLM Extension">WebLLM Extension</option>28 <option value="3" data-i18n="WebLLM Extension">WebLLM Extension</option>
29 </select>29 </select>
public/scripts/extensions/gallery/index.js+0 -1
@@ -441,7 +441,6 @@ SlashCommandParser.addCommandObject(SlashCommand.fromProps({
441 description: 'character name',441 description: 'character name',
442 typeList: [ARGUMENT_TYPE.STRING],442 typeList: [ARGUMENT_TYPE.STRING],
443 enumProvider: commonEnumProviders.characters('character'),443 enumProvider: commonEnumProviders.characters('character'),
444 forceEnum: true,
445 }),444 }),
446 SlashCommandNamedArgument.fromProps({445 SlashCommandNamedArgument.fromProps({
447 name: 'group',446 name: 'group',
public/scripts/extensions/memory/settings.html+1 -1
@@ -12,7 +12,7 @@
12 <label for="summary_source" data-i18n="ext_sum_with">Summarize with:</label>12 <label for="summary_source" data-i18n="ext_sum_with">Summarize with:</label>
13 <select id="summary_source">13 <select id="summary_source">
14 <option value="main" data-i18n="ext_sum_main_api">Main API</option>14 <option value="main" data-i18n="ext_sum_main_api">Main API</option>
15 <option value="extras">Extras API</option>15 <option value="extras">Extras API (deprecated)</option>
16 <option value="webllm" data-i18n="ext_sum_webllm">WebLLM Extension</option>16 <option value="webllm" data-i18n="ext_sum_webllm">WebLLM Extension</option>
17 </select><br>17 </select><br>
1818
public/scripts/extensions/quick-reply/src/QuickReply.js+9 -8
@@ -10,6 +10,7 @@ import { SlashCommandExecutor } from '../../../slash-commands/SlashCommandExecut
10import { SlashCommandParser } from '../../../slash-commands/SlashCommandParser.js';10import { SlashCommandParser } from '../../../slash-commands/SlashCommandParser.js';
11import { SlashCommandParserError } from '../../../slash-commands/SlashCommandParserError.js';11import { SlashCommandParserError } from '../../../slash-commands/SlashCommandParserError.js';
12import { SlashCommandScope } from '../../../slash-commands/SlashCommandScope.js';12import { SlashCommandScope } from '../../../slash-commands/SlashCommandScope.js';
13import { accountStorage } from '../../../util/AccountStorage.js';
13import { debounce, delay, getSortableDelay, showFontAwesomePicker } from '../../../utils.js';14import { debounce, delay, getSortableDelay, showFontAwesomePicker } from '../../../utils.js';
14import { log, quickReplyApi, warn } from '../index.js';15import { log, quickReplyApi, warn } from '../index.js';
15import { QuickReplyContextLink } from './QuickReplyContextLink.js';16import { QuickReplyContextLink } from './QuickReplyContextLink.js';
@@ -544,9 +545,9 @@ export class QuickReply {
544 this.editorSyntax = messageSyntaxInner;545 this.editorSyntax = messageSyntaxInner;
545 /**@type {HTMLInputElement}*/546 /**@type {HTMLInputElement}*/
546 const wrap = dom.querySelector('#qr--modal-wrap');547 const wrap = dom.querySelector('#qr--modal-wrap');
547 wrap.checked = JSON.parse(localStorage.getItem('qr--wrap') ?? 'false');548 wrap.checked = JSON.parse(accountStorage.getItem('qr--wrap') ?? 'false');
548 wrap.addEventListener('click', () => {549 wrap.addEventListener('click', () => {
549 localStorage.setItem('qr--wrap', JSON.stringify(wrap.checked));550 accountStorage.setItem('qr--wrap', JSON.stringify(wrap.checked));
550 updateWrap();551 updateWrap();
551 });552 });
552 const updateWrap = () => {553 const updateWrap = () => {
@@ -594,27 +595,27 @@ export class QuickReply {
594 };595 };
595 /**@type {HTMLInputElement}*/596 /**@type {HTMLInputElement}*/
596 const tabSize = dom.querySelector('#qr--modal-tabSize');597 const tabSize = dom.querySelector('#qr--modal-tabSize');
597 tabSize.value = JSON.parse(localStorage.getItem('qr--tabSize') ?? '4');598 tabSize.value = JSON.parse(accountStorage.getItem('qr--tabSize') ?? '4');
598 const updateTabSize = () => {599 const updateTabSize = () => {
599 message.style.tabSize = tabSize.value;600 message.style.tabSize = tabSize.value;
600 messageSyntaxInner.style.tabSize = tabSize.value;601 messageSyntaxInner.style.tabSize = tabSize.value;
601 updateScrollDebounced();602 updateScrollDebounced();
602 };603 };
603 tabSize.addEventListener('change', () => {604 tabSize.addEventListener('change', () => {
604 localStorage.setItem('qr--tabSize', JSON.stringify(Number(tabSize.value)));605 accountStorage.setItem('qr--tabSize', JSON.stringify(Number(tabSize.value)));
605 updateTabSize();606 updateTabSize();
606 });607 });
607 /**@type {HTMLInputElement}*/608 /**@type {HTMLInputElement}*/
608 const executeShortcut = dom.querySelector('#qr--modal-executeShortcut');609 const executeShortcut = dom.querySelector('#qr--modal-executeShortcut');
609 executeShortcut.checked = JSON.parse(localStorage.getItem('qr--executeShortcut') ?? 'true');610 executeShortcut.checked = JSON.parse(accountStorage.getItem('qr--executeShortcut') ?? 'true');
610 executeShortcut.addEventListener('click', () => {611 executeShortcut.addEventListener('click', () => {
611 localStorage.setItem('qr--executeShortcut', JSON.stringify(executeShortcut.checked));612 accountStorage.setItem('qr--executeShortcut', JSON.stringify(executeShortcut.checked));
612 });613 });
613 /**@type {HTMLInputElement}*/614 /**@type {HTMLInputElement}*/
614 const syntax = dom.querySelector('#qr--modal-syntax');615 const syntax = dom.querySelector('#qr--modal-syntax');
615 syntax.checked = JSON.parse(localStorage.getItem('qr--syntax') ?? 'true');616 syntax.checked = JSON.parse(accountStorage.getItem('qr--syntax') ?? 'true');
616 syntax.addEventListener('click', () => {617 syntax.addEventListener('click', () => {
617 localStorage.setItem('qr--syntax', JSON.stringify(syntax.checked));618 accountStorage.setItem('qr--syntax', JSON.stringify(syntax.checked));
618 updateSyntaxEnabled();619 updateSyntaxEnabled();
619 });620 });
620 if (navigator.keyboard) {621 if (navigator.keyboard) {
public/scripts/extensions/quick-reply/src/QuickReplySet.js+6 -28
@@ -1,15 +1,14 @@
1import { getRequestHeaders, substituteParams } from '../../../../script.js';1import { getRequestHeaders, substituteParams } from '../../../../script.js';
2import { Popup, POPUP_RESULT, POPUP_TYPE } from '../../../popup.js';2import { Popup, POPUP_RESULT, POPUP_TYPE } from '../../../popup.js';
3import { executeSlashCommands, executeSlashCommandsOnChatInput, executeSlashCommandsWithOptions } from '../../../slash-commands.js';3import { executeSlashCommandsOnChatInput, executeSlashCommandsWithOptions } from '../../../slash-commands.js';
4import { SlashCommandParser } from '../../../slash-commands/SlashCommandParser.js';
5import { SlashCommandScope } from '../../../slash-commands/SlashCommandScope.js';4import { SlashCommandScope } from '../../../slash-commands/SlashCommandScope.js';
6import { debounceAsync, log, warn } from '../index.js';5import { SlashCommandParser } from '../../../slash-commands/SlashCommandParser.js';
6import { debounceAsync, warn } from '../index.js';
7import { QuickReply } from './QuickReply.js';7import { QuickReply } from './QuickReply.js';
88
9export class QuickReplySet {9export class QuickReplySet {
10 /**@type {QuickReplySet[]}*/ static list = [];10 /**@type {QuickReplySet[]}*/ static list = [];
1111
12
13 static from(props) {12 static from(props) {
14 props.qrList = []; //props.qrList?.map(it=>QuickReply.from(it));13 props.qrList = []; //props.qrList?.map(it=>QuickReply.from(it));
15 const instance = Object.assign(new this(), props);14 const instance = Object.assign(new this(), props);
@@ -24,9 +23,6 @@ export class QuickReplySet {
24 return this.list.find(it=>it.name == name);23 return this.list.find(it=>it.name == name);
25 }24 }
2625
27
28
29
30 /**@type {string}*/ name;26 /**@type {string}*/ name;
31 /**@type {boolean}*/ disableSend = false;27 /**@type {boolean}*/ disableSend = false;
32 /**@type {boolean}*/ placeBeforeInput = false;28 /**@type {boolean}*/ placeBeforeInput = false;
@@ -34,19 +30,12 @@ export class QuickReplySet {
34 /**@type {string}*/ color = 'transparent';30 /**@type {string}*/ color = 'transparent';
35 /**@type {boolean}*/ onlyBorderColor = false;31 /**@type {boolean}*/ onlyBorderColor = false;
36 /**@type {QuickReply[]}*/ qrList = [];32 /**@type {QuickReply[]}*/ qrList = [];
37
38 /**@type {number}*/ idIndex = 0;33 /**@type {number}*/ idIndex = 0;
39
40 /**@type {boolean}*/ isDeleted = false;34 /**@type {boolean}*/ isDeleted = false;
41
42 /**@type {function}*/ save;35 /**@type {function}*/ save;
43
44 /**@type {HTMLElement}*/ dom;36 /**@type {HTMLElement}*/ dom;
45 /**@type {HTMLElement}*/ settingsDom;37 /**@type {HTMLElement}*/ settingsDom;
4638
47
48
49
50 constructor() {39 constructor() {
51 this.save = debounceAsync(()=>this.performSave(), 200);40 this.save = debounceAsync(()=>this.performSave(), 200);
52 }41 }
@@ -55,9 +44,6 @@ export class QuickReplySet {
55 this.qrList.forEach(qr=>this.hookQuickReply(qr));44 this.qrList.forEach(qr=>this.hookQuickReply(qr));
56 }45 }
5746
58
59
60
61 unrender() {47 unrender() {
62 this.dom?.remove();48 this.dom?.remove();
63 this.dom = null;49 this.dom = null;
@@ -100,9 +86,6 @@ export class QuickReplySet {
100 }86 }
101 }87 }
10288
103
104
105
106 renderSettings() {89 renderSettings() {
107 if (!this.settingsDom) {90 if (!this.settingsDom) {
108 this.settingsDom = document.createElement('div'); {91 this.settingsDom = document.createElement('div'); {
@@ -123,9 +106,6 @@ export class QuickReplySet {
123 this.settingsDom.append(qr.renderSettings(idx));106 this.settingsDom.append(qr.renderSettings(idx));
124 }107 }
125108
126
127
128
129 /**109 /**
130 *110 *
131 * @param {QuickReply} qr111 * @param {QuickReply} qr
@@ -138,6 +118,7 @@ export class QuickReplySet {
138 closure.scope.setMacro('arg::*', '');118 closure.scope.setMacro('arg::*', '');
139 return (await closure.execute())?.pipe;119 return (await closure.execute())?.pipe;
140 }120 }
121
141 /**122 /**
142 *123 *
143 * @param {QuickReply} qr The QR to execute.124 * @param {QuickReply} qr The QR to execute.
@@ -207,6 +188,7 @@ export class QuickReplySet {
207 document.querySelector('#send_but').click();188 document.querySelector('#send_but').click();
208 }189 }
209 }190 }
191
210 /**192 /**
211 * @param {QuickReply} qr193 * @param {QuickReply} qr
212 * @param {string} [message] - optional altered message to be used194 * @param {string} [message] - optional altered message to be used
@@ -220,9 +202,6 @@ export class QuickReplySet {
220 });202 });
221 }203 }
222204
223
224
225
226 addQuickReply(data = {}) {205 addQuickReply(data = {}) {
227 const id = Math.max(this.idIndex, this.qrList.reduce((max,qr)=>Math.max(max,qr.id),0)) + 1;206 const id = Math.max(this.idIndex, this.qrList.reduce((max,qr)=>Math.max(max,qr.id),0)) + 1;
228 data.id =207 data.id =
@@ -239,6 +218,7 @@ export class QuickReplySet {
239 this.save();218 this.save();
240 return qr;219 return qr;
241 }220 }
221
242 addQuickReplyFromText(qrJson) {222 addQuickReplyFromText(qrJson) {
243 let data;223 let data;
244 if (qrJson) {224 if (qrJson) {
@@ -371,7 +351,6 @@ export class QuickReplySet {
371 this.save();351 this.save();
372 }352 }
373353
374
375 toJSON() {354 toJSON() {
376 return {355 return {
377 version: 2,356 version: 2,
@@ -386,7 +365,6 @@ export class QuickReplySet {
386 };365 };
387 }366 }
388367
389
390 async performSave() {368 async performSave() {
391 const response = await fetch('/api/quick-replies/save', {369 const response = await fetch('/api/quick-replies/save', {
392 method: 'POST',370 method: 'POST',
public/scripts/extensions/quick-reply/src/ui/SettingsUi.js+1 -1
@@ -346,7 +346,7 @@ export class SettingsUi {
346 }346 }
347347
348 async addQrSet() {348 async addQrSet() {
349 const name = await Popup.show.input('Create a new World Info', 'Enter a name for the new Quick Reply Set:');349 const name = await Popup.show.input('Create a new Quick Reply Set', 'Enter a name for the new Quick Reply Set:');
350 if (name && name.length > 0) {350 if (name && name.length > 0) {
351 const oldQrs = QuickReplySet.get(name);351 const oldQrs = QuickReplySet.get(name);
352 if (oldQrs) {352 if (oldQrs) {
public/scripts/extensions/regex/index.js+3 -2
@@ -10,6 +10,7 @@ import { SlashCommandParser } from '../../slash-commands/SlashCommandParser.js';
10import { download, getFileText, getSortableDelay, uuidv4 } from '../../utils.js';10import { download, getFileText, getSortableDelay, uuidv4 } from '../../utils.js';
11import { regex_placement, runRegexScript, substitute_find_regex } from './engine.js';11import { regex_placement, runRegexScript, substitute_find_regex } from './engine.js';
12import { t } from '../../i18n.js';12import { t } from '../../i18n.js';
13import { accountStorage } from '../../util/AccountStorage.js';
1314
14/**15/**
15 * @typedef {object} RegexScript16 * @typedef {object} RegexScript
@@ -440,8 +441,8 @@ async function checkEmbeddedRegexScripts() {
440 if (avatar && !extension_settings.character_allowed_regex.includes(avatar)) {441 if (avatar && !extension_settings.character_allowed_regex.includes(avatar)) {
441 const checkKey = `AlertRegex_${characters[chid].avatar}`;442 const checkKey = `AlertRegex_${characters[chid].avatar}`;
442443
443 if (!localStorage.getItem(checkKey)) {444 if (!accountStorage.getItem(checkKey)) {
444 localStorage.setItem(checkKey, 'true');445 accountStorage.setItem(checkKey, 'true');
445 const template = await renderExtensionTemplateAsync('regex', 'embeddedScripts', {});446 const template = await renderExtensionTemplateAsync('regex', 'embeddedScripts', {});
446 const result = await callGenericPopup(template, POPUP_TYPE.CONFIRM, '', { okButton: 'Yes' });447 const result = await callGenericPopup(template, POPUP_TYPE.CONFIRM, '', { okButton: 'Yes' });
447448
public/scripts/extensions/stable-diffusion/index.js+67 -0
@@ -81,6 +81,7 @@ const sources = {
81 huggingface: 'huggingface',81 huggingface: 'huggingface',
82 nanogpt: 'nanogpt',82 nanogpt: 'nanogpt',
83 bfl: 'bfl',83 bfl: 'bfl',
84 falai: 'falai',
84};85};
8586
86const initiators = {87const initiators = {
@@ -1169,6 +1170,10 @@ async function onBflKeyClick() {
1169 return onApiKeyClick('BFL API Key:', SECRET_KEYS.BFL);1170 return onApiKeyClick('BFL API Key:', SECRET_KEYS.BFL);
1170}1171}
11711172
1173async function onFalaiKeyClick() {
1174 return onApiKeyClick('FALAI API Key:', SECRET_KEYS.FALAI);
1175}
1176
1172function onBflUpsamplingInput() {1177function onBflUpsamplingInput() {
1173 extension_settings.sd.bfl_upsampling = !!$('#sd_bfl_upsampling').prop('checked');1178 extension_settings.sd.bfl_upsampling = !!$('#sd_bfl_upsampling').prop('checked');
1174 saveSettingsDebounced();1179 saveSettingsDebounced();
@@ -1299,6 +1304,7 @@ async function onModelChange() {
1299 sources.huggingface,1304 sources.huggingface,
1300 sources.nanogpt,1305 sources.nanogpt,
1301 sources.bfl,1306 sources.bfl,
1307 sources.falai,
1302 ];1308 ];
13031309
1304 if (cloudSources.includes(extension_settings.sd.source)) {1310 if (cloudSources.includes(extension_settings.sd.source)) {
@@ -1707,6 +1713,9 @@ async function loadModels() {
1707 case sources.bfl:1713 case sources.bfl:
1708 models = await loadBflModels();1714 models = await loadBflModels();
1709 break;1715 break;
1716 case sources.falai:
1717 models = await loadFalaiModels();
1718 break;
1710 }1719 }
17111720
1712 for (const model of models) {1721 for (const model of models) {
@@ -1744,6 +1753,21 @@ async function loadBflModels() {
1744 ];1753 ];
1745}1754}
17461755
1756async function loadFalaiModels() {
1757 $('#sd_falai_key').toggleClass('success', !!secret_state[SECRET_KEYS.FALAI]);
1758
1759 const result = await fetch('/api/sd/falai/models', {
1760 method: 'POST',
1761 headers: getRequestHeaders(),
1762 });
1763
1764 if (result.ok) {
1765 return await result.json();
1766 }
1767
1768 return [];
1769}
1770
1747async function loadPollinationsModels() {1771async function loadPollinationsModels() {
1748 const result = await fetch('/api/sd/pollinations/models', {1772 const result = await fetch('/api/sd/pollinations/models', {
1749 method: 'POST',1773 method: 'POST',
@@ -2081,6 +2105,9 @@ async function loadSchedulers() {
2081 case sources.bfl:2105 case sources.bfl:
2082 schedulers = ['N/A'];2106 schedulers = ['N/A'];
2083 break;2107 break;
2108 case sources.falai:
2109 schedulers = ['N/A'];
2110 break;
2084 }2111 }
20852112
2086 for (const scheduler of schedulers) {2113 for (const scheduler of schedulers) {
@@ -2735,6 +2762,9 @@ async function sendGenerationRequest(generationType, prompt, additionalNegativeP
2735 case sources.bfl:2762 case sources.bfl:
2736 result = await generateBflImage(prefixedPrompt, signal);2763 result = await generateBflImage(prefixedPrompt, signal);
2737 break;2764 break;
2765 case sources.falai:
2766 result = await generateFalaiImage(prefixedPrompt, negativePrompt, signal);
2767 break;
2738 }2768 }
27392769
2740 if (!result.data) {2770 if (!result.data) {
@@ -3496,6 +3526,40 @@ async function generateBflImage(prompt, signal) {
3496 }3526 }
3497}3527}
34983528
3529/**
3530 * Generates an image using the FAL.AI API.
3531 * @param {string} prompt - The main instruction used to guide the image generation.
3532 * @param {string} negativePrompt - The negative prompt used to guide the image generation.
3533 * @param {AbortSignal} signal - An AbortSignal object that can be used to cancel the request.
3534 * @returns {Promise<{format: string, data: string}>} - A promise that resolves when the image generation and processing are complete.
3535 */
3536async function generateFalaiImage(prompt, negativePrompt, signal) {
3537 const result = await fetch('/api/sd/falai/generate', {
3538 method: 'POST',
3539 headers: getRequestHeaders(),
3540 signal: signal,
3541 body: JSON.stringify({
3542 prompt: prompt,
3543 negative_prompt: negativePrompt,
3544 model: extension_settings.sd.model,
3545 steps: clamp(extension_settings.sd.steps, 1, 50),
3546 guidance: clamp(extension_settings.sd.scale, 1.5, 5),
3547 width: clamp(extension_settings.sd.width, 256, 1440),
3548 height: clamp(extension_settings.sd.height, 256, 1440),
3549 seed: extension_settings.sd.seed >= 0 ? extension_settings.sd.seed : undefined,
3550 }),
3551 });
3552
3553 if (result.ok) {
3554 const data = await result.json();
3555 return { format: 'jpg', data: data.image };
3556 } else {
3557 const text = await result.text();
3558 console.log(text);
3559 throw new Error(text);
3560 }
3561}
3562
3499async function onComfyOpenWorkflowEditorClick() {3563async function onComfyOpenWorkflowEditorClick() {
3500 let workflow = await (await fetch('/api/sd/comfy/workflow', {3564 let workflow = await (await fetch('/api/sd/comfy/workflow', {
3501 method: 'POST',3565 method: 'POST',
@@ -3782,6 +3846,8 @@ function isValidState() {
3782 return secret_state[SECRET_KEYS.NANOGPT];3846 return secret_state[SECRET_KEYS.NANOGPT];
3783 case sources.bfl:3847 case sources.bfl:
3784 return secret_state[SECRET_KEYS.BFL];3848 return secret_state[SECRET_KEYS.BFL];
3849 case sources.falai:
3850 return secret_state[SECRET_KEYS.FALAI];
3785 }3851 }
3786}3852}
37873853
@@ -4443,6 +4509,7 @@ jQuery(async () => {
4443 $('#sd_function_tool').on('input', onFunctionToolInput);4509 $('#sd_function_tool').on('input', onFunctionToolInput);
4444 $('#sd_bfl_key').on('click', onBflKeyClick);4510 $('#sd_bfl_key').on('click', onBflKeyClick);
4445 $('#sd_bfl_upsampling').on('input', onBflUpsamplingInput);4511 $('#sd_bfl_upsampling').on('input', onBflUpsamplingInput);
4512 $('#sd_falai_key').on('click', onFalaiKeyClick);
44464513
4447 if (!CSS.supports('field-sizing', 'content')) {4514 if (!CSS.supports('field-sizing', 'content')) {
4448 $('.sd_settings .inline-drawer-toggle').on('click', function () {4515 $('.sd_settings .inline-drawer-toggle').on('click', function () {
public/scripts/extensions/stable-diffusion/settings.html+16 -1
@@ -41,7 +41,8 @@
41 <option value="blockentropy">Block Entropy</option>41 <option value="blockentropy">Block Entropy</option>
42 <option value="comfy">ComfyUI</option>42 <option value="comfy">ComfyUI</option>
43 <option value="drawthings">DrawThings HTTP API</option>43 <option value="drawthings">DrawThings HTTP API</option>
44 <option value="extras">Extras API (local / remote)</option>44 <option value="extras">Extras API (deprecated)</option>
45 <option value="falai">FAL.AI</option>
45 <option value="huggingface">HuggingFace Inference API (serverless)</option>46 <option value="huggingface">HuggingFace Inference API (serverless)</option>
46 <option value="nanogpt">NanoGPT</option>47 <option value="nanogpt">NanoGPT</option>
47 <option value="novel">NovelAI Diffusion</option>48 <option value="novel">NovelAI Diffusion</option>
@@ -256,6 +257,20 @@
256 </label>257 </label>
257 </div>258 </div>
258259
260 <div data-sd-source="falai">
261 <div class="flex-container flexnowrap alignItemsBaseline marginBot5">
262 <a href="https://fal.ai/dashboard" target="_blank" rel="noopener noreferrer">
263 <strong data-i18n="API Key">API Key</strong>
264 <i class="fa-solid fa-share-from-square"></i>
265 </a>
266 <span class="expander"></span>
267 <div id="sd_falai_key" class="menu_button menu_button_icon">
268 <i class="fa-fw fa-solid fa-key"></i>
269 <span data-i18n="Click to set">Click to set</span>
270 </div>
271 </div>
272 </div>
273
259 <div class="flex-container">274 <div class="flex-container">
260 <div class="flex1">275 <div class="flex1">
261 <label for="sd_model" data-i18n="Model">Model</label>276 <label for="sd_model" data-i18n="Model">Model</label>
public/scripts/extensions/token-counter/index.js+6 -20
@@ -6,6 +6,8 @@ import { getFriendlyTokenizerName, getTextTokens, getTokenCountAsync, tokenizers
6import { resetScrollHeight, debounce } from '../../utils.js';6import { resetScrollHeight, debounce } from '../../utils.js';
7import { debounce_timeout } from '../../constants.js';7import { debounce_timeout } from '../../constants.js';
8import { POPUP_TYPE, callGenericPopup } from '../../popup.js';8import { POPUP_TYPE, callGenericPopup } from '../../popup.js';
9import { renderExtensionTemplateAsync } from '../../extensions.js';
10import { t } from '../../i18n.js';
911
10function rgb2hex(rgb) {12function rgb2hex(rgb) {
11 rgb = rgb.match(/^rgba?[\s+]?\([\s+]?(\d+)[\s+]?,[\s+]?(\d+)[\s+]?,[\s+]?(\d+)[\s+]?/i);13 rgb = rgb.match(/^rgba?[\s+]?\([\s+]?(\d+)[\s+]?,[\s+]?(\d+)[\s+]?,[\s+]?(\d+)[\s+]?/i);
@@ -22,23 +24,7 @@ $('button').click(function () {
2224
23async function doTokenCounter() {25async function doTokenCounter() {
24 const { tokenizerName, tokenizerId } = getFriendlyTokenizerName(main_api);26 const { tokenizerName, tokenizerId } = getFriendlyTokenizerName(main_api);
25 const html = `27 const html = await renderExtensionTemplateAsync('token-counter', 'window', {tokenizerName});
26 <div class="wide100p">
27 <h3>Token Counter</h3>
28 <div class="justifyLeft flex-container flexFlowColumn">
29 <h4>Type / paste in the box below to see the number of tokens in the text.</h4>
30 <p>Selected tokenizer: ${tokenizerName}</p>
31 <div>Input:</div>
32 <textarea id="token_counter_textarea" class="wide100p textarea_compact" rows="1"></textarea>
33 <div>Tokens: <span id="token_counter_result">0</span></div>
34 <hr>
35 <div>Tokenized text:</div>
36 <div id="tokenized_chunks_display" class="wide100p">—</div>
37 <hr>
38 <div>Token IDs:</div>
39 <textarea id="token_counter_ids" class="wide100p textarea_compact" readonly rows="1">—</textarea>
40 </div>
41 </div>`;
4228
43 const dialog = $(html);29 const dialog = $(html);
44 const countDebounced = debounce(async () => {30 const countDebounced = debounce(async () => {
@@ -131,9 +117,9 @@ async function doCount() {
131jQuery(() => {117jQuery(() => {
132 const buttonHtml = `118 const buttonHtml = `
133 <div id="token_counter" class="list-group-item flex-container flexGap5">119 <div id="token_counter" class="list-group-item flex-container flexGap5">
134 <div class="fa-solid fa-1 extensionsMenuExtensionButton" /></div>120 <div class="fa-solid fa-1 extensionsMenuExtensionButton" /></div>` +
135 Token Counter121 t`Token Counter` +
136 </div>`;122 '</div>';
137 $('#token_counter_wand_container').append(buttonHtml);123 $('#token_counter_wand_container').append(buttonHtml);
138 $('#token_counter').on('click', doTokenCounter);124 $('#token_counter').on('click', doTokenCounter);
139 SlashCommandParser.addCommandObject(SlashCommand.fromProps({125 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
public/scripts/extensions/token-counter/window.html+16 -0
@@ -0,0 +1,16 @@
1<div class="wide100p">
2 <h3 data-i18n="Token Counter">Token Counter</h3>
3 <div class="justifyLeft flex-container flexFlowColumn">
4 <h4 data-i18n="Type / paste in the box below to see the number of tokens in the text.">Type / paste in the box below to see the number of tokens in the text.</h4>
5 <p><span data-i18n="Selected tokenizer:">Selected tokenizer:</span> {{tokenizerName}}</p>
6 <div data-i18n="Input:">Input:</div>
7 <textarea id="token_counter_textarea" class="wide100p textarea_compact" rows="1"></textarea>
8 <div><span data-i18n="Tokens:">Tokens:</span> <span id="token_counter_result">0</span></div>
9 <hr>
10 <div data-i18n="Tokenized text:">Tokenized text:</div>
11 <div id="tokenized_chunks_display" class="wide100p">—</div>
12 <hr>
13 <div data-i18n="Token IDs:">Token IDs:</div>
14 <textarea id="token_counter_ids" class="wide100p textarea_compact" readonly rows="1">—</textarea>
15 </div>
16</div>
\ No newline at end of file16 \ No newline at end of file
public/scripts/extensions/tts/openai-compatible.js+3 -3
@@ -25,7 +25,7 @@ class OpenAICompatibleTtsProvider {
25 <label for="openai_compatible_tts_endpoint">Provider Endpoint:</label>25 <label for="openai_compatible_tts_endpoint">Provider Endpoint:</label>
26 <div class="flex-container alignItemsCenter">26 <div class="flex-container alignItemsCenter">
27 <div class="flex1">27 <div class="flex1">
28 <input id="openai_compatible_tts_endpoint" type="text" class="text_pole" maxlength="250" value="${this.defaultSettings.provider_endpoint}"/>28 <input id="openai_compatible_tts_endpoint" type="text" class="text_pole" maxlength="500" value="${this.defaultSettings.provider_endpoint}"/>
29 </div>29 </div>
30 <div id="openai_compatible_tts_key" class="menu_button menu_button_icon">30 <div id="openai_compatible_tts_key" class="menu_button menu_button_icon">
31 <i class="fa-solid fa-key"></i>31 <i class="fa-solid fa-key"></i>
@@ -33,9 +33,9 @@ class OpenAICompatibleTtsProvider {
33 </div>33 </div>
34 </div>34 </div>
35 <label for="openai_compatible_model">Model:</label>35 <label for="openai_compatible_model">Model:</label>
36 <input id="openai_compatible_model" type="text" class="text_pole" maxlength="250" value="${this.defaultSettings.model}"/>36 <input id="openai_compatible_model" type="text" class="text_pole" maxlength="500" value="${this.defaultSettings.model}"/>
37 <label for="openai_compatible_tts_voices">Available Voices (comma separated):</label>37 <label for="openai_compatible_tts_voices">Available Voices (comma separated):</label>
38 <input id="openai_compatible_tts_voices" type="text" class="text_pole" maxlength="250" value="${this.defaultSettings.available_voices.join()}"/>38 <input id="openai_compatible_tts_voices" type="text" class="text_pole" value="${this.defaultSettings.available_voices.join()}"/>
39 <label for="openai_compatible_tts_speed">Speed: <span id="openai_compatible_tts_speed_output"></span></label>39 <label for="openai_compatible_tts_speed">Speed: <span id="openai_compatible_tts_speed_output"></span></label>
40 <input type="range" id="openai_compatible_tts_speed" value="1" min="0.25" max="4" step="0.05">`;40 <input type="range" id="openai_compatible_tts_speed" value="1" min="0.25" max="4" step="0.05">`;
41 return html;41 return html;
public/scripts/extensions/vectors/index.js+58 -66
@@ -746,74 +746,65 @@ async function getQueryText(chat, initiator) {
746}746}
747747
748/**748/**
749 * Gets the saved hashes for a collection749 * Gets common body parameters for vector requests.
750* @param {string} collectionId750 * @returns {object}
751* @returns {Promise<number[]>} Saved hashes
752 */751 */
753async function getSavedHashes(collectionId) {752function getVectorsRequestBody() {
754 const response = await fetch('/api/vector/list', {753 const body = {};
755 method: 'POST',
756 headers: getVectorHeaders(),
757 body: JSON.stringify({
758 collectionId: collectionId,
759 source: settings.source,
760 }),
761 });
762
763 if (!response.ok) {
764 throw new Error(`Failed to get saved hashes for collection ${collectionId}`);
765 }
766
767 const hashes = await response.json();
768 return hashes;
769}
770
771function getVectorHeaders() {
772 const headers = getRequestHeaders();
773 switch (settings.source) {754 switch (settings.source) {
774 case 'extras':755 case 'extras':
775 Object.assign(headers, {756 body.extrasUrl = extension_settings.apiUrl;
776 'X-Extras-Url': extension_settings.apiUrl,757 body.extrasKey = extension_settings.apiKey;
777 'X-Extras-Key': extension_settings.apiKey,
778 });
779 break;758 break;
780 case 'togetherai':759 case 'togetherai':
781 Object.assign(headers, {760 body.model = extension_settings.vectors.togetherai_model;
782 'X-Togetherai-Model': extension_settings.vectors.togetherai_model,
783 });
784 break;761 break;
785 case 'openai':762 case 'openai':
786 Object.assign(headers, {763 body.model = extension_settings.vectors.openai_model;
787 'X-OpenAI-Model': extension_settings.vectors.openai_model,
788 });
789 break;764 break;
790 case 'cohere':765 case 'cohere':
791 Object.assign(headers, {766 body.model = extension_settings.vectors.cohere_model;
792 'X-Cohere-Model': extension_settings.vectors.cohere_model,
793 });
794 break;767 break;
795 case 'ollama':768 case 'ollama':
796 Object.assign(headers, {769 body.model = extension_settings.vectors.ollama_model;
797 'X-Ollama-Model': extension_settings.vectors.ollama_model,770 body.apiUrl = textgenerationwebui_settings.server_urls[textgen_types.OLLAMA];
798 'X-Ollama-URL': textgenerationwebui_settings.server_urls[textgen_types.OLLAMA],771 body.keep = !!extension_settings.vectors.ollama_keep;
799 'X-Ollama-Keep': !!extension_settings.vectors.ollama_keep,
800 });
801 break;772 break;
802 case 'llamacpp':773 case 'llamacpp':
803 Object.assign(headers, {774 body.apiUrl = textgenerationwebui_settings.server_urls[textgen_types.LLAMACPP];
804 'X-LlamaCpp-URL': textgenerationwebui_settings.server_urls[textgen_types.LLAMACPP],
805 });
806 break;775 break;
807 case 'vllm':776 case 'vllm':
808 Object.assign(headers, {777 body.apiUrl = textgenerationwebui_settings.server_urls[textgen_types.VLLM];
809 'X-Vllm-URL': textgenerationwebui_settings.server_urls[textgen_types.VLLM],778 body.model = extension_settings.vectors.vllm_model;
810 'X-Vllm-Model': extension_settings.vectors.vllm_model,
811 });
812 break;779 break;
813 default:780 default:
814 break;781 break;
815 }782 }
816 return headers;783 return body;
784}
785
786/**
787 * Gets the saved hashes for a collection
788* @param {string} collectionId
789* @returns {Promise<number[]>} Saved hashes
790*/
791async function getSavedHashes(collectionId) {
792 const response = await fetch('/api/vector/list', {
793 method: 'POST',
794 headers: getRequestHeaders(),
795 body: JSON.stringify({
796 ...getVectorsRequestBody(),
797 collectionId: collectionId,
798 source: settings.source,
799 }),
800 });
801
802 if (!response.ok) {
803 throw new Error(`Failed to get saved hashes for collection ${collectionId}`);
804 }
805
806 const hashes = await response.json();
807 return hashes;
817}808}
818809
819/**810/**
@@ -825,12 +816,11 @@ function getVectorHeaders() {
825async function insertVectorItems(collectionId, items) {816async function insertVectorItems(collectionId, items) {
826 throwIfSourceInvalid();817 throwIfSourceInvalid();
827818
828 const headers = getVectorHeaders();
829
830 const response = await fetch('/api/vector/insert', {819 const response = await fetch('/api/vector/insert', {
831 method: 'POST',820 method: 'POST',
832 headers: headers,821 headers: getRequestHeaders(),
833 body: JSON.stringify({822 body: JSON.stringify({
823 ...getVectorsRequestBody(),
834 collectionId: collectionId,824 collectionId: collectionId,
835 items: items,825 items: items,
836 source: settings.source,826 source: settings.source,
@@ -879,8 +869,9 @@ function throwIfSourceInvalid() {
879async function deleteVectorItems(collectionId, hashes) {869async function deleteVectorItems(collectionId, hashes) {
880 const response = await fetch('/api/vector/delete', {870 const response = await fetch('/api/vector/delete', {
881 method: 'POST',871 method: 'POST',
882 headers: getVectorHeaders(),872 headers: getRequestHeaders(),
883 body: JSON.stringify({873 body: JSON.stringify({
874 ...getVectorsRequestBody(),
884 collectionId: collectionId,875 collectionId: collectionId,
885 hashes: hashes,876 hashes: hashes,
886 source: settings.source,877 source: settings.source,
@@ -899,12 +890,11 @@ async function deleteVectorItems(collectionId, hashes) {
899 * @returns {Promise<{ hashes: number[], metadata: object[]}>} - Hashes of the results890 * @returns {Promise<{ hashes: number[], metadata: object[]}>} - Hashes of the results
900 */891 */
901async function queryCollection(collectionId, searchText, topK) {892async function queryCollection(collectionId, searchText, topK) {
902 const headers = getVectorHeaders();
903
904 const response = await fetch('/api/vector/query', {893 const response = await fetch('/api/vector/query', {
905 method: 'POST',894 method: 'POST',
906 headers: headers,895 headers: getRequestHeaders(),
907 body: JSON.stringify({896 body: JSON.stringify({
897 ...getVectorsRequestBody(),
908 collectionId: collectionId,898 collectionId: collectionId,
909 searchText: searchText,899 searchText: searchText,
910 topK: topK,900 topK: topK,
@@ -929,12 +919,11 @@ async function queryCollection(collectionId, searchText, topK) {
929 * @returns {Promise<Record<string, { hashes: number[], metadata: object[] }>>} - Results mapped to collection IDs919 * @returns {Promise<Record<string, { hashes: number[], metadata: object[] }>>} - Results mapped to collection IDs
930 */920 */
931async function queryMultipleCollections(collectionIds, searchText, topK, threshold) {921async function queryMultipleCollections(collectionIds, searchText, topK, threshold) {
932 const headers = getVectorHeaders();
933
934 const response = await fetch('/api/vector/query-multi', {922 const response = await fetch('/api/vector/query-multi', {
935 method: 'POST',923 method: 'POST',
936 headers: headers,924 headers: getRequestHeaders(),
937 body: JSON.stringify({925 body: JSON.stringify({
926 ...getVectorsRequestBody(),
938 collectionIds: collectionIds,927 collectionIds: collectionIds,
939 searchText: searchText,928 searchText: searchText,
940 topK: topK,929 topK: topK,
@@ -965,8 +954,9 @@ async function purgeFileVectorIndex(fileUrl) {
965954
966 const response = await fetch('/api/vector/purge', {955 const response = await fetch('/api/vector/purge', {
967 method: 'POST',956 method: 'POST',
968 headers: getVectorHeaders(),957 headers: getRequestHeaders(),
969 body: JSON.stringify({958 body: JSON.stringify({
959 ...getVectorsRequestBody(),
970 collectionId: collectionId,960 collectionId: collectionId,
971 }),961 }),
972 });962 });
@@ -994,8 +984,9 @@ async function purgeVectorIndex(collectionId) {
994984
995 const response = await fetch('/api/vector/purge', {985 const response = await fetch('/api/vector/purge', {
996 method: 'POST',986 method: 'POST',
997 headers: getVectorHeaders(),987 headers: getRequestHeaders(),
998 body: JSON.stringify({988 body: JSON.stringify({
989 ...getVectorsRequestBody(),
999 collectionId: collectionId,990 collectionId: collectionId,
1000 }),991 }),
1001 });992 });
@@ -1019,7 +1010,10 @@ async function purgeAllVectorIndexes() {
1019 try {1010 try {
1020 const response = await fetch('/api/vector/purge-all', {1011 const response = await fetch('/api/vector/purge-all', {
1021 method: 'POST',1012 method: 'POST',
1022 headers: getVectorHeaders(),1013 headers: getRequestHeaders(),
1014 body: JSON.stringify({
1015 ...getVectorsRequestBody(),
1016 }),
1023 });1017 });
10241018
1025 if (!response.ok) {1019 if (!response.ok) {
@@ -1638,14 +1632,12 @@ jQuery(async () => {
1638 }1632 }
1639 return textResult;1633 return textResult;
1640 };1634 };
1641
1642 if (args.return === 'chunks') {1635 if (args.return === 'chunks') {
1643 return getChunksText();1636 return getChunksText();
1644 }1637 }
16451638
1646 // @ts-ignore1639 // @ts-ignore
1647 return slashCommandReturnHelper.doReturn(args.return ?? 'object', urls, { objectToStringFunc: list => list.join('\n') });1640 return slashCommandReturnHelper.doReturn(args.return ?? 'object', urls, { objectToStringFunc: list => list.join('\n') });
1648
1649 },1641 },
1650 aliases: ['databank-search', 'data-bank-search'],1642 aliases: ['databank-search', 'data-bank-search'],
1651 helpString: 'Search the Data Bank for a specific query using vector similarity. Returns a list of file URLs with the most relevant content.',1643 helpString: 'Search the Data Bank for a specific query using vector similarity. Returns a list of file URLs with the most relevant content.',
@@ -1660,10 +1652,10 @@ jQuery(async () => {
1660 defaultValue: 'object',1652 defaultValue: 'object',
1661 enumList: [1653 enumList: [
1662 new SlashCommandEnumValue('chunks', 'Return the actual content chunks', enumTypes.enum, '{}'),1654 new SlashCommandEnumValue('chunks', 'Return the actual content chunks', enumTypes.enum, '{}'),
1663 ...slashCommandReturnHelper.enumList({ allowObject: true })1655 ...slashCommandReturnHelper.enumList({ allowObject: true }),
1664 ],1656 ],
1665 forceEnum: true,1657 forceEnum: true,
1666 })1658 }),
1667 ],1659 ],
1668 unnamedArgumentList: [1660 unnamedArgumentList: [
1669 new SlashCommandArgument('Query to search by.', ARGUMENT_TYPE.STRING, true, false),1661 new SlashCommandArgument('Query to search by.', ARGUMENT_TYPE.STRING, true, false),
public/scripts/extensions/vectors/settings.html+1 -1
@@ -11,7 +11,7 @@
11 </label>11 </label>
12 <select id="vectors_source" class="text_pole">12 <select id="vectors_source" class="text_pole">
13 <option value="cohere">Cohere</option>13 <option value="cohere">Cohere</option>
14 <option value="extras">Extras</option>14 <option value="extras">Extras (deprecated)</option>
15 <option value="palm">Google AI Studio</option>15 <option value="palm">Google AI Studio</option>
16 <option value="llamacpp">llama.cpp</option>16 <option value="llamacpp">llama.cpp</option>
17 <option value="transformers" data-i18n="Local (Transformers)">Local (Transformers)</option>17 <option value="transformers" data-i18n="Local (Transformers)">Local (Transformers)</option>
public/scripts/f-localStorage.js+15 -0
@@ -1,18 +1,30 @@
1////////////////// LOCAL STORAGE HANDLING /////////////////////1////////////////// LOCAL STORAGE HANDLING /////////////////////
22
3/**
4 * @deprecated THIS FUNCTION IS OBSOLETE. DO NOT USE
5 */
3export function SaveLocal(target, val) {6export function SaveLocal(target, val) {
4 localStorage.setItem(target, val);7 localStorage.setItem(target, val);
5 console.debug('SaveLocal -- ' + target + ' : ' + val);8 console.debug('SaveLocal -- ' + target + ' : ' + val);
6}9}
10/**
11 * @deprecated THIS FUNCTION IS OBSOLETE. DO NOT USE
12 */
7export function LoadLocal(target) {13export function LoadLocal(target) {
8 console.debug('LoadLocal -- ' + target);14 console.debug('LoadLocal -- ' + target);
9 return localStorage.getItem(target);15 return localStorage.getItem(target);
1016
11}17}
18/**
19 * @deprecated THIS FUNCTION IS OBSOLETE. DO NOT USE
20 */
12export function LoadLocalBool(target) {21export function LoadLocalBool(target) {
13 let result = localStorage.getItem(target) === 'true';22 let result = localStorage.getItem(target) === 'true';
14 return result;23 return result;
15}24}
25/**
26 * @deprecated THIS FUNCTION IS OBSOLETE. DO NOT USE
27 */
16export function CheckLocal() {28export function CheckLocal() {
17 console.log('----------local storage---------');29 console.log('----------local storage---------');
18 var i;30 var i;
@@ -22,6 +34,9 @@ export function CheckLocal() {
22 console.log('------------------------------');34 console.log('------------------------------');
23}35}
2436
37/**
38 * @deprecated THIS FUNCTION IS OBSOLETE. DO NOT USE
39 */
25export function ClearLocal() { localStorage.clear(); console.log('Removed All Local Storage'); }40export function ClearLocal() { localStorage.clear(); console.log('Removed All Local Storage'); }
2641
27/////////////////////////////////////////////////////////////////////////42/////////////////////////////////////////////////////////////////////////
public/scripts/group-chats.js+37 -39
@@ -78,6 +78,7 @@ import { FILTER_TYPES, FilterHelper } from './filters.js';
78import { isExternalMediaAllowed } from './chats.js';78import { isExternalMediaAllowed } from './chats.js';
79import { POPUP_TYPE, Popup, callGenericPopup } from './popup.js';79import { POPUP_TYPE, Popup, callGenericPopup } from './popup.js';
80import { t } from './i18n.js';80import { t } from './i18n.js';
81import { accountStorage } from './util/AccountStorage.js';
8182
82export {83export {
83 selected_group,84 selected_group,
@@ -292,10 +293,11 @@ export function getGroupNames() {
292293
293/**294/**
294 * Finds the character ID for a group member.295 * Finds the character ID for a group member.
295 * @param {string} arg 0-based member index or character name296 * @param {number|string} arg 0-based member index or character name
296 * @returns {number} 0-based character ID297 * @param {Boolean} full Whether to return a key-value object containing extra data
298 * @returns {number|Object} 0-based character ID or key-value object if full is true
297 */299 */
298export function findGroupMemberId(arg) {300export function findGroupMemberId(arg, full = false) {
299 arg = arg?.trim();301 arg = arg?.trim();
300302
301 if (!arg) {303 if (!arg) {
@@ -311,15 +313,19 @@ export function findGroupMemberId(arg) {
311 }313 }
312314
313 const index = parseInt(arg);315 const index = parseInt(arg);
314 const searchByName = isNaN(index);316 const searchByString = isNaN(index);
315317
316 if (searchByName) {318 if (searchByString) {
317 const memberNames = group.members.map(x => ({ name: characters.find(y => y.avatar === x)?.name, index: characters.findIndex(y => y.avatar === x) }));319 const memberNames = group.members.map(x => ({
318 const fuse = new Fuse(memberNames, { keys: ['name'] });320 avatar: x,
321 name: characters.find(y => y.avatar === x)?.name,
322 index: characters.findIndex(y => y.avatar === x),
323 }));
324 const fuse = new Fuse(memberNames, { keys: ['avatar', 'name'] });
319 const result = fuse.search(arg);325 const result = fuse.search(arg);
320326
321 if (!result.length) {327 if (!result.length) {
322 console.warn(`WARN: No group member found with name ${arg}`);328 console.warn(`WARN: No group member found using string ${arg}`);
323 return;329 return;
324 }330 }
325331
@@ -330,9 +336,11 @@ export function findGroupMemberId(arg) {
330 return;336 return;
331 }337 }
332338
333 console.log(`Triggering group member ${chid} (${arg}) from search result`, result[0]);339 console.log(`Targeting group member ${chid} (${arg}) from search result`, result[0]);
334 return chid;340
335 } else {341 return !full ? chid : { ...{ id: chid }, ...result[0].item };
342 }
343 else {
336 const memberAvatar = group.members[index];344 const memberAvatar = group.members[index];
337345
338 if (memberAvatar === undefined) {346 if (memberAvatar === undefined) {
@@ -347,8 +355,14 @@ export function findGroupMemberId(arg) {
347 return;355 return;
348 }356 }
349357
350 console.log(`Triggering group member ${memberAvatar} at index ${index}`);358 console.log(`Targeting group member ${memberAvatar} at index ${index}`);
351 return chid;359
360 return !full ? chid : {
361 id: chid,
362 avatar: memberAvatar,
363 name: characters.find(y => y.avatar === memberAvatar)?.name,
364 index: index,
365 };
352 }366 }
353}367}
354368
@@ -805,7 +819,6 @@ async function generateGroupWrapper(by_auto_mode, type = null, params = {}) {
805819
806 /** @type {any} Caution: JS war crimes ahead */820 /** @type {any} Caution: JS war crimes ahead */
807 let textResult = '';821 let textResult = '';
808 let typingIndicator = $('#chat .typing_indicator');
809 const group = groups.find((x) => x.id === selected_group);822 const group = groups.find((x) => x.id === selected_group);
810823
811 if (!group || !Array.isArray(group.members) || !group.members.length) {824 if (!group || !Array.isArray(group.members) || !group.members.length) {
@@ -821,14 +834,6 @@ async function generateGroupWrapper(by_auto_mode, type = null, params = {}) {
821 setCharacterId(undefined);834 setCharacterId(undefined);
822 const userInput = String($('#send_textarea').val());835 const userInput = String($('#send_textarea').val());
823836
824 if (typingIndicator.length === 0 && !isStreamingEnabled()) {
825 typingIndicator = $(
826 '#typing_indicator_template .typing_indicator',
827 ).clone();
828 typingIndicator.hide();
829 $('#chat').append(typingIndicator);
830 }
831
832 // id of this specific batch for regeneration purposes837 // id of this specific batch for regeneration purposes
833 group_generation_id = Date.now();838 group_generation_id = Date.now();
834 const lastMessage = chat[chat.length - 1];839 const lastMessage = chat[chat.length - 1];
@@ -906,14 +911,6 @@ async function generateGroupWrapper(by_auto_mode, type = null, params = {}) {
906 }911 }
907 await eventSource.emit(event_types.GROUP_MEMBER_DRAFTED, chId);912 await eventSource.emit(event_types.GROUP_MEMBER_DRAFTED, chId);
908913
909 if (type !== 'swipe' && type !== 'impersonate' && !isStreamingEnabled()) {
910 // update indicator and scroll down
911 typingIndicator
912 .find('.typing_indicator_name')
913 .text(characters[chId].name);
914 typingIndicator.show();
915 }
916
917 // Wait for generation to finish914 // Wait for generation to finish
918 textResult = await Generate(generateType, { automatic_trigger: by_auto_mode, ...(params || {}) });915 textResult = await Generate(generateType, { automatic_trigger: by_auto_mode, ...(params || {}) });
919 let messageChunk = textResult?.messageChunk;916 let messageChunk = textResult?.messageChunk;
@@ -930,8 +927,6 @@ async function generateGroupWrapper(by_auto_mode, type = null, params = {}) {
930 }927 }
931 }928 }
932 } finally {929 } finally {
933 typingIndicator.hide();
934
935 is_group_generating = false;930 is_group_generating = false;
936 setSendButtonState(false);931 setSendButtonState(false);
937 setCharacterId(undefined);932 setCharacterId(undefined);
@@ -1315,10 +1310,10 @@ function printGroupCandidates() {
1315 formatNavigator: PAGINATION_TEMPLATE,1310 formatNavigator: PAGINATION_TEMPLATE,
1316 showNavigator: true,1311 showNavigator: true,
1317 showSizeChanger: true,1312 showSizeChanger: true,
1318 pageSize: Number(localStorage.getItem(storageKey)) || 5,1313 pageSize: Number(accountStorage.getItem(storageKey)) || 5,
1319 sizeChangerOptions: [5, 10, 25, 50, 100, 200, 500, 1000],1314 sizeChangerOptions: [5, 10, 25, 50, 100, 200, 500, 1000],
1320 afterSizeSelectorChange: function (e) {1315 afterSizeSelectorChange: function (e) {
1321 localStorage.setItem(storageKey, e.target.value);1316 accountStorage.setItem(storageKey, e.target.value);
1322 },1317 },
1323 callback: function (data) {1318 callback: function (data) {
1324 $('#rm_group_add_members').empty();1319 $('#rm_group_add_members').empty();
@@ -1342,10 +1337,10 @@ function printGroupMembers() {
1342 formatNavigator: PAGINATION_TEMPLATE,1337 formatNavigator: PAGINATION_TEMPLATE,
1343 showNavigator: true,1338 showNavigator: true,
1344 showSizeChanger: true,1339 showSizeChanger: true,
1345 pageSize: Number(localStorage.getItem(storageKey)) || 5,1340 pageSize: Number(accountStorage.getItem(storageKey)) || 5,
1346 sizeChangerOptions: [5, 10, 25, 50, 100, 200, 500, 1000],1341 sizeChangerOptions: [5, 10, 25, 50, 100, 200, 500, 1000],
1347 afterSizeSelectorChange: function (e) {1342 afterSizeSelectorChange: function (e) {
1348 localStorage.setItem(storageKey, e.target.value);1343 accountStorage.setItem(storageKey, e.target.value);
1349 },1344 },
1350 callback: function (data) {1345 callback: function (data) {
1351 $('.rm_group_members').empty();1346 $('.rm_group_members').empty();
@@ -1669,12 +1664,12 @@ function updateFavButtonState(state) {
1669export async function openGroupById(groupId) {1664export async function openGroupById(groupId) {
1670 if (isChatSaving) {1665 if (isChatSaving) {
1671 toastr.info(t`Please wait until the chat is saved before switching characters.`, t`Your chat is still saving...`);1666 toastr.info(t`Please wait until the chat is saved before switching characters.`, t`Your chat is still saving...`);
1672 return;1667 return false;
1673 }1668 }
16741669
1675 if (!groups.find(x => x.id === groupId)) {1670 if (!groups.find(x => x.id === groupId)) {
1676 console.log('Group not found', groupId);1671 console.log('Group not found', groupId);
1677 return;1672 return false;
1678 }1673 }
16791674
1680 if (!is_send_press && !is_group_generating) {1675 if (!is_send_press && !is_group_generating) {
@@ -1691,8 +1686,11 @@ export async function openGroupById(groupId) {
1691 updateChatMetadata({}, true);1686 updateChatMetadata({}, true);
1692 chat.length = 0;1687 chat.length = 0;
1693 await getGroupChat(groupId);1688 await getGroupChat(groupId);
1689 return true;
1694 }1690 }
1695 }1691 }
1692
1693 return false;
1696}1694}
16971695
1698function openCharacterDefinition(characterSelect) {1696function openCharacterDefinition(characterSelect) {
public/scripts/openai.js+57 -31
@@ -73,6 +73,7 @@ import { SlashCommandEnumValue } from './slash-commands/SlashCommandEnumValue.js
73import { Popup, POPUP_RESULT } from './popup.js';73import { Popup, POPUP_RESULT } from './popup.js';
74import { t } from './i18n.js';74import { t } from './i18n.js';
75import { ToolManager } from './tool-calling.js';75import { ToolManager } from './tool-calling.js';
76import { accountStorage } from './util/AccountStorage.js';
7677
77export {78export {
78 openai_messages_count,79 openai_messages_count,
@@ -82,7 +83,6 @@ export {
82 setOpenAIMessageExamples,83 setOpenAIMessageExamples,
83 setupChatCompletionPromptManager,84 setupChatCompletionPromptManager,
84 sendOpenAIRequest,85 sendOpenAIRequest,
85 getChatCompletionModel,
86 TokenHandler,86 TokenHandler,
87 IdentifierNotFoundError,87 IdentifierNotFoundError,
88 Message,88 Message,
@@ -259,7 +259,7 @@ const default_settings = {
259 mistralai_model: 'mistral-large-latest',259 mistralai_model: 'mistral-large-latest',
260 cohere_model: 'command-r-plus',260 cohere_model: 'command-r-plus',
261 perplexity_model: 'sonar-pro',261 perplexity_model: 'sonar-pro',
262 groq_model: 'llama-3.1-70b-versatile',262 groq_model: 'llama-3.3-70b-versatile',
263 nanogpt_model: 'gpt-4o-mini',263 nanogpt_model: 'gpt-4o-mini',
264 zerooneai_model: 'yi-large',264 zerooneai_model: 'yi-large',
265 blockentropy_model: 'be-70b-base-llama3.1',265 blockentropy_model: 'be-70b-base-llama3.1',
@@ -299,6 +299,7 @@ const default_settings = {
299 continue_postfix: continue_postfix_types.SPACE,299 continue_postfix: continue_postfix_types.SPACE,
300 custom_prompt_post_processing: custom_prompt_post_processing_types.NONE,300 custom_prompt_post_processing: custom_prompt_post_processing_types.NONE,
301 show_thoughts: true,301 show_thoughts: true,
302 reasoning_effort: 'medium',
302 seed: -1,303 seed: -1,
303 n: 1,304 n: 1,
304};305};
@@ -378,6 +379,7 @@ const oai_settings = {
378 continue_postfix: continue_postfix_types.SPACE,379 continue_postfix: continue_postfix_types.SPACE,
379 custom_prompt_post_processing: custom_prompt_post_processing_types.NONE,380 custom_prompt_post_processing: custom_prompt_post_processing_types.NONE,
380 show_thoughts: true,381 show_thoughts: true,
382 reasoning_effort: 'medium',
381 seed: -1,383 seed: -1,
382 n: 1,384 n: 1,
383};385};
@@ -412,7 +414,7 @@ async function validateReverseProxy() {
412 throw err;414 throw err;
413 }415 }
414 const rememberKey = `Proxy_SkipConfirm_${getStringHash(oai_settings.reverse_proxy)}`;416 const rememberKey = `Proxy_SkipConfirm_${getStringHash(oai_settings.reverse_proxy)}`;
415 const skipConfirm = localStorage.getItem(rememberKey) === 'true';417 const skipConfirm = accountStorage.getItem(rememberKey) === 'true';
416418
417 const confirmation = skipConfirm || await Popup.show.confirm(t`Connecting To Proxy`, await renderTemplateAsync('proxyConnectionWarning', { proxyURL: DOMPurify.sanitize(oai_settings.reverse_proxy) }));419 const confirmation = skipConfirm || await Popup.show.confirm(t`Connecting To Proxy`, await renderTemplateAsync('proxyConnectionWarning', { proxyURL: DOMPurify.sanitize(oai_settings.reverse_proxy) }));
418420
@@ -423,7 +425,7 @@ async function validateReverseProxy() {
423 throw new Error('Proxy connection denied.');425 throw new Error('Proxy connection denied.');
424 }426 }
425427
426 localStorage.setItem(rememberKey, String(true));428 accountStorage.setItem(rememberKey, String(true));
427}429}
428430
429/**431/**
@@ -1443,9 +1445,7 @@ async function sendWindowAIRequest(messages, signal, stream) {
1443 }1445 }
14441446
1445 const onStreamResult = (res, err) => {1447 const onStreamResult = (res, err) => {
1446 if (err) {1448 if (err) return;
1447 return;
1448 }
14491449
1450 const thisContent = res?.message?.content;1450 const thisContent = res?.message?.content;
14511451
@@ -1497,7 +1497,7 @@ async function sendWindowAIRequest(messages, signal, stream) {
1497 }1497 }
1498}1498}
14991499
1500function getChatCompletionModel() {1500export function getChatCompletionModel() {
1501 switch (oai_settings.chat_completion_source) {1501 switch (oai_settings.chat_completion_source) {
1502 case chat_completion_sources.CLAUDE:1502 case chat_completion_sources.CLAUDE:
1503 return oai_settings.claude_model;1503 return oai_settings.claude_model;
@@ -1869,7 +1869,7 @@ async function sendOpenAIRequest(type, messages, signal) {
1869 const isQuiet = type === 'quiet';1869 const isQuiet = type === 'quiet';
1870 const isImpersonate = type === 'impersonate';1870 const isImpersonate = type === 'impersonate';
1871 const isContinue = type === 'continue';1871 const isContinue = type === 'continue';
1872 const stream = oai_settings.stream_openai && !isQuiet && !isScale && !(isGoogle && oai_settings.google_model.includes('bison')) && !(isOAI && (oai_settings.openai_model.startsWith('o1') || oai_settings.openai_model.startsWith('o3')));1872 const stream = oai_settings.stream_openai && !isQuiet && !isScale && !(isOAI && ['o1-2024-12-17', 'o1'].includes(oai_settings.openai_model));
1873 const useLogprobs = !!power_user.request_token_probabilities;1873 const useLogprobs = !!power_user.request_token_probabilities;
1874 const canMultiSwipe = oai_settings.n > 1 && !isContinue && !isImpersonate && !isQuiet && (isOAI || isCustom);1874 const canMultiSwipe = oai_settings.n > 1 && !isContinue && !isImpersonate && !isQuiet && (isOAI || isCustom);
18751875
@@ -1914,8 +1914,13 @@ async function sendOpenAIRequest(type, messages, signal) {
1914 'char_name': name2,1914 'char_name': name2,
1915 'group_names': getGroupNames(),1915 'group_names': getGroupNames(),
1916 'include_reasoning': Boolean(oai_settings.show_thoughts),1916 'include_reasoning': Boolean(oai_settings.show_thoughts),
1917 'reasoning_effort': String(oai_settings.reasoning_effort),
1917 };1918 };
19181919
1920 if (!canMultiSwipe && ToolManager.canPerformToolCalls(type)) {
1921 await ToolManager.registerFunctionToolsOpenAI(generate_data);
1922 }
1923
1919 // Empty array will produce a validation error1924 // Empty array will produce a validation error
1920 if (!Array.isArray(generate_data.stop) || !generate_data.stop.length) {1925 if (!Array.isArray(generate_data.stop) || !generate_data.stop.length) {
1921 delete generate_data.stop;1926 delete generate_data.stop;
@@ -2039,6 +2044,8 @@ async function sendOpenAIRequest(type, messages, signal) {
2039 delete generate_data.top_logprobs;2044 delete generate_data.top_logprobs;
2040 delete generate_data.logprobs;2045 delete generate_data.logprobs;
2041 delete generate_data.logit_bias;2046 delete generate_data.logit_bias;
2047 delete generate_data.tools;
2048 delete generate_data.tool_choice;
2042 }2049 }
2043 }2050 }
20442051
@@ -2046,10 +2053,6 @@ async function sendOpenAIRequest(type, messages, signal) {
2046 generate_data['seed'] = oai_settings.seed;2053 generate_data['seed'] = oai_settings.seed;
2047 }2054 }
20482055
2049 if (!canMultiSwipe && ToolManager.canPerformToolCalls(type)) {
2050 await ToolManager.registerFunctionToolsOpenAI(generate_data);
2051 }
2052
2053 if (isOAI && (oai_settings.openai_model.startsWith('o1') || oai_settings.openai_model.startsWith('o3'))) {2056 if (isOAI && (oai_settings.openai_model.startsWith('o1') || oai_settings.openai_model.startsWith('o3'))) {
2054 generate_data.messages.forEach((msg) => {2057 generate_data.messages.forEach((msg) => {
2055 if (msg.role === 'system') {2058 if (msg.role === 'system') {
@@ -2058,7 +2061,6 @@ async function sendOpenAIRequest(type, messages, signal) {
2058 });2061 });
2059 generate_data.max_completion_tokens = generate_data.max_tokens;2062 generate_data.max_completion_tokens = generate_data.max_tokens;
2060 delete generate_data.max_tokens;2063 delete generate_data.max_tokens;
2061 delete generate_data.stream;
2062 delete generate_data.logprobs;2064 delete generate_data.logprobs;
2063 delete generate_data.top_logprobs;2065 delete generate_data.top_logprobs;
2064 delete generate_data.n;2066 delete generate_data.n;
@@ -2069,8 +2071,7 @@ async function sendOpenAIRequest(type, messages, signal) {
2069 delete generate_data.tools;2071 delete generate_data.tools;
2070 delete generate_data.tool_choice;2072 delete generate_data.tool_choice;
2071 delete generate_data.stop;2073 delete generate_data.stop;
2072 // It does support logit_bias, but the tokenizer used and its effect is yet unknown.2074 delete generate_data.logit_bias;
2073 // delete generate_data.logit_bias;
2074 }2075 }
20752076
2076 await eventSource.emit(event_types.CHAT_COMPLETION_SETTINGS_READY, generate_data);2077 await eventSource.emit(event_types.CHAT_COMPLETION_SETTINGS_READY, generate_data);
@@ -3124,6 +3125,7 @@ function loadOpenAISettings(data, settings) {
3124 oai_settings.inline_image_quality = settings.inline_image_quality ?? default_settings.inline_image_quality;3125 oai_settings.inline_image_quality = settings.inline_image_quality ?? default_settings.inline_image_quality;
3125 oai_settings.bypass_status_check = settings.bypass_status_check ?? default_settings.bypass_status_check;3126 oai_settings.bypass_status_check = settings.bypass_status_check ?? default_settings.bypass_status_check;
3126 oai_settings.show_thoughts = settings.show_thoughts ?? default_settings.show_thoughts;3127 oai_settings.show_thoughts = settings.show_thoughts ?? default_settings.show_thoughts;
3128 oai_settings.reasoning_effort = settings.reasoning_effort ?? default_settings.reasoning_effort;
3127 oai_settings.seed = settings.seed ?? default_settings.seed;3129 oai_settings.seed = settings.seed ?? default_settings.seed;
3128 oai_settings.n = settings.n ?? default_settings.n;3130 oai_settings.n = settings.n ?? default_settings.n;
31293131
@@ -3253,6 +3255,9 @@ function loadOpenAISettings(data, settings) {
3253 $('#n_openai').val(oai_settings.n);3255 $('#n_openai').val(oai_settings.n);
3254 $('#openai_show_thoughts').prop('checked', oai_settings.show_thoughts);3256 $('#openai_show_thoughts').prop('checked', oai_settings.show_thoughts);
32553257
3258 $('#openai_reasoning_effort').val(oai_settings.reasoning_effort);
3259 $(`#openai_reasoning_effort option[value="${oai_settings.reasoning_effort}"]`).prop('selected', true);
3260
3256 if (settings.reverse_proxy !== undefined) oai_settings.reverse_proxy = settings.reverse_proxy;3261 if (settings.reverse_proxy !== undefined) oai_settings.reverse_proxy = settings.reverse_proxy;
3257 $('#openai_reverse_proxy').val(oai_settings.reverse_proxy);3262 $('#openai_reverse_proxy').val(oai_settings.reverse_proxy);
32583263
@@ -3513,6 +3518,7 @@ async function saveOpenAIPreset(name, settings, triggerUi = true) {
3513 continue_postfix: settings.continue_postfix,3518 continue_postfix: settings.continue_postfix,
3514 function_calling: settings.function_calling,3519 function_calling: settings.function_calling,
3515 show_thoughts: settings.show_thoughts,3520 show_thoughts: settings.show_thoughts,
3521 reasoning_effort: settings.reasoning_effort,
3516 seed: settings.seed,3522 seed: settings.seed,
3517 n: settings.n,3523 n: settings.n,
3518 };3524 };
@@ -3971,6 +3977,7 @@ function onSettingsPresetChange() {
3971 continue_postfix: ['#continue_postfix', 'continue_postfix', false],3977 continue_postfix: ['#continue_postfix', 'continue_postfix', false],
3972 function_calling: ['#openai_function_calling', 'function_calling', true],3978 function_calling: ['#openai_function_calling', 'function_calling', true],
3973 show_thoughts: ['#openai_show_thoughts', 'show_thoughts', true],3979 show_thoughts: ['#openai_show_thoughts', 'show_thoughts', true],
3980 reasoning_effort: ['#openai_reasoning_effort', 'reasoning_effort', false],
3974 seed: ['#seed_openai', 'seed', false],3981 seed: ['#seed_openai', 'seed', false],
3975 n: ['#n_openai', 'n', false],3982 n: ['#n_openai', 'n', false],
3976 };3983 };
@@ -4232,9 +4239,9 @@ async function onModelChange() {
4232 $('#openai_max_context').attr('max', max_2mil);4239 $('#openai_max_context').attr('max', max_2mil);
4233 } else if (value.includes('gemini-exp-1114') || value.includes('gemini-exp-1121') || value.includes('gemini-2.0-flash-thinking-exp-1219')) {4240 } else if (value.includes('gemini-exp-1114') || value.includes('gemini-exp-1121') || value.includes('gemini-2.0-flash-thinking-exp-1219')) {
4234 $('#openai_max_context').attr('max', max_32k);4241 $('#openai_max_context').attr('max', max_32k);
4235 } else if (value.includes('gemini-1.5-pro') || value.includes('gemini-exp-1206')) {4242 } else if (value.includes('gemini-1.5-pro') || value.includes('gemini-exp-1206') || value.includes('gemini-2.0-pro')) {
4236 $('#openai_max_context').attr('max', max_2mil);4243 $('#openai_max_context').attr('max', max_2mil);
4237 } else if (value.includes('gemini-1.5-flash') || value.includes('gemini-2.0-flash-exp') || value.includes('gemini-2.0-flash-thinking-exp')) {4244 } else if (value.includes('gemini-1.5-flash') || value.includes('gemini-2.0-flash')) {
4238 $('#openai_max_context').attr('max', max_1mil);4245 $('#openai_max_context').attr('max', max_1mil);
4239 } else if (value.includes('gemini-1.0-pro') || value === 'gemini-pro') {4246 } else if (value.includes('gemini-1.0-pro') || value === 'gemini-pro') {
4240 $('#openai_max_context').attr('max', max_32k);4247 $('#openai_max_context').attr('max', max_32k);
@@ -4403,24 +4410,30 @@ async function onModelChange() {
4403 if (oai_settings.chat_completion_source == chat_completion_sources.GROQ) {4410 if (oai_settings.chat_completion_source == chat_completion_sources.GROQ) {
4404 if (oai_settings.max_context_unlocked) {4411 if (oai_settings.max_context_unlocked) {
4405 $('#openai_max_context').attr('max', unlocked_max);4412 $('#openai_max_context').attr('max', unlocked_max);
4406 }4413 } else if (oai_settings.groq_model.includes('gemma2-9b-it')) {
4407 else if (oai_settings.groq_model.includes('llama-3.2') && oai_settings.groq_model.includes('-preview')) {
4408 $('#openai_max_context').attr('max', max_8k);4414 $('#openai_max_context').attr('max', max_8k);
4409 }4415 } else if (oai_settings.groq_model.includes('llama-3.3-70b-versatile')) {
4410 else if (oai_settings.groq_model.includes('llama-3.3') || oai_settings.groq_model.includes('llama-3.2') || oai_settings.groq_model.includes('llama-3.1')) {
4411 $('#openai_max_context').attr('max', max_128k);4416 $('#openai_max_context').attr('max', max_128k);
4412 }4417 } else if (oai_settings.groq_model.includes('llama-3.1-8b-instant')) {
4413 else if (oai_settings.groq_model.includes('llama3-groq')) {4418 $('#openai_max_context').attr('max', max_128k);
4419 } else if (oai_settings.groq_model.includes('llama3-70b-8192')) {
4414 $('#openai_max_context').attr('max', max_8k);4420 $('#openai_max_context').attr('max', max_8k);
4415 }4421 } else if (oai_settings.groq_model.includes('llama3-8b-8192')) {
4416 else if (['llama3-8b-8192', 'llama3-70b-8192', 'gemma-7b-it', 'gemma2-9b-it'].includes(oai_settings.groq_model)) {
4417 $('#openai_max_context').attr('max', max_8k);4422 $('#openai_max_context').attr('max', max_8k);
4418 }4423 } else if (oai_settings.groq_model.includes('mixtral-8x7b-32768')) {
4419 else if (['mixtral-8x7b-32768'].includes(oai_settings.groq_model)) {
4420 $('#openai_max_context').attr('max', max_32k);4424 $('#openai_max_context').attr('max', max_32k);
4421 }4425 } else if (oai_settings.groq_model.includes('deepseek-r1-distill-llama-70b')) {
4422 else {4426 $('#openai_max_context').attr('max', max_128k);
4423 $('#openai_max_context').attr('max', max_4k);4427 } else if (oai_settings.groq_model.includes('llama-3.3-70b-specdec')) {
4428 $('#openai_max_context').attr('max', max_8k);
4429 } else if (oai_settings.groq_model.includes('llama-3.2-1b-preview')) {
4430 $('#openai_max_context').attr('max', max_128k);
4431 } else if (oai_settings.groq_model.includes('llama-3.2-3b-preview')) {
4432 $('#openai_max_context').attr('max', max_128k);
4433 } else if (oai_settings.groq_model.includes('llama-3.2-11b-vision-preview')) {
4434 $('#openai_max_context').attr('max', max_128k);
4435 } else if (oai_settings.groq_model.includes('llama-3.2-90b-vision-preview')) {
4436 $('#openai_max_context').attr('max', max_128k);
4424 }4437 }
4425 oai_settings.openai_max_context = Math.min(Number($('#openai_max_context').attr('max')), oai_settings.openai_max_context);4438 oai_settings.openai_max_context = Math.min(Number($('#openai_max_context').attr('max')), oai_settings.openai_max_context);
4426 $('#openai_max_context').val(oai_settings.openai_max_context).trigger('input');4439 $('#openai_max_context').val(oai_settings.openai_max_context).trigger('input');
@@ -4921,6 +4934,12 @@ export function isImageInliningSupported() {
4921 // gultra just isn't being offered as multimodal, thanks google.4934 // gultra just isn't being offered as multimodal, thanks google.
4922 const visionSupportedModels = [4935 const visionSupportedModels = [
4923 'gpt-4-vision',4936 'gpt-4-vision',
4937 'gemini-2.0-pro-exp',
4938 'gemini-2.0-pro-exp-02-05',
4939 'gemini-2.0-flash-lite-preview',
4940 'gemini-2.0-flash-lite-preview-02-05',
4941 'gemini-2.0-flash',
4942 'gemini-2.0-flash-001',
4924 'gemini-2.0-flash-thinking-exp-1219',4943 'gemini-2.0-flash-thinking-exp-1219',
4925 'gemini-2.0-flash-thinking-exp-01-21',4944 'gemini-2.0-flash-thinking-exp-01-21',
4926 'gemini-2.0-flash-thinking-exp',4945 'gemini-2.0-flash-thinking-exp',
@@ -4948,6 +4967,8 @@ export function isImageInliningSupported() {
4948 'gpt-4-turbo',4967 'gpt-4-turbo',
4949 'gpt-4o',4968 'gpt-4o',
4950 'gpt-4o-mini',4969 'gpt-4o-mini',
4970 'o1',
4971 'o1-2024-12-17',
4951 'chatgpt-4o-latest',4972 'chatgpt-4o-latest',
4952 'yi-vision',4973 'yi-vision',
4953 'pixtral-latest',4974 'pixtral-latest',
@@ -5506,6 +5527,11 @@ export function initOpenAI() {
5506 saveSettingsDebounced();5527 saveSettingsDebounced();
5507 });5528 });
55085529
5530 $('#openai_reasoning_effort').on('input', function () {
5531 oai_settings.reasoning_effort = String($(this).val());
5532 saveSettingsDebounced();
5533 });
5534
5509 if (!CSS.supports('field-sizing', 'content')) {5535 if (!CSS.supports('field-sizing', 'content')) {
5510 $(document).on('input', '#openai_settings .autoSetHeight', function () {5536 $(document).on('input', '#openai_settings .autoSetHeight', function () {
5511 resetScrollHeight($(this));5537 resetScrollHeight($(this));
public/scripts/personas.js+6 -5
@@ -25,6 +25,7 @@ import { POPUP_RESULT, POPUP_TYPE, Popup, callGenericPopup } from './popup.js';
25import { t } from './i18n.js';25import { t } from './i18n.js';
26import { openWorldInfoEditor, world_names } from './world-info.js';26import { openWorldInfoEditor, world_names } from './world-info.js';
27import { renderTemplateAsync } from './templates.js';27import { renderTemplateAsync } from './templates.js';
28import { accountStorage } from './util/AccountStorage.js';
2829
29let savePersonasPage = 0;30let savePersonasPage = 0;
30const GRID_STORAGE_KEY = 'Personas_GridView';31const GRID_STORAGE_KEY = 'Personas_GridView';
@@ -34,7 +35,7 @@ export let user_avatar = '';
34export const personasFilter = new FilterHelper(debounce(getUserAvatars, debounce_timeout.quick));35export const personasFilter = new FilterHelper(debounce(getUserAvatars, debounce_timeout.quick));
3536
36function switchPersonaGridView() {37function switchPersonaGridView() {
37 const state = localStorage.getItem(GRID_STORAGE_KEY) === 'true';38 const state = accountStorage.getItem(GRID_STORAGE_KEY) === 'true';
38 $('#user_avatar_block').toggleClass('gridView', state);39 $('#user_avatar_block').toggleClass('gridView', state);
39}40}
4041
@@ -182,7 +183,7 @@ export async function getUserAvatars(doRender = true, openPageAt = '') {
182183
183 const storageKey = 'Personas_PerPage';184 const storageKey = 'Personas_PerPage';
184 const listId = '#user_avatar_block';185 const listId = '#user_avatar_block';
185 const perPage = Number(localStorage.getItem(storageKey)) || 5;186 const perPage = Number(accountStorage.getItem(storageKey)) || 5;
186187
187 $('#persona_pagination_container').pagination({188 $('#persona_pagination_container').pagination({
188 dataSource: entities,189 dataSource: entities,
@@ -205,7 +206,7 @@ export async function getUserAvatars(doRender = true, openPageAt = '') {
205 highlightSelectedAvatar();206 highlightSelectedAvatar();
206 },207 },
207 afterSizeSelectorChange: function (e) {208 afterSizeSelectorChange: function (e) {
208 localStorage.setItem(storageKey, e.target.value);209 accountStorage.setItem(storageKey, e.target.value);
209 },210 },
210 afterPaging: function (e) {211 afterPaging: function (e) {
211 savePersonasPage = e;212 savePersonasPage = e;
@@ -1132,8 +1133,8 @@ export function initPersonas() {
1132 saveSettingsDebounced();1133 saveSettingsDebounced();
1133 });1134 });
1134 $('#persona_grid_toggle').on('click', () => {1135 $('#persona_grid_toggle').on('click', () => {
1135 const state = localStorage.getItem(GRID_STORAGE_KEY) === 'true';1136 const state = accountStorage.getItem(GRID_STORAGE_KEY) === 'true';
1136 localStorage.setItem(GRID_STORAGE_KEY, String(!state));1137 accountStorage.setItem(GRID_STORAGE_KEY, String(!state));
1137 switchPersonaGridView();1138 switchPersonaGridView();
1138 });1139 });
11391140
public/scripts/popup.js+12 -1
@@ -24,6 +24,15 @@ export const POPUP_RESULT = {
24 AFFIRMATIVE: 1,24 AFFIRMATIVE: 1,
25 NEGATIVE: 0,25 NEGATIVE: 0,
26 CANCELLED: null,26 CANCELLED: null,
27 CUSTOM1: 1001,
28 CUSTOM2: 1002,
29 CUSTOM3: 1003,
30 CUSTOM4: 1004,
31 CUSTOM5: 1005,
32 CUSTOM6: 1006,
33 CUSTOM7: 1007,
34 CUSTOM8: 1008,
35 CUSTOM9: 1009,
27};36};
2837
29/**38/**
@@ -37,6 +46,7 @@ export const POPUP_RESULT = {
37 * @property {boolean?} [transparent=false] - Whether to display the popup in transparent mode (no background, border, shadow or anything, only its content)46 * @property {boolean?} [transparent=false] - Whether to display the popup in transparent mode (no background, border, shadow or anything, only its content)
38 * @property {boolean?} [allowHorizontalScrolling=false] - Whether to allow horizontal scrolling in the popup47 * @property {boolean?} [allowHorizontalScrolling=false] - Whether to allow horizontal scrolling in the popup
39 * @property {boolean?} [allowVerticalScrolling=false] - Whether to allow vertical scrolling in the popup48 * @property {boolean?} [allowVerticalScrolling=false] - Whether to allow vertical scrolling in the popup
49 * @property {boolean?} [leftAlign=false] - Whether the popup content should be left-aligned by default
40 * @property {'slow'|'fast'|'none'?} [animation='slow'] - Animation speed for the popup (opening, closing, ...)50 * @property {'slow'|'fast'|'none'?} [animation='slow'] - Animation speed for the popup (opening, closing, ...)
41 * @property {POPUP_RESULT|number?} [defaultResult=POPUP_RESULT.AFFIRMATIVE] - The default result of this popup when Enter is pressed. Can be changed from `POPUP_RESULT.AFFIRMATIVE`.51 * @property {POPUP_RESULT|number?} [defaultResult=POPUP_RESULT.AFFIRMATIVE] - The default result of this popup when Enter is pressed. Can be changed from `POPUP_RESULT.AFFIRMATIVE`.
42 * @property {CustomPopupButton[]|string[]?} [customButtons=null] - Custom buttons to add to the popup. If only strings are provided, the buttons will be added with default options, and their result will be in order from `2` onward.52 * @property {CustomPopupButton[]|string[]?} [customButtons=null] - Custom buttons to add to the popup. If only strings are provided, the buttons will be added with default options, and their result will be in order from `2` onward.
@@ -164,7 +174,7 @@ export class Popup {
164 * @param {string} [inputValue=''] - The initial value of the input field174 * @param {string} [inputValue=''] - The initial value of the input field
165 * @param {PopupOptions} [options={}] - Additional options for the popup175 * @param {PopupOptions} [options={}] - Additional options for the popup
166 */176 */
167 constructor(content, type, inputValue = '', { okButton = null, cancelButton = null, rows = 1, wide = false, wider = false, large = false, transparent = false, allowHorizontalScrolling = false, allowVerticalScrolling = false, animation = 'fast', defaultResult = POPUP_RESULT.AFFIRMATIVE, customButtons = null, customInputs = null, onClosing = null, onClose = null, cropAspect = null, cropImage = null } = {}) {177 constructor(content, type, inputValue = '', { okButton = null, cancelButton = null, rows = 1, wide = false, wider = false, large = false, transparent = false, allowHorizontalScrolling = false, allowVerticalScrolling = false, leftAlign = false, animation = 'fast', defaultResult = POPUP_RESULT.AFFIRMATIVE, customButtons = null, customInputs = null, onClosing = null, onClose = null, cropAspect = null, cropImage = null } = {}) {
168 Popup.util.popups.push(this);178 Popup.util.popups.push(this);
169179
170 // Make this popup uniquely identifiable180 // Make this popup uniquely identifiable
@@ -209,6 +219,7 @@ export class Popup {
209 if (transparent) this.dlg.classList.add('transparent_dialogue_popup');219 if (transparent) this.dlg.classList.add('transparent_dialogue_popup');
210 if (allowHorizontalScrolling) this.dlg.classList.add('horizontal_scrolling_dialogue_popup');220 if (allowHorizontalScrolling) this.dlg.classList.add('horizontal_scrolling_dialogue_popup');
211 if (allowVerticalScrolling) this.dlg.classList.add('vertical_scrolling_dialogue_popup');221 if (allowVerticalScrolling) this.dlg.classList.add('vertical_scrolling_dialogue_popup');
222 if (leftAlign) this.dlg.classList.add('left_aligned_dialogue_popup');
212 if (animation) this.dlg.classList.add('popup--animation-' + animation);223 if (animation) this.dlg.classList.add('popup--animation-' + animation);
213224
214 // If custom button captions are provided, we set them beforehand225 // If custom button captions are provided, we set them beforehand
public/scripts/power-user.js+6 -3
@@ -54,6 +54,7 @@ import { commonEnumProviders, enumIcons } from './slash-commands/SlashCommandCom
54import { POPUP_TYPE, callGenericPopup } from './popup.js';54import { POPUP_TYPE, callGenericPopup } from './popup.js';
55import { loadSystemPrompts } from './sysprompt.js';55import { loadSystemPrompts } from './sysprompt.js';
56import { fuzzySearchCategories } from './filters.js';56import { fuzzySearchCategories } from './filters.js';
57import { accountStorage } from './util/AccountStorage.js';
5758
58export {59export {
59 loadPowerUserSettings,60 loadPowerUserSettings,
@@ -256,6 +257,8 @@ let power_user = {
256 reasoning: {257 reasoning: {
257 auto_parse: false,258 auto_parse: false,
258 add_to_prompts: false,259 add_to_prompts: false,
260 auto_expand: false,
261 show_hidden: false,
259 prefix: '<think>\n',262 prefix: '<think>\n',
260 suffix: '\n</think>',263 suffix: '\n</think>',
261 separator: '\n\n',264 separator: '\n\n',
@@ -2019,7 +2022,7 @@ export function renderStoryString(params) {
2019 */2022 */
2020function validateStoryString(storyString, params) {2023function validateStoryString(storyString, params) {
2021 /** @type {{hashCache: {[hash: string]: {fieldsWarned: {[key: string]: boolean}}}}} */2024 /** @type {{hashCache: {[hash: string]: {fieldsWarned: {[key: string]: boolean}}}}} */
2022 const cache = JSON.parse(localStorage.getItem(storage_keys.storyStringValidationCache)) ?? { hashCache: {} };2025 const cache = JSON.parse(accountStorage.getItem(storage_keys.storyStringValidationCache)) ?? { hashCache: {} };
20232026
2024 const hash = getStringHash(storyString);2027 const hash = getStringHash(storyString);
20252028
@@ -2056,7 +2059,7 @@ function validateStoryString(storyString, params) {
2056 toastr.warning(`The story string does not contain the following fields, but they would contain content: ${fieldsList}`, 'Story String Validation');2059 toastr.warning(`The story string does not contain the following fields, but they would contain content: ${fieldsList}`, 'Story String Validation');
2057 }2060 }
20582061
2059 localStorage.setItem(storage_keys.storyStringValidationCache, JSON.stringify(cache));2062 accountStorage.setItem(storage_keys.storyStringValidationCache, JSON.stringify(cache));
2060}2063}
20612064
20622065
@@ -2451,7 +2454,7 @@ async function resetMovablePanels(type) {
2451 }2454 }
24522455
2453 saveSettingsDebounced();2456 saveSettingsDebounced();
2454 eventSource.emit(event_types.MOVABLE_PANELS_RESET);2457 await eventSource.emit(event_types.MOVABLE_PANELS_RESET);
24552458
2456 eventSource.once(event_types.SETTINGS_UPDATED, () => {2459 eventSource.once(event_types.SETTINGS_UPDATED, () => {
2457 $('.resizing').removeClass('resizing');2460 $('.resizing').removeClass('resizing');
public/scripts/preset-manager.js+2 -0
@@ -587,6 +587,8 @@ class PresetManager {
587 'derived',587 'derived',
588 'generic_model',588 'generic_model',
589 'include_reasoning',589 'include_reasoning',
590 'global_banned_tokens',
591 'send_banned_tokens',
590 ];592 ];
591 const settings = Object.assign({}, getSettingsByApiId(this.apiId));593 const settings = Object.assign({}, getSettingsByApiId(this.apiId));
592594
public/scripts/reasoning.js+615 -28
@@ -1,14 +1,32 @@
1import { chat, closeMessageEditor, event_types, eventSource, saveChatConditional, saveSettingsDebounced, substituteParams, updateMessageBlock } from '../script.js';1import {
2 moment,
3} from '../lib.js';
4import { chat, closeMessageEditor, event_types, eventSource, main_api, messageFormatting, saveChatConditional, saveSettingsDebounced, substituteParams, updateMessageBlock } from '../script.js';
2import { getRegexedString, regex_placement } from './extensions/regex/engine.js';5import { getRegexedString, regex_placement } from './extensions/regex/engine.js';
3import { t } from './i18n.js';6import { getCurrentLocale, t, translate } from './i18n.js';
4import { MacrosParser } from './macros.js';7import { MacrosParser } from './macros.js';
8import { chat_completion_sources, getChatCompletionModel, oai_settings } from './openai.js';
5import { Popup } from './popup.js';9import { Popup } from './popup.js';
6import { power_user } from './power-user.js';10import { power_user } from './power-user.js';
7import { SlashCommand } from './slash-commands/SlashCommand.js';11import { SlashCommand } from './slash-commands/SlashCommand.js';
8import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from './slash-commands/SlashCommandArgument.js';12import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from './slash-commands/SlashCommandArgument.js';
9import { commonEnumProviders } from './slash-commands/SlashCommandCommonEnumsProvider.js';13import { commonEnumProviders, enumIcons } from './slash-commands/SlashCommandCommonEnumsProvider.js';
14import { enumTypes, SlashCommandEnumValue } from './slash-commands/SlashCommandEnumValue.js';
10import { SlashCommandParser } from './slash-commands/SlashCommandParser.js';15import { SlashCommandParser } from './slash-commands/SlashCommandParser.js';
11import { copyText, escapeRegex, isFalseBoolean } from './utils.js';16import { textgen_types, textgenerationwebui_settings } from './textgen-settings.js';
17import { copyText, escapeRegex, isFalseBoolean, setDatasetProperty, trimSpaces } from './utils.js';
18
19/**
20 * Enum representing the type of the reasoning for a message (where it came from)
21 * @enum {string}
22 * @readonly
23 */
24export const ReasoningType = {
25 Model: 'model',
26 Parsed: 'parsed',
27 Manual: 'manual',
28 Edited: 'edited',
29};
1230
13/**31/**
14 * Gets a message from a jQuery element.32 * Gets a message from a jQuery element.
@@ -23,12 +41,467 @@ function getMessageFromJquery(element) {
23}41}
2442
25/**43/**
44 * Toggles the auto-expand state of reasoning blocks.
45 */
46function toggleReasoningAutoExpand() {
47 const reasoningBlocks = document.querySelectorAll('details.mes_reasoning_details');
48 reasoningBlocks.forEach((block) => {
49 if (block instanceof HTMLDetailsElement) {
50 block.open = power_user.reasoning.auto_expand;
51 }
52 });
53}
54
55/**
56 * Extracts the reasoning from the response data.
57 * @param {object} data Response data
58 * @returns {string} Extracted reasoning
59 */
60export function extractReasoningFromData(data) {
61 switch (main_api) {
62 case 'textgenerationwebui':
63 switch (textgenerationwebui_settings.type) {
64 case textgen_types.OPENROUTER:
65 return data?.choices?.[0]?.reasoning ?? '';
66 }
67 break;
68
69 case 'openai':
70 if (!oai_settings.show_thoughts) break;
71
72 switch (oai_settings.chat_completion_source) {
73 case chat_completion_sources.DEEPSEEK:
74 return data?.choices?.[0]?.message?.reasoning_content ?? '';
75 case chat_completion_sources.OPENROUTER:
76 return data?.choices?.[0]?.message?.reasoning ?? '';
77 case chat_completion_sources.MAKERSUITE:
78 return data?.responseContent?.parts?.filter(part => part.thought)?.map(part => part.text)?.join('\n\n') ?? '';
79 }
80 break;
81 }
82
83 return '';
84}
85
86/**
87 * Check if the model supports reasoning, but does not send back the reasoning
88 * @returns {boolean} True if the model supports reasoning
89 */
90export function isHiddenReasoningModel() {
91 if (main_api !== 'openai') {
92 return false;
93 }
94
95 /** @typedef {{ (currentModel: string, supportedModel: string): boolean }} MatchingFunc */
96 /** @type {Record.<string, MatchingFunc>} */
97 const FUNCS = {
98 equals: (currentModel, supportedModel) => currentModel === supportedModel,
99 startsWith: (currentModel, supportedModel) => currentModel.startsWith(supportedModel),
100 };
101
102 /** @type {{ name: string; func: MatchingFunc; }[]} */
103 const hiddenReasoningModels = [
104 { name: 'o1', func: FUNCS.startsWith },
105 { name: 'o3', func: FUNCS.startsWith },
106 { name: 'gemini-2.0-flash-thinking-exp', func: FUNCS.startsWith },
107 { name: 'gemini-2.0-pro-exp', func: FUNCS.startsWith },
108 ];
109
110 const model = getChatCompletionModel() || '';
111
112 const isHidden = hiddenReasoningModels.some(({ name, func }) => func(model, name));
113 return isHidden;
114}
115
116/**
117 * Updates the Reasoning UI for a specific message
118 * @param {number|JQuery<HTMLElement>|HTMLElement} messageIdOrElement The message ID or the message element
119 * @param {Object} [options={}] - Optional arguments
120 * @param {boolean} [options.reset=false] - Whether to reset state, and not take the current mess properties (for example when swiping)
121 */
122export function updateReasoningUI(messageIdOrElement, { reset = false } = {}) {
123 const handler = new ReasoningHandler();
124 handler.initHandleMessage(messageIdOrElement, { reset });
125}
126
127
128/**
129 * Enum for representing the state of reasoning
130 * @enum {string}
131 * @readonly
132 */
133export const ReasoningState = {
134 None: 'none',
135 Thinking: 'thinking',
136 Done: 'done',
137 Hidden: 'hidden',
138};
139
140/**
141 * Handles reasoning-specific logic and DOM updates for messages.
142 * This class is used inside the {@link StreamingProcessor} to manage reasoning states and UI updates.
143 */
144export class ReasoningHandler {
145 /** @type {boolean} True if the model supports reasoning, but hides the reasoning output */
146 #isHiddenReasoningModel;
147 /** @type {boolean} True if the handler is currently handling a manual parse of reasoning blocks */
148 #isParsingReasoning = false;
149 /** @type {number?} When reasoning is being parsed manually, and the reasoning has ended, this will be the index at which the actual messages starts */
150 #parsingReasoningMesStartIndex = null;
151
152 /**
153 * @param {Date?} [timeStarted=null] - When the generation started
154 */
155 constructor(timeStarted = null) {
156 /** @type {ReasoningState} The current state of the reasoning process */
157 this.state = ReasoningState.None;
158 /** @type {ReasoningType?} The type of the reasoning (where it came from) */
159 this.type = null;
160 /** @type {string} The reasoning output */
161 this.reasoning = '';
162 /** @type {Date} When the reasoning started */
163 this.startTime = null;
164 /** @type {Date} When the reasoning ended */
165 this.endTime = null;
166
167 /** @type {Date} Initial starting time of the generation */
168 this.initialTime = timeStarted ?? new Date();
169
170 this.#isHiddenReasoningModel = isHiddenReasoningModel();
171
172 // Cached DOM elements for reasoning
173 /** @type {HTMLElement} Main message DOM element `.mes` */
174 this.messageDom = null;
175 /** @type {HTMLDetailsElement} Reasoning details DOM element `.mes_reasoning_details` */
176 this.messageReasoningDetailsDom = null;
177 /** @type {HTMLElement} Reasoning content DOM element `.mes_reasoning` */
178 this.messageReasoningContentDom = null;
179 /** @type {HTMLElement} Reasoning header DOM element `.mes_reasoning_header_title` */
180 this.messageReasoningHeaderDom = null;
181 }
182
183 /**
184 * Initializes the reasoning handler for a specific message.
185 *
186 * Can be used to update the DOM elements or read other reasoning states.
187 * It will internally take the message-saved data and write the states back into the handler, as if during streaming of the message.
188 * The state will always be either done/hidden or none.
189 *
190 * @param {number|JQuery<HTMLElement>|HTMLElement} messageIdOrElement - The message ID or the message element
191 * @param {Object} [options={}] - Optional arguments
192 * @param {boolean} [options.reset=false] - Whether to reset state of the handler, and not take the current mess properties (for example when swiping)
193 */
194 initHandleMessage(messageIdOrElement, { reset = false } = {}) {
195 /** @type {HTMLElement} */
196 const messageElement = typeof messageIdOrElement === 'number'
197 ? document.querySelector(`#chat [mesid="${messageIdOrElement}"]`)
198 : messageIdOrElement instanceof HTMLElement
199 ? messageIdOrElement
200 : $(messageIdOrElement)[0];
201 const messageId = Number(messageElement.getAttribute('mesid'));
202
203 if (isNaN(messageId) || !chat[messageId]) return;
204
205 if (!chat[messageId].extra) {
206 chat[messageId].extra = {};
207 }
208 const extra = chat[messageId].extra;
209
210 if (extra.reasoning) {
211 this.state = ReasoningState.Done;
212 } else if (extra.reasoning_duration) {
213 this.state = ReasoningState.Hidden;
214 }
215
216 this.type = extra?.reasoning_type;
217 this.reasoning = extra?.reasoning ?? '';
218
219 if (this.state !== ReasoningState.None) {
220 this.initialTime = new Date(chat[messageId].gen_started);
221 this.startTime = this.initialTime;
222 this.endTime = new Date(this.startTime.getTime() + (extra?.reasoning_duration ?? 0));
223 }
224
225 // Prefill main dom element, as message might not have been rendered yet
226 this.messageDom = messageElement;
227
228 // Make sure reset correctly clears all relevant states
229 if (reset) {
230 this.state = this.#isHiddenReasoningModel ? ReasoningState.Thinking : ReasoningState.None;
231 this.type = null;
232 this.reasoning = '';
233 this.initialTime = new Date();
234 this.startTime = null;
235 this.endTime = null;
236 }
237
238 this.updateDom(messageId);
239
240 if (power_user.reasoning.auto_expand && this.state !== ReasoningState.Hidden) {
241 this.messageReasoningDetailsDom.open = true;
242 }
243 }
244
245 /**
246 * Gets the duration of the reasoning in milliseconds.
247 *
248 * @returns {number?} The duration in milliseconds, or null if the start or end time is not set
249 */
250 getDuration() {
251 if (this.startTime && this.endTime) {
252 return this.endTime.getTime() - this.startTime.getTime();
253 }
254 return null;
255 }
256
257 /**
258 * Updates the reasoning text/string for a message.
259 *
260 * @param {number} messageId - The ID of the message to update
261 * @param {string?} [reasoning=null] - The reasoning text to update - If null or empty, uses the current reasoning
262 * @param {Object} [options={}] - Optional arguments
263 * @param {boolean} [options.persist=false] - Whether to persist the reasoning to the message object
264 * @param {boolean} [options.allowReset=false] - Whether to allow empty reasoning provided to reset the reasoning, instead of just taking the existing one
265 * @returns {boolean} - Returns true if the reasoning was changed, otherwise false
266 */
267 updateReasoning(messageId, reasoning = null, { persist = false, allowReset = false } = {}) {
268 if (messageId == -1 || !chat[messageId]) {
269 return false;
270 }
271
272 reasoning = allowReset ? reasoning ?? this.reasoning : reasoning || this.reasoning;
273 reasoning = trimSpaces(reasoning);
274
275 // Ensure the chat extra exists
276 if (!chat[messageId].extra) {
277 chat[messageId].extra = {};
278 }
279 const extra = chat[messageId].extra;
280
281 const reasoningChanged = extra.reasoning !== reasoning;
282 this.reasoning = getRegexedString(reasoning ?? '', regex_placement.REASONING);
283
284 this.type = (this.#isParsingReasoning || this.#parsingReasoningMesStartIndex) ? ReasoningType.Parsed : ReasoningType.Model;
285
286 if (persist) {
287 // Build and save the reasoning data to message extras
288 extra.reasoning = this.reasoning;
289 extra.reasoning_duration = this.getDuration();
290 extra.reasoning_type = (this.#isParsingReasoning || this.#parsingReasoningMesStartIndex) ? ReasoningType.Parsed : ReasoningType.Model;
291 }
292
293 return reasoningChanged;
294 }
295
296
297 /**
298 * Handles processing of reasoning for a message.
299 *
300 * This is usually called by the message processor when a message is changed.
301 *
302 * @param {number} messageId - The ID of the message to process
303 * @param {boolean} mesChanged - Whether the message has changed
304 * @returns {Promise<void>}
305 */
306 async process(messageId, mesChanged) {
307 mesChanged = this.#autoParseReasoningFromMessage(messageId, mesChanged);
308
309 if (!this.reasoning && !this.#isHiddenReasoningModel)
310 return;
311
312 // Ensure reasoning string is updated and regexes are applied correctly
313 const reasoningChanged = this.updateReasoning(messageId, null, { persist: true });
314
315 if ((this.#isHiddenReasoningModel || reasoningChanged) && this.state === ReasoningState.None) {
316 this.state = ReasoningState.Thinking;
317 this.startTime = this.initialTime;
318 }
319 if ((this.#isHiddenReasoningModel || !reasoningChanged) && mesChanged && this.state === ReasoningState.Thinking) {
320 this.endTime = new Date();
321 await this.finish(messageId);
322 }
323 }
324
325 #autoParseReasoningFromMessage(messageId, mesChanged) {
326 if (!power_user.reasoning.auto_parse)
327 return;
328 if (!power_user.reasoning.prefix || !power_user.reasoning.suffix)
329 return mesChanged;
330
331 /** @type {{ mes: string, [key: string]: any}} */
332 const message = chat[messageId];
333 if (!message) return mesChanged;
334
335 // If we are done with reasoning parse, we just split the message correctly so the reasoning doesn't show up inside of it.
336 if (this.#parsingReasoningMesStartIndex) {
337 message.mes = trimSpaces(message.mes.slice(this.#parsingReasoningMesStartIndex));
338 return mesChanged;
339 }
340
341 if (this.state === ReasoningState.None) {
342 // If streamed message starts with the opening, cut it out and put all inside reasoning
343 if (message.mes.startsWith(power_user.reasoning.prefix) && message.mes.length > power_user.reasoning.prefix.length) {
344 this.#isParsingReasoning = true;
345
346 // Manually set starting state here, as we might already have received the ending suffix
347 this.state = ReasoningState.Thinking;
348 this.startTime = this.initialTime;
349 }
350 }
351
352 if (!this.#isParsingReasoning)
353 return mesChanged;
354
355 // If we are in manual parsing mode, all currently streaming mes tokens will go the the reasoning block
356 const originalMes = message.mes;
357 this.reasoning = originalMes.slice(power_user.reasoning.prefix.length);
358 message.mes = '';
359
360 // If the reasoning contains the ending suffix, we cut that off and continue as message streaming
361 if (this.reasoning.includes(power_user.reasoning.suffix)) {
362 this.reasoning = this.reasoning.slice(0, this.reasoning.indexOf(power_user.reasoning.suffix));
363 this.#parsingReasoningMesStartIndex = originalMes.indexOf(power_user.reasoning.suffix) + power_user.reasoning.suffix.length;
364 message.mes = trimSpaces(originalMes.slice(this.#parsingReasoningMesStartIndex));
365 this.#isParsingReasoning = false;
366 }
367
368 // Only return the original mesChanged value if we haven't cut off the complete message
369 return message.mes.length ? mesChanged : false;
370 }
371
372 /**
373 * Completes the reasoning process for a message.
374 *
375 * Records the finish time if it was not set during streaming and updates the reasoning state.
376 * Emits an event to signal the completion of reasoning and updates the DOM elements accordingly.
377 *
378 * @param {number} messageId - The ID of the message to complete reasoning for
379 * @returns {Promise<void>}
380 */
381 async finish(messageId) {
382 if (this.state === ReasoningState.None) return;
383
384 // Make sure the finish time is recorded if a reasoning was in process and it wasn't ended correctly during streaming
385 if (this.startTime !== null && this.endTime === null) {
386 this.endTime = new Date();
387 }
388
389 if (this.state === ReasoningState.Thinking) {
390 this.state = this.#isHiddenReasoningModel ? ReasoningState.Hidden : ReasoningState.Done;
391 this.updateReasoning(messageId, null, { persist: true });
392 await eventSource.emit(event_types.STREAM_REASONING_DONE, this.reasoning, this.getDuration(), messageId, this.state);
393 }
394
395 this.updateDom(messageId);
396 }
397
398 /**
399 * Updates the reasoning UI elements for a message.
400 *
401 * Toggles the CSS class, updates states, reasoning message, and duration.
402 *
403 * @param {number} messageId - The ID of the message to update
404 */
405 updateDom(messageId) {
406 this.#checkDomElements(messageId);
407
408 // Main CSS class to show this message includes reasoning
409 this.messageDom.classList.toggle('reasoning', this.state !== ReasoningState.None);
410
411 // Update states to the relevant DOM elements
412 setDatasetProperty(this.messageDom, 'reasoningState', this.state !== ReasoningState.None ? this.state : null);
413 setDatasetProperty(this.messageReasoningDetailsDom, 'state', this.state);
414 setDatasetProperty(this.messageReasoningDetailsDom, 'type', this.type);
415
416 // Update the reasoning message
417 const reasoning = trimSpaces(this.reasoning);
418 const displayReasoning = messageFormatting(reasoning, '', false, false, messageId, {}, true);
419 this.messageReasoningContentDom.innerHTML = displayReasoning;
420
421 // Update tooltip for hidden reasoning edit
422 /** @type {HTMLElement} */
423 const button = this.messageDom.querySelector('.mes_edit_add_reasoning');
424 button.title = this.state === ReasoningState.Hidden ? t`Hidden reasoning - Add reasoning block` : t`Add reasoning block`;
425
426 // Make sure that hidden reasoning headers are collapsed by default, to not show a useless edit button
427 if (this.state === ReasoningState.Hidden) {
428 this.messageReasoningDetailsDom.open = false;
429 }
430
431 // Update the reasoning duration in the UI
432 this.#updateReasoningTimeUI();
433 }
434
435 /**
436 * Finds and caches reasoning-related DOM elements for the given message.
437 *
438 * @param {number} messageId - The ID of the message to cache the DOM elements for
439 */
440 #checkDomElements(messageId) {
441 // Make sure we reset dom elements if we are checking for a different message (shouldn't happen, but be sure)
442 if (this.messageDom !== null && this.messageDom.getAttribute('mesid') !== messageId.toString()) {
443 this.messageDom = null;
444 }
445
446 // Cache the DOM elements once
447 if (this.messageDom === null) {
448 this.messageDom = document.querySelector(`#chat .mes[mesid="${messageId}"]`);
449 if (this.messageDom === null) throw new Error('message dom does not exist');
450 }
451 if (this.messageReasoningDetailsDom === null) {
452 this.messageReasoningDetailsDom = this.messageDom.querySelector('.mes_reasoning_details');
453 }
454 if (this.messageReasoningContentDom === null) {
455 this.messageReasoningContentDom = this.messageDom.querySelector('.mes_reasoning');
456 }
457 if (this.messageReasoningHeaderDom === null) {
458 this.messageReasoningHeaderDom = this.messageDom.querySelector('.mes_reasoning_header_title');
459 }
460 }
461
462 /**
463 * Updates the reasoning time display in the UI.
464 *
465 * Shows the duration in a human-readable format with a tooltip for exact seconds.
466 * Displays "Thinking..." if still processing, or a generic message otherwise.
467 */
468 #updateReasoningTimeUI() {
469 const element = this.messageReasoningHeaderDom;
470 const duration = this.getDuration();
471 let data = null;
472 let title = '';
473 if (duration) {
474 const seconds = moment.duration(duration).asSeconds();
475
476 const durationStr = moment.duration(duration).locale(getCurrentLocale()).humanize({ s: 50, ss: 3 });
477 element.textContent = t`Thought for ${durationStr}`;
478 data = String(seconds);
479 title = `${seconds} seconds`;
480 } else if ([ReasoningState.Done, ReasoningState.Hidden].includes(this.state)) {
481 element.textContent = t`Thought for some time`;
482 data = 'unknown';
483 } else {
484 element.textContent = t`Thinking...`;
485 data = null;
486 }
487
488 if (this.type !== ReasoningType.Model) {
489 title += ` [${translate(this.type)}]`;
490 title = title.trim();
491 }
492 element.title = title;
493
494 setDatasetProperty(this.messageReasoningDetailsDom, 'duration', data);
495 setDatasetProperty(element, 'duration', data);
496 }
497}
498
499/**
26 * Helper class for adding reasoning to messages.500 * Helper class for adding reasoning to messages.
27 * Keeps track of the number of reasoning additions.501 * Keeps track of the number of reasoning additions.
28 */502 */
29export class PromptReasoning {503export class PromptReasoning {
30 static REASONING_PLACEHOLDER = '\u200B';504 static REASONING_PLACEHOLDER = '\u200B';
31 static REASONING_PLACEHOLDER_REGEX = new RegExp(`${PromptReasoning.REASONING_PLACEHOLDER}$`);
32505
33 constructor() {506 constructor() {
34 this.counter = 0;507 this.counter = 0;
@@ -59,7 +532,7 @@ export class PromptReasoning {
59 return content;532 return content;
60 }533 }
61534
62 // No reasoning provided or a placeholder535 // No reasoning provided or a legacy placeholder
63 if (!reasoning || reasoning === PromptReasoning.REASONING_PLACEHOLDER) {536 if (!reasoning || reasoning === PromptReasoning.REASONING_PLACEHOLDER) {
64 return content;537 return content;
65 }538 }
@@ -118,6 +591,22 @@ function loadReasoningSettings() {
118 power_user.reasoning.auto_parse = !!$(this).prop('checked');591 power_user.reasoning.auto_parse = !!$(this).prop('checked');
119 saveSettingsDebounced();592 saveSettingsDebounced();
120 });593 });
594
595 $('#reasoning_auto_expand').prop('checked', power_user.reasoning.auto_expand);
596 $('#reasoning_auto_expand').on('change', function () {
597 power_user.reasoning.auto_expand = !!$(this).prop('checked');
598 toggleReasoningAutoExpand();
599 saveSettingsDebounced();
600 });
601 toggleReasoningAutoExpand();
602
603 $('#reasoning_show_hidden').prop('checked', power_user.reasoning.show_hidden);
604 $('#reasoning_show_hidden').on('change', function () {
605 power_user.reasoning.show_hidden = !!$(this).prop('checked');
606 $('#chat').attr('data-show-hidden-reasoning', power_user.reasoning.show_hidden ? 'true' : null);
607 saveSettingsDebounced();
608 });
609 $('#chat').attr('data-show-hidden-reasoning', power_user.reasoning.show_hidden ? 'true' : null);
121}610}
122611
123function registerReasoningSlashCommands() {612function registerReasoningSlashCommands() {
@@ -134,10 +623,10 @@ function registerReasoningSlashCommands() {
134 }),623 }),
135 ],624 ],
136 callback: (_args, value) => {625 callback: (_args, value) => {
137 const messageId = !isNaN(Number(value)) ? Number(value) : chat.length - 1;626 const messageId = !isNaN(parseInt(value.toString())) ? parseInt(value.toString()) : chat.length - 1;
138 const message = chat[messageId];627 const message = chat[messageId];
139 const reasoning = String(message?.extra?.reasoning ?? '');628 const reasoning = String(message?.extra?.reasoning ?? '');
140 return reasoning.replace(PromptReasoning.REASONING_PLACEHOLDER_REGEX, '');629 return reasoning;
141 },630 },
142 }));631 }));
143632
@@ -163,11 +652,16 @@ function registerReasoningSlashCommands() {
163 callback: async (args, value) => {652 callback: async (args, value) => {
164 const messageId = !isNaN(Number(args.at)) ? Number(args.at) : chat.length - 1;653 const messageId = !isNaN(Number(args.at)) ? Number(args.at) : chat.length - 1;
165 const message = chat[messageId];654 const message = chat[messageId];
166 if (!message?.extra) {655 if (!message) {
167 return '';656 return '';
168 }657 }
658 // Make sure the message has an extra object
659 if (!message.extra || typeof message.extra !== 'object') {
660 message.extra = {};
661 }
169662
170 message.extra.reasoning = String(value ?? '');663 message.extra.reasoning = String(value ?? '');
664 message.extra.reasoning_type = ReasoningType.Manual;
171 await saveChatConditional();665 await saveChatConditional();
172666
173 closeMessageEditor('reasoning');667 closeMessageEditor('reasoning');
@@ -188,7 +682,26 @@ function registerReasoningSlashCommands() {
188 typeList: [ARGUMENT_TYPE.BOOLEAN],682 typeList: [ARGUMENT_TYPE.BOOLEAN],
189 defaultValue: 'true',683 defaultValue: 'true',
190 isRequired: false,684 isRequired: false,
191 enumProvider: commonEnumProviders.boolean('trueFalse'),685 enumList: commonEnumProviders.boolean('trueFalse')(),
686 }),
687 SlashCommandNamedArgument.fromProps({
688 name: 'return',
689 description: 'Whether to return the parsed reasoning or the content without reasoning',
690 typeList: [ARGUMENT_TYPE.STRING],
691 defaultValue: 'reasoning',
692 isRequired: false,
693 enumList: [
694 new SlashCommandEnumValue('reasoning', null, enumTypes.enum, enumIcons.reasoning),
695 new SlashCommandEnumValue('content', null, enumTypes.enum, enumIcons.message),
696 ],
697 }),
698 SlashCommandNamedArgument.fromProps({
699 name: 'strict',
700 description: 'Whether to require the reasoning block to be at the beginning of the string (excluding whitespaces).',
701 typeList: [ARGUMENT_TYPE.BOOLEAN],
702 defaultValue: 'true',
703 isRequired: false,
704 enumList: commonEnumProviders.boolean('trueFalse')(),
192 }),705 }),
193 ],706 ],
194 unnamedArgumentList: [707 unnamedArgumentList: [
@@ -198,19 +711,27 @@ function registerReasoningSlashCommands() {
198 }),711 }),
199 ],712 ],
200 callback: (args, value) => {713 callback: (args, value) => {
201 if (!value) {714 if (!value || typeof value !== 'string') {
202 return '';715 return '';
203 }716 }
204717
205 if (!power_user.reasoning.prefix || !power_user.reasoning.suffix) {718 if (!power_user.reasoning.prefix || !power_user.reasoning.suffix) {
206 toastr.warning(t`Both prefix and suffix must be set in the Reasoning Formatting settings.`);719 toastr.warning(t`Both prefix and suffix must be set in the Reasoning Formatting settings.`, t`Reasoning Parse`);
207 return String(value);720 return value;
721 }
722 if (typeof args.return !== 'string' || !['reasoning', 'content'].includes(args.return)) {
723 toastr.warning(t`Invalid return type '${args.return}', defaulting to 'reasoning'.`, t`Reasoning Parse`);
208 }724 }
209725
210 const parsedReasoning = parseReasoningFromString(String(value));726 const returnMessage = args.return === 'content';
211727
728 const parsedReasoning = parseReasoningFromString(value, { strict: !isFalseBoolean(String(args.strict ?? '')) });
212 if (!parsedReasoning) {729 if (!parsedReasoning) {
213 return '';730 return returnMessage ? value : '';
731 }
732
733 if (returnMessage) {
734 return parsedReasoning.content;
214 }735 }
215736
216 const applyRegex = !isFalseBoolean(String(args.regex ?? ''));737 const applyRegex = !isFalseBoolean(String(args.regex ?? ''));
@@ -228,6 +749,31 @@ function registerReasoningMacros() {
228}749}
229750
230function setReasoningEventHandlers() {751function setReasoningEventHandlers() {
752 $(document).on('click', '.mes_reasoning_details', function (e) {
753 if (!e.target.closest('.mes_reasoning_actions') && !e.target.closest('.mes_reasoning_header')) {
754 e.preventDefault();
755 }
756 });
757
758 $(document).on('click', '.mes_reasoning_header', function (e) {
759 const details = $(this).closest('.mes_reasoning_details');
760 // Along with the CSS rules to mark blocks not toggle-able when they are empty, prevent them from actually being toggled, or being edited
761 if (details.find('.mes_reasoning').is(':empty')) {
762 e.preventDefault();
763 return;
764 }
765
766 // If we are in message edit mode and reasoning area is closed, a click opens and edits it
767 const mes = $(this).closest('.mes');
768 const mesEditArea = mes.find('#curEditTextarea');
769 if (mesEditArea.length) {
770 const summary = $(mes).find('.mes_reasoning_summary');
771 if (!summary.attr('open')) {
772 summary.find('.mes_reasoning_edit').trigger('click');
773 }
774 }
775 });
776
231 $(document).on('click', '.mes_reasoning_copy', (e) => {777 $(document).on('click', '.mes_reasoning_copy', (e) => {
232 e.stopPropagation();778 e.stopPropagation();
233 e.preventDefault();779 e.preventDefault();
@@ -246,7 +792,7 @@ function setReasoningEventHandlers() {
246 const textarea = document.createElement('textarea');792 const textarea = document.createElement('textarea');
247 const reasoningBlock = messageBlock.find('.mes_reasoning');793 const reasoningBlock = messageBlock.find('.mes_reasoning');
248 textarea.classList.add('reasoning_edit_textarea');794 textarea.classList.add('reasoning_edit_textarea');
249 textarea.value = reasoning.replace(PromptReasoning.REASONING_PLACEHOLDER_REGEX, '');795 textarea.value = reasoning;
250 $(textarea).insertBefore(reasoningBlock);796 $(textarea).insertBefore(reasoningBlock);
251797
252 if (!CSS.supports('field-sizing', 'content')) {798 if (!CSS.supports('field-sizing', 'content')) {
@@ -285,9 +831,12 @@ function setReasoningEventHandlers() {
285 const textarea = messageBlock.find('.reasoning_edit_textarea');831 const textarea = messageBlock.find('.reasoning_edit_textarea');
286 const reasoning = getRegexedString(String(textarea.val()), regex_placement.REASONING, { isEdit: true });832 const reasoning = getRegexedString(String(textarea.val()), regex_placement.REASONING, { isEdit: true });
287 message.extra.reasoning = reasoning;833 message.extra.reasoning = reasoning;
834 message.extra.reasoning_type = message.extra.reasoning_type ? ReasoningType.Edited : ReasoningType.Manual;
288 await saveChatConditional();835 await saveChatConditional();
289 updateMessageBlock(messageId, message);836 updateMessageBlock(messageId, message);
290 textarea.remove();837 textarea.remove();
838
839 messageBlock.find('.mes_edit_done:visible').trigger('click');
291 });840 });
292841
293 $(document).on('click', '.mes_reasoning_edit_cancel', function (e) {842 $(document).on('click', '.mes_reasoning_edit_cancel', function (e) {
@@ -297,10 +846,14 @@ function setReasoningEventHandlers() {
297 const { messageBlock } = getMessageFromJquery(this);846 const { messageBlock } = getMessageFromJquery(this);
298 const textarea = messageBlock.find('.reasoning_edit_textarea');847 const textarea = messageBlock.find('.reasoning_edit_textarea');
299 textarea.remove();848 textarea.remove();
849
850 messageBlock.find('.mes_reasoning_edit_cancel:visible').trigger('click');
851
852 updateReasoningUI(messageBlock);
300 });853 });
301854
302 $(document).on('click', '.mes_edit_add_reasoning', async function () {855 $(document).on('click', '.mes_edit_add_reasoning', async function () {
303 const { message, messageId } = getMessageFromJquery(this);856 const { message, messageBlock } = getMessageFromJquery(this);
304 if (!message?.extra) {857 if (!message?.extra) {
305 return;858 return;
306 }859 }
@@ -310,34 +863,46 @@ function setReasoningEventHandlers() {
310 return;863 return;
311 }864 }
312865
313 message.extra.reasoning = PromptReasoning.REASONING_PLACEHOLDER;866 messageBlock.addClass('reasoning');
867
868 // To make hidden reasoning blocks editable, we just set them to "Done" here already.
869 // They will be done on save anyway - and on cancel the reasoning block gets rerendered too.
870 if (messageBlock.attr('data-reasoning-state') === ReasoningState.Hidden) {
871 messageBlock.attr('data-reasoning-state', ReasoningState.Done);
872 }
873
874 // Open the reasoning area so we can actually edit it
875 messageBlock.find('.mes_reasoning_details').attr('open', '');
876 messageBlock.find('.mes_reasoning_edit').trigger('click');
314 await saveChatConditional();877 await saveChatConditional();
315 closeMessageEditor();
316 updateMessageBlock(messageId, message);
317 });878 });
318879
319 $(document).on('click', '.mes_reasoning_delete', async function (e) {880 $(document).on('click', '.mes_reasoning_delete', async function (e) {
320 e.stopPropagation();881 e.stopPropagation();
321 e.preventDefault();882 e.preventDefault();
322883
323 const confirm = await Popup.show.confirm(t`Are you sure you want to clear the reasoning?`, t`Visible message contents will stay intact.`);884 const confirm = await Popup.show.confirm(t`Remove Reasoning`, t`Are you sure you want to clear the reasoning?<br />Visible message contents will stay intact.`);
324885
325 if (!confirm) {886 if (!confirm) {
326 return;887 return;
327 }888 }
328889
329 const { message, messageId } = getMessageFromJquery(this);890 const { message, messageId, messageBlock } = getMessageFromJquery(this);
330 if (!message?.extra) {891 if (!message?.extra) {
331 return;892 return;
332 }893 }
333 message.extra.reasoning = '';894 message.extra.reasoning = '';
895 delete message.extra.reasoning_type;
896 delete message.extra.reasoning_duration;
334 await saveChatConditional();897 await saveChatConditional();
335 updateMessageBlock(messageId, message);898 updateMessageBlock(messageId, message);
899 const textarea = messageBlock.find('.reasoning_edit_textarea');
900 textarea.remove();
336 });901 });
337902
338 $(document).on('pointerup', '.mes_reasoning_copy', async function () {903 $(document).on('pointerup', '.mes_reasoning_copy', async function () {
339 const { message } = getMessageFromJquery(this);904 const { message } = getMessageFromJquery(this);
340 const reasoning = String(message?.extra?.reasoning ?? '').replace(PromptReasoning.REASONING_PLACEHOLDER_REGEX, '');905 const reasoning = String(message?.extra?.reasoning ?? '');
341906
342 if (!reasoning) {907 if (!reasoning) {
343 return;908 return;
@@ -349,21 +914,37 @@ function setReasoningEventHandlers() {
349}914}
350915
351/**916/**
917 * Removes reasoning from a string if auto-parsing is enabled.
918 * @param {string} str Input string
919 * @returns {string} Output string
920 */
921export function removeReasoningFromString(str) {
922 if (!power_user.reasoning.auto_parse) {
923 return str;
924 }
925
926 const parsedReasoning = parseReasoningFromString(str);
927 return parsedReasoning?.content ?? str;
928}
929
930/**
352 * Parses reasoning from a string using the power user reasoning settings.931 * Parses reasoning from a string using the power user reasoning settings.
353 * @typedef {Object} ParsedReasoning932 * @typedef {Object} ParsedReasoning
354 * @property {string} reasoning Reasoning block933 * @property {string} reasoning Reasoning block
355 * @property {string} content Message content934 * @property {string} content Message content
356 * @param {string} str Content of the message935 * @param {string} str Content of the message
936 * @param {Object} options Optional arguments
937 * @param {boolean} [options.strict=true] Whether the reasoning block **has** to be at the beginning of the provided string (excluding whitespaces), or can be anywhere in it
357 * @returns {ParsedReasoning|null} Parsed reasoning block and message content938 * @returns {ParsedReasoning|null} Parsed reasoning block and message content
358 */939 */
359function parseReasoningFromString(str) {940function parseReasoningFromString(str, { strict = true } = {}) {
360 // Both prefix and suffix must be defined941 // Both prefix and suffix must be defined
361 if (!power_user.reasoning.prefix || !power_user.reasoning.suffix) {942 if (!power_user.reasoning.prefix || !power_user.reasoning.suffix) {
362 return null;943 return null;
363 }944 }
364945
365 try {946 try {
366 const regex = new RegExp(`${escapeRegex(power_user.reasoning.prefix)}(.*?)${escapeRegex(power_user.reasoning.suffix)}`, 's');947 const regex = new RegExp(`${(strict ? '^\\s*?' : '')}${escapeRegex(power_user.reasoning.prefix)}(.*?)${escapeRegex(power_user.reasoning.suffix)}`, 's');
367948
368 let didReplace = false;949 let didReplace = false;
369 let reasoning = '';950 let reasoning = '';
@@ -373,9 +954,9 @@ function parseReasoningFromString(str) {
373 return '';954 return '';
374 });955 });
375956
376 if (didReplace && power_user.trim_spaces) {957 if (didReplace) {
377 reasoning = reasoning.trim();958 reasoning = trimSpaces(reasoning);
378 content = content.trim();959 content = trimSpaces(content);
379 }960 }
380961
381 return { reasoning, content };962 return { reasoning, content };
@@ -404,6 +985,11 @@ function registerReasoningAppEvents() {
404 return null;985 return null;
405 }986 }
406987
988 if (message.extra?.reasoning) {
989 console.debug('[Reasoning] Message already has reasoning', idx);
990 return null;
991 }
992
407 const parsedReasoning = parseReasoningFromString(message.mes);993 const parsedReasoning = parseReasoningFromString(message.mes);
408994
409 // No reasoning block found995 // No reasoning block found
@@ -421,6 +1007,7 @@ function registerReasoningAppEvents() {
421 // If reasoning was found, add it to the message1007 // If reasoning was found, add it to the message
422 if (parsedReasoning.reasoning) {1008 if (parsedReasoning.reasoning) {
423 message.extra.reasoning = getRegexedString(parsedReasoning.reasoning, regex_placement.REASONING);1009 message.extra.reasoning = getRegexedString(parsedReasoning.reasoning, regex_placement.REASONING);
1010 message.extra.reasoning_type = ReasoningType.Parsed;
424 }1011 }
4251012
426 // Update the message text if it was changed1013 // Update the message text if it was changed
public/scripts/secrets.js+2 -0
@@ -40,6 +40,8 @@ export const SECRET_KEYS = {
40 BFL: 'api_key_bfl',40 BFL: 'api_key_bfl',
41 GENERIC: 'api_key_generic',41 GENERIC: 'api_key_generic',
42 DEEPSEEK: 'api_key_deepseek',42 DEEPSEEK: 'api_key_deepseek',
43 SERPER: 'api_key_serper',
44 FALAI: 'api_key_falai',
43};45};
4446
45const INPUT_MAP = {47const INPUT_MAP = {
public/scripts/slash-commands.js+63 -14
@@ -75,6 +75,7 @@ import { commonEnumProviders, enumIcons } from './slash-commands/SlashCommandCom
75import { SlashCommandBreakController } from './slash-commands/SlashCommandBreakController.js';75import { SlashCommandBreakController } from './slash-commands/SlashCommandBreakController.js';
76import { SlashCommandExecutionError } from './slash-commands/SlashCommandExecutionError.js';76import { SlashCommandExecutionError } from './slash-commands/SlashCommandExecutionError.js';
77import { slashCommandReturnHelper } from './slash-commands/SlashCommandReturnHelper.js';77import { slashCommandReturnHelper } from './slash-commands/SlashCommandReturnHelper.js';
78import { accountStorage } from './util/AccountStorage.js';
78export {79export {
79 executeSlashCommands, executeSlashCommandsWithOptions, getSlashCommandsHelp, registerSlashCommand,80 executeSlashCommands, executeSlashCommandsWithOptions, getSlashCommandsHelp, registerSlashCommand,
80};81};
@@ -235,7 +236,6 @@ export function initDefaultSlashCommands() {
235 description: 'Character name - or unique character identifier (avatar key)',236 description: 'Character name - or unique character identifier (avatar key)',
236 typeList: [ARGUMENT_TYPE.STRING],237 typeList: [ARGUMENT_TYPE.STRING],
237 enumProvider: commonEnumProviders.characters('character'),238 enumProvider: commonEnumProviders.characters('character'),
238 forceEnum: false,
239 }),239 }),
240 ],240 ],
241 helpString: `241 helpString: `
@@ -274,7 +274,6 @@ export function initDefaultSlashCommands() {
274 typeList: [ARGUMENT_TYPE.STRING],274 typeList: [ARGUMENT_TYPE.STRING],
275 isRequired: true,275 isRequired: true,
276 enumProvider: commonEnumProviders.characters('character'),276 enumProvider: commonEnumProviders.characters('character'),
277 forceEnum: false,
278 }),277 }),
279 SlashCommandNamedArgument.fromProps({278 SlashCommandNamedArgument.fromProps({
280 name: 'avatar',279 name: 'avatar',
@@ -518,7 +517,6 @@ export function initDefaultSlashCommands() {
518 typeList: [ARGUMENT_TYPE.STRING],517 typeList: [ARGUMENT_TYPE.STRING],
519 isRequired: true,518 isRequired: true,
520 enumProvider: commonEnumProviders.characters('all'),519 enumProvider: commonEnumProviders.characters('all'),
521 forceEnum: true,
522 }),520 }),
523 ],521 ],
524 helpString: 'Opens up a chat with the character or group by its name',522 helpString: 'Opens up a chat with the character or group by its name',
@@ -734,6 +732,57 @@ export function initDefaultSlashCommands() {
734 helpString: 'Unhides a message from the prompt.',732 helpString: 'Unhides a message from the prompt.',
735 }));733 }));
736 SlashCommandParser.addCommandObject(SlashCommand.fromProps({734 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
735 name: 'member-get',
736 aliases: ['getmember', 'memberget'],
737 callback: (async ({ field = 'name' }, arg) => {
738 if (!selected_group) {
739 toastr.warning('Cannot run /member-get command outside of a group chat.');
740 return '';
741 }
742 if (field === '') {
743 toastr.warning('\'/member-get field=\' argument required!');
744 return '';
745 }
746 field = field.toString();
747 arg = arg.toString();
748 if (!['name', 'index', 'id', 'avatar'].includes(field)) {
749 toastr.warning('\'/member-get field=\' argument required!');
750 return '';
751 }
752 const isId = !isNaN(parseInt(arg));
753 const groupMember = findGroupMemberId(arg, true);
754 if (!groupMember) {
755 toastr.warn(`No group member found using ${isId ? 'id' : 'string'} ${arg}`);
756 return '';
757 }
758 return groupMember[field];
759 }),
760 namedArgumentList: [
761 SlashCommandNamedArgument.fromProps({
762 name: 'field',
763 description: 'Whether to retrieve the name, index, id, or avatar.',
764 typeList: [ARGUMENT_TYPE.STRING],
765 isRequired: true,
766 defaultValue: 'name',
767 enumList: [
768 new SlashCommandEnumValue('name', 'Character name'),
769 new SlashCommandEnumValue('index', 'Group member index'),
770 new SlashCommandEnumValue('avatar', 'Character avatar'),
771 new SlashCommandEnumValue('id', 'Character index'),
772 ],
773 }),
774 ],
775 unnamedArgumentList: [
776 SlashCommandArgument.fromProps({
777 description: 'member index (starts with 0), name, or avatar',
778 typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.STRING],
779 isRequired: true,
780 enumProvider: commonEnumProviders.groupMembers(),
781 }),
782 ],
783 helpString: 'Retrieves a group member\'s name, index, id, or avatar.',
784 }));
785 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
737 name: 'member-disable',786 name: 'member-disable',
738 callback: disableGroupMemberCallback,787 callback: disableGroupMemberCallback,
739 aliases: ['disable', 'disablemember', 'memberdisable'],788 aliases: ['disable', 'disablemember', 'memberdisable'],
@@ -843,7 +892,8 @@ export function initDefaultSlashCommands() {
843 helpString: 'Moves a group member down in the group chat list.',892 helpString: 'Moves a group member down in the group chat list.',
844 }));893 }));
845 SlashCommandParser.addCommandObject(SlashCommand.fromProps({894 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
846 name: 'peek',895 name: 'member-peek',
896 aliases: ['peek', 'memberpeek', 'peekmember'],
847 callback: peekCallback,897 callback: peekCallback,
848 unnamedArgumentList: [898 unnamedArgumentList: [
849 SlashCommandArgument.fromProps({899 SlashCommandArgument.fromProps({
@@ -1009,7 +1059,6 @@ export function initDefaultSlashCommands() {
1009 typeList: [ARGUMENT_TYPE.STRING],1059 typeList: [ARGUMENT_TYPE.STRING],
1010 defaultValue: 'System',1060 defaultValue: 'System',
1011 enumProvider: () => [...commonEnumProviders.characters('character')(), new SlashCommandEnumValue('System', null, enumTypes.enum, enumIcons.assistant)],1061 enumProvider: () => [...commonEnumProviders.characters('character')(), new SlashCommandEnumValue('System', null, enumTypes.enum, enumIcons.assistant)],
1012 forceEnum: false,
1013 }),1062 }),
1014 new SlashCommandNamedArgument(1063 new SlashCommandNamedArgument(
1015 'length', 'API response length in tokens', [ARGUMENT_TYPE.NUMBER], false,1064 'length', 'API response length in tokens', [ARGUMENT_TYPE.NUMBER], false,
@@ -3047,7 +3096,7 @@ function performGroupMemberAction(chid, action) {
30473096
3048async function disableGroupMemberCallback(_, arg) {3097async function disableGroupMemberCallback(_, arg) {
3049 if (!selected_group) {3098 if (!selected_group) {
3050 toastr.warning('Cannot run /disable command outside of a group chat.');3099 toastr.warning('Cannot run /member-disable command outside of a group chat.');
3051 return '';3100 return '';
3052 }3101 }
30533102
@@ -3064,7 +3113,7 @@ async function disableGroupMemberCallback(_, arg) {
30643113
3065async function enableGroupMemberCallback(_, arg) {3114async function enableGroupMemberCallback(_, arg) {
3066 if (!selected_group) {3115 if (!selected_group) {
3067 toastr.warning('Cannot run /enable command outside of a group chat.');3116 toastr.warning('Cannot run /member-enable command outside of a group chat.');
3068 return '';3117 return '';
3069 }3118 }
30703119
@@ -3081,7 +3130,7 @@ async function enableGroupMemberCallback(_, arg) {
30813130
3082async function moveGroupMemberUpCallback(_, arg) {3131async function moveGroupMemberUpCallback(_, arg) {
3083 if (!selected_group) {3132 if (!selected_group) {
3084 toastr.warning('Cannot run /memberup command outside of a group chat.');3133 toastr.warning('Cannot run /member-up command outside of a group chat.');
3085 return '';3134 return '';
3086 }3135 }
30873136
@@ -3098,7 +3147,7 @@ async function moveGroupMemberUpCallback(_, arg) {
30983147
3099async function moveGroupMemberDownCallback(_, arg) {3148async function moveGroupMemberDownCallback(_, arg) {
3100 if (!selected_group) {3149 if (!selected_group) {
3101 toastr.warning('Cannot run /memberdown command outside of a group chat.');3150 toastr.warning('Cannot run /member-down command outside of a group chat.');
3102 return '';3151 return '';
3103 }3152 }
31043153
@@ -3115,12 +3164,12 @@ async function moveGroupMemberDownCallback(_, arg) {
31153164
3116async function peekCallback(_, arg) {3165async function peekCallback(_, arg) {
3117 if (!selected_group) {3166 if (!selected_group) {
3118 toastr.warning('Cannot run /peek command outside of a group chat.');3167 toastr.warning('Cannot run /member-peek command outside of a group chat.');
3119 return '';3168 return '';
3120 }3169 }
31213170
3122 if (is_group_generating) {3171 if (is_group_generating) {
3123 toastr.warning('Cannot run /peek command while the group reply is generating.');3172 toastr.warning('Cannot run /member-peek command while the group reply is generating.');
3124 return '';3173 return '';
3125 }3174 }
31263175
@@ -3137,7 +3186,7 @@ async function peekCallback(_, arg) {
31373186
3138async function removeGroupMemberCallback(_, arg) {3187async function removeGroupMemberCallback(_, arg) {
3139 if (!selected_group) {3188 if (!selected_group) {
3140 toastr.warning('Cannot run /memberremove command outside of a group chat.');3189 toastr.warning('Cannot run /member-remove command outside of a group chat.');
3141 return '';3190 return '';
3142 }3191 }
31433192
@@ -3558,9 +3607,9 @@ export async function sendMessageAs(args, text) {
35583607
3559 if (!name) {3608 if (!name) {
3560 const namelessWarningKey = 'sendAsNamelessWarningShown';3609 const namelessWarningKey = 'sendAsNamelessWarningShown';
3561 if (localStorage.getItem(namelessWarningKey) !== 'true') {3610 if (accountStorage.getItem(namelessWarningKey) !== 'true') {
3562 toastr.warning('To avoid confusion, please use /sendas name="Character Name"', 'Name defaulted to {{char}}', { timeOut: 10000 });3611 toastr.warning('To avoid confusion, please use /sendas name="Character Name"', 'Name defaulted to {{char}}', { timeOut: 10000 });
3563 localStorage.setItem(namelessWarningKey, 'true');3612 accountStorage.setItem(namelessWarningKey, 'true');
3564 }3613 }
3565 name = name2;3614 name = name2;
3566 }3615 }
public/scripts/slash-commands/SlashCommandCommonEnumsProvider.js+1 -0
@@ -34,6 +34,7 @@ export const enumIcons = {
34 preset: '⚙️',34 preset: '⚙️',
35 file: '📄',35 file: '📄',
36 message: '💬',36 message: '💬',
37 reasoning: '💡',
37 voice: '🎤',38 voice: '🎤',
38 server: '🖥️',39 server: '🖥️',
39 popup: '🗔',40 popup: '🗔',
public/scripts/st-context.js+7 -0
@@ -68,11 +68,14 @@ import { tag_map, tags } from './tags.js';
68import { textgenerationwebui_settings } from './textgen-settings.js';68import { textgenerationwebui_settings } from './textgen-settings.js';
69import { tokenizers, getTextTokens, getTokenCount, getTokenCountAsync, getTokenizerModel } from './tokenizers.js';69import { tokenizers, getTextTokens, getTokenCount, getTokenCountAsync, getTokenizerModel } from './tokenizers.js';
70import { ToolManager } from './tool-calling.js';70import { ToolManager } from './tool-calling.js';
71import { accountStorage } from './util/AccountStorage.js';
71import { timestampToMoment, uuidv4 } from './utils.js';72import { timestampToMoment, uuidv4 } from './utils.js';
72import { getGlobalVariable, getLocalVariable, setGlobalVariable, setLocalVariable } from './variables.js';73import { getGlobalVariable, getLocalVariable, setGlobalVariable, setLocalVariable } from './variables.js';
74import { convertCharacterBook, loadWorldInfo, saveWorldInfo, updateWorldInfoList } from './world-info.js';
7375
74export function getContext() {76export function getContext() {
75 return {77 return {
78 accountStorage,
76 chat,79 chat,
77 characters,80 characters,
78 groups,81 groups,
@@ -186,6 +189,10 @@ export function getContext() {
186 set: setGlobalVariable,189 set: setGlobalVariable,
187 },190 },
188 },191 },
192 loadWorldInfo,
193 saveWorldInfo,
194 updateWorldInfoList,
195 convertCharacterBook,
189 };196 };
190}197}
191198
public/scripts/templates/itemizationChat.html+1 -1
@@ -146,5 +146,5 @@
146</div>146</div>
147<hr>147<hr>
148<div id="rawPromptPopup" class="list-group">148<div id="rawPromptPopup" class="list-group">
149 <div id="rawPromptWrapper" class="tokenItemizingSubclass"></div>149 <div id="rawPromptWrapper" class="tokenItemizingMaintext"></div>
150</div>150</div>
public/scripts/textgen-models.js+9 -3
@@ -6,6 +6,7 @@ import { tokenizers } from './tokenizers.js';
6import { renderTemplateAsync } from './templates.js';6import { renderTemplateAsync } from './templates.js';
7import { POPUP_TYPE, callGenericPopup } from './popup.js';7import { POPUP_TYPE, callGenericPopup } from './popup.js';
8import { t } from './i18n.js';8import { t } from './i18n.js';
9import { accountStorage } from './util/AccountStorage.js';
910
10let mancerModels = [];11let mancerModels = [];
11let togetherModels = [];12let togetherModels = [];
@@ -57,9 +58,14 @@ const OPENROUTER_PROVIDERS = [
57 'Minimax',58 'Minimax',
58 'Nineteen',59 'Nineteen',
59 'Liquid',60 'Liquid',
61 'InferenceNet',
62 'Friendli',
63 'AionLabs',
64 'Alibaba',
60 'Nebius',65 'Nebius',
61 'Chutes',66 'Chutes',
62 'Kluster',67 'Kluster',
68 'Targon',
63 '01.AI',69 '01.AI',
64 'HuggingFace',70 'HuggingFace',
65 'Mancer',71 'Mancer',
@@ -336,7 +342,7 @@ export async function loadFeatherlessModels(data) {
336 populateClassSelection(data);342 populateClassSelection(data);
337343
338 // Retrieve the stored number of items per page or default to 10344 // Retrieve the stored number of items per page or default to 10
339 const perPage = Number(localStorage.getItem(storageKey)) || 10;345 const perPage = Number(accountStorage.getItem(storageKey)) || 10;
340346
341 // Initialize pagination347 // Initialize pagination
342 applyFiltersAndSort();348 applyFiltersAndSort();
@@ -412,7 +418,7 @@ export async function loadFeatherlessModels(data) {
412 },418 },
413 afterSizeSelectorChange: function (e) {419 afterSizeSelectorChange: function (e) {
414 const newPerPage = e.target.value;420 const newPerPage = e.target.value;
415 localStorage.setItem('Models_PerPage', newPerPage);421 accountStorage.setItem(storageKey, newPerPage);
416 setupPagination(models, Number(newPerPage), featherlessCurrentPage); // Use the stored current page number422 setupPagination(models, Number(newPerPage), featherlessCurrentPage); // Use the stored current page number
417 },423 },
418 });424 });
@@ -513,7 +519,7 @@ export async function loadFeatherlessModels(data) {
513 const currentModelIndex = filteredModels.findIndex(x => x.id === textgen_settings.featherless_model);519 const currentModelIndex = filteredModels.findIndex(x => x.id === textgen_settings.featherless_model);
514 featherlessCurrentPage = currentModelIndex >= 0 ? (currentModelIndex / perPage) + 1 : 1;520 featherlessCurrentPage = currentModelIndex >= 0 ? (currentModelIndex / perPage) + 1 : 1;
515521
516 setupPagination(filteredModels, Number(localStorage.getItem(storageKey)) || perPage, featherlessCurrentPage);522 setupPagination(filteredModels, Number(accountStorage.getItem(storageKey)) || perPage, featherlessCurrentPage);
517 }523 }
518524
519 // Required to keep the /model command function525 // Required to keep the /model command function
public/scripts/textgen-settings.js+37 -7
@@ -10,6 +10,7 @@ import {
10 setOnlineStatus,10 setOnlineStatus,
11 substituteParams,11 substituteParams,
12} from '../script.js';12} from '../script.js';
13import { t } from './i18n.js';
13import { BIAS_CACHE, createNewLogitBiasEntry, displayLogitBias, getLogitBiasListResult } from './logit-bias.js';14import { BIAS_CACHE, createNewLogitBiasEntry, displayLogitBias, getLogitBiasListResult } from './logit-bias.js';
1415
15import { power_user, registerDebugFunction } from './power-user.js';16import { power_user, registerDebugFunction } from './power-user.js';
@@ -182,6 +183,8 @@ const settings = {
182 grammar_string: '',183 grammar_string: '',
183 json_schema: {},184 json_schema: {},
184 banned_tokens: '',185 banned_tokens: '',
186 global_banned_tokens: '',
187 send_banned_tokens: true,
185 sampler_priority: OOBA_DEFAULT_ORDER,188 sampler_priority: OOBA_DEFAULT_ORDER,
186 samplers: LLAMACPP_DEFAULT_ORDER,189 samplers: LLAMACPP_DEFAULT_ORDER,
187 samplers_priorities: APHRODITE_DEFAULT_ORDER,190 samplers_priorities: APHRODITE_DEFAULT_ORDER,
@@ -274,6 +277,8 @@ export const setting_names = [
274 'grammar_string',277 'grammar_string',
275 'json_schema',278 'json_schema',
276 'banned_tokens',279 'banned_tokens',
280 'global_banned_tokens',
281 'send_banned_tokens',
277 'ignore_eos_token',282 'ignore_eos_token',
278 'spaces_between_special_tokens',283 'spaces_between_special_tokens',
279 'speculative_ngram',284 'speculative_ngram',
@@ -306,7 +311,7 @@ export function validateTextGenUrl() {
306 const formattedUrl = formatTextGenURL(url);311 const formattedUrl = formatTextGenURL(url);
307312
308 if (!formattedUrl) {313 if (!formattedUrl) {
309 toastr.error('Enter a valid API URL', 'Text Completion API');314 toastr.error(t`Enter a valid API URL`, 'Text Completion API');
310 return;315 return;
311 }316 }
312317
@@ -394,7 +399,7 @@ function getTokenizerForTokenIds() {
394 * @returns {TokenBanResult} String with comma-separated banned token IDs399 * @returns {TokenBanResult} String with comma-separated banned token IDs
395 */400 */
396function getCustomTokenBans() {401function getCustomTokenBans() {
397 if (!settings.banned_tokens && !textgenerationwebui_banned_in_macros.length) {402 if (!settings.send_banned_tokens || (!settings.banned_tokens && !settings.global_banned_tokens && !textgenerationwebui_banned_in_macros.length)) {
398 return {403 return {
399 banned_tokens: '',404 banned_tokens: '',
400 banned_strings: [],405 banned_strings: [],
@@ -404,8 +409,9 @@ function getCustomTokenBans() {
404 const tokenizer = getTokenizerForTokenIds();409 const tokenizer = getTokenizerForTokenIds();
405 const banned_tokens = [];410 const banned_tokens = [];
406 const banned_strings = [];411 const banned_strings = [];
407 const sequences = settings.banned_tokens412 const sequences = []
408 .split('\n')413 .concat(settings.banned_tokens.split('\n'))
414 .concat(settings.global_banned_tokens.split('\n'))
409 .concat(textgenerationwebui_banned_in_macros)415 .concat(textgenerationwebui_banned_in_macros)
410 .filter(x => x.length > 0)416 .filter(x => x.length > 0)
411 .filter(onlyUnique);417 .filter(onlyUnique);
@@ -453,6 +459,18 @@ function getCustomTokenBans() {
453}459}
454460
455/**461/**
462 * Sets the banned strings kill switch toggle.
463 * @param {boolean} isEnabled Kill switch state
464 * @param {string} title Label title
465 */
466function toggleBannedStringsKillSwitch(isEnabled, title) {
467 $('#send_banned_tokens_textgenerationwebui').prop('checked', isEnabled);
468 $('#send_banned_tokens_label').find('.menu_button').toggleClass('toggleEnabled', isEnabled).prop('title', title);
469 settings.send_banned_tokens = isEnabled;
470 saveSettingsDebounced();
471}
472
473/**
456 * Calculates logit bias object from the logit bias list.474 * Calculates logit bias object from the logit bias list.
457 * @returns {object} Logit bias object475 * @returns {object} Logit bias object
458 */476 */
@@ -594,6 +612,14 @@ function sortAphroditeItemsByOrder(orderArray) {
594}612}
595613
596jQuery(function () {614jQuery(function () {
615 $('#send_banned_tokens_textgenerationwebui').on('change', function () {
616 const checked = !!$(this).prop('checked');
617 toggleBannedStringsKillSwitch(checked,
618 checked
619 ? t`Banned tokens/strings are being sent in the request.`
620 : t`Banned tokens/strings are NOT being sent in the request.`);
621 });
622
597 $('#koboldcpp_order').sortable({623 $('#koboldcpp_order').sortable({
598 delay: getSortableDelay(),624 delay: getSortableDelay(),
599 stop: function () {625 stop: function () {
@@ -932,6 +958,10 @@ function setSettingByName(setting, value, trigger) {
932 if (isCheckbox) {958 if (isCheckbox) {
933 const val = Boolean(value);959 const val = Boolean(value);
934 $(`#${setting}_textgenerationwebui`).prop('checked', val);960 $(`#${setting}_textgenerationwebui`).prop('checked', val);
961
962 if ('send_banned_tokens' === setting) {
963 $(`#${setting}_textgenerationwebui`).trigger('change');
964 }
935 }965 }
936 else if (isText) {966 else if (isText) {
937 $(`#${setting}_textgenerationwebui`).val(value);967 $(`#${setting}_textgenerationwebui`).val(value);
@@ -1157,7 +1187,7 @@ export function getTextGenModel() {
1157 return settings.aphrodite_model;1187 return settings.aphrodite_model;
1158 case OLLAMA:1188 case OLLAMA:
1159 if (!settings.ollama_model) {1189 if (!settings.ollama_model) {
1160 toastr.error('No Ollama model selected.', 'Text Completion API');1190 toastr.error(t`No Ollama model selected.`, 'Text Completion API');
1161 throw new Error('No Ollama model selected');1191 throw new Error('No Ollama model selected');
1162 }1192 }
1163 return settings.ollama_model;1193 return settings.ollama_model;
@@ -1221,7 +1251,7 @@ function replaceMacrosInList(str) {
1221 }1251 }
1222}1252}
12231253
1224export function getTextGenGenerationData(finalPrompt, maxTokens, isImpersonate, isContinue, cfgValues, type) {1254export async function getTextGenGenerationData(finalPrompt, maxTokens, isImpersonate, isContinue, cfgValues, type) {
1225 const canMultiSwipe = !isContinue && !isImpersonate && type !== 'quiet';1255 const canMultiSwipe = !isContinue && !isImpersonate && type !== 'quiet';
1226 const dynatemp = isDynamicTemperatureSupported();1256 const dynatemp = isDynamicTemperatureSupported();
1227 const { banned_tokens, banned_strings } = getCustomTokenBans();1257 const { banned_tokens, banned_strings } = getCustomTokenBans();
@@ -1449,7 +1479,7 @@ export function getTextGenGenerationData(finalPrompt, maxTokens, isImpersonate,
1449 }1479 }
1450 }1480 }
14511481
1452 eventSource.emitAndWait(event_types.TEXT_COMPLETION_SETTINGS_READY, params);1482 await eventSource.emit(event_types.TEXT_COMPLETION_SETTINGS_READY, params);
14531483
1454 // Grammar conflicts with with json_schema1484 // Grammar conflicts with with json_schema
1455 if (settings.type === LLAMACPP) {1485 if (settings.type === LLAMACPP) {
public/scripts/tool-calling.js+1 -0
@@ -563,6 +563,7 @@ export class ToolManager {
563 chat_completion_sources.OPENROUTER,563 chat_completion_sources.OPENROUTER,
564 chat_completion_sources.GROQ,564 chat_completion_sources.GROQ,
565 chat_completion_sources.COHERE,565 chat_completion_sources.COHERE,
566 chat_completion_sources.DEEPSEEK,
566 ];567 ];
567 return supportedSources.includes(oai_settings.chat_completion_source);568 return supportedSources.includes(oai_settings.chat_completion_source);
568 }569 }
public/scripts/user.js+8 -0
@@ -44,6 +44,14 @@ export function isAdmin() {
44}44}
4545
46/**46/**
47 * Gets the handle string of the current user.
48 * @returns {string} User handle
49 */
50export function getCurrentUserHandle() {
51 return currentUser?.handle || 'default-user';
52}
53
54/**
47 * Get the current user.55 * Get the current user.
48 * @returns {Promise<void>}56 * @returns {Promise<void>}
49 */57 */
public/scripts/util/AccountStorage.js+139 -0
@@ -0,0 +1,139 @@
1import { saveSettingsDebounced } from '../../script.js';
2
3const MIGRATED_MARKER = '__migrated';
4const MIGRATABLE_KEYS = [
5 /^AlertRegex_/,
6 /^AlertWI_/,
7 /^Assets_SkipConfirm_/,
8 /^Characters_PerPage$/,
9 /^DataBank_sortField$/,
10 /^DataBank_sortOrder$/,
11 /^extension_update_nag$/,
12 /^extensions_sortByName$/,
13 /^FeatherlessModels_PerPage$/,
14 /^GroupMembers_PerPage$/,
15 /^GroupCandidates_PerPage$/,
16 /^LNavLockOn$/,
17 /^LNavOpened$/,
18 /^mediaWarningShown:/,
19 /^NavLockOn$/,
20 /^NavOpened$/,
21 /^Personas_PerPage$/,
22 /^Personas_GridView$/,
23 /^Proxy_SkipConfirm_/,
24 /^qr--executeShortcut$/,
25 /^qr--syntax$/,
26 /^qr--tabSize$/,
27 /^qr--wrap$/,
28 /^RegenerateWithCtrlEnter$/,
29 /^SelectedNavTab$/,
30 /^sendAsNamelessWarningShown$/,
31 /^StoryStringValidationCache$/,
32 /^WINavOpened$/,
33 /^WI_PerPage$/,
34 /^world_info_sort_order$/,
35];
36
37/**
38 * Provides access to account storage of arbitrary key-value pairs.
39 */
40class AccountStorage {
41 /**
42 * @type {Record<string, string>} Storage state
43 */
44 #state = {};
45
46 /**
47 * @type {boolean} If the storage was initialized
48 */
49 #ready = false;
50
51 #migrateLocalStorage() {
52 const localStorageKeys = [];
53 for (let i = 0; i < globalThis.localStorage.length; i++) {
54 localStorageKeys.push(globalThis.localStorage.key(i));
55 }
56 for (const key of localStorageKeys) {
57 if (MIGRATABLE_KEYS.some(k => k.test(key))) {
58 const value = globalThis.localStorage.getItem(key);
59 this.#state[key] = value;
60 globalThis.localStorage.removeItem(key);
61 }
62 }
63 }
64
65 /**
66 * Initialize the account storage.
67 * @param {Object} state Initial state
68 */
69 init(state) {
70 if (state && typeof state === 'object') {
71 this.#state = Object.assign(this.#state, state);
72 }
73
74 if (!Object.hasOwn(this.#state, MIGRATED_MARKER)) {
75 this.#migrateLocalStorage();
76 this.#state[MIGRATED_MARKER] = '1';
77 saveSettingsDebounced();
78 }
79
80 this.#ready = true;
81 }
82
83 /**
84 * Get the value of a key in account storage.
85 * @param {string} key Key to get
86 * @returns {string|null} Value of the key
87 */
88 getItem(key) {
89 if (!this.#ready) {
90 console.warn(`AccountStorage not ready (trying to read from ${key})`);
91 }
92
93 return Object.hasOwn(this.#state, key) ? String(this.#state[key]) : null;
94 }
95
96 /**
97 * Set a key in account storage.
98 * @param {string} key Key to set
99 * @param {string} value Value to set
100 */
101 setItem(key, value) {
102 if (!this.#ready) {
103 console.warn(`AccountStorage not ready (trying to write to ${key})`);
104 }
105
106 this.#state[key] = String(value);
107 saveSettingsDebounced();
108 }
109
110 /**
111 * Remove a key from account storage.
112 * @param {string} key Key to remove
113 */
114 removeItem(key) {
115 if (!this.#ready) {
116 console.warn(`AccountStorage not ready (trying to remove ${key})`);
117 }
118
119 if (!Object.hasOwn(this.#state, key)) {
120 return;
121 }
122
123 delete this.#state[key];
124 saveSettingsDebounced();
125 }
126
127 /**
128 * Gets a snapshot of the storage state.
129 * @returns {Record<string, string>} A deep clone of the storage state
130 */
131 getState() {
132 return structuredClone(this.#state);
133 }
134}
135
136/**
137 * Account storage instance.
138 */
139export const accountStorage = new AccountStorage();
public/scripts/utils.js+41 -6
@@ -8,7 +8,7 @@ import {
8import { getContext } from './extensions.js';8import { getContext } from './extensions.js';
9import { characters, getRequestHeaders, this_chid } from '../script.js';9import { characters, getRequestHeaders, this_chid } from '../script.js';
10import { isMobile } from './RossAscends-mods.js';10import { isMobile } from './RossAscends-mods.js';
11import { collapseNewlines } from './power-user.js';11import { collapseNewlines, power_user } from './power-user.js';
12import { debounce_timeout } from './constants.js';12import { debounce_timeout } from './constants.js';
13import { Popup, POPUP_RESULT, POPUP_TYPE } from './popup.js';13import { Popup, POPUP_RESULT, POPUP_TYPE } from './popup.js';
14import { SlashCommandClosure } from './slash-commands/SlashCommandClosure.js';14import { SlashCommandClosure } from './slash-commands/SlashCommandClosure.js';
@@ -677,6 +677,19 @@ export function sortByCssOrder(a, b) {
677}677}
678678
679/**679/**
680 * Trims leading and trailing whitespace from the input string based on a configuration setting.
681 * @param {string} input - The string to be trimmed
682 * @returns {string} The trimmed string if trimming is enabled; otherwise, returns the original string
683 */
684
685export function trimSpaces(input) {
686 if (!input || typeof input !== 'string') {
687 return input;
688 }
689 return power_user.trim_spaces ? input.trim() : input;
690}
691
692/**
680 * Trims a string to the end of a nearest sentence.693 * Trims a string to the end of a nearest sentence.
681 * @param {string} input The string to trim.694 * @param {string} input The string to trim.
682 * @returns {string} The trimmed string.695 * @returns {string} The trimmed string.
@@ -994,13 +1007,18 @@ export function getImageSizeFromDataURL(dataUrl) {
994 });1007 });
995}1008}
9961009
997export function getCharaFilename(chid) {1010/**
1011 * Gets the filename of the character avatar without extension
1012 * @param {number?} [chid=null] - Character ID. If not provided, uses the current character ID
1013 * @param {object} [options={}] - Options arguments
1014 * @param {string?} [options.manualAvatarKey=null] - Manually take the following avatar key, instead of using the chid to determine the name
1015 * @returns {string?} The filename of the character avatar without extension, or null if the character ID is invalid
1016 */
1017export function getCharaFilename(chid = null, { manualAvatarKey = null } = {}) {
998 const context = getContext();1018 const context = getContext();
999 const fileName = context.characters[chid ?? context.characterId]?.avatar;1019 const fileName = manualAvatarKey ?? context.characters[chid ?? context.characterId]?.avatar;
10001020
1001 if (fileName) {1021 return fileName?.replace(/\.[^/.]+$/, '') ?? null;
1002 return fileName.replace(/\.[^/.]+$/, '');
1003 }
1004}1022}
10051023
1006/**1024/**
@@ -2059,6 +2077,23 @@ export function toggleDrawer(drawer, expand = true) {
2059 }2077 }
2060}2078}
20612079
2080/**
2081 * Sets or removes a dataset property on an HTMLElement
2082 *
2083 * Utility function to make it easier to reset dataset properties on null, without them being "null" as value.
2084 *
2085 * @param {HTMLElement} element - The element to modify
2086 * @param {string} name - The name of the dataset property
2087 * @param {string|null} value - The value to set - If null, the dataset property will be removed
2088 */
2089export function setDatasetProperty(element, name, value) {
2090 if (value === null) {
2091 delete element.dataset[name];
2092 } else {
2093 element.dataset[name] = value;
2094 }
2095}
2096
2062export async function fetchFaFile(name) {2097export async function fetchFaFile(name) {
2063 const style = document.createElement('style');2098 const style = document.createElement('style');
2064 style.innerHTML = await (await fetch(`/css/${name}`)).text();2099 style.innerHTML = await (await fetch(`/css/${name}`)).text();
public/scripts/world-info.js+36 -18
@@ -21,6 +21,7 @@ import { callGenericPopup, Popup, POPUP_TYPE } from './popup.js';
21import { StructuredCloneMap } from './util/StructuredCloneMap.js';21import { StructuredCloneMap } from './util/StructuredCloneMap.js';
22import { renderTemplateAsync } from './templates.js';22import { renderTemplateAsync } from './templates.js';
23import { t } from './i18n.js';23import { t } from './i18n.js';
24import { accountStorage } from './util/AccountStorage.js';
2425
25export const world_info_insertion_strategy = {26export const world_info_insertion_strategy = {
26 evenly: 0,27 evenly: 0,
@@ -400,6 +401,12 @@ class WorldInfoTimedEffects {
400 #entries = [];401 #entries = [];
401402
402 /**403 /**
404 * Is this a dry run?
405 * @type {boolean}
406 */
407 #isDryRun = false;
408
409 /**
403 * Buffer for active timed effects.410 * Buffer for active timed effects.
404 * @type {Record<TimedEffectType, WIScanEntry[]>}411 * @type {Record<TimedEffectType, WIScanEntry[]>}
405 */412 */
@@ -448,10 +455,12 @@ class WorldInfoTimedEffects {
448 * Initialize the timed effects with the given messages.455 * Initialize the timed effects with the given messages.
449 * @param {string[]} chat Array of chat messages456 * @param {string[]} chat Array of chat messages
450 * @param {WIScanEntry[]} entries Array of entries457 * @param {WIScanEntry[]} entries Array of entries
458 * @param {boolean} isDryRun Whether the operation is a dry run
451 */459 */
452 constructor(chat, entries) {460 constructor(chat, entries, isDryRun = false) {
453 this.#chat = chat;461 this.#chat = chat;
454 this.#entries = entries;462 this.#entries = entries;
463 this.#isDryRun = isDryRun;
455 this.#ensureChatMetadata();464 this.#ensureChatMetadata();
456 }465 }
457466
@@ -583,8 +592,10 @@ class WorldInfoTimedEffects {
583 * Checks for timed effects on chat messages.592 * Checks for timed effects on chat messages.
584 */593 */
585 checkTimedEffects() {594 checkTimedEffects() {
595 if (!this.#isDryRun) {
586 this.#checkTimedEffectOfType('sticky', this.#buffer.sticky, this.#onEnded.sticky.bind(this));596 this.#checkTimedEffectOfType('sticky', this.#buffer.sticky, this.#onEnded.sticky.bind(this));
587 this.#checkTimedEffectOfType('cooldown', this.#buffer.cooldown, this.#onEnded.cooldown.bind(this));597 this.#checkTimedEffectOfType('cooldown', this.#buffer.cooldown, this.#onEnded.cooldown.bind(this));
598 }
588 this.#checkDelayEffect(this.#buffer.delay);599 this.#checkDelayEffect(this.#buffer.delay);
589 }600 }
590601
@@ -629,6 +640,7 @@ class WorldInfoTimedEffects {
629 * @param {WIScanEntry[]} activatedEntries Entries that were activated640 * @param {WIScanEntry[]} activatedEntries Entries that were activated
630 */641 */
631 setTimedEffects(activatedEntries) {642 setTimedEffects(activatedEntries) {
643 if (this.#isDryRun) return;
632 for (const entry of activatedEntries) {644 for (const entry of activatedEntries) {
633 this.#setTimedEffectOfType('sticky', entry);645 this.#setTimedEffectOfType('sticky', entry);
634 this.#setTimedEffectOfType('cooldown', entry);646 this.#setTimedEffectOfType('cooldown', entry);
@@ -645,6 +657,9 @@ class WorldInfoTimedEffects {
645 if (!this.isValidEffectType(type)) {657 if (!this.isValidEffectType(type)) {
646 return;658 return;
647 }659 }
660 if (this.#isDryRun && type !== 'delay') {
661 return;
662 }
648663
649 const key = this.#getEntryKey(entry);664 const key = this.#getEntryKey(entry);
650 delete chat_metadata.timedWorldInfo[type][key];665 delete chat_metadata.timedWorldInfo[type][key];
@@ -858,7 +873,7 @@ export function setWorldInfoSettings(settings, data) {
858 $('#world_editor_select').append(`<option value='${i}'>${item}</option>`);873 $('#world_editor_select').append(`<option value='${i}'>${item}</option>`);
859 });874 });
860875
861 $('#world_info_sort_order').val(localStorage.getItem(SORT_ORDER_KEY) || '0');876 $('#world_info_sort_order').val(accountStorage.getItem(SORT_ORDER_KEY) || '0');
862 $('#world_info').trigger('change');877 $('#world_info').trigger('change');
863 $('#world_editor_select').trigger('change');878 $('#world_editor_select').trigger('change');
864879
@@ -1708,7 +1723,7 @@ export async function loadWorldInfo(name) {
1708 return null;1723 return null;
1709}1724}
17101725
1711async function updateWorldInfoList() {1726export async function updateWorldInfoList() {
1712 const result = await fetch('/api/settings/get', {1727 const result = await fetch('/api/settings/get', {
1713 method: 'POST',1728 method: 'POST',
1714 headers: getRequestHeaders(),1729 headers: getRequestHeaders(),
@@ -1933,13 +1948,13 @@ function displayWorldEntries(name, data, navigation = navigation_option.none, fl
1933 if (typeof navigation === 'number' && Number(navigation) >= 0) {1948 if (typeof navigation === 'number' && Number(navigation) >= 0) {
1934 const data = getDataArray();1949 const data = getDataArray();
1935 const uidIndex = data.findIndex(x => x.uid === navigation);1950 const uidIndex = data.findIndex(x => x.uid === navigation);
1936 const perPage = Number(localStorage.getItem(storageKey)) || perPageDefault;1951 const perPage = Number(accountStorage.getItem(storageKey)) || perPageDefault;
1937 startPage = Math.floor(uidIndex / perPage) + 1;1952 startPage = Math.floor(uidIndex / perPage) + 1;
1938 }1953 }
19391954
1940 $('#world_info_pagination').pagination({1955 $('#world_info_pagination').pagination({
1941 dataSource: getDataArray,1956 dataSource: getDataArray,
1942 pageSize: Number(localStorage.getItem(storageKey)) || perPageDefault,1957 pageSize: Number(accountStorage.getItem(storageKey)) || perPageDefault,
1943 sizeChangerOptions: [10, 25, 50, 100, 500, 1000],1958 sizeChangerOptions: [10, 25, 50, 100, 500, 1000],
1944 showSizeChanger: true,1959 showSizeChanger: true,
1945 pageRange: 1,1960 pageRange: 1,
@@ -1969,7 +1984,7 @@ function displayWorldEntries(name, data, navigation = navigation_option.none, fl
1969 worldEntriesList.append(blocks);1984 worldEntriesList.append(blocks);
1970 },1985 },
1971 afterSizeSelectorChange: function (e) {1986 afterSizeSelectorChange: function (e) {
1972 localStorage.setItem(storageKey, e.target.value);1987 accountStorage.setItem(storageKey, e.target.value);
1973 },1988 },
1974 afterPaging: function () {1989 afterPaging: function () {
1975 $('#world_popup_entries_list textarea[name="comment"]').each(function () {1990 $('#world_popup_entries_list textarea[name="comment"]').each(function () {
@@ -2174,7 +2189,7 @@ function verifyWorldInfoSearchSortRule() {
2174 // If search got cleared, we make sure to hide the option and go back to the one before2189 // If search got cleared, we make sure to hide the option and go back to the one before
2175 if (!searchTerm && !isHidden) {2190 if (!searchTerm && !isHidden) {
2176 searchOption.attr('hidden', '');2191 searchOption.attr('hidden', '');
2177 selector.val(localStorage.getItem(SORT_ORDER_KEY) || '0');2192 selector.val(accountStorage.getItem(SORT_ORDER_KEY) || '0');
2178 }2193 }
2179}2194}
21802195
@@ -2423,7 +2438,9 @@ export async function getWorldEntry(name, data, entry) {
2423 setWIOriginalDataValue(data, uid, originalDataValueName, data.entries[uid][entryPropName]);2438 setWIOriginalDataValue(data, uid, originalDataValueName, data.entries[uid][entryPropName]);
2424 await saveWorldInfo(name, data);2439 await saveWorldInfo(name, data);
2425 }2440 }
2441 $(this).toggleClass('empty', !data.entries[uid][entryPropName].length);
2426 });2442 });
2443 input.toggleClass('empty', !entry[entryPropName].length);
2427 input.on('select2:select', /** @type {function(*):void} */ event => updateWorldEntryKeyOptionsCache([event.params.data]));2444 input.on('select2:select', /** @type {function(*):void} */ event => updateWorldEntryKeyOptionsCache([event.params.data]));
2428 input.on('select2:unselect', /** @type {function(*):void} */ event => updateWorldEntryKeyOptionsCache([event.params.data], { remove: true }));2445 input.on('select2:unselect', /** @type {function(*):void} */ event => updateWorldEntryKeyOptionsCache([event.params.data], { remove: true }));
24292446
@@ -2458,6 +2475,7 @@ export async function getWorldEntry(name, data, entry) {
2458 data.entries[uid][entryPropName] = splitKeywordsAndRegexes(value);2475 data.entries[uid][entryPropName] = splitKeywordsAndRegexes(value);
2459 setWIOriginalDataValue(data, uid, originalDataValueName, data.entries[uid][entryPropName]);2476 setWIOriginalDataValue(data, uid, originalDataValueName, data.entries[uid][entryPropName]);
2460 await saveWorldInfo(name, data);2477 await saveWorldInfo(name, data);
2478 $(this).toggleClass('empty', !data.entries[uid][entryPropName].length);
2461 }2479 }
2462 });2480 });
2463 input.val(entry[entryPropName].join(', ')).trigger('input', { skipReset: true });2481 input.val(entry[entryPropName].join(', ')).trigger('input', { skipReset: true });
@@ -3435,7 +3453,7 @@ async function _save(name, data) {
3435 headers: getRequestHeaders(),3453 headers: getRequestHeaders(),
3436 body: JSON.stringify({ name: name, data: data }),3454 body: JSON.stringify({ name: name, data: data }),
3437 });3455 });
3438 eventSource.emit(event_types.WORLDINFO_UPDATED, name, data);3456 await eventSource.emit(event_types.WORLDINFO_UPDATED, name, data);
3439}3457}
34403458
34413459
@@ -3847,7 +3865,7 @@ export async function checkWorldInfo(chat, maxContext, isDryRun) {
3847 const context = getContext();3865 const context = getContext();
3848 const buffer = new WorldInfoBuffer(chat);3866 const buffer = new WorldInfoBuffer(chat);
38493867
3850 console.debug(`[WI] --- START WI SCAN (on ${chat.length} messages) ---`);3868 console.debug(`[WI] --- START WI SCAN (on ${chat.length} messages)${isDryRun ? ' (DRY RUN)' : ''} ---`);
38513869
3852 // Combine the chat3870 // Combine the chat
38533871
@@ -3879,9 +3897,9 @@ export async function checkWorldInfo(chat, maxContext, isDryRun) {
38793897
3880 console.debug(`[WI] Context size: ${maxContext}; WI budget: ${budget} (max% = ${world_info_budget}%, cap = ${world_info_budget_cap})`);3898 console.debug(`[WI] Context size: ${maxContext}; WI budget: ${budget} (max% = ${world_info_budget}%, cap = ${world_info_budget_cap})`);
3881 const sortedEntries = await getSortedEntries();3899 const sortedEntries = await getSortedEntries();
3882 const timedEffects = new WorldInfoTimedEffects(chat, sortedEntries);3900 const timedEffects = new WorldInfoTimedEffects(chat, sortedEntries, isDryRun);
38833901
3884 !isDryRun && timedEffects.checkTimedEffects();3902 timedEffects.checkTimedEffects();
38853903
3886 if (sortedEntries.length === 0) {3904 if (sortedEntries.length === 0) {
3887 return { worldInfoBefore: '', worldInfoAfter: '', WIDepthEntries: [], EMEntries: [], allActivatedEntries: new Set() };3905 return { worldInfoBefore: '', worldInfoAfter: '', WIDepthEntries: [], EMEntries: [], allActivatedEntries: new Set() };
@@ -4324,12 +4342,12 @@ export async function checkWorldInfo(chat, maxContext, isDryRun) {
4324 context.setExtensionPrompt(NOTE_MODULE_NAME, ANWithWI, chat_metadata[metadata_keys.position], chat_metadata[metadata_keys.depth], extension_settings.note.allowWIScan, chat_metadata[metadata_keys.role]);4342 context.setExtensionPrompt(NOTE_MODULE_NAME, ANWithWI, chat_metadata[metadata_keys.position], chat_metadata[metadata_keys.depth], extension_settings.note.allowWIScan, chat_metadata[metadata_keys.role]);
4325 }4343 }
43264344
4327 !isDryRun && timedEffects.setTimedEffects(Array.from(allActivatedEntries.values()));4345 timedEffects.setTimedEffects(Array.from(allActivatedEntries.values()));
4328 buffer.resetExternalEffects();4346 buffer.resetExternalEffects();
4329 timedEffects.cleanUp();4347 timedEffects.cleanUp();
43304348
4331 console.log(`[WI] Adding ${allActivatedEntries.size} entries to prompt`, Array.from(allActivatedEntries.values()));4349 console.log(`[WI] ${isDryRun ? 'Hypothetically adding' : 'Adding'} ${allActivatedEntries.size} entries to prompt`, Array.from(allActivatedEntries.values()));
4332 console.debug('[WI] --- DONE ---');4350 console.debug(`[WI] --- DONE${isDryRun ? ' (DRY RUN)' : ''} ---`);
43334351
4334 return { worldInfoBefore, worldInfoAfter, EMEntries, WIDepthEntries, allActivatedEntries: new Set(allActivatedEntries.values()) };4352 return { worldInfoBefore, worldInfoAfter, EMEntries, WIDepthEntries, allActivatedEntries: new Set(allActivatedEntries.values()) };
4335}4353}
@@ -4658,7 +4676,7 @@ function convertNovelLorebook(inputObj) {
4658 return outputObj;4676 return outputObj;
4659}4677}
46604678
4661function convertCharacterBook(characterBook) {4679export function convertCharacterBook(characterBook) {
4662 const result = { entries: {}, originalData: characterBook };4680 const result = { entries: {}, originalData: characterBook };
46634681
4664 characterBook.entries.forEach((entry, index) => {4682 characterBook.entries.forEach((entry, index) => {
@@ -4736,8 +4754,8 @@ export function checkEmbeddedWorld(chid) {
4736 // Only show the alert once per character4754 // Only show the alert once per character
4737 const checkKey = `AlertWI_${characters[chid].avatar}`;4755 const checkKey = `AlertWI_${characters[chid].avatar}`;
4738 const worldName = characters[chid]?.data?.extensions?.world;4756 const worldName = characters[chid]?.data?.extensions?.world;
4739 if (!localStorage.getItem(checkKey) && (!worldName || !world_names.includes(worldName))) {4757 if (!accountStorage.getItem(checkKey) && (!worldName || !world_names.includes(worldName))) {
4740 localStorage.setItem(checkKey, 'true');4758 accountStorage.setItem(checkKey, 'true');
47414759
4742 if (power_user.world_import_dialog) {4760 if (power_user.world_import_dialog) {
4743 const html = `<h3>This character has an embedded World/Lorebook.</h3>4761 const html = `<h3>This character has an embedded World/Lorebook.</h3>
@@ -5181,7 +5199,7 @@ jQuery(() => {
5181 $('#world_info_sort_order').on('change', function () {5199 $('#world_info_sort_order').on('change', function () {
5182 const value = String($(this).find(':selected').val());5200 const value = String($(this).find(':selected').val());
5183 // Save sort order, but do not save search sorting, as this is a temporary sorting option5201 // Save sort order, but do not save search sorting, as this is a temporary sorting option
5184 if (value !== 'search') localStorage.setItem(SORT_ORDER_KEY, value);5202 if (value !== 'search') accountStorage.setItem(SORT_ORDER_KEY, value);
5185 updateEditor(navigation_option.none);5203 updateEditor(navigation_option.none);
5186 });5204 });
51875205
public/style.css+131 -50
@@ -106,6 +106,8 @@
106 --tool-cool-color-picker-btn-bg: transparent;106 --tool-cool-color-picker-btn-bg: transparent;
107 --tool-cool-color-picker-btn-border-color: transparent;107 --tool-cool-color-picker-btn-border-color: transparent;
108108
109 --mes-right-spacing: 30px;
110
109 --avatar-base-height: 50px;111 --avatar-base-height: 50px;
110 --avatar-base-width: 50px;112 --avatar-base-width: 50px;
111 --avatar-base-border-radius: 2px;113 --avatar-base-border-radius: 2px;
@@ -260,6 +262,10 @@ input[type='checkbox']:focus-visible {
260 color: var(--SmartThemeEmColor);262 color: var(--SmartThemeEmColor);
261}263}
262264
265.tokenItemizingMaintext {
266 font-size: calc(var(--mainFontSize) * 0.8);
267}
268
263.tokenGraph {269.tokenGraph {
264 border-radius: 10px;270 border-radius: 10px;
265 border: 1px solid var(--SmartThemeBorderColor);271 border: 1px solid var(--SmartThemeBorderColor);
@@ -342,18 +348,68 @@ input[type='checkbox']:focus-visible {
342348
343.mes_reasoning {349.mes_reasoning {
344 display: block;350 display: block;
345 border: 1px solid var(--SmartThemeBorderColor);351 border-left: 2px solid var(--SmartThemeEmColor);
346 background-color: var(--black30a);352 border-radius: 2px;
347 border-radius: 5px;
348 padding: 5px;353 padding: 5px;
349 margin: 5px 0;354 padding-left: 14px;
355 margin-bottom: 0.5em;
350 overflow-y: auto;356 overflow-y: auto;
357 color: var(--SmartThemeEmColor);
358}
359
360.mes_reasoning_details {
361 margin-right: var(--mes-right-spacing);
351}362}
352363
353.mes_reasoning_summary {364.mes_reasoning_details .mes_reasoning_summary {
365 list-style: none;
366 margin-right: calc(var(--mes-right-spacing) * -1);
367}
368
369.mes_reasoning_details summary::-webkit-details-marker {
370 display: none;
371}
372
373.mes_reasoning *:last-child {
374 margin-bottom: 0;
375}
376
377.mes_reasoning em,
378.mes_reasoning i,
379.mes_reasoning u,
380.mes_reasoning q,
381.mes_reasoning blockquote {
382 filter: saturate(0.5);
383}
384
385.mes_reasoning_details .mes_reasoning em {
386 color: color-mix(in srgb, var(--SmartThemeEmColor) 67%, var(--SmartThemeBlurTintColor) 33%);
387}
388
389.mes_reasoning_header_block {
390 flex-grow: 1;
391}
392
393.mes_reasoning_header {
354 cursor: pointer;394 cursor: pointer;
355 position: relative;395 position: relative;
356 margin: 2px;396 user-select: none;
397 margin: 0.5em 2px;
398 padding: 7px 14px;
399 padding-right: calc(0.7em + 14px);
400 border-radius: 5px;
401 background-color: var(--grey30);
402 font-size: calc(var(--mainFontSize) * 0.9);
403 align-items: baseline;
404}
405
406.mes:has(.mes_reasoning:empty) .mes_reasoning_header {
407 cursor: default;
408}
409
410/* TWIMC: Remove with custom CSS to show the icon */
411.mes_reasoning_header>.icon-svg {
412 display: none;
357}413}
358414
359@supports not selector(:has(*)) {415@supports not selector(:has(*)) {
@@ -363,29 +419,41 @@ input[type='checkbox']:focus-visible {
363}419}
364420
365.mes_bias:empty,421.mes_bias:empty,
366.mes_reasoning:empty,422.mes:not(.reasoning) .mes_reasoning_details,
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,423.mes_reasoning_details:not([open]) .mes_reasoning_actions,
370.mes_reasoning_details:has(.reasoning_edit_textarea) .mes_reasoning,424.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,425.mes_reasoning_details:has(.reasoning_edit_textarea) .mes_reasoning_header,
372.mes_reasoning_details:not(:has(.reasoning_edit_textarea)) .mes_reasoning_actions .mes_button.mes_reasoning_edit_cancel,426.mes_reasoning_details:has(.reasoning_edit_textarea) .mes_reasoning_actions .mes_button:not(.edit_button),
373.mes_reasoning_details:has(.reasoning_edit_textarea) .mes_reasoning_actions .mes_button:not(.mes_reasoning_edit_done, .mes_reasoning_edit_cancel) {427.mes_reasoning_details:not(:has(.reasoning_edit_textarea)) .mes_reasoning_actions .edit_button,
428.mes_block:has(.edit_textarea):has(.reasoning_edit_textarea) .mes_reasoning_actions,
429.mes.reasoning:not([data-reasoning-state="hidden"]) .mes_edit_add_reasoning,
430.mes:has(.mes_reasoning:empty) .mes_reasoning_arrow,
431.mes:has(.mes_reasoning:empty) .mes_reasoning,
432.mes:has(.mes_reasoning:empty) .mes_reasoning_copy {
374 display: none;433 display: none;
375}434}
376435
377.mes_reasoning_actions {436.mes[data-reasoning-state="hidden"] .mes_edit_add_reasoning {
437 background-color: color-mix(in srgb, var(--SmartThemeQuoteColor) 33%, var(--SmartThemeBlurTintColor) 66%);
438}
439
440/** If hidden reasoning should not be shown, we hide all blocks that don't have content */
441#chat:not([data-show-hidden-reasoning="true"]):not(:has(.reasoning_edit_textarea)) .mes:has(.mes_reasoning:empty) .mes_reasoning_details {
442 display: none;
443}
444
445.mes_reasoning_details .mes_reasoning_arrow {
378 position: absolute;446 position: absolute;
379 right: 0;447 top: 50%;
380 top: 0;448 right: 7px;
449 transform: translateY(-50%);
450 font-size: calc(var(--mainFontSize) * 0.7);
451 width: calc(var(--mainFontSize) * 0.7);
452 height: calc(var(--mainFontSize) * 0.7);
453}
381454
382 display: flex;455.mes_reasoning_details:not([open]) .mes_reasoning_arrow {
383 gap: 4px;456 transform: translateY(-50%) rotate(180deg);
384 flex-wrap: nowrap;
385 justify-content: flex-end;
386 transition: all 200ms;
387 overflow-x: hidden;
388 padding: 1px;
389}457}
390458
391.mes_reasoning_summary>span {459.mes_reasoning_summary>span {
@@ -1100,13 +1168,8 @@ body .panelControlBar {
1100 /*only affects bubblechat to make it sit nicely at the bottom*/1168 /*only affects bubblechat to make it sit nicely at the bottom*/
1101}1169}
11021170
1103.last_mes:has(.mes_text:empty):has(.mes_reasoning_details[open]) .mes_reasoning:not(:empty) {1171.last_mes:has(.mes_text:empty):has(.mes_reasoning_details) .mes_reasoning:not(:empty) {
1104 margin-bottom: 30px;1172 margin-bottom: var(--mes-right-spacing);
1105}
1106
1107.last_mes .mes_reasoning,
1108.last_mes .mes_text {
1109 padding-right: 30px;
1110}1173}
11111174
1112/* SWIPE RELATED STYLES*/1175/* SWIPE RELATED STYLES*/
@@ -1330,6 +1393,7 @@ body.swipeAllMessages .mes:not(.last_mes) .swipes-counter {
1330 padding-left: 0;1393 padding-left: 0;
1331 padding-top: 5px;1394 padding-top: 5px;
1332 padding-bottom: 5px;1395 padding-bottom: 5px;
1396 padding-right: var(--mes-right-spacing);
1333}1397}
13341398
1335br {1399br {
@@ -2815,9 +2879,8 @@ select option:not(:checked) {
2815 color: var(--active) !important;2879 color: var(--active) !important;
2816}2880}
28172881
2818#instruct_enabled_label .menu_button:not(.toggleEnabled),2882.menu_button.togglable:not(.toggleEnabled) {
2819#sysprompt_enabled_label .menu_button:not(.toggleEnabled) {2883 color: red;
2820 color: Red;
2821}2884}
28222885
2823.displayBlock {2886.displayBlock {
@@ -3000,6 +3063,7 @@ input[type=search]:focus::-webkit-search-cancel-button {
3000.mes_block .ch_name {3063.mes_block .ch_name {
3001 max-width: 100%;3064 max-width: 100%;
3002 min-height: 22px;3065 min-height: 22px;
3066 align-items: flex-start;
3003}3067}
30043068
3005/*applies to both groups and solos chars in the char list*/3069/*applies to both groups and solos chars in the char list*/
@@ -4224,7 +4288,13 @@ input[type="range"]::-webkit-slider-thumb {
4224 transition: 0.3s ease-in-out;4288 transition: 0.3s ease-in-out;
4225}4289}
42264290
4227.mes_edit_buttons .menu_button {4291.mes_reasoning_actions {
4292 margin: 0;
4293 margin-top: 0.5em;
4294}
4295
4296.mes_edit_buttons .menu_button,
4297.mes_reasoning_actions .edit_button {
4228 opacity: 0.5;4298 opacity: 0.5;
4229 padding: 0px;4299 padding: 0px;
4230 font-size: 1rem;4300 font-size: 1rem;
@@ -4237,6 +4307,12 @@ input[type="range"]::-webkit-slider-thumb {
4237 align-items: center;4307 align-items: center;
4238}4308}
42394309
4310.mes_reasoning_actions .edit_button {
4311 margin-bottom: 0.5em;
4312 opacity: 1;
4313 filter: brightness(0.7);
4314}
4315
4240.mes_reasoning_edit_cancel,4316.mes_reasoning_edit_cancel,
4241.mes_edit_cancel.menu_button {4317.mes_edit_cancel.menu_button {
4242 background-color: var(--crimson70a);4318 background-color: var(--crimson70a);
@@ -4263,6 +4339,14 @@ input[type="range"]::-webkit-slider-thumb {
4263 field-sizing: content;4339 field-sizing: content;
4264}4340}
42654341
4342body[data-generating="true"] #send_but,
4343body[data-generating="true"] #mes_continue,
4344body[data-generating="true"] #mes_impersonate,
4345body[data-generating="true"] #chat .last_mes .mes_buttons,
4346body[data-generating="true"] #chat .last_mes .mes_reasoning_actions {
4347 display: none;
4348}
4349
4266#anchor_order {4350#anchor_order {
4267 margin-bottom: 15px;4351 margin-bottom: 15px;
4268}4352}
@@ -4602,23 +4686,6 @@ body .ui-widget-content li:hover {
4602 opacity: 1;4686 opacity: 1;
4603}4687}
46044688
4605.typing_indicator {
4606 position: sticky;
4607 bottom: 10px;
4608 margin: 10px;
4609 opacity: 0.85;
4610 text-shadow: 0px 0px calc(var(--shadowWidth) * 1px) var(--SmartThemeShadowColor);
4611 order: 9999;
4612}
4613
4614.typing_indicator:after {
4615 display: inline-block;
4616 vertical-align: bottom;
4617 animation: ellipsis steps(4, end) 1500ms infinite;
4618 content: "";
4619 width: 0px;
4620}
4621
4622#group_avatar_preview .missing-avatar {4689#group_avatar_preview .missing-avatar {
4623 display: inline;4690 display: inline;
4624 vertical-align: middle;4691 vertical-align: middle;
@@ -5707,11 +5774,13 @@ body:not(.movingUI) .drawer-content.maximized {
5707 overflow-wrap: anywhere;5774 overflow-wrap: anywhere;
5708}5775}
57095776
5777#SystemPromptColumn summary,
5710#InstructSequencesColumn summary {5778#InstructSequencesColumn summary {
5711 font-size: 0.95em;5779 font-size: 0.95em;
5712 cursor: pointer;5780 cursor: pointer;
5713}5781}
57145782
5783#SystemPromptColumn details,
5715#InstructSequencesColumn details:not(:last-of-type) {5784#InstructSequencesColumn details:not(:last-of-type) {
5716 margin-bottom: 5px;5785 margin-bottom: 5px;
5717}5786}
@@ -5875,3 +5944,15 @@ body:not(.movingUI) .drawer-content.maximized {
5875.mes_text div[data-type="assistant_note"]:has(.assistant_note_export)>div:not(.assistant_note_export) {5944.mes_text div[data-type="assistant_note"]:has(.assistant_note_export)>div:not(.assistant_note_export) {
5876 flex: 1;5945 flex: 1;
5877}5946}
5947
5948.oneline-dropdown label {
5949 margin-top: 3px;
5950 margin-bottom: 5px;
5951 flex-grow: 1;
5952 text-align: left;
5953}
5954
5955.oneline-dropdown select {
5956 min-width: fit-content;
5957 width: 40%;
5958}
server.js+232 -43
@@ -4,6 +4,7 @@
4import fs from 'node:fs';4import fs from 'node:fs';
5import http from 'node:http';5import http from 'node:http';
6import https from 'node:https';6import https from 'node:https';
7import os from 'os';
7import path from 'node:path';8import path from 'node:path';
8import util from 'node:util';9import util from 'node:util';
9import net from 'node:net';10import net from 'node:net';
@@ -29,6 +30,7 @@ import bodyParser from 'body-parser';
2930
30// net related library imports31// net related library imports
31import fetch from 'node-fetch';32import fetch from 'node-fetch';
33import ipRegex from 'ip-regex';
3234
33// Unrestrict console logs display limit35// Unrestrict console logs display limit
34util.inspect.defaultOptions.maxArrayLength = null;36util.inspect.defaultOptions.maxArrayLength = null;
@@ -55,9 +57,10 @@ import {
5557
56import getWebpackServeMiddleware from './src/middleware/webpack-serve.js';58import getWebpackServeMiddleware from './src/middleware/webpack-serve.js';
57import basicAuthMiddleware from './src/middleware/basicAuth.js';59import basicAuthMiddleware from './src/middleware/basicAuth.js';
58import whitelistMiddleware from './src/middleware/whitelist.js';60import whitelistMiddleware, { getAccessLogPath, migrateAccessLog } from './src/middleware/whitelist.js';
59import multerMonkeyPatch from './src/middleware/multerMonkeyPatch.js';61import multerMonkeyPatch from './src/middleware/multerMonkeyPatch.js';
60import initRequestProxy from './src/request-proxy.js';62import initRequestProxy from './src/request-proxy.js';
63import getCacheBusterMiddleware from './src/middleware/cacheBuster.js';
61import {64import {
62 getVersion,65 getVersion,
63 getConfigValue,66 getConfigValue,
@@ -65,7 +68,11 @@ import {
65 forwardFetchResponse,68 forwardFetchResponse,
66 removeColorFormatting,69 removeColorFormatting,
67 getSeparator,70 getSeparator,
71 stringToBool,
72 urlHostnameToIPv6,
73 canResolve,
68 safeReadFileSync,74 safeReadFileSync,
75 setupLogLevel,
69} from './src/util.js';76} from './src/util.js';
70import { UPLOADS_DIRECTORY } from './src/constants.js';77import { UPLOADS_DIRECTORY } from './src/constants.js';
71import { ensureThumbnailCache } from './src/endpoints/thumbnails.js';78import { ensureThumbnailCache } from './src/endpoints/thumbnails.js';
@@ -125,6 +132,8 @@ if (process.versions && process.versions.node && process.versions.node.match(/20
125const DEFAULT_PORT = 8000;132const DEFAULT_PORT = 8000;
126const DEFAULT_AUTORUN = false;133const DEFAULT_AUTORUN = false;
127const DEFAULT_LISTEN = false;134const DEFAULT_LISTEN = false;
135const DEFAULT_LISTEN_ADDRESS_IPV6 = '[::]';
136const DEFAULT_LISTEN_ADDRESS_IPV4 = '0.0.0.0';
128const DEFAULT_CORS_PROXY = false;137const DEFAULT_CORS_PROXY = false;
129const DEFAULT_WHITELIST = true;138const DEFAULT_WHITELIST = true;
130const DEFAULT_ACCOUNTS = false;139const DEFAULT_ACCOUNTS = false;
@@ -149,11 +158,11 @@ const DEFAULT_PROXY_BYPASS = [];
149const cliArguments = yargs(hideBin(process.argv))158const cliArguments = yargs(hideBin(process.argv))
150 .usage('Usage: <your-start-script> <command> [options]')159 .usage('Usage: <your-start-script> <command> [options]')
151 .option('enableIPv6', {160 .option('enableIPv6', {
152 type: 'boolean',161 type: 'string',
153 default: null,162 default: null,
154 describe: `Enables IPv6.\n[config default: ${DEFAULT_ENABLE_IPV6}]`,163 describe: `Enables IPv6.\n[config default: ${DEFAULT_ENABLE_IPV6}]`,
155 }).option('enableIPv4', {164 }).option('enableIPv4', {
156 type: 'boolean',165 type: 'string',
157 default: null,166 default: null,
158 describe: `Enables IPv4.\n[config default: ${DEFAULT_ENABLE_IPV4}]`,167 describe: `Enables IPv4.\n[config default: ${DEFAULT_ENABLE_IPV4}]`,
159 }).option('port', {168 }).option('port', {
@@ -180,6 +189,14 @@ const cliArguments = yargs(hideBin(process.argv))
180 type: 'boolean',189 type: 'boolean',
181 default: null,190 default: null,
182 describe: `SillyTavern is listening on all network interfaces (Wi-Fi, LAN, localhost). If false, will limit it only to internal localhost (127.0.0.1).\nIf not provided falls back to yaml config 'listen'.\n[config default: ${DEFAULT_LISTEN}]`,191 describe: `SillyTavern is listening on all network interfaces (Wi-Fi, LAN, localhost). If false, will limit it only to internal localhost (127.0.0.1).\nIf not provided falls back to yaml config 'listen'.\n[config default: ${DEFAULT_LISTEN}]`,
192 }).option('listenAddressIPv6', {
193 type: 'string',
194 default: null,
195 describe: 'Set SillyTavern to listen to a specific IPv6 address. If not set, it will fallback to listen to all.\n[config default: [::] ]',
196 }).option('listenAddressIPv4', {
197 type: 'string',
198 default: null,
199 describe: 'Set SillyTavern to listen to a specific IPv4 address. If not set, it will fallback to listen to all.\n[config default: 0.0.0.0 ]',
183 }).option('corsProxy', {200 }).option('corsProxy', {
184 type: 'boolean',201 type: 'boolean',
185 default: null,202 default: null,
@@ -242,27 +259,46 @@ app.use(helmet({
242app.use(compression());259app.use(compression());
243app.use(responseTime());260app.use(responseTime());
244261
262
263/** @type {number} */
245const server_port = cliArguments.port ?? process.env.SILLY_TAVERN_PORT ?? getConfigValue('port', DEFAULT_PORT);264const server_port = cliArguments.port ?? process.env.SILLY_TAVERN_PORT ?? getConfigValue('port', DEFAULT_PORT);
265/** @type {boolean} */
246const autorun = (cliArguments.autorun ?? getConfigValue('autorun', DEFAULT_AUTORUN)) && !cliArguments.ssl;266const autorun = (cliArguments.autorun ?? getConfigValue('autorun', DEFAULT_AUTORUN)) && !cliArguments.ssl;
267/** @type {boolean} */
247const listen = cliArguments.listen ?? getConfigValue('listen', DEFAULT_LISTEN);268const listen = cliArguments.listen ?? getConfigValue('listen', DEFAULT_LISTEN);
269/** @type {string} */
270const listenAddressIPv6 = cliArguments.listenAddressIPv6 ?? getConfigValue('listenAddress.ipv6', DEFAULT_LISTEN_ADDRESS_IPV6);
271/** @type {string} */
272const listenAddressIPv4 = cliArguments.listenAddressIPv4 ?? getConfigValue('listenAddress.ipv4', DEFAULT_LISTEN_ADDRESS_IPV4);
273/** @type {boolean} */
248const enableCorsProxy = cliArguments.corsProxy ?? getConfigValue('enableCorsProxy', DEFAULT_CORS_PROXY);274const enableCorsProxy = cliArguments.corsProxy ?? getConfigValue('enableCorsProxy', DEFAULT_CORS_PROXY);
249const enableWhitelist = cliArguments.whitelist ?? getConfigValue('whitelistMode', DEFAULT_WHITELIST);275const enableWhitelist = cliArguments.whitelist ?? getConfigValue('whitelistMode', DEFAULT_WHITELIST);
276/** @type {string} */
250const dataRoot = cliArguments.dataRoot ?? getConfigValue('dataRoot', './data');277const dataRoot = cliArguments.dataRoot ?? getConfigValue('dataRoot', './data');
278/** @type {boolean} */
251const disableCsrf = cliArguments.disableCsrf ?? getConfigValue('disableCsrfProtection', DEFAULT_CSRF_DISABLED);279const disableCsrf = cliArguments.disableCsrf ?? getConfigValue('disableCsrfProtection', DEFAULT_CSRF_DISABLED);
252const basicAuthMode = cliArguments.basicAuthMode ?? getConfigValue('basicAuthMode', DEFAULT_BASIC_AUTH);280const basicAuthMode = cliArguments.basicAuthMode ?? getConfigValue('basicAuthMode', DEFAULT_BASIC_AUTH);
253const perUserBasicAuth = getConfigValue('perUserBasicAuth', DEFAULT_PER_USER_BASIC_AUTH);281const perUserBasicAuth = getConfigValue('perUserBasicAuth', DEFAULT_PER_USER_BASIC_AUTH);
282/** @type {boolean} */
254const enableAccounts = getConfigValue('enableUserAccounts', DEFAULT_ACCOUNTS);283const enableAccounts = getConfigValue('enableUserAccounts', DEFAULT_ACCOUNTS);
255284
256const uploadsPath = path.join(dataRoot, UPLOADS_DIRECTORY);285const uploadsPath = path.join(dataRoot, UPLOADS_DIRECTORY);
257286
258const enableIPv6 = cliArguments.enableIPv6 ?? getConfigValue('protocol.ipv6', DEFAULT_ENABLE_IPV6);
259const enableIPv4 = cliArguments.enableIPv4 ?? getConfigValue('protocol.ipv4', DEFAULT_ENABLE_IPV4);
260287
288/** @type {boolean | "auto"} */
289let enableIPv6 = stringToBool(cliArguments.enableIPv6) ?? getConfigValue('protocol.ipv6', DEFAULT_ENABLE_IPV6);
290/** @type {boolean | "auto"} */
291let enableIPv4 = stringToBool(cliArguments.enableIPv4) ?? getConfigValue('protocol.ipv4', DEFAULT_ENABLE_IPV4);
292
293/** @type {string} */
261const autorunHostname = cliArguments.autorunHostname ?? getConfigValue('autorunHostname', DEFAULT_AUTORUN_HOSTNAME);294const autorunHostname = cliArguments.autorunHostname ?? getConfigValue('autorunHostname', DEFAULT_AUTORUN_HOSTNAME);
295/** @type {number} */
262const autorunPortOverride = cliArguments.autorunPortOverride ?? getConfigValue('autorunPortOverride', DEFAULT_AUTORUN_PORT);296const autorunPortOverride = cliArguments.autorunPortOverride ?? getConfigValue('autorunPortOverride', DEFAULT_AUTORUN_PORT);
263297
298/** @type {boolean} */
264const dnsPreferIPv6 = cliArguments.dnsPreferIPv6 ?? getConfigValue('dnsPreferIPv6', DEFAULT_PREFER_IPV6);299const dnsPreferIPv6 = cliArguments.dnsPreferIPv6 ?? getConfigValue('dnsPreferIPv6', DEFAULT_PREFER_IPV6);
265300
301/** @type {boolean} */
266const avoidLocalhost = cliArguments.avoidLocalhost ?? getConfigValue('avoidLocalhost', DEFAULT_AVOID_LOCALHOST);302const avoidLocalhost = cliArguments.avoidLocalhost ?? getConfigValue('avoidLocalhost', DEFAULT_AVOID_LOCALHOST);
267303
268const proxyEnabled = cliArguments.requestProxyEnabled ?? getConfigValue('requestProxy.enabled', DEFAULT_PROXY_ENABLED);304const proxyEnabled = cliArguments.requestProxyEnabled ?? getConfigValue('requestProxy.enabled', DEFAULT_PROXY_ENABLED);
@@ -279,7 +315,19 @@ if (dnsPreferIPv6) {
279 console.log('Preferring IPv4 for DNS resolution');315 console.log('Preferring IPv4 for DNS resolution');
280}316}
281317
282if (!enableIPv6 && !enableIPv4) {318
319const ipOptions = [true, 'auto', false];
320
321if (!ipOptions.includes(enableIPv6)) {
322 console.warn(color.red('`protocol: ipv6` option invalid'), '\n use:', ipOptions, '\n setting to:', DEFAULT_ENABLE_IPV6);
323 enableIPv6 = DEFAULT_ENABLE_IPV6;
324}
325if (!ipOptions.includes(enableIPv4)) {
326 console.warn(color.red('`protocol: ipv4` option invalid'), '\n use:', ipOptions, '\n setting to:', DEFAULT_ENABLE_IPV4);
327 enableIPv4 = DEFAULT_ENABLE_IPV4;
328}
329
330if (enableIPv6 === false && enableIPv4 === false) {
283 console.error('error: You can\'t disable all internet protocols: at least IPv6 or IPv4 must be enabled.');331 console.error('error: You can\'t disable all internet protocols: at least IPv6 or IPv4 must be enabled.');
284 process.exit(1);332 process.exit(1);
285}333}
@@ -364,6 +412,55 @@ function getSessionCookieAge() {
364 return undefined;412 return undefined;
365}413}
366414
415/**
416 * Checks the network interfaces to determine the presence of IPv6 and IPv4 addresses.
417 *
418 * @returns {Promise<[boolean, boolean, boolean, boolean]>} A promise that resolves to an array containing:
419 * - [0]: `hasIPv6` (boolean) - Whether the computer has any IPv6 address, including (`::1`).
420 * - [1]: `hasIPv4` (boolean) - Whether the computer has any IPv4 address, including (`127.0.0.1`).
421 * - [2]: `hasIPv6Local` (boolean) - Whether the computer has local IPv6 address (`::1`).
422 * - [3]: `hasIPv4Local` (boolean) - Whether the computer has local IPv4 address (`127.0.0.1`).
423 */
424async function getHasIP() {
425 let hasIPv6 = false;
426 let hasIPv6Local = false;
427
428 let hasIPv4 = false;
429 let hasIPv4Local = false;
430
431 const interfaces = os.networkInterfaces();
432
433 for (const iface of Object.values(interfaces)) {
434 if (iface === undefined) {
435 continue;
436 }
437
438 for (const info of iface) {
439 if (info.family === 'IPv6') {
440 hasIPv6 = true;
441 if (info.address === '::1') {
442 hasIPv6Local = true;
443 }
444 }
445
446 if (info.family === 'IPv4') {
447 hasIPv4 = true;
448 if (info.address === '127.0.0.1') {
449 hasIPv4Local = true;
450 }
451 }
452 if (hasIPv6 && hasIPv4 && hasIPv6Local && hasIPv4Local) break;
453 }
454 if (hasIPv6 && hasIPv4 && hasIPv6Local && hasIPv4Local) break;
455 }
456 return [
457 hasIPv6,
458 hasIPv4,
459 hasIPv6Local,
460 hasIPv4Local,
461 ];
462}
463
367app.use(cookieSession({464app.use(cookieSession({
368 name: getCookieSessionName(),465 name: getCookieSessionName(),
369 sameSite: 'strict',466 sameSite: 'strict',
@@ -419,7 +516,7 @@ if (!disableCsrf) {
419516
420// Static files517// Static files
421// Host index page518// Host index page
422app.get('/', (request, response) => {519app.get('/', getCacheBusterMiddleware(), (request, response) => {
423 if (shouldRedirectToLogin(request)) {520 if (shouldRedirectToLogin(request)) {
424 const query = request.url.split('?')[1];521 const query = request.url.split('?')[1];
425 const redirectUrl = query ? `/login?${query}` : '/login';522 const redirectUrl = query ? `/login?${query}` : '/login';
@@ -627,13 +724,13 @@ app.use('/api/azure', azureRouter);
627724
628const tavernUrlV6 = new URL(725const tavernUrlV6 = new URL(
629 (cliArguments.ssl ? 'https://' : 'http://') +726 (cliArguments.ssl ? 'https://' : 'http://') +
630 (listen ? '[::]' : '[::1]') +727 (listen ? (ipRegex.v6({ exact: true }).test(listenAddressIPv6) ? listenAddressIPv6 : '[::]') : '[::1]') +
631 (':' + server_port),728 (':' + server_port),
632);729);
633730
634const tavernUrl = new URL(731const tavernUrl = new URL(
635 (cliArguments.ssl ? 'https://' : 'http://') +732 (cliArguments.ssl ? 'https://' : 'http://') +
636 (listen ? '0.0.0.0' : '127.0.0.1') +733 (listen ? (ipRegex.v4({ exact: true }).test(listenAddressIPv4) ? listenAddressIPv4 : '0.0.0.0') : '127.0.0.1') +
637 (':' + server_port),734 (':' + server_port),
638);735);
639736
@@ -657,6 +754,7 @@ const preSetupTasks = async function () {
657 await checkForNewContent(directories);754 await checkForNewContent(directories);
658 await ensureThumbnailCache();755 await ensureThumbnailCache();
659 cleanUploads();756 cleanUploads();
757 migrateAccessLog();
660758
661 await settingsInit();759 await settingsInit();
662 await statsInit();760 await statsInit();
@@ -693,20 +791,23 @@ const preSetupTasks = async function () {
693791
694/**792/**
695 * Gets the hostname to use for autorun in the browser.793 * Gets the hostname to use for autorun in the browser.
696 * @returns {string} The hostname to use for autorun794 * @param {boolean} useIPv6 If use IPv6
795 * @param {boolean} useIPv4 If use IPv4
796 * @returns Promise<string> The hostname to use for autorun
697 */797 */
698function getAutorunHostname() {798async function getAutorunHostname(useIPv6, useIPv4) {
699 if (autorunHostname === 'auto') {799 if (autorunHostname === 'auto') {
700 if (enableIPv6 && enableIPv4) {800 let localhostResolve = await canResolve('localhost', useIPv6, useIPv4);
701 if (avoidLocalhost) return '[::1]';801
702 return 'localhost';802 if (useIPv6 && useIPv4) {
803 return (avoidLocalhost || !localhostResolve) ? '[::1]' : 'localhost';
703 }804 }
704805
705 if (enableIPv6) {806 if (useIPv6) {
706 return '[::1]';807 return '[::1]';
707 }808 }
708809
709 if (enableIPv4) {810 if (useIPv4) {
710 return '127.0.0.1';811 return '127.0.0.1';
711 }812 }
712 }813 }
@@ -718,11 +819,13 @@ function getAutorunHostname() {
718 * Tasks that need to be run after the server starts listening.819 * Tasks that need to be run after the server starts listening.
719 * @param {boolean} v6Failed If the server failed to start on IPv6820 * @param {boolean} v6Failed If the server failed to start on IPv6
720 * @param {boolean} v4Failed If the server failed to start on IPv4821 * @param {boolean} v4Failed If the server failed to start on IPv4
822 * @param {boolean} useIPv6 If the server is using IPv6
823 * @param {boolean} useIPv4 If the server is using IPv4
721 */824 */
722const postSetupTasks = async function (v6Failed, v4Failed) {825const postSetupTasks = async function (v6Failed, v4Failed, useIPv6, useIPv4) {
723 const autorunUrl = new URL(826 const autorunUrl = new URL(
724 (cliArguments.ssl ? 'https://' : 'http://') +827 (cliArguments.ssl ? 'https://' : 'http://') +
725 (getAutorunHostname()) +828 (await getAutorunHostname(useIPv6, useIPv4)) +
726 (':') +829 (':') +
727 ((autorunPortOverride >= 0) ? autorunPortOverride : server_port),830 ((autorunPortOverride >= 0) ? autorunPortOverride : server_port),
728 );831 );
@@ -735,36 +838,48 @@ const postSetupTasks = async function (v6Failed, v4Failed) {
735838
736 let logListen = 'SillyTavern is listening on';839 let logListen = 'SillyTavern is listening on';
737840
738 if (enableIPv6 && !v6Failed) {841 if (useIPv6 && !v6Failed) {
739 logListen += color.green(' IPv6: ' + tavernUrlV6.host);842 logListen += color.green(
843 ' IPv6: ' + tavernUrlV6.host,
844 );
740 }845 }
741846
742 if (enableIPv4 && !v4Failed) {847 if (useIPv4 && !v4Failed) {
743 logListen += color.green(' IPv4: ' + tavernUrl.host);848 logListen += color.green(
849 ' IPv4: ' + tavernUrl.host,
850 );
744 }851 }
745852
746 const goToLog = 'Go to: ' + color.blue(autorunUrl) + ' to open SillyTavern';853 const goToLog = 'Go to: ' + color.blue(autorunUrl) + ' to open SillyTavern';
747 const plainGoToLog = removeColorFormatting(goToLog);854 const plainGoToLog = removeColorFormatting(goToLog);
748855
749 console.log(logListen);856 console.log(logListen);
857 if (listen) {
858 console.log();
859 console.log('To limit connections to internal localhost only ([::1] or 127.0.0.1), change the setting in config.yaml to "listen: false".');
860 console.log('Check the "access.log" file in the data directory to inspect incoming connections:', color.green(getAccessLogPath()));
861 }
750 console.log('\n' + getSeparator(plainGoToLog.length) + '\n');862 console.log('\n' + getSeparator(plainGoToLog.length) + '\n');
751 console.log(goToLog);863 console.log(goToLog);
752 console.log('\n' + getSeparator(plainGoToLog.length) + '\n');864 console.log('\n' + getSeparator(plainGoToLog.length) + '\n');
753865
754 if (listen) {
755 console.log('[::] or 0.0.0.0 means SillyTavern is listening on all network interfaces (Wi-Fi, LAN, localhost). If you want to limit it only to internal localhost ([::1] or 127.0.0.1), change the setting in config.yaml to "listen: false". Check "access.log" file in the SillyTavern directory if you want to inspect incoming connections.\n');
756 }
757866
758 if (basicAuthMode) {867 if (basicAuthMode) {
759 if (perUserBasicAuth && !enableAccounts) {868 if (perUserBasicAuth && !enableAccounts) {
760 console.error(color.red('Per-user basic authentication is enabled, but user accounts are disabled. This configuration may be insecure.'));869 console.error(color.red(
870 'Per-user basic authentication is enabled, but user accounts are disabled. This configuration may be insecure.',
871 ));
761 } else if (!perUserBasicAuth) {872 } else if (!perUserBasicAuth) {
762 const basicAuthUser = getConfigValue('basicAuthUser', {});873 const basicAuthUser = getConfigValue('basicAuthUser', {});
763 if (!basicAuthUser?.username || !basicAuthUser?.password) {874 if (!basicAuthUser?.username || !basicAuthUser?.password) {
764 console.warn(color.yellow('Basic Authentication is enabled, but username or password is not set or empty!'));875 console.warn(color.yellow(
876 'Basic Authentication is enabled, but username or password is not set or empty!',
877 ));
765 }878 }
766 }879 }
767 }880 }
881
882 setupLogLevel();
768};883};
769884
770/**885/**
@@ -814,14 +929,16 @@ function logSecurityAlert(message) {
814 * Handles the case where the server failed to start on one or both protocols.929 * Handles the case where the server failed to start on one or both protocols.
815 * @param {boolean} v6Failed If the server failed to start on IPv6930 * @param {boolean} v6Failed If the server failed to start on IPv6
816 * @param {boolean} v4Failed If the server failed to start on IPv4931 * @param {boolean} v4Failed If the server failed to start on IPv4
932 * @param {boolean} useIPv6 If use IPv6
933 * @param {boolean} useIPv4 If use IPv4
817 */934 */
818function handleServerListenFail(v6Failed, v4Failed) {935function handleServerListenFail(v6Failed, v4Failed, useIPv6, useIPv4) {
819 if (v6Failed && !enableIPv4) {936 if (v6Failed && !useIPv4) {
820 console.error(color.red('fatal error: Failed to start server on IPv6 and IPv4 disabled'));937 console.error(color.red('fatal error: Failed to start server on IPv6 and IPv4 disabled'));
821 process.exit(1);938 process.exit(1);
822 }939 }
823940
824 if (v4Failed && !enableIPv6) {941 if (v4Failed && !useIPv6) {
825 console.error(color.red('fatal error: Failed to start server on IPv4 and IPv6 disabled'));942 console.error(color.red('fatal error: Failed to start server on IPv4 and IPv6 disabled'));
826 process.exit(1);943 process.exit(1);
827 }944 }
@@ -835,10 +952,11 @@ function handleServerListenFail(v6Failed, v4Failed) {
835/**952/**
836 * Creates an HTTPS server.953 * Creates an HTTPS server.
837 * @param {URL} url The URL to listen on954 * @param {URL} url The URL to listen on
955 * @param {number} ipVersion the ip version to use
838 * @returns {Promise<void>} A promise that resolves when the server is listening956 * @returns {Promise<void>} A promise that resolves when the server is listening
839 * @throws {Error} If the server fails to start957 * @throws {Error} If the server fails to start
840 */958 */
841function createHttpsServer(url) {959function createHttpsServer(url, ipVersion) {
842 return new Promise((resolve, reject) => {960 return new Promise((resolve, reject) => {
843 const server = https.createServer(961 const server = https.createServer(
844 {962 {
@@ -847,34 +965,56 @@ function createHttpsServer(url) {
847 }, app);965 }, app);
848 server.on('error', reject);966 server.on('error', reject);
849 server.on('listening', resolve);967 server.on('listening', resolve);
850 server.listen(Number(url.port || 443), url.hostname);968
969 let host = url.hostname;
970 if (ipVersion === 6) host = urlHostnameToIPv6(url.hostname);
971 server.listen({
972 host: host,
973 port: Number(url.port || 443),
974 // see https://nodejs.org/api/net.html#serverlisten for why ipv6Only is used
975 ipv6Only: true,
976 });
851 });977 });
852}978}
853979
854/**980/**
855 * Creates an HTTP server.981 * Creates an HTTP server.
856 * @param {URL} url The URL to listen on982 * @param {URL} url The URL to listen on
983 * @param {number} ipVersion the ip version to use
857 * @returns {Promise<void>} A promise that resolves when the server is listening984 * @returns {Promise<void>} A promise that resolves when the server is listening
858 * @throws {Error} If the server fails to start985 * @throws {Error} If the server fails to start
859 */986 */
860function createHttpServer(url) {987function createHttpServer(url, ipVersion) {
861 return new Promise((resolve, reject) => {988 return new Promise((resolve, reject) => {
862 const server = http.createServer(app);989 const server = http.createServer(app);
863 server.on('error', reject);990 server.on('error', reject);
864 server.on('listening', resolve);991 server.on('listening', resolve);
865 server.listen(Number(url.port || 80), url.hostname);992
993 let host = url.hostname;
994 if (ipVersion === 6) host = urlHostnameToIPv6(url.hostname);
995 server.listen({
996 host: host,
997 port: Number(url.port || 80),
998 // see https://nodejs.org/api/net.html#serverlisten for why ipv6Only is used
999 ipv6Only: true,
1000 });
866 });1001 });
867}1002}
8681003
869async function startHTTPorHTTPS() {1004/**
1005 * Starts the server using http or https depending on config
1006 * @param {boolean} useIPv6 If use IPv6
1007 * @param {boolean} useIPv4 If use IPv4
1008 */
1009async function startHTTPorHTTPS(useIPv6, useIPv4) {
870 let v6Failed = false;1010 let v6Failed = false;
871 let v4Failed = false;1011 let v4Failed = false;
8721012
873 const createFunc = cliArguments.ssl ? createHttpsServer : createHttpServer;1013 const createFunc = cliArguments.ssl ? createHttpsServer : createHttpServer;
8741014
875 if (enableIPv6) {1015 if (useIPv6) {
876 try {1016 try {
877 await createFunc(tavernUrlV6);1017 await createFunc(tavernUrlV6, 6);
878 } catch (error) {1018 } catch (error) {
879 console.error('non-fatal error: failed to start server on IPv6');1019 console.error('non-fatal error: failed to start server on IPv6');
880 console.error(error);1020 console.error(error);
@@ -883,9 +1023,9 @@ async function startHTTPorHTTPS() {
883 }1023 }
884 }1024 }
8851025
886 if (enableIPv4) {1026 if (useIPv4) {
887 try {1027 try {
888 await createFunc(tavernUrl);1028 await createFunc(tavernUrl, 4);
889 } catch (error) {1029 } catch (error) {
890 console.error('non-fatal error: failed to start server on IPv4');1030 console.error('non-fatal error: failed to start server on IPv4');
891 console.error(error);1031 console.error(error);
@@ -898,10 +1038,59 @@ async function startHTTPorHTTPS() {
898}1038}
8991039
900async function startServer() {1040async function startServer() {
901 const [v6Failed, v4Failed] = await startHTTPorHTTPS();1041 let useIPv6 = (enableIPv6 === true);
1042 let useIPv4 = (enableIPv4 === true);
1043
1044 let hasIPv6 = false,
1045 hasIPv4 = false,
1046 hasIPv6Local = false,
1047 hasIPv4Local = false,
1048 hasIPv6Any = false,
1049 hasIPv4Any = false;
1050
1051 if (enableIPv6 === 'auto' || enableIPv4 === 'auto') {
1052 [hasIPv6Any, hasIPv4Any, hasIPv6Local, hasIPv4Local] = await getHasIP();
1053
1054 hasIPv6 = listen ? hasIPv6Any : hasIPv6Local;
1055 if (enableIPv6 === 'auto') {
1056 useIPv6 = hasIPv6;
1057 }
1058 if (hasIPv6) {
1059 if (useIPv6) {
1060 console.log(color.green('IPv6 support detected'));
1061 } else {
1062 console.log('IPv6 support detected (but disabled)');
1063 }
1064 }
1065
1066 hasIPv4 = listen ? hasIPv4Any : hasIPv4Local;
1067 if (enableIPv4 === 'auto') {
1068 useIPv4 = hasIPv4;
1069 }
1070 if (hasIPv4) {
1071 if (useIPv4) {
1072 console.log(color.green('IPv4 support detected'));
1073 } else {
1074 console.log('IPv4 support detected (but disabled)');
1075 }
1076 }
1077
1078 if (enableIPv6 === 'auto' && enableIPv4 === 'auto') {
1079 if (!hasIPv6 && !hasIPv4) {
1080 console.error('Both IPv6 and IPv4 are not detected');
1081 process.exit(1);
1082 }
1083 }
1084 }
1085
1086 if (!useIPv6 && !useIPv4) {
1087 console.error('Both IPv6 and IPv4 are disabled,\nP.S. you should never see this error, at least at one point it was checked for before this, with the rest of the config options');
1088 process.exit(1);
1089 }
9021090
903 handleServerListenFail(v6Failed, v4Failed);1091 const [v6Failed, v4Failed] = await startHTTPorHTTPS(useIPv6, useIPv4);
904 postSetupTasks(v6Failed, v4Failed);1092 handleServerListenFail(v6Failed, v4Failed, useIPv6, useIPv4);
1093 postSetupTasks(v6Failed, v4Failed, useIPv6, useIPv4);
905}1094}
9061095
907async function verifySecuritySettings() {1096async function verifySecuritySettings() {
@@ -911,7 +1100,7 @@ async function verifySecuritySettings() {
911 }1100 }
9121101
913 if (!enableAccounts) {1102 if (!enableAccounts) {
914 logSecurityAlert('Your SillyTavern is currently insecurely open to the public. Enable whitelisting, basic authentication or user accounts.');1103 logSecurityAlert('Your current SillyTavern configuration is insecure (listening to non-localhost). Enable whitelisting, basic authentication or user accounts.');
915 }1104 }
9161105
917 const users = await getAllEnabledUsers();1106 const users = await getAllEnabledUsers();
src/constants.js+12 -4
@@ -139,19 +139,19 @@ export const UNSAFE_EXTENSIONS = [
139export const GEMINI_SAFETY = [139export const GEMINI_SAFETY = [
140 {140 {
141 category: 'HARM_CATEGORY_HARASSMENT',141 category: 'HARM_CATEGORY_HARASSMENT',
142 threshold: 'BLOCK_NONE',142 threshold: 'OFF',
143 },143 },
144 {144 {
145 category: 'HARM_CATEGORY_HATE_SPEECH',145 category: 'HARM_CATEGORY_HATE_SPEECH',
146 threshold: 'BLOCK_NONE',146 threshold: 'OFF',
147 },147 },
148 {148 {
149 category: 'HARM_CATEGORY_SEXUALLY_EXPLICIT',149 category: 'HARM_CATEGORY_SEXUALLY_EXPLICIT',
150 threshold: 'BLOCK_NONE',150 threshold: 'OFF',
151 },151 },
152 {152 {
153 category: 'HARM_CATEGORY_DANGEROUS_CONTENT',153 category: 'HARM_CATEGORY_DANGEROUS_CONTENT',
154 threshold: 'BLOCK_NONE',154 threshold: 'OFF',
155 },155 },
156 {156 {
157 category: 'HARM_CATEGORY_CIVIC_INTEGRITY',157 category: 'HARM_CATEGORY_CIVIC_INTEGRITY',
@@ -304,6 +304,7 @@ export const TOGETHERAI_KEYS = [
304export const OLLAMA_KEYS = [304export const OLLAMA_KEYS = [
305 'num_predict',305 'num_predict',
306 'num_ctx',306 'num_ctx',
307 'num_batch',
307 'stop',308 'stop',
308 'temperature',309 'temperature',
309 'repeat_penalty',310 'repeat_penalty',
@@ -414,3 +415,10 @@ export const VLLM_KEYS = [
414 'guided_decoding_backend',415 'guided_decoding_backend',
415 'guided_whitespace_pattern',416 'guided_whitespace_pattern',
416];417];
418
419export const LOG_LEVELS = {
420 DEBUG: 0,
421 INFO: 1,
422 WARN: 2,
423 ERROR: 3,
424};
src/endpoints/anthropic.js+3 -3
@@ -32,7 +32,7 @@ router.post('/caption-image', jsonParser, async (request, response) => {
32 max_tokens: 4096,32 max_tokens: 4096,
33 };33 };
3434
35 console.log('Multimodal captioning request', body);35 console.debug('Multimodal captioning request', body);
3636
37 const result = await fetch(url, {37 const result = await fetch(url, {
38 body: JSON.stringify(body),38 body: JSON.stringify(body),
@@ -46,14 +46,14 @@ router.post('/caption-image', jsonParser, async (request, response) => {
4646
47 if (!result.ok) {47 if (!result.ok) {
48 const text = await result.text();48 const text = await result.text();
49 console.log(`Claude API returned error: ${result.status} ${result.statusText}`, text);49 console.warn(`Claude API returned error: ${result.status} ${result.statusText}`, text);
50 return response.status(result.status).send({ error: true });50 return response.status(result.status).send({ error: true });
51 }51 }
5252
53 /** @type {any} */53 /** @type {any} */
54 const generateResponseJson = await result.json();54 const generateResponseJson = await result.json();
55 const caption = generateResponseJson.content[0].text;55 const caption = generateResponseJson.content[0].text;
56 console.log('Claude response:', generateResponseJson);56 console.debug('Claude response:', generateResponseJson);
5757
58 if (!caption) {58 if (!caption) {
59 return response.status(500).send('No caption found');59 return response.status(500).send('No caption found');
src/endpoints/assets.js+13 -12
@@ -176,7 +176,7 @@ router.post('/get', jsonParser, async (request, response) => {
176 }176 }
177 }177 }
178 catch (err) {178 catch (err) {
179 console.log(err);179 console.error(err);
180 }180 }
181 return response.send(output);181 return response.send(output);
182});182});
@@ -200,7 +200,7 @@ router.post('/download', jsonParser, async (request, response) => {
200 category = i;200 category = i;
201201
202 if (category === null) {202 if (category === null) {
203 console.debug('Bad request: unsupported asset category.');203 console.error('Bad request: unsupported asset category.');
204 return response.sendStatus(400);204 return response.sendStatus(400);
205 }205 }
206206
@@ -212,7 +212,7 @@ router.post('/download', jsonParser, async (request, response) => {
212212
213 const temp_path = path.join(request.user.directories.assets, 'temp', request.body.filename);213 const temp_path = path.join(request.user.directories.assets, 'temp', request.body.filename);
214 const file_path = path.join(request.user.directories.assets, category, request.body.filename);214 const file_path = path.join(request.user.directories.assets, category, request.body.filename);
215 console.debug('Request received to download', url, 'to', file_path);215 console.info('Request received to download', url, 'to', file_path);
216216
217 try {217 try {
218 // Download to temp218 // Download to temp
@@ -241,13 +241,13 @@ router.post('/download', jsonParser, async (request, response) => {
241 }241 }
242242
243 // Move into asset place243 // Move into asset place
244 console.debug('Download finished, moving file from', temp_path, 'to', file_path);244 console.info('Download finished, moving file from', temp_path, 'to', file_path);
245 fs.copyFileSync(temp_path, file_path);245 fs.copyFileSync(temp_path, file_path);
246 fs.rmSync(temp_path);246 fs.rmSync(temp_path);
247 response.sendStatus(200);247 response.sendStatus(200);
248 }248 }
249 catch (error) {249 catch (error) {
250 console.log(error);250 console.error(error);
251 response.sendStatus(500);251 response.sendStatus(500);
252 }252 }
253});253});
@@ -270,7 +270,7 @@ router.post('/delete', jsonParser, async (request, response) => {
270 category = i;270 category = i;
271271
272 if (category === null) {272 if (category === null) {
273 console.debug('Bad request: unsupported asset category.');273 console.error('Bad request: unsupported asset category.');
274 return response.sendStatus(400);274 return response.sendStatus(400);
275 }275 }
276276
@@ -280,7 +280,7 @@ router.post('/delete', jsonParser, async (request, response) => {
280 return response.status(400).send(validation.message);280 return response.status(400).send(validation.message);
281281
282 const file_path = path.join(request.user.directories.assets, category, request.body.filename);282 const file_path = path.join(request.user.directories.assets, category, request.body.filename);
283 console.debug('Request received to delete', category, file_path);283 console.info('Request received to delete', category, file_path);
284284
285 try {285 try {
286 // Delete if previous download failed286 // Delete if previous download failed
@@ -288,17 +288,17 @@ router.post('/delete', jsonParser, async (request, response) => {
288 fs.unlink(file_path, (err) => {288 fs.unlink(file_path, (err) => {
289 if (err) throw err;289 if (err) throw err;
290 });290 });
291 console.debug('Asset deleted.');291 console.info('Asset deleted.');
292 }292 }
293 else {293 else {
294 console.debug('Asset not found.');294 console.error('Asset not found.');
295 response.sendStatus(400);295 response.sendStatus(400);
296 }296 }
297 // Move into asset place297 // Move into asset place
298 response.sendStatus(200);298 response.sendStatus(200);
299 }299 }
300 catch (error) {300 catch (error) {
301 console.log(error);301 console.error(error);
302 response.sendStatus(500);302 response.sendStatus(500);
303 }303 }
304});304});
@@ -314,6 +314,7 @@ router.post('/delete', jsonParser, async (request, response) => {
314 */314 */
315router.post('/character', jsonParser, async (request, response) => {315router.post('/character', jsonParser, async (request, response) => {
316 if (request.query.name === undefined) return response.sendStatus(400);316 if (request.query.name === undefined) return response.sendStatus(400);
317
317 // For backwards compatibility, don't reject invalid character names, just sanitize them318 // For backwards compatibility, don't reject invalid character names, just sanitize them
318 const name = sanitize(request.query.name.toString());319 const name = sanitize(request.query.name.toString());
319 const inputCategory = request.query.category;320 const inputCategory = request.query.category;
@@ -325,7 +326,7 @@ router.post('/character', jsonParser, async (request, response) => {
325 category = i;326 category = i;
326327
327 if (category === null) {328 if (category === null) {
328 console.debug('Bad request: unsupported asset category.');329 console.error('Bad request: unsupported asset category.');
329 return response.sendStatus(400);330 return response.sendStatus(400);
330 }331 }
331332
@@ -364,7 +365,7 @@ router.post('/character', jsonParser, async (request, response) => {
364 return response.send(output);365 return response.send(output);
365 }366 }
366 catch (err) {367 catch (err) {
367 console.log(err);368 console.error(err);
368 return response.sendStatus(500);369 return response.sendStatus(500);
369 }370 }
370});371});
src/endpoints/azure.js+6 -6
@@ -11,14 +11,14 @@ router.post('/list', jsonParser, async (req, res) => {
11 const key = readSecret(req.user.directories, SECRET_KEYS.AZURE_TTS);11 const key = readSecret(req.user.directories, SECRET_KEYS.AZURE_TTS);
1212
13 if (!key) {13 if (!key) {
14 console.error('Azure TTS API Key not set');14 console.warn('Azure TTS API Key not set');
15 return res.sendStatus(403);15 return res.sendStatus(403);
16 }16 }
1717
18 const region = req.body.region;18 const region = req.body.region;
1919
20 if (!region) {20 if (!region) {
21 console.error('Azure TTS region not set');21 console.warn('Azure TTS region not set');
22 return res.sendStatus(400);22 return res.sendStatus(400);
23 }23 }
2424
@@ -32,7 +32,7 @@ router.post('/list', jsonParser, async (req, res) => {
32 });32 });
3333
34 if (!response.ok) {34 if (!response.ok) {
35 console.error('Azure Request failed', response.status, response.statusText);35 console.warn('Azure Request failed', response.status, response.statusText);
36 return res.sendStatus(500);36 return res.sendStatus(500);
37 }37 }
3838
@@ -49,13 +49,13 @@ router.post('/generate', jsonParser, async (req, res) => {
49 const key = readSecret(req.user.directories, SECRET_KEYS.AZURE_TTS);49 const key = readSecret(req.user.directories, SECRET_KEYS.AZURE_TTS);
5050
51 if (!key) {51 if (!key) {
52 console.error('Azure TTS API Key not set');52 console.warn('Azure TTS API Key not set');
53 return res.sendStatus(403);53 return res.sendStatus(403);
54 }54 }
5555
56 const { text, voice, region } = req.body;56 const { text, voice, region } = req.body;
57 if (!text || !voice || !region) {57 if (!text || !voice || !region) {
58 console.error('Missing required parameters');58 console.warn('Missing required parameters');
59 return res.sendStatus(400);59 return res.sendStatus(400);
60 }60 }
6161
@@ -75,7 +75,7 @@ router.post('/generate', jsonParser, async (req, res) => {
75 });75 });
7676
77 if (!response.ok) {77 if (!response.ok) {
78 console.error('Azure Request failed', response.status, response.statusText);78 console.warn('Azure Request failed', response.status, response.statusText);
79 return res.sendStatus(500);79 return res.sendStatus(500);
80 }80 }
8181
src/endpoints/backends/chat-completions.js+78 -65
@@ -114,7 +114,7 @@ async function sendClaudeRequest(request, response) {
114 }114 }
115115
116 if (!apiKey) {116 if (!apiKey) {
117 console.log(color.red(`Claude API key is missing.\n${divider}`));117 console.warn(color.red(`Claude API key is missing.\n${divider}`));
118 return response.status(400).send({ error: true });118 return response.status(400).send({ error: true });
119 }119 }
120120
@@ -179,7 +179,7 @@ async function sendClaudeRequest(request, response) {
179 additionalHeaders['anthropic-beta'] = 'prompt-caching-2024-07-31';179 additionalHeaders['anthropic-beta'] = 'prompt-caching-2024-07-31';
180 }180 }
181181
182 console.log('Claude request:', requestBody);182 console.debug('Claude request:', requestBody);
183183
184 const generateResponse = await fetch(apiUrl + '/messages', {184 const generateResponse = await fetch(apiUrl + '/messages', {
185 method: 'POST',185 method: 'POST',
@@ -199,21 +199,21 @@ async function sendClaudeRequest(request, response) {
199 } else {199 } else {
200 if (!generateResponse.ok) {200 if (!generateResponse.ok) {
201 const generateResponseText = await generateResponse.text();201 const generateResponseText = await generateResponse.text();
202 console.log(color.red(`Claude API returned error: ${generateResponse.status} ${generateResponse.statusText}\n${generateResponseText}\n${divider}`));202 console.warn(color.red(`Claude API returned error: ${generateResponse.status} ${generateResponse.statusText}\n${generateResponseText}\n${divider}`));
203 return response.status(generateResponse.status).send({ error: true });203 return response.status(generateResponse.status).send({ error: true });
204 }204 }
205205
206 /** @type {any} */206 /** @type {any} */
207 const generateResponseJson = await generateResponse.json();207 const generateResponseJson = await generateResponse.json();
208 const responseText = generateResponseJson?.content?.[0]?.text || '';208 const responseText = generateResponseJson?.content?.[0]?.text || '';
209 console.log('Claude response:', generateResponseJson);209 console.debug('Claude response:', generateResponseJson);
210210
211 // Wrap it back to OAI format + save the original content211 // Wrap it back to OAI format + save the original content
212 const reply = { choices: [{ 'message': { 'content': responseText } }], content: generateResponseJson.content };212 const reply = { choices: [{ 'message': { 'content': responseText } }], content: generateResponseJson.content };
213 return response.send(reply);213 return response.send(reply);
214 }214 }
215 } catch (error) {215 } catch (error) {
216 console.log(color.red(`Error communicating with Claude: ${error}\n${divider}`));216 console.error(color.red(`Error communicating with Claude: ${error}\n${divider}`));
217 if (!response.headersSent) {217 if (!response.headersSent) {
218 return response.status(500).send({ error: true });218 return response.status(500).send({ error: true });
219 }219 }
@@ -230,12 +230,12 @@ async function sendScaleRequest(request, response) {
230 const apiKey = readSecret(request.user.directories, SECRET_KEYS.SCALE);230 const apiKey = readSecret(request.user.directories, SECRET_KEYS.SCALE);
231231
232 if (!apiKey) {232 if (!apiKey) {
233 console.log('Scale API key is missing.');233 console.warn('Scale API key is missing.');
234 return response.status(400).send({ error: true });234 return response.status(400).send({ error: true });
235 }235 }
236236
237 const requestPrompt = convertTextCompletionPrompt(request.body.messages);237 const requestPrompt = convertTextCompletionPrompt(request.body.messages);
238 console.log('Scale request:', requestPrompt);238 console.debug('Scale request:', requestPrompt);
239239
240 try {240 try {
241 const controller = new AbortController();241 const controller = new AbortController();
@@ -254,18 +254,18 @@ async function sendScaleRequest(request, response) {
254 });254 });
255255
256 if (!generateResponse.ok) {256 if (!generateResponse.ok) {
257 console.log(`Scale API returned error: ${generateResponse.status} ${generateResponse.statusText} ${await generateResponse.text()}`);257 console.warn(`Scale API returned error: ${generateResponse.status} ${generateResponse.statusText} ${await generateResponse.text()}`);
258 return response.status(500).send({ error: true });258 return response.status(500).send({ error: true });
259 }259 }
260260
261 /** @type {any} */261 /** @type {any} */
262 const generateResponseJson = await generateResponse.json();262 const generateResponseJson = await generateResponse.json();
263 console.log('Scale response:', generateResponseJson);263 console.debug('Scale response:', generateResponseJson);
264264
265 const reply = { choices: [{ 'message': { 'content': generateResponseJson.output } }] };265 const reply = { choices: [{ 'message': { 'content': generateResponseJson.output } }] };
266 return response.send(reply);266 return response.send(reply);
267 } catch (error) {267 } catch (error) {
268 console.log(error);268 console.error(error);
269 if (!response.headersSent) {269 if (!response.headersSent) {
270 return response.status(500).send({ error: true });270 return response.status(500).send({ error: true });
271 }271 }
@@ -282,13 +282,12 @@ async function sendMakerSuiteRequest(request, response) {
282 const apiKey = request.body.reverse_proxy ? request.body.proxy_password : readSecret(request.user.directories, SECRET_KEYS.MAKERSUITE);282 const apiKey = request.body.reverse_proxy ? request.body.proxy_password : readSecret(request.user.directories, SECRET_KEYS.MAKERSUITE);
283283
284 if (!request.body.reverse_proxy && !apiKey) {284 if (!request.body.reverse_proxy && !apiKey) {
285 console.log('Google AI Studio API key is missing.');285 console.warn('Google AI Studio API key is missing.');
286 return response.status(400).send({ error: true });286 return response.status(400).send({ error: true });
287 }287 }
288288
289 const model = String(request.body.model);289 const model = String(request.body.model);
290 const stream = Boolean(request.body.stream);290 const stream = Boolean(request.body.stream);
291 const showThoughts = Boolean(request.body.include_reasoning);
292 const isThinking = model.includes('thinking');291 const isThinking = model.includes('thinking');
293292
294 const generationConfig = {293 const generationConfig = {
@@ -306,8 +305,9 @@ async function sendMakerSuiteRequest(request, response) {
306 }305 }
307306
308 const should_use_system_prompt = (307 const should_use_system_prompt = (
308 model.includes('gemini-2.0-pro') ||
309 model.includes('gemini-2.0-flash') ||
309 model.includes('gemini-2.0-flash-thinking-exp') ||310 model.includes('gemini-2.0-flash-thinking-exp') ||
310 model.includes('gemini-2.0-flash-exp') ||
311 model.includes('gemini-1.5-flash') ||311 model.includes('gemini-1.5-flash') ||
312 model.includes('gemini-1.5-pro') ||312 model.includes('gemini-1.5-pro') ||
313 model.startsWith('gemini-exp')313 model.startsWith('gemini-exp')
@@ -316,9 +316,15 @@ async function sendMakerSuiteRequest(request, response) {
316 const prompt = convertGooglePrompt(request.body.messages, model, should_use_system_prompt, getPromptNames(request));316 const prompt = convertGooglePrompt(request.body.messages, model, should_use_system_prompt, getPromptNames(request));
317 let safetySettings = GEMINI_SAFETY;317 let safetySettings = GEMINI_SAFETY;
318318
319 if (model.includes('gemini-2.0-flash-exp')) {319 // These old models do not support setting the threshold to OFF at all.
320 if (['gemini-1.5-pro-001', 'gemini-1.5-flash-001', 'gemini-1.5-flash-8b-exp-0827', 'gemini-1.5-flash-8b-exp-0924', 'gemini-pro', 'gemini-1.0-pro', 'gemini-1.0-pro-001'].includes(model)) {
321 safetySettings = GEMINI_SAFETY.map(setting => ({ ...setting, threshold: 'BLOCK_NONE' }));
322 }
323 // Interestingly, Gemini 2.0 Flash does support setting the threshold for HARM_CATEGORY_CIVIC_INTEGRITY to OFF.
324 else if (['gemini-2.0-flash', 'gemini-2.0-flash-001', 'gemini-2.0-flash-exp'].includes(model)) {
320 safetySettings = GEMINI_SAFETY.map(setting => ({ ...setting, threshold: 'OFF' }));325 safetySettings = GEMINI_SAFETY.map(setting => ({ ...setting, threshold: 'OFF' }));
321 }326 }
327 // Most of the other models allow for setting the threshold of filters, except for HARM_CATEGORY_CIVIC_INTEGRITY, to OFF.
322328
323 let body = {329 let body = {
324 contents: prompt.contents,330 contents: prompt.contents,
@@ -330,17 +336,11 @@ async function sendMakerSuiteRequest(request, response) {
330 body.systemInstruction = prompt.system_instruction;336 body.systemInstruction = prompt.system_instruction;
331 }337 }
332338
333 if (isThinking && showThoughts) {
334 generationConfig.thinkingConfig = {
335 includeThoughts: true,
336 };
337 }
338
339 return body;339 return body;
340 }340 }
341341
342 const body = getGeminiBody();342 const body = getGeminiBody();
343 console.log('Google AI Studio request:', body);343 console.debug('Google AI Studio request:', body);
344344
345 try {345 try {
346 const controller = new AbortController();346 const controller = new AbortController();
@@ -366,14 +366,14 @@ async function sendMakerSuiteRequest(request, response) {
366 // Pipe remote SSE stream to Express response366 // Pipe remote SSE stream to Express response
367 forwardFetchResponse(generateResponse, response);367 forwardFetchResponse(generateResponse, response);
368 } catch (error) {368 } catch (error) {
369 console.log('Error forwarding streaming response:', error);369 console.error('Error forwarding streaming response:', error);
370 if (!response.headersSent) {370 if (!response.headersSent) {
371 return response.status(500).send({ error: true });371 return response.status(500).send({ error: true });
372 }372 }
373 }373 }
374 } else {374 } else {
375 if (!generateResponse.ok) {375 if (!generateResponse.ok) {
376 console.log(`Google AI Studio API returned error: ${generateResponse.status} ${generateResponse.statusText} ${await generateResponse.text()}`);376 console.warn(`Google AI Studio API returned error: ${generateResponse.status} ${generateResponse.statusText} ${await generateResponse.text()}`);
377 return response.status(generateResponse.status).send({ error: true });377 return response.status(generateResponse.status).send({ error: true });
378 }378 }
379379
@@ -383,7 +383,7 @@ async function sendMakerSuiteRequest(request, response) {
383 const candidates = generateResponseJson?.candidates;383 const candidates = generateResponseJson?.candidates;
384 if (!candidates || candidates.length === 0) {384 if (!candidates || candidates.length === 0) {
385 let message = 'Google AI Studio API returned no candidate';385 let message = 'Google AI Studio API returned no candidate';
386 console.log(message, generateResponseJson);386 console.warn(message, generateResponseJson);
387 if (generateResponseJson?.promptFeedback?.blockReason) {387 if (generateResponseJson?.promptFeedback?.blockReason) {
388 message += `\nPrompt was blocked due to : ${generateResponseJson.promptFeedback.blockReason}`;388 message += `\nPrompt was blocked due to : ${generateResponseJson.promptFeedback.blockReason}`;
389 }389 }
@@ -391,12 +391,12 @@ async function sendMakerSuiteRequest(request, response) {
391 }391 }
392392
393 const responseContent = candidates[0].content ?? candidates[0].output;393 const responseContent = candidates[0].content ?? candidates[0].output;
394 console.log('Google AI Studio response:', responseContent);394 console.warn('Google AI Studio response:', responseContent);
395395
396 const responseText = typeof responseContent === 'string' ? responseContent : responseContent?.parts?.filter(part => !part.thought)?.map(part => part.text)?.join('\n\n');396 const responseText = typeof responseContent === 'string' ? responseContent : responseContent?.parts?.filter(part => !part.thought)?.map(part => part.text)?.join('\n\n');
397 if (!responseText) {397 if (!responseText) {
398 let message = 'Google AI Studio Candidate text empty';398 let message = 'Google AI Studio Candidate text empty';
399 console.log(message, generateResponseJson);399 console.warn(message, generateResponseJson);
400 return response.send({ error: { message } });400 return response.send({ error: { message } });
401 }401 }
402402
@@ -405,7 +405,7 @@ async function sendMakerSuiteRequest(request, response) {
405 return response.send(reply);405 return response.send(reply);
406 }406 }
407 } catch (error) {407 } catch (error) {
408 console.log('Error communicating with Google AI Studio API: ', error);408 console.error('Error communicating with Google AI Studio API: ', error);
409 if (!response.headersSent) {409 if (!response.headersSent) {
410 return response.status(500).send({ error: true });410 return response.status(500).send({ error: true });
411 }411 }
@@ -419,8 +419,9 @@ async function sendMakerSuiteRequest(request, response) {
419 */419 */
420async function sendAI21Request(request, response) {420async function sendAI21Request(request, response) {
421 if (!request.body) return response.sendStatus(400);421 if (!request.body) return response.sendStatus(400);
422
422 const controller = new AbortController();423 const controller = new AbortController();
423 console.log(request.body.messages);424 console.debug(request.body.messages);
424 request.socket.removeAllListeners('close');425 request.socket.removeAllListeners('close');
425 request.socket.on('close', function () {426 request.socket.on('close', function () {
426 controller.abort();427 controller.abort();
@@ -446,7 +447,7 @@ async function sendAI21Request(request, response) {
446 signal: controller.signal,447 signal: controller.signal,
447 };448 };
448449
449 console.log('AI21 request:', body);450 console.debug('AI21 request:', body);
450451
451 try {452 try {
452 const generateResponse = await fetch(API_AI21 + '/chat/completions', options);453 const generateResponse = await fetch(API_AI21 + '/chat/completions', options);
@@ -455,16 +456,16 @@ async function sendAI21Request(request, response) {
455 } else {456 } else {
456 if (!generateResponse.ok) {457 if (!generateResponse.ok) {
457 const errorText = await generateResponse.text();458 const errorText = await generateResponse.text();
458 console.log(`AI21 API returned error: ${generateResponse.status} ${generateResponse.statusText} ${errorText}`);459 console.warn(`AI21 API returned error: ${generateResponse.status} ${generateResponse.statusText} ${errorText}`);
459 const errorJson = tryParse(errorText) ?? { error: true };460 const errorJson = tryParse(errorText) ?? { error: true };
460 return response.status(500).send(errorJson);461 return response.status(500).send(errorJson);
461 }462 }
462 const generateResponseJson = await generateResponse.json();463 const generateResponseJson = await generateResponse.json();
463 console.log('AI21 response:', generateResponseJson);464 console.debug('AI21 response:', generateResponseJson);
464 return response.send(generateResponseJson);465 return response.send(generateResponseJson);
465 }466 }
466 } catch (error) {467 } catch (error) {
467 console.log('Error communicating with AI21 API: ', error);468 console.error('Error communicating with AI21 API: ', error);
468 if (!response.headersSent) {469 if (!response.headersSent) {
469 response.send({ error: true });470 response.send({ error: true });
470 } else {471 } else {
@@ -483,7 +484,7 @@ async function sendMistralAIRequest(request, response) {
483 const apiKey = request.body.reverse_proxy ? request.body.proxy_password : readSecret(request.user.directories, SECRET_KEYS.MISTRALAI);484 const apiKey = request.body.reverse_proxy ? request.body.proxy_password : readSecret(request.user.directories, SECRET_KEYS.MISTRALAI);
484485
485 if (!apiKey) {486 if (!apiKey) {
486 console.log('MistralAI API key is missing.');487 console.warn('MistralAI API key is missing.');
487 return response.status(400).send({ error: true });488 return response.status(400).send({ error: true });
488 }489 }
489490
@@ -524,7 +525,7 @@ async function sendMistralAIRequest(request, response) {
524 timeout: 0,525 timeout: 0,
525 };526 };
526527
527 console.log('MisralAI request:', requestBody);528 console.debug('MisralAI request:', requestBody);
528529
529 const generateResponse = await fetch(apiUrl + '/chat/completions', config);530 const generateResponse = await fetch(apiUrl + '/chat/completions', config);
530 if (request.body.stream) {531 if (request.body.stream) {
@@ -532,16 +533,16 @@ async function sendMistralAIRequest(request, response) {
532 } else {533 } else {
533 if (!generateResponse.ok) {534 if (!generateResponse.ok) {
534 const errorText = await generateResponse.text();535 const errorText = await generateResponse.text();
535 console.log(`MistralAI API returned error: ${generateResponse.status} ${generateResponse.statusText} ${errorText}`);536 console.warn(`MistralAI API returned error: ${generateResponse.status} ${generateResponse.statusText} ${errorText}`);
536 const errorJson = tryParse(errorText) ?? { error: true };537 const errorJson = tryParse(errorText) ?? { error: true };
537 return response.status(500).send(errorJson);538 return response.status(500).send(errorJson);
538 }539 }
539 const generateResponseJson = await generateResponse.json();540 const generateResponseJson = await generateResponse.json();
540 console.log('MistralAI response:', generateResponseJson);541 console.debug('MistralAI response:', generateResponseJson);
541 return response.send(generateResponseJson);542 return response.send(generateResponseJson);
542 }543 }
543 } catch (error) {544 } catch (error) {
544 console.log('Error communicating with MistralAI API: ', error);545 console.error('Error communicating with MistralAI API: ', error);
545 if (!response.headersSent) {546 if (!response.headersSent) {
546 response.send({ error: true });547 response.send({ error: true });
547 } else {548 } else {
@@ -564,7 +565,7 @@ async function sendCohereRequest(request, response) {
564 });565 });
565566
566 if (!apiKey) {567 if (!apiKey) {
567 console.log('Cohere API key is missing.');568 console.warn('Cohere API key is missing.');
568 return response.status(400).send({ error: true });569 return response.status(400).send({ error: true });
569 }570 }
570571
@@ -603,7 +604,7 @@ async function sendCohereRequest(request, response) {
603 requestBody.safety_mode = 'OFF';604 requestBody.safety_mode = 'OFF';
604 }605 }
605606
606 console.log('Cohere request:', requestBody);607 console.debug('Cohere request:', requestBody);
607608
608 const config = {609 const config = {
609 method: 'POST',610 method: 'POST',
@@ -625,16 +626,16 @@ async function sendCohereRequest(request, response) {
625 const generateResponse = await fetch(apiUrl, config);626 const generateResponse = await fetch(apiUrl, config);
626 if (!generateResponse.ok) {627 if (!generateResponse.ok) {
627 const errorText = await generateResponse.text();628 const errorText = await generateResponse.text();
628 console.log(`Cohere API returned error: ${generateResponse.status} ${generateResponse.statusText} ${errorText}`);629 console.warn(`Cohere API returned error: ${generateResponse.status} ${generateResponse.statusText} ${errorText}`);
629 const errorJson = tryParse(errorText) ?? { error: true };630 const errorJson = tryParse(errorText) ?? { error: true };
630 return response.status(500).send(errorJson);631 return response.status(500).send(errorJson);
631 }632 }
632 const generateResponseJson = await generateResponse.json();633 const generateResponseJson = await generateResponse.json();
633 console.log('Cohere response:', generateResponseJson);634 console.debug('Cohere response:', generateResponseJson);
634 return response.send(generateResponseJson);635 return response.send(generateResponseJson);
635 }636 }
636 } catch (error) {637 } catch (error) {
637 console.log('Error communicating with Cohere API: ', error);638 console.error('Error communicating with Cohere API: ', error);
638 if (!response.headersSent) {639 if (!response.headersSent) {
639 response.send({ error: true });640 response.send({ error: true });
640 } else {641 } else {
@@ -653,7 +654,7 @@ async function sendDeepSeekRequest(request, response) {
653 const apiKey = request.body.reverse_proxy ? request.body.proxy_password : readSecret(request.user.directories, SECRET_KEYS.DEEPSEEK);654 const apiKey = request.body.reverse_proxy ? request.body.proxy_password : readSecret(request.user.directories, SECRET_KEYS.DEEPSEEK);
654655
655 if (!apiKey && !request.body.reverse_proxy) {656 if (!apiKey && !request.body.reverse_proxy) {
656 console.log('DeepSeek API key is missing.');657 console.warn('DeepSeek API key is missing.');
657 return response.status(400).send({ error: true });658 return response.status(400).send({ error: true });
658 }659 }
659660
@@ -671,6 +672,11 @@ async function sendDeepSeekRequest(request, response) {
671 bodyParams['logprobs'] = true;672 bodyParams['logprobs'] = true;
672 }673 }
673674
675 if (Array.isArray(request.body.tools) && request.body.tools.length > 0) {
676 bodyParams['tools'] = request.body.tools;
677 bodyParams['tool_choice'] = request.body.tool_choice;
678 }
679
674 const postProcessType = String(request.body.model).endsWith('-reasoner') ? 'deepseek-reasoner' : 'deepseek';680 const postProcessType = String(request.body.model).endsWith('-reasoner') ? 'deepseek-reasoner' : 'deepseek';
675 const processedMessages = postProcessPrompt(request.body.messages, postProcessType, getPromptNames(request));681 const processedMessages = postProcessPrompt(request.body.messages, postProcessType, getPromptNames(request));
676682
@@ -698,7 +704,7 @@ async function sendDeepSeekRequest(request, response) {
698 signal: controller.signal,704 signal: controller.signal,
699 };705 };
700706
701 console.log('DeepSeek request:', requestBody);707 console.debug('DeepSeek request:', requestBody);
702708
703 const generateResponse = await fetch(apiUrl + '/chat/completions', config);709 const generateResponse = await fetch(apiUrl + '/chat/completions', config);
704710
@@ -707,16 +713,16 @@ async function sendDeepSeekRequest(request, response) {
707 } else {713 } else {
708 if (!generateResponse.ok) {714 if (!generateResponse.ok) {
709 const errorText = await generateResponse.text();715 const errorText = await generateResponse.text();
710 console.log(`DeepSeek API returned error: ${generateResponse.status} ${generateResponse.statusText} ${errorText}`);716 console.warn(`DeepSeek API returned error: ${generateResponse.status} ${generateResponse.statusText} ${errorText}`);
711 const errorJson = tryParse(errorText) ?? { error: true };717 const errorJson = tryParse(errorText) ?? { error: true };
712 return response.status(500).send(errorJson);718 return response.status(500).send(errorJson);
713 }719 }
714 const generateResponseJson = await generateResponse.json();720 const generateResponseJson = await generateResponse.json();
715 console.log('DeepSeek response:', generateResponseJson);721 console.debug('DeepSeek response:', generateResponseJson);
716 return response.send(generateResponseJson);722 return response.send(generateResponseJson);
717 }723 }
718 } catch (error) {724 } catch (error) {
719 console.log('Error communicating with DeepSeek API: ', error);725 console.error('Error communicating with DeepSeek API: ', error);
720 if (!response.headersSent) {726 if (!response.headersSent) {
721 response.send({ error: true });727 response.send({ error: true });
722 } else {728 } else {
@@ -774,12 +780,12 @@ router.post('/status', jsonParser, async function (request, response_getstatus_o
774 api_key_openai = request.body.reverse_proxy ? request.body.proxy_password : readSecret(request.user.directories, SECRET_KEYS.DEEPSEEK);780 api_key_openai = request.body.reverse_proxy ? request.body.proxy_password : readSecret(request.user.directories, SECRET_KEYS.DEEPSEEK);
775 headers = {};781 headers = {};
776 } else {782 } else {
777 console.log('This chat completion source is not supported yet.');783 console.warn('This chat completion source is not supported yet.');
778 return response_getstatus_openai.status(400).send({ error: true });784 return response_getstatus_openai.status(400).send({ error: true });
779 }785 }
780786
781 if (!api_key_openai && !request.body.reverse_proxy && request.body.chat_completion_source !== CHAT_COMPLETION_SOURCES.CUSTOM) {787 if (!api_key_openai && !request.body.reverse_proxy && request.body.chat_completion_source !== CHAT_COMPLETION_SOURCES.CUSTOM) {
782 console.log('Chat Completion API key is missing.');788 console.warn('Chat Completion API key is missing.');
783 return response_getstatus_openai.status(400).send({ error: true });789 return response_getstatus_openai.status(400).send({ error: true });
784 }790 }
785791
@@ -814,23 +820,23 @@ router.post('/status', jsonParser, async function (request, response_getstatus_o
814 };820 };
815 });821 });
816822
817 console.log('Available OpenRouter models:', models);823 console.info('Available OpenRouter models:', models);
818 } else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.MISTRALAI) {824 } else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.MISTRALAI) {
819 const models = data?.data;825 const models = data?.data;
820 console.log(models);826 console.info(models);
821 } else {827 } else {
822 const models = data?.data;828 const models = data?.data;
823829
824 if (Array.isArray(models)) {830 if (Array.isArray(models)) {
825 const modelIds = models.filter(x => x && typeof x === 'object').map(x => x.id).sort();831 const modelIds = models.filter(x => x && typeof x === 'object').map(x => x.id).sort();
826 console.log('Available models:', modelIds);832 console.info('Available models:', modelIds);
827 } else {833 } else {
828 console.log('Chat Completion endpoint did not return a list of models.');834 console.warn('Chat Completion endpoint did not return a list of models.');
829 }835 }
830 }836 }
831 }837 }
832 else {838 else {
833 console.log('Chat Completion status check failed. Either Access Token is incorrect or API endpoint is down.');839 console.error('Chat Completion status check failed. Either Access Token is incorrect or API endpoint is down.');
834 response_getstatus_openai.send({ error: true, can_bypass: true, data: { data: [] } });840 response_getstatus_openai.send({ error: true, can_bypass: true, data: { data: [] } });
835 }841 }
836 } catch (e) {842 } catch (e) {
@@ -863,7 +869,7 @@ router.post('/bias', jsonParser, async function (request, response) {
863 const tokenizer = getSentencepiceTokenizer(model);869 const tokenizer = getSentencepiceTokenizer(model);
864 const instance = await tokenizer?.get();870 const instance = await tokenizer?.get();
865 if (!instance) {871 if (!instance) {
866 console.warn('Tokenizer not initialized:', model);872 console.error('Tokenizer not initialized:', model);
867 return response.send({});873 return response.send({});
868 }874 }
869 encodeFunction = (text) => new Uint32Array(instance.encodeIds(text));875 encodeFunction = (text) => new Uint32Array(instance.encodeIds(text));
@@ -1025,7 +1031,7 @@ router.post('/generate', jsonParser, function (request, response) {
1025 mergeObjectWithYaml(headers, request.body.custom_include_headers);1031 mergeObjectWithYaml(headers, request.body.custom_include_headers);
10261032
1027 if (request.body.custom_prompt_post_processing) {1033 if (request.body.custom_prompt_post_processing) {
1028 console.log('Applying custom prompt post-processing of type', request.body.custom_prompt_post_processing);1034 console.info('Applying custom prompt post-processing of type', request.body.custom_prompt_post_processing);
1029 request.body.messages = postProcessPrompt(1035 request.body.messages = postProcessPrompt(
1030 request.body.messages,1036 request.body.messages,
1031 request.body.custom_prompt_post_processing,1037 request.body.custom_prompt_post_processing,
@@ -1058,12 +1064,19 @@ router.post('/generate', jsonParser, function (request, response) {
1058 headers = {};1064 headers = {};
1059 bodyParams = {};1065 bodyParams = {};
1060 } else {1066 } else {
1061 console.log('This chat completion source is not supported yet.');1067 console.warn('This chat completion source is not supported yet.');
1062 return response.status(400).send({ error: true });1068 return response.status(400).send({ error: true });
1063 }1069 }
10641070
1071 // A few of OpenAIs reasoning models support reasoning effort
1072 if ([CHAT_COMPLETION_SOURCES.CUSTOM, CHAT_COMPLETION_SOURCES.OPENAI].includes(request.body.chat_completion_source)) {
1073 if (['o1', 'o3-mini', 'o3-mini-2025-01-31'].includes(request.body.model)) {
1074 bodyParams['reasoning_effort'] = request.body.reasoning_effort;
1075 }
1076 }
1077
1065 if (!apiKey && !request.body.reverse_proxy && request.body.chat_completion_source !== CHAT_COMPLETION_SOURCES.CUSTOM) {1078 if (!apiKey && !request.body.reverse_proxy && request.body.chat_completion_source !== CHAT_COMPLETION_SOURCES.CUSTOM) {
1066 console.log('OpenAI API key is missing.');1079 console.warn('OpenAI API key is missing.');
1067 return response.status(400).send({ error: true });1080 return response.status(400).send({ error: true });
1068 }1081 }
10691082
@@ -1123,7 +1136,7 @@ router.post('/generate', jsonParser, function (request, response) {
1123 signal: controller.signal,1136 signal: controller.signal,
1124 };1137 };
11251138
1126 console.log(requestBody);1139 console.debug(requestBody);
11271140
1128 makeRequest(config, response, request);1141 makeRequest(config, response, request);
11291142
@@ -1140,7 +1153,7 @@ router.post('/generate', jsonParser, function (request, response) {
1140 const fetchResponse = await fetch(endpointUrl, config);1153 const fetchResponse = await fetch(endpointUrl, config);
11411154
1142 if (request.body.stream) {1155 if (request.body.stream) {
1143 console.log('Streaming request in progress');1156 console.info('Streaming request in progress');
1144 forwardFetchResponse(fetchResponse, response);1157 forwardFetchResponse(fetchResponse, response);
1145 return;1158 return;
1146 }1159 }
@@ -1149,10 +1162,10 @@ router.post('/generate', jsonParser, function (request, response) {
1149 /** @type {any} */1162 /** @type {any} */
1150 let json = await fetchResponse.json();1163 let json = await fetchResponse.json();
1151 response.send(json);1164 response.send(json);
1152 console.log(json);1165 console.debug(json);
1153 console.log(json?.choices?.[0]?.message);1166 console.debug(json?.choices?.[0]?.message);
1154 } else if (fetchResponse.status === 429 && retries > 0) {1167 } else if (fetchResponse.status === 429 && retries > 0) {
1155 console.log(`Out of quota, retrying in ${Math.round(timeout / 1000)}s`);1168 console.warn(`Out of quota, retrying in ${Math.round(timeout / 1000)}s`);
1156 setTimeout(() => {1169 setTimeout(() => {
1157 timeout *= 2;1170 timeout *= 2;
1158 makeRequest(config, response, request, retries - 1, timeout);1171 makeRequest(config, response, request, retries - 1, timeout);
@@ -1161,7 +1174,7 @@ router.post('/generate', jsonParser, function (request, response) {
1161 await handleErrorResponse(fetchResponse);1174 await handleErrorResponse(fetchResponse);
1162 }1175 }
1163 } catch (error) {1176 } catch (error) {
1164 console.log('Generation failed', error);1177 console.error('Generation failed', error);
1165 const message = error.code === 'ECONNREFUSED'1178 const message = error.code === 'ECONNREFUSED'
1166 ? `Connection refused: ${error.message}`1179 ? `Connection refused: ${error.message}`
1167 : error.message || 'Unknown error occurred';1180 : error.message || 'Unknown error occurred';
@@ -1183,7 +1196,7 @@ router.post('/generate', jsonParser, function (request, response) {
11831196
1184 const message = errorResponse.statusText || 'Unknown error occurred';1197 const message = errorResponse.statusText || 'Unknown error occurred';
1185 const quota_error = errorResponse.status === 429 && errorData?.error?.type === 'insufficient_quota';1198 const quota_error = errorResponse.status === 429 && errorData?.error?.type === 'insufficient_quota';
1186 console.log('Chat completion request error: ', message, responseText);1199 console.error('Chat completion request error: ', message, responseText);
11871200
1188 if (!response.headersSent) {1201 if (!response.headersSent) {
1189 response.send({ error: { message }, quota_error: quota_error });1202 response.send({ error: { message }, quota_error: quota_error });
src/endpoints/backends/kobold.js+14 -14
@@ -22,17 +22,17 @@ router.post('/generate', jsonParser, async function (request, response_generate)
22 request.socket.on('close', async function () {22 request.socket.on('close', async function () {
23 if (request.body.can_abort && !response_generate.writableEnded) {23 if (request.body.can_abort && !response_generate.writableEnded) {
24 try {24 try {
25 console.log('Aborting Kobold generation...');25 console.info('Aborting Kobold generation...');
26 // send abort signal to koboldcpp26 // send abort signal to koboldcpp
27 const abortResponse = await fetch(`${request.body.api_server}/extra/abort`, {27 const abortResponse = await fetch(`${request.body.api_server}/extra/abort`, {
28 method: 'POST',28 method: 'POST',
29 });29 });
3030
31 if (!abortResponse.ok) {31 if (!abortResponse.ok) {
32 console.log('Error sending abort request to Kobold:', abortResponse.status);32 console.error('Error sending abort request to Kobold:', abortResponse.status);
33 }33 }
34 } catch (error) {34 } catch (error) {
35 console.log(error);35 console.error(error);
36 }36 }
37 }37 }
38 controller.abort();38 controller.abort();
@@ -81,7 +81,7 @@ router.post('/generate', jsonParser, async function (request, response_generate)
81 }81 }
82 }82 }
8383
84 console.log(this_settings);84 console.debug(this_settings);
85 const args = {85 const args = {
86 body: JSON.stringify(this_settings),86 body: JSON.stringify(this_settings),
87 headers: Object.assign(87 headers: Object.assign(
@@ -105,7 +105,7 @@ router.post('/generate', jsonParser, async function (request, response_generate)
105 } else {105 } else {
106 if (!response.ok) {106 if (!response.ok) {
107 const errorText = await response.text();107 const errorText = await response.text();
108 console.log(`Kobold returned error: ${response.status} ${response.statusText} ${errorText}`);108 console.warn(`Kobold returned error: ${response.status} ${response.statusText} ${errorText}`);
109109
110 try {110 try {
111 const errorJson = JSON.parse(errorText);111 const errorJson = JSON.parse(errorText);
@@ -117,7 +117,7 @@ router.post('/generate', jsonParser, async function (request, response_generate)
117 }117 }
118118
119 const data = await response.json();119 const data = await response.json();
120 console.log('Endpoint response:', data);120 console.debug('Endpoint response:', data);
121 return response_generate.send(data);121 return response_generate.send(data);
122 }122 }
123 } catch (error) {123 } catch (error) {
@@ -125,19 +125,19 @@ router.post('/generate', jsonParser, async function (request, response_generate)
125 switch (error?.status) {125 switch (error?.status) {
126 case 403:126 case 403:
127 case 503: // retry in case of temporary service issue, possibly caused by a queue failure?127 case 503: // retry in case of temporary service issue, possibly caused by a queue failure?
128 console.debug(`KoboldAI is busy. Retry attempt ${i + 1} of ${MAX_RETRIES}...`);128 console.warn(`KoboldAI is busy. Retry attempt ${i + 1} of ${MAX_RETRIES}...`);
129 await delay(delayAmount);129 await delay(delayAmount);
130 break;130 break;
131 default:131 default:
132 if ('status' in error) {132 if ('status' in error) {
133 console.log('Status Code from Kobold:', error.status);133 console.error('Status Code from Kobold:', error.status);
134 }134 }
135 return response_generate.send({ error: true });135 return response_generate.send({ error: true });
136 }136 }
137 }137 }
138 }138 }
139139
140 console.log('Max retries exceeded. Giving up.');140 console.error('Max retries exceeded. Giving up.');
141 return response_generate.send({ error: true });141 return response_generate.send({ error: true });
142});142});
143143
@@ -193,16 +193,16 @@ router.post('/transcribe-audio', urlencodedParser, async function (request, resp
193 const server = request.body.server;193 const server = request.body.server;
194194
195 if (!server) {195 if (!server) {
196 console.log('Server is not set');196 console.error('Server is not set');
197 return response.sendStatus(400);197 return response.sendStatus(400);
198 }198 }
199199
200 if (!request.file) {200 if (!request.file) {
201 console.log('No audio file found');201 console.error('No audio file found');
202 return response.sendStatus(400);202 return response.sendStatus(400);
203 }203 }
204204
205 console.log('Transcribing audio with KoboldCpp', server);205 console.debug('Transcribing audio with KoboldCpp', server);
206206
207 const fileBase64 = fs.readFileSync(request.file.path).toString('base64');207 const fileBase64 = fs.readFileSync(request.file.path).toString('base64');
208 fs.rmSync(request.file.path);208 fs.rmSync(request.file.path);
@@ -226,12 +226,12 @@ router.post('/transcribe-audio', urlencodedParser, async function (request, resp
226226
227 if (!result.ok) {227 if (!result.ok) {
228 const text = await result.text();228 const text = await result.text();
229 console.log('KoboldCpp request failed', result.statusText, text);229 console.error('KoboldCpp request failed', result.statusText, text);
230 return response.status(500).send(text);230 return response.status(500).send(text);
231 }231 }
232232
233 const data = await result.json();233 const data = await result.json();
234 console.log('KoboldCpp transcription response', data);234 console.debug('KoboldCpp transcription response', data);
235 return response.json(data);235 return response.json(data);
236 } catch (error) {236 } catch (error) {
237 console.error('KoboldCpp transcription failed', error);237 console.error('KoboldCpp transcription failed', error);
src/endpoints/backends/scale-alt.js+5 -5
@@ -13,7 +13,7 @@ router.post('/generate', jsonParser, async function (request, response) {
13 const cookie = readSecret(request.user.directories, SECRET_KEYS.SCALE_COOKIE);13 const cookie = readSecret(request.user.directories, SECRET_KEYS.SCALE_COOKIE);
1414
15 if (!cookie) {15 if (!cookie) {
16 console.log('No Scale cookie found');16 console.error('No Scale cookie found');
17 return response.sendStatus(400);17 return response.sendStatus(400);
18 }18 }
1919
@@ -62,7 +62,7 @@ router.post('/generate', jsonParser, async function (request, response) {
62 },62 },
63 };63 };
6464
65 console.log('Scale request:', body);65 console.debug('Scale request:', body);
6666
67 const result = await fetch('https://dashboard.scale.com/spellbook/api/trpc/v2.variant.run', {67 const result = await fetch('https://dashboard.scale.com/spellbook/api/trpc/v2.variant.run', {
68 method: 'POST',68 method: 'POST',
@@ -75,7 +75,7 @@ router.post('/generate', jsonParser, async function (request, response) {
7575
76 if (!result.ok) {76 if (!result.ok) {
77 const text = await result.text();77 const text = await result.text();
78 console.log('Scale request failed', result.statusText, text);78 console.error('Scale request failed', result.statusText, text);
79 return response.status(500).send({ error: { message: result.statusText } });79 return response.status(500).send({ error: { message: result.statusText } });
80 }80 }
8181
@@ -83,7 +83,7 @@ router.post('/generate', jsonParser, async function (request, response) {
83 const data = await result.json();83 const data = await result.json();
84 const output = data?.result?.data?.json?.outputs?.[0] || '';84 const output = data?.result?.data?.json?.outputs?.[0] || '';
8585
86 console.log('Scale response:', data);86 console.debug('Scale response:', data);
8787
88 if (!output) {88 if (!output) {
89 console.warn('Scale response is empty');89 console.warn('Scale response is empty');
@@ -92,7 +92,7 @@ router.post('/generate', jsonParser, async function (request, response) {
9292
93 return response.json({ output });93 return response.json({ output });
94 } catch (error) {94 } catch (error) {
95 console.log(error);95 console.error(error);
96 return response.sendStatus(500);96 return response.sendStatus(500);
97 }97 }
98});98});
src/endpoints/backends/text-completions.js+43 -45
@@ -58,12 +58,12 @@ async function parseOllamaStream(jsonStream, request, response) {
58 });58 });
5959
60 jsonStream.body.on('end', () => {60 jsonStream.body.on('end', () => {
61 console.log('Streaming request finished');61 console.info('Streaming request finished');
62 response.write('data: [DONE]\n\n');62 response.write('data: [DONE]\n\n');
63 response.end();63 response.end();
64 });64 });
65 } catch (error) {65 } catch (error) {
66 console.log('Error forwarding streaming response:', error);66 console.error('Error forwarding streaming response:', error);
67 if (!response.headersSent) {67 if (!response.headersSent) {
68 return response.status(500).send({ error: true });68 return response.status(500).send({ error: true });
69 } else {69 } else {
@@ -79,16 +79,16 @@ async function parseOllamaStream(jsonStream, request, response) {
79 */79 */
80async function abortKoboldCppRequest(url) {80async function abortKoboldCppRequest(url) {
81 try {81 try {
82 console.log('Aborting Kobold generation...');82 console.info('Aborting Kobold generation...');
83 const abortResponse = await fetch(`${url}/api/extra/abort`, {83 const abortResponse = await fetch(`${url}/api/extra/abort`, {
84 method: 'POST',84 method: 'POST',
85 });85 });
8686
87 if (!abortResponse.ok) {87 if (!abortResponse.ok) {
88 console.log('Error sending abort request to Kobold:', abortResponse.status, abortResponse.statusText);88 console.error('Error sending abort request to Kobold:', abortResponse.status, abortResponse.statusText);
89 }89 }
90 } catch (error) {90 } catch (error) {
91 console.log(error);91 console.error(error);
92 }92 }
93}93}
9494
@@ -101,7 +101,7 @@ router.post('/status', jsonParser, async function (request, response) {
101 request.body.api_server = request.body.api_server.replace('localhost', '127.0.0.1');101 request.body.api_server = request.body.api_server.replace('localhost', '127.0.0.1');
102 }102 }
103103
104 console.log('Trying to connect to API:', request.body);104 console.debug('Trying to connect to API', request.body);
105 const baseUrl = trimV1(request.body.api_server);105 const baseUrl = trimV1(request.body.api_server);
106106
107 const args = {107 const args = {
@@ -123,6 +123,7 @@ router.post('/status', jsonParser, async function (request, response) {
123 case TEXTGEN_TYPES.LLAMACPP:123 case TEXTGEN_TYPES.LLAMACPP:
124 case TEXTGEN_TYPES.INFERMATICAI:124 case TEXTGEN_TYPES.INFERMATICAI:
125 case TEXTGEN_TYPES.OPENROUTER:125 case TEXTGEN_TYPES.OPENROUTER:
126 case TEXTGEN_TYPES.FEATHERLESS:
126 url += '/v1/models';127 url += '/v1/models';
127 break;128 break;
128 case TEXTGEN_TYPES.DREAMGEN:129 case TEXTGEN_TYPES.DREAMGEN:
@@ -140,9 +141,6 @@ router.post('/status', jsonParser, async function (request, response) {
140 case TEXTGEN_TYPES.OLLAMA:141 case TEXTGEN_TYPES.OLLAMA:
141 url += '/api/tags';142 url += '/api/tags';
142 break;143 break;
143 case TEXTGEN_TYPES.FEATHERLESS:
144 url += '/v1/models';
145 break;
146 case TEXTGEN_TYPES.HUGGINGFACE:144 case TEXTGEN_TYPES.HUGGINGFACE:
147 url += '/info';145 url += '/info';
148 break;146 break;
@@ -152,7 +150,7 @@ router.post('/status', jsonParser, async function (request, response) {
152 const isPossiblyLmStudio = modelsReply.headers.get('x-powered-by') === 'Express';150 const isPossiblyLmStudio = modelsReply.headers.get('x-powered-by') === 'Express';
153151
154 if (!modelsReply.ok) {152 if (!modelsReply.ok) {
155 console.log('Models endpoint is offline.');153 console.error('Models endpoint is offline.');
156 return response.sendStatus(400);154 return response.sendStatus(400);
157 }155 }
158156
@@ -173,12 +171,12 @@ router.post('/status', jsonParser, async function (request, response) {
173 }171 }
174172
175 if (!Array.isArray(data.data)) {173 if (!Array.isArray(data.data)) {
176 console.log('Models response is not an array.');174 console.error('Models response is not an array.');
177 return response.sendStatus(400);175 return response.sendStatus(400);
178 }176 }
179177
180 const modelIds = data.data.map(x => x.id);178 const modelIds = data.data.map(x => x.id);
181 console.log('Models available:', modelIds);179 console.info('Models available:', modelIds);
182180
183 // Set result to the first model ID181 // Set result to the first model ID
184 result = modelIds[0] || 'Valid';182 result = modelIds[0] || 'Valid';
@@ -191,7 +189,7 @@ router.post('/status', jsonParser, async function (request, response) {
191 if (modelInfoReply.ok) {189 if (modelInfoReply.ok) {
192 /** @type {any} */190 /** @type {any} */
193 const modelInfo = await modelInfoReply.json();191 const modelInfo = await modelInfoReply.json();
194 console.log('Ooba model info:', modelInfo);192 console.debug('Ooba model info:', modelInfo);
195193
196 const modelName = modelInfo?.model_name;194 const modelName = modelInfo?.model_name;
197 result = modelName || result;195 result = modelName || result;
@@ -208,7 +206,7 @@ router.post('/status', jsonParser, async function (request, response) {
208 if (modelInfoReply.ok) {206 if (modelInfoReply.ok) {
209 /** @type {any} */207 /** @type {any} */
210 const modelInfo = await modelInfoReply.json();208 const modelInfo = await modelInfoReply.json();
211 console.log('Tabby model info:', modelInfo);209 console.debug('Tabby model info:', modelInfo);
212210
213 const modelName = modelInfo?.id;211 const modelName = modelInfo?.id;
214 result = modelName || result;212 result = modelName || result;
@@ -255,7 +253,7 @@ router.post('/props', jsonParser, async function (request, response) {
255 props['chat_template'] = props['chat_template'].slice(0, -1) + '\n';253 props['chat_template'] = props['chat_template'].slice(0, -1) + '\n';
256 }254 }
257 props['chat_template_hash'] = createHash('sha256').update(props['chat_template']).digest('hex');255 props['chat_template_hash'] = createHash('sha256').update(props['chat_template']).digest('hex');
258 console.log(`Model properties: ${JSON.stringify(props)}`);256 console.debug(`Model properties: ${JSON.stringify(props)}`);
259 return response.send(props);257 return response.send(props);
260 } catch (error) {258 } catch (error) {
261 console.error(error);259 console.error(error);
@@ -273,7 +271,7 @@ router.post('/generate', jsonParser, async function (request, response) {
273271
274 const apiType = request.body.api_type;272 const apiType = request.body.api_type;
275 const baseUrl = request.body.api_server;273 const baseUrl = request.body.api_server;
276 console.log(request.body);274 console.debug(request.body);
277275
278 const controller = new AbortController();276 const controller = new AbortController();
279 request.socket.removeAllListeners('close');277 request.socket.removeAllListeners('close');
@@ -375,6 +373,10 @@ router.post('/generate', jsonParser, async function (request, response) {
375373
376 if (request.body.api_type === TEXTGEN_TYPES.OLLAMA) {374 if (request.body.api_type === TEXTGEN_TYPES.OLLAMA) {
377 const keepAlive = getConfigValue('ollama.keepAlive', -1);375 const keepAlive = getConfigValue('ollama.keepAlive', -1);
376 const numBatch = getConfigValue('ollama.batchSize', -1);
377 if (numBatch > 0) {
378 request.body['num_batch'] = numBatch;
379 }
378 args.body = JSON.stringify({380 args.body = JSON.stringify({
379 model: request.body.model,381 model: request.body.model,
380 prompt: request.body.prompt,382 prompt: request.body.prompt,
@@ -399,7 +401,7 @@ router.post('/generate', jsonParser, async function (request, response) {
399 if (completionsReply.ok) {401 if (completionsReply.ok) {
400 /** @type {any} */402 /** @type {any} */
401 const data = await completionsReply.json();403 const data = await completionsReply.json();
402 console.log('Endpoint response:', data);404 console.debug('Endpoint response:', data);
403405
404 // Map InfermaticAI response to OAI completions format406 // Map InfermaticAI response to OAI completions format
405 if (apiType === TEXTGEN_TYPES.INFERMATICAI) {407 if (apiType === TEXTGEN_TYPES.INFERMATICAI) {
@@ -411,24 +413,20 @@ router.post('/generate', jsonParser, async function (request, response) {
411 const text = await completionsReply.text();413 const text = await completionsReply.text();
412 const errorBody = { error: true, status: completionsReply.status, response: text };414 const errorBody = { error: true, status: completionsReply.status, response: text };
413415
414 if (!response.headersSent) {416 return !response.headersSent
415 return response.send(errorBody);417 ? response.send(errorBody)
416 }418 : response.end();
417
418 return response.end();
419 }419 }
420 }420 }
421 } catch (error) {421 } catch (error) {
422 const status = error?.status ?? error?.code ?? 'UNKNOWN';422 const status = error?.status ?? error?.code ?? 'UNKNOWN';
423 const text = error?.error ?? error?.statusText ?? error?.message ?? 'Unknown error on /generate endpoint';423 const text = error?.error ?? error?.statusText ?? error?.message ?? 'Unknown error on /generate endpoint';
424 let value = { error: true, status: status, response: text };424 let value = { error: true, status: status, response: text };
425 console.log('Endpoint error:', error);425 console.error('Endpoint error:', error);
426426
427 if (!response.headersSent) {427 return !response.headersSent
428 return response.send(value);428 ? response.send(value)
429 }429 : response.end();
430
431 return response.end();
432 }430 }
433});431});
434432
@@ -451,7 +449,7 @@ ollama.post('/download', jsonParser, async function (request, response) {
451 });449 });
452450
453 if (!fetchResponse.ok) {451 if (!fetchResponse.ok) {
454 console.log('Download error:', fetchResponse.status, fetchResponse.statusText);452 console.error('Download error:', fetchResponse.status, fetchResponse.statusText);
455 return response.status(fetchResponse.status).send({ error: true });453 return response.status(fetchResponse.status).send({ error: true });
456 }454 }
457455
@@ -468,7 +466,7 @@ ollama.post('/caption-image', jsonParser, async function (request, response) {
468 return response.sendStatus(400);466 return response.sendStatus(400);
469 }467 }
470468
471 console.log('Ollama caption request:', request.body);469 console.debug('Ollama caption request:', request.body);
472 const baseUrl = trimV1(request.body.server_url);470 const baseUrl = trimV1(request.body.server_url);
473471
474 const fetchResponse = await fetch(`${baseUrl}/api/generate`, {472 const fetchResponse = await fetch(`${baseUrl}/api/generate`, {
@@ -483,18 +481,18 @@ ollama.post('/caption-image', jsonParser, async function (request, response) {
483 });481 });
484482
485 if (!fetchResponse.ok) {483 if (!fetchResponse.ok) {
486 console.log('Ollama caption error:', fetchResponse.status, fetchResponse.statusText);484 console.error('Ollama caption error:', fetchResponse.status, fetchResponse.statusText);
487 return response.status(500).send({ error: true });485 return response.status(500).send({ error: true });
488 }486 }
489487
490 /** @type {any} */488 /** @type {any} */
491 const data = await fetchResponse.json();489 const data = await fetchResponse.json();
492 console.log('Ollama caption response:', data);490 console.debug('Ollama caption response:', data);
493491
494 const caption = data?.response || '';492 const caption = data?.response || '';
495493
496 if (!caption) {494 if (!caption) {
497 console.log('Ollama caption is empty.');495 console.error('Ollama caption is empty.');
498 return response.status(500).send({ error: true });496 return response.status(500).send({ error: true });
499 }497 }
500498
@@ -513,7 +511,7 @@ llamacpp.post('/caption-image', jsonParser, async function (request, response) {
513 return response.sendStatus(400);511 return response.sendStatus(400);
514 }512 }
515513
516 console.log('LlamaCpp caption request:', request.body);514 console.debug('LlamaCpp caption request:', request.body);
517 const baseUrl = trimV1(request.body.server_url);515 const baseUrl = trimV1(request.body.server_url);
518516
519 const fetchResponse = await fetch(`${baseUrl}/completion`, {517 const fetchResponse = await fetch(`${baseUrl}/completion`, {
@@ -529,18 +527,18 @@ llamacpp.post('/caption-image', jsonParser, async function (request, response) {
529 });527 });
530528
531 if (!fetchResponse.ok) {529 if (!fetchResponse.ok) {
532 console.log('LlamaCpp caption error:', fetchResponse.status, fetchResponse.statusText);530 console.error('LlamaCpp caption error:', fetchResponse.status, fetchResponse.statusText);
533 return response.status(500).send({ error: true });531 return response.status(500).send({ error: true });
534 }532 }
535533
536 /** @type {any} */534 /** @type {any} */
537 const data = await fetchResponse.json();535 const data = await fetchResponse.json();
538 console.log('LlamaCpp caption response:', data);536 console.debug('LlamaCpp caption response:', data);
539537
540 const caption = data?.content || '';538 const caption = data?.content || '';
541539
542 if (!caption) {540 if (!caption) {
543 console.log('LlamaCpp caption is empty.');541 console.error('LlamaCpp caption is empty.');
544 return response.status(500).send({ error: true });542 return response.status(500).send({ error: true });
545 }543 }
546544
@@ -558,7 +556,7 @@ llamacpp.post('/props', jsonParser, async function (request, response) {
558 return response.sendStatus(400);556 return response.sendStatus(400);
559 }557 }
560558
561 console.log('LlamaCpp props request:', request.body);559 console.debug('LlamaCpp props request:', request.body);
562 const baseUrl = trimV1(request.body.server_url);560 const baseUrl = trimV1(request.body.server_url);
563561
564 const fetchResponse = await fetch(`${baseUrl}/props`, {562 const fetchResponse = await fetch(`${baseUrl}/props`, {
@@ -566,12 +564,12 @@ llamacpp.post('/props', jsonParser, async function (request, response) {
566 });564 });
567565
568 if (!fetchResponse.ok) {566 if (!fetchResponse.ok) {
569 console.log('LlamaCpp props error:', fetchResponse.status, fetchResponse.statusText);567 console.error('LlamaCpp props error:', fetchResponse.status, fetchResponse.statusText);
570 return response.status(500).send({ error: true });568 return response.status(500).send({ error: true });
571 }569 }
572570
573 const data = await fetchResponse.json();571 const data = await fetchResponse.json();
574 console.log('LlamaCpp props response:', data);572 console.debug('LlamaCpp props response:', data);
575573
576 return response.send(data);574 return response.send(data);
577575
@@ -590,7 +588,7 @@ llamacpp.post('/slots', jsonParser, async function (request, response) {
590 return response.sendStatus(400);588 return response.sendStatus(400);
591 }589 }
592590
593 console.log('LlamaCpp slots request:', request.body);591 console.debug('LlamaCpp slots request:', request.body);
594 const baseUrl = trimV1(request.body.server_url);592 const baseUrl = trimV1(request.body.server_url);
595593
596 let fetchResponse;594 let fetchResponse;
@@ -616,12 +614,12 @@ llamacpp.post('/slots', jsonParser, async function (request, response) {
616 }614 }
617615
618 if (!fetchResponse.ok) {616 if (!fetchResponse.ok) {
619 console.log('LlamaCpp slots error:', fetchResponse.status, fetchResponse.statusText);617 console.error('LlamaCpp slots error:', fetchResponse.status, fetchResponse.statusText);
620 return response.status(500).send({ error: true });618 return response.status(500).send({ error: true });
621 }619 }
622620
623 const data = await fetchResponse.json();621 const data = await fetchResponse.json();
624 console.log('LlamaCpp slots response:', data);622 console.debug('LlamaCpp slots response:', data);
625623
626 return response.send(data);624 return response.send(data);
627625
@@ -659,14 +657,14 @@ tabby.post('/download', jsonParser, async function (request, response) {
659 return response.status(403).send({ error: true });657 return response.status(403).send({ error: true });
660 }658 }
661 } else {659 } else {
662 console.log('API Permission error:', permissionResponse.status, permissionResponse.statusText);660 console.error('API Permission error:', permissionResponse.status, permissionResponse.statusText);
663 return response.status(permissionResponse.status).send({ error: true });661 return response.status(permissionResponse.status).send({ error: true });
664 }662 }
665663
666 const fetchResponse = await fetch(`${baseUrl}/v1/download`, args);664 const fetchResponse = await fetch(`${baseUrl}/v1/download`, args);
667665
668 if (!fetchResponse.ok) {666 if (!fetchResponse.ok) {
669 console.log('Download error:', fetchResponse.status, fetchResponse.statusText);667 console.error('Download error:', fetchResponse.status, fetchResponse.statusText);
670 return response.status(fetchResponse.status).send({ error: true });668 return response.status(fetchResponse.status).send({ error: true });
671 }669 }
672670
src/endpoints/backgrounds.js+3 -3
@@ -27,7 +27,7 @@ router.post('/delete', jsonParser, getFileNameValidationFunction('bg'), function
27 const fileName = path.join(request.user.directories.backgrounds, sanitize(request.body.bg));27 const fileName = path.join(request.user.directories.backgrounds, sanitize(request.body.bg));
2828
29 if (!fs.existsSync(fileName)) {29 if (!fs.existsSync(fileName)) {
30 console.log('BG file not found');30 console.error('BG file not found');
31 return response.sendStatus(400);31 return response.sendStatus(400);
32 }32 }
3333
@@ -43,12 +43,12 @@ router.post('/rename', jsonParser, function (request, response) {
43 const newFileName = path.join(request.user.directories.backgrounds, sanitize(request.body.new_bg));43 const newFileName = path.join(request.user.directories.backgrounds, sanitize(request.body.new_bg));
4444
45 if (!fs.existsSync(oldFileName)) {45 if (!fs.existsSync(oldFileName)) {
46 console.log('BG file not found');46 console.error('BG file not found');
47 return response.sendStatus(400);47 return response.sendStatus(400);
48 }48 }
4949
50 if (fs.existsSync(newFileName)) {50 if (fs.existsSync(newFileName)) {
51 console.log('New BG file already exists');51 console.error('New BG file already exists');
52 return response.sendStatus(400);52 return response.sendStatus(400);
53 }53 }
5454
src/endpoints/caption.js+4 -4
@@ -2,10 +2,10 @@ import express from 'express';
2import { jsonParser } from '../express-common.js';2import { jsonParser } from '../express-common.js';
3import { getPipeline, getRawImage } from '../transformers.js';3import { getPipeline, getRawImage } from '../transformers.js';
44
5const TASK = 'image-to-text';
6
7export const router = express.Router();5export const router = express.Router();
86
7const TASK = 'image-to-text';
8
9router.post('/', jsonParser, async (req, res) => {9router.post('/', jsonParser, async (req, res) => {
10 try {10 try {
11 const { image } = req.body;11 const { image } = req.body;
@@ -13,14 +13,14 @@ router.post('/', jsonParser, async (req, res) => {
13 const rawImage = await getRawImage(image);13 const rawImage = await getRawImage(image);
1414
15 if (!rawImage) {15 if (!rawImage) {
16 console.log('Failed to parse captioned image');16 console.warn('Failed to parse captioned image');
17 return res.sendStatus(400);17 return res.sendStatus(400);
18 }18 }
1919
20 const pipe = await getPipeline(TASK);20 const pipe = await getPipeline(TASK);
21 const result = await pipe(rawImage);21 const result = await pipe(rawImage);
22 const text = result[0].generated_text;22 const text = result[0].generated_text;
23 console.log('Image caption:', text);23 console.info('Image caption:', text);
2424
25 return res.json({ caption: text });25 return res.json({ caption: text });
26 } catch (error) {26 } catch (error) {
src/endpoints/characters.js+37 -35
@@ -97,7 +97,7 @@ async function writeCharacterData(inputFile, data, outputFile, request, crop = u
97 writeFileAtomicSync(outputImagePath, outputImage);97 writeFileAtomicSync(outputImagePath, outputImage);
98 return true;98 return true;
99 } catch (err) {99 } catch (err) {
100 console.log(err);100 console.error(err);
101 return false;101 return false;
102 }102 }
103}103}
@@ -166,7 +166,7 @@ async function tryReadImage(imgPath, crop) {
166 }166 }
167 // If it's an unsupported type of image (APNG) - just read the file as buffer167 // If it's an unsupported type of image (APNG) - just read the file as buffer
168 catch (error) {168 catch (error) {
169 console.log(`Failed to read image: ${imgPath}`, error);169 console.error(`Failed to read image: ${imgPath}`, error);
170 return fs.readFileSync(imgPath);170 return fs.readFileSync(imgPath);
171 }171 }
172}172}
@@ -229,12 +229,12 @@ const processCharacter = async (item, directories) => {
229 return character;229 return character;
230 }230 }
231 catch (err) {231 catch (err) {
232 console.log(`Could not process character: ${item}`);232 console.error(`Could not process character: ${item}`);
233233
234 if (err instanceof SyntaxError) {234 if (err instanceof SyntaxError) {
235 console.log(`${item} does not contain a valid JSON object.`);235 console.error(`${item} does not contain a valid JSON object.`);
236 } else {236 } else {
237 console.log('An unexpected error occurred: ', err);237 console.error('An unexpected error occurred: ', err);
238 }238 }
239239
240 return {240 return {
@@ -322,7 +322,7 @@ function readFromV2(char) {
322 };322 };
323323
324 _.forEach(fieldMappings, (v2Path, charField) => {324 _.forEach(fieldMappings, (v2Path, charField) => {
325 //console.log(`Migrating field: ${charField} from ${v2Path}`);325 //console.info(`Migrating field: ${charField} from ${v2Path}`);
326 const v2Value = _.get(char.data, v2Path);326 const v2Value = _.get(char.data, v2Path);
327 if (_.isUndefined(v2Value)) {327 if (_.isUndefined(v2Value)) {
328 let defaultValue = undefined;328 let defaultValue = undefined;
@@ -337,15 +337,15 @@ function readFromV2(char) {
337 }337 }
338338
339 if (!_.isUndefined(defaultValue)) {339 if (!_.isUndefined(defaultValue)) {
340 //console.debug(`Spec v2 extension data missing for field: ${charField}, using default value: ${defaultValue}`);340 //console.warn(`Spec v2 extension data missing for field: ${charField}, using default value: ${defaultValue}`);
341 char[charField] = defaultValue;341 char[charField] = defaultValue;
342 } else {342 } else {
343 console.debug(`Char ${char['name']} has Spec v2 data missing for unknown field: ${charField}`);343 console.warn(`Char ${char['name']} has Spec v2 data missing for unknown field: ${charField}`);
344 return;344 return;
345 }345 }
346 }346 }
347 if (!_.isUndefined(char[charField]) && !_.isUndefined(v2Value) && String(char[charField]) !== String(v2Value)) {347 if (!_.isUndefined(char[charField]) && !_.isUndefined(v2Value) && String(char[charField]) !== String(v2Value)) {
348 console.debug(`Char ${char['name']} has Spec v2 data mismatch with Spec v1 for field: ${charField}`, char[charField], v2Value);348 console.warn(`Char ${char['name']} has Spec v2 data mismatch with Spec v1 for field: ${charField}`, char[charField], v2Value);
349 }349 }
350 char[charField] = v2Value;350 char[charField] = v2Value;
351 });351 });
@@ -442,7 +442,7 @@ function charaFormatData(data, directories) {
442 }442 }
443443
444 } catch {444 } catch {
445 console.debug(`Failed to read world info file: ${data.world}. Character book will not be available.`);445 console.warn(`Failed to read world info file: ${data.world}. Character book will not be available.`);
446 }446 }
447 }447 }
448448
@@ -452,7 +452,7 @@ function charaFormatData(data, directories) {
452 // Deep merge the extensions object452 // Deep merge the extensions object
453 _.set(char, 'data.extensions', deepMerge(char.data.extensions, extensions));453 _.set(char, 'data.extensions', deepMerge(char.data.extensions, extensions));
454 } catch {454 } catch {
455 console.debug(`Failed to parse extensions JSON: ${data.extensions}`);455 console.warn(`Failed to parse extensions JSON: ${data.extensions}`);
456 }456 }
457 }457 }
458458
@@ -526,7 +526,7 @@ async function importFromYaml(uploadPath, context, preservedFileName) {
526 const fileText = fs.readFileSync(uploadPath, 'utf8');526 const fileText = fs.readFileSync(uploadPath, 'utf8');
527 fs.rmSync(uploadPath);527 fs.rmSync(uploadPath);
528 const yamlData = yaml.parse(fileText);528 const yamlData = yaml.parse(fileText);
529 console.log('Importing from YAML');529 console.info('Importing from YAML');
530 yamlData.name = sanitize(yamlData.name);530 yamlData.name = sanitize(yamlData.name);
531 const fileName = preservedFileName || getPngName(yamlData.name, context.request.user.directories);531 const fileName = preservedFileName || getPngName(yamlData.name, context.request.user.directories);
532 let char = convertToV2({532 let char = convertToV2({
@@ -559,7 +559,7 @@ async function importFromYaml(uploadPath, context, preservedFileName) {
559async function importFromCharX(uploadPath, { request }, preservedFileName) {559async function importFromCharX(uploadPath, { request }, preservedFileName) {
560 const data = fs.readFileSync(uploadPath).buffer;560 const data = fs.readFileSync(uploadPath).buffer;
561 fs.rmSync(uploadPath);561 fs.rmSync(uploadPath);
562 console.log('Importing from CharX');562 console.info('Importing from CharX');
563 const cardBuffer = await extractFileFromZipBuffer(data, 'card.json');563 const cardBuffer = await extractFileFromZipBuffer(data, 'card.json');
564564
565 if (!cardBuffer) {565 if (!cardBuffer) {
@@ -608,7 +608,7 @@ async function importFromJson(uploadPath, { request }, preservedFileName) {
608 let jsonData = JSON.parse(data);608 let jsonData = JSON.parse(data);
609609
610 if (jsonData.spec !== undefined) {610 if (jsonData.spec !== undefined) {
611 console.log(`Importing from ${jsonData.spec} json`);611 console.info(`Importing from ${jsonData.spec} json`);
612 importRisuSprites(request.user.directories, jsonData);612 importRisuSprites(request.user.directories, jsonData);
613 unsetFavFlag(jsonData);613 unsetFavFlag(jsonData);
614 jsonData = readFromV2(jsonData);614 jsonData = readFromV2(jsonData);
@@ -618,7 +618,7 @@ async function importFromJson(uploadPath, { request }, preservedFileName) {
618 const result = await writeCharacterData(defaultAvatarPath, char, pngName, request);618 const result = await writeCharacterData(defaultAvatarPath, char, pngName, request);
619 return result ? pngName : '';619 return result ? pngName : '';
620 } else if (jsonData.name !== undefined) {620 } else if (jsonData.name !== undefined) {
621 console.log('Importing from v1 json');621 console.info('Importing from v1 json');
622 jsonData.name = sanitize(jsonData.name);622 jsonData.name = sanitize(jsonData.name);
623 if (jsonData.creator_notes) {623 if (jsonData.creator_notes) {
624 jsonData.creator_notes = jsonData.creator_notes.replace('Creator\'s notes go here.', '');624 jsonData.creator_notes = jsonData.creator_notes.replace('Creator\'s notes go here.', '');
@@ -644,7 +644,7 @@ async function importFromJson(uploadPath, { request }, preservedFileName) {
644 const result = await writeCharacterData(defaultAvatarPath, charJSON, pngName, request);644 const result = await writeCharacterData(defaultAvatarPath, charJSON, pngName, request);
645 return result ? pngName : '';645 return result ? pngName : '';
646 } else if (jsonData.char_name !== undefined) {//json Pygmalion notepad646 } else if (jsonData.char_name !== undefined) {//json Pygmalion notepad
647 console.log('Importing from gradio json');647 console.info('Importing from gradio json');
648 jsonData.char_name = sanitize(jsonData.char_name);648 jsonData.char_name = sanitize(jsonData.char_name);
649 if (jsonData.creator_notes) {649 if (jsonData.creator_notes) {
650 jsonData.creator_notes = jsonData.creator_notes.replace('Creator\'s notes go here.', '');650 jsonData.creator_notes = jsonData.creator_notes.replace('Creator\'s notes go here.', '');
@@ -691,7 +691,7 @@ async function importFromPng(uploadPath, { request }, preservedFileName) {
691 const pngName = preservedFileName || getPngName(jsonData.name, request.user.directories);691 const pngName = preservedFileName || getPngName(jsonData.name, request.user.directories);
692692
693 if (jsonData.spec !== undefined) {693 if (jsonData.spec !== undefined) {
694 console.log(`Found a ${jsonData.spec} character file.`);694 console.info(`Found a ${jsonData.spec} character file.`);
695 importRisuSprites(request.user.directories, jsonData);695 importRisuSprites(request.user.directories, jsonData);
696 unsetFavFlag(jsonData);696 unsetFavFlag(jsonData);
697 jsonData = readFromV2(jsonData);697 jsonData = readFromV2(jsonData);
@@ -701,7 +701,7 @@ async function importFromPng(uploadPath, { request }, preservedFileName) {
701 fs.unlinkSync(uploadPath);701 fs.unlinkSync(uploadPath);
702 return result ? pngName : '';702 return result ? pngName : '';
703 } else if (jsonData.name !== undefined) {703 } else if (jsonData.name !== undefined) {
704 console.log('Found a v1 character file.');704 console.info('Found a v1 character file.');
705705
706 if (jsonData.creator_notes) {706 if (jsonData.creator_notes) {
707 jsonData.creator_notes = jsonData.creator_notes.replace('Creator\'s notes go here.', '');707 jsonData.creator_notes = jsonData.creator_notes.replace('Creator\'s notes go here.', '');
@@ -812,13 +812,13 @@ router.post('/rename', jsonParser, validateAvatarUrlMiddleware, async function (
812812
813router.post('/edit', urlencodedParser, validateAvatarUrlMiddleware, async function (request, response) {813router.post('/edit', urlencodedParser, validateAvatarUrlMiddleware, async function (request, response) {
814 if (!request.body) {814 if (!request.body) {
815 console.error('Error: no response body detected');815 console.warn('Error: no response body detected');
816 response.status(400).send('Error: no response body detected');816 response.status(400).send('Error: no response body detected');
817 return;817 return;
818 }818 }
819819
820 if (request.body.ch_name === '' || request.body.ch_name === undefined || request.body.ch_name === '.') {820 if (request.body.ch_name === '' || request.body.ch_name === undefined || request.body.ch_name === '.') {
821 console.error('Error: invalid name.');821 console.warn('Error: invalid name.');
822 response.status(400).send('Error: invalid name.');822 response.status(400).send('Error: invalid name.');
823 return;823 return;
824 }824 }
@@ -839,6 +839,9 @@ router.post('/edit', urlencodedParser, validateAvatarUrlMiddleware, async functi
839 invalidateThumbnail(request.user.directories, 'avatar', request.body.avatar_url);839 invalidateThumbnail(request.user.directories, 'avatar', request.body.avatar_url);
840 await writeCharacterData(newAvatarPath, char, targetFile, request, crop);840 await writeCharacterData(newAvatarPath, char, targetFile, request, crop);
841 fs.unlinkSync(newAvatarPath);841 fs.unlinkSync(newAvatarPath);
842
843 // Bust cache to reload the new avatar
844 response.setHeader('Clear-Site-Data', '"cache"');
842 }845 }
843846
844 return response.sendStatus(200);847 return response.sendStatus(200);
@@ -860,14 +863,14 @@ router.post('/edit', urlencodedParser, validateAvatarUrlMiddleware, async functi
860 * @returns {void}863 * @returns {void}
861 */864 */
862router.post('/edit-attribute', jsonParser, validateAvatarUrlMiddleware, async function (request, response) {865router.post('/edit-attribute', jsonParser, validateAvatarUrlMiddleware, async function (request, response) {
863 console.log(request.body);866 console.debug(request.body);
864 if (!request.body) {867 if (!request.body) {
865 console.error('Error: no response body detected');868 console.warn('Error: no response body detected');
866 return response.status(400).send('Error: no response body detected');869 return response.status(400).send('Error: no response body detected');
867 }870 }
868871
869 if (request.body.ch_name === '' || request.body.ch_name === undefined || request.body.ch_name === '.') {872 if (request.body.ch_name === '' || request.body.ch_name === undefined || request.body.ch_name === '.') {
870 console.error('Error: invalid name.');873 console.warn('Error: invalid name.');
871 return response.status(400).send('Error: invalid name.');874 return response.status(400).send('Error: invalid name.');
872 }875 }
873876
@@ -879,7 +882,7 @@ router.post('/edit-attribute', jsonParser, validateAvatarUrlMiddleware, async fu
879 const char = JSON.parse(charJSON);882 const char = JSON.parse(charJSON);
880 //check if the field exists883 //check if the field exists
881 if (char[request.body.field] === undefined && char.data[request.body.field] === undefined) {884 if (char[request.body.field] === undefined && char.data[request.body.field] === undefined) {
882 console.error('Error: invalid field.');885 console.warn('Error: invalid field.');
883 response.status(400).send('Error: invalid field.');886 response.status(400).send('Error: invalid field.');
884 return;887 return;
885 }888 }
@@ -928,7 +931,7 @@ router.post('/merge-attributes', jsonParser, getFileNameValidationFunction('avat
928 await writeCharacterData(avatarPath, JSON.stringify(character), targetImg, request);931 await writeCharacterData(avatarPath, JSON.stringify(character), targetImg, request);
929 response.sendStatus(200);932 response.sendStatus(200);
930 } else {933 } else {
931 console.log(validator.lastValidationError);934 console.warn(validator.lastValidationError);
932 response.status(400).send({ message: `Validation failed for ${character.name}`, error: validator.lastValidationError });935 response.status(400).send({ message: `Validation failed for ${character.name}`, error: validator.lastValidationError });
933 }936 }
934 } catch (exception) {937 } catch (exception) {
@@ -1050,7 +1053,7 @@ router.post('/chats', jsonParser, validateAvatarUrlMiddleware, async function (r
1050 const fileSizeInKB = `${(stats.size / 1024).toFixed(2)}kb`;1053 const fileSizeInKB = `${(stats.size / 1024).toFixed(2)}kb`;
10511054
1052 if (stats.size === 0) {1055 if (stats.size === 0) {
1053 console.log(`Found an empty chat file: ${pathToFile}`);1056 console.warn(`Found an empty chat file: ${pathToFile}`);
1054 res({});1057 res({});
1055 return;1058 return;
1056 }1059 }
@@ -1082,7 +1085,7 @@ router.post('/chats', jsonParser, validateAvatarUrlMiddleware, async function (r
10821085
1083 res(chatData);1086 res(chatData);
1084 } else {1087 } else {
1085 console.log('Found an invalid or corrupted chat file:', pathToFile);1088 console.warn('Found an invalid or corrupted chat file:', pathToFile);
1086 res({});1089 res({});
1087 }1090 }
1088 }1091 }
@@ -1095,7 +1098,7 @@ router.post('/chats', jsonParser, validateAvatarUrlMiddleware, async function (r
10951098
1096 return response.send(validFiles);1099 return response.send(validFiles);
1097 } catch (error) {1100 } catch (error) {
1098 console.log(error);1101 console.error(error);
1099 return response.send({ error: true });1102 return response.send({ error: true });
1100 }1103 }
1101});1104});
@@ -1152,7 +1155,7 @@ router.post('/import', urlencodedParser, async function (request, response) {
1152 const fileName = await importFunction(uploadPath, { request, response }, preservedFileName);1155 const fileName = await importFunction(uploadPath, { request, response }, preservedFileName);
11531156
1154 if (!fileName) {1157 if (!fileName) {
1155 console.error('Failed to import character');1158 console.warn('Failed to import character');
1156 return response.sendStatus(400);1159 return response.sendStatus(400);
1157 }1160 }
11581161
@@ -1162,7 +1165,7 @@ router.post('/import', urlencodedParser, async function (request, response) {
11621165
1163 response.send({ file_name: fileName });1166 response.send({ file_name: fileName });
1164 } catch (err) {1167 } catch (err) {
1165 console.log(err);1168 console.error(err);
1166 response.send({ error: true });1169 response.send({ error: true });
1167 }1170 }
1168});1171});
@@ -1170,14 +1173,13 @@ router.post('/import', urlencodedParser, async function (request, response) {
1170router.post('/duplicate', jsonParser, validateAvatarUrlMiddleware, async function (request, response) {1173router.post('/duplicate', jsonParser, validateAvatarUrlMiddleware, async function (request, response) {
1171 try {1174 try {
1172 if (!request.body.avatar_url) {1175 if (!request.body.avatar_url) {
1173 console.log('avatar URL not found in request body');1176 console.warn('avatar URL not found in request body');
1174 console.log(request.body);1177 console.debug(request.body);
1175 return response.sendStatus(400);1178 return response.sendStatus(400);
1176 }1179 }
1177 let filename = path.join(request.user.directories.characters, sanitize(request.body.avatar_url));1180 let filename = path.join(request.user.directories.characters, sanitize(request.body.avatar_url));
1178 if (!fs.existsSync(filename)) {1181 if (!fs.existsSync(filename)) {
1179 console.log('file for dupe not found');1182 console.error('file for dupe not found', filename);
1180 console.log(filename);
1181 return response.sendStatus(404);1183 return response.sendStatus(404);
1182 }1184 }
1183 let suffix = 1;1185 let suffix = 1;
@@ -1205,7 +1207,7 @@ router.post('/duplicate', jsonParser, validateAvatarUrlMiddleware, async functio
1205 }1207 }
12061208
1207 fs.copyFileSync(filename, newFilename);1209 fs.copyFileSync(filename, newFilename);
1208 console.log(`${filename} was copied to ${newFilename}`);1210 console.info(`${filename} was copied to ${newFilename}`);
1209 response.send({ path: path.parse(newFilename).base });1211 response.send({ path: path.parse(newFilename).base });
1210 }1212 }
1211 catch (error) {1213 catch (error) {
src/endpoints/chats.js+17 -18
@@ -50,7 +50,7 @@ function backupChat(directory, name, chat) {
5050
51 removeOldBackups(directory, 'chat_', maxTotalChatBackups);51 removeOldBackups(directory, 'chat_', maxTotalChatBackups);
52 } catch (err) {52 } catch (err) {
53 console.log(`Could not backup chat for ${name}`, err);53 console.error(`Could not backup chat for ${name}`, err);
54 }54 }
55}55}
5656
@@ -306,8 +306,8 @@ router.post('/save', jsonParser, validateAvatarUrlMiddleware, function (request,
306 getBackupFunction(request.user.profile.handle)(request.user.directories.backups, directoryName, jsonlData);306 getBackupFunction(request.user.profile.handle)(request.user.directories.backups, directoryName, jsonlData);
307 return response.send({ result: 'ok' });307 return response.send({ result: 'ok' });
308 } catch (error) {308 } catch (error) {
309 response.send(error);309 console.error(error);
310 return console.log(error);310 return response.send(error);
311 }311 }
312});312});
313313
@@ -359,17 +359,17 @@ router.post('/rename', jsonParser, validateAvatarUrlMiddleware, async function (
359 const pathToOriginalFile = path.join(pathToFolder, sanitize(request.body.original_file));359 const pathToOriginalFile = path.join(pathToFolder, sanitize(request.body.original_file));
360 const pathToRenamedFile = path.join(pathToFolder, sanitize(request.body.renamed_file));360 const pathToRenamedFile = path.join(pathToFolder, sanitize(request.body.renamed_file));
361 const sanitizedFileName = path.parse(pathToRenamedFile).name;361 const sanitizedFileName = path.parse(pathToRenamedFile).name;
362 console.log('Old chat name', pathToOriginalFile);362 console.info('Old chat name', pathToOriginalFile);
363 console.log('New chat name', pathToRenamedFile);363 console.info('New chat name', pathToRenamedFile);
364364
365 if (!fs.existsSync(pathToOriginalFile) || fs.existsSync(pathToRenamedFile)) {365 if (!fs.existsSync(pathToOriginalFile) || fs.existsSync(pathToRenamedFile)) {
366 console.log('Either Source or Destination files are not available');366 console.error('Either Source or Destination files are not available');
367 return response.status(400).send({ error: true });367 return response.status(400).send({ error: true });
368 }368 }
369369
370 fs.copyFileSync(pathToOriginalFile, pathToRenamedFile);370 fs.copyFileSync(pathToOriginalFile, pathToRenamedFile);
371 fs.rmSync(pathToOriginalFile);371 fs.rmSync(pathToOriginalFile);
372 console.log('Successfully renamed.');372 console.info('Successfully renamed.');
373 return response.send({ ok: true, sanitizedFileName });373 return response.send({ ok: true, sanitizedFileName });
374});374});
375375
@@ -380,12 +380,12 @@ router.post('/delete', jsonParser, validateAvatarUrlMiddleware, function (reques
380 const chatFileExists = fs.existsSync(filePath);380 const chatFileExists = fs.existsSync(filePath);
381381
382 if (!chatFileExists) {382 if (!chatFileExists) {
383 console.log(`Chat file not found '${filePath}'`);383 console.error(`Chat file not found '${filePath}'`);
384 return response.sendStatus(400);384 return response.sendStatus(400);
385 }385 }
386386
387 fs.rmSync(filePath);387 fs.rmSync(filePath);
388 console.log('Deleted chat file: ' + filePath);388 console.info(`Deleted chat file: ${filePath}`);
389 return response.send('ok');389 return response.send('ok');
390});390});
391391
@@ -402,7 +402,7 @@ router.post('/export', jsonParser, validateAvatarUrlMiddleware, async function (
402 const errorMessage = {402 const errorMessage = {
403 message: `Could not find JSONL file to export. Source chat file: ${filename}.`,403 message: `Could not find JSONL file to export. Source chat file: ${filename}.`,
404 };404 };
405 console.log(errorMessage.message);405 console.error(errorMessage.message);
406 return response.status(404).json(errorMessage);406 return response.status(404).json(errorMessage);
407 }407 }
408 try {408 try {
@@ -415,14 +415,14 @@ router.post('/export', jsonParser, validateAvatarUrlMiddleware, async function (
415 result: rawFile,415 result: rawFile,
416 };416 };
417417
418 console.log(`Chat exported as ${exportfilename}`);418 console.info(`Chat exported as ${exportfilename}`);
419 return response.status(200).json(successMessage);419 return response.status(200).json(successMessage);
420 } catch (err) {420 } catch (err) {
421 console.error(err);421 console.error(err);
422 const errorMessage = {422 const errorMessage = {
423 message: `Could not read JSONL file to export. Source chat file: ${filename}.`,423 message: `Could not read JSONL file to export. Source chat file: ${filename}.`,
424 };424 };
425 console.log(errorMessage.message);425 console.error(errorMessage.message);
426 return response.status(500).json(errorMessage);426 return response.status(500).json(errorMessage);
427 }427 }
428 }428 }
@@ -449,12 +449,11 @@ router.post('/export', jsonParser, validateAvatarUrlMiddleware, async function (
449 message: `Chat saved to ${exportfilename}`,449 message: `Chat saved to ${exportfilename}`,
450 result: buffer,450 result: buffer,
451 };451 };
452 console.log(`Chat exported as ${exportfilename}`);452 console.info(`Chat exported as ${exportfilename}`);
453 return response.status(200).json(successMessage);453 return response.status(200).json(successMessage);
454 });454 });
455 } catch (err) {455 } catch (err) {
456 console.log('chat export failed.');456 console.error('chat export failed.', err);
457 console.log(err);
458 return response.sendStatus(400);457 return response.sendStatus(400);
459 }458 }
460});459});
@@ -513,7 +512,7 @@ router.post('/import', urlencodedParser, validateAvatarUrlMiddleware, function (
513 } else if (jsonData.type === 'risuChat') { // RisuAI format512 } else if (jsonData.type === 'risuChat') { // RisuAI format
514 importFunc = importRisuChat;513 importFunc = importRisuChat;
515 } else { // Unknown format514 } else { // Unknown format
516 console.log('Incorrect chat format .json');515 console.error('Incorrect chat format .json');
517 return response.send({ error: true });516 return response.send({ error: true });
518 }517 }
519518
@@ -541,7 +540,7 @@ router.post('/import', urlencodedParser, validateAvatarUrlMiddleware, function (
541 const jsonData = JSON.parse(header);540 const jsonData = JSON.parse(header);
542541
543 if (!(jsonData.user_name !== undefined || jsonData.name !== undefined)) {542 if (!(jsonData.user_name !== undefined || jsonData.name !== undefined)) {
544 console.log('Incorrect chat format .jsonl');543 console.error('Incorrect chat format .jsonl');
545 return response.send({ error: true });544 return response.send({ error: true });
546 }545 }
547546
@@ -647,7 +646,7 @@ router.post('/search', jsonParser, validateAvatarUrlMiddleware, function (reques
647 break;646 break;
648 }647 }
649 } catch (error) {648 } catch (error) {
650 console.error(groupFile, 'group file is corrupted:', error);649 console.warn(groupFile, 'group file is corrupted:', error);
651 }650 }
652 }651 }
653652
src/endpoints/classify.js+2 -2
@@ -44,9 +44,9 @@ router.post('/', jsonParser, async (req, res) => {
44 }44 }
45 }45 }
4646
47 console.log('Classify input:', text);47 console.debug('Classify input:', text);
48 const result = await getResult(text);48 const result = await getResult(text);
49 console.log('Classify output:', result);49 console.debug('Classify output:', result);
5050
51 return res.json({ classification: result });51 return res.json({ classification: result });
52 } catch (error) {52 } catch (error) {
src/endpoints/content-manager.js+26 -26
@@ -71,7 +71,7 @@ export function getDefaultPresets(directories) {
7171
72 return presets;72 return presets;
73 } catch (err) {73 } catch (err) {
74 console.log('Failed to get default presets', err);74 console.warn('Failed to get default presets', err);
75 return [];75 return [];
76 }76 }
77}77}
@@ -92,7 +92,7 @@ export function getDefaultPresetFile(filename) {
92 const fileContent = fs.readFileSync(contentPath, 'utf8');92 const fileContent = fs.readFileSync(contentPath, 'utf8');
93 return JSON.parse(fileContent);93 return JSON.parse(fileContent);
94 } catch (err) {94 } catch (err) {
95 console.log(`Failed to get default file ${filename}`, err);95 console.warn(`Failed to get default file ${filename}`, err);
96 return null;96 return null;
97 }97 }
98}98}
@@ -121,21 +121,21 @@ async function seedContentForUser(contentIndex, directories, forceCategories) {
121 }121 }
122122
123 if (!contentItem.folder) {123 if (!contentItem.folder) {
124 console.log(`Content file ${contentItem.filename} has no parent folder`);124 console.warn(`Content file ${contentItem.filename} has no parent folder`);
125 continue;125 continue;
126 }126 }
127127
128 const contentPath = path.join(contentItem.folder, contentItem.filename);128 const contentPath = path.join(contentItem.folder, contentItem.filename);
129129
130 if (!fs.existsSync(contentPath)) {130 if (!fs.existsSync(contentPath)) {
131 console.log(`Content file ${contentItem.filename} is missing`);131 console.warn(`Content file ${contentItem.filename} is missing`);
132 continue;132 continue;
133 }133 }
134134
135 const contentTarget = getTargetByType(contentItem.type, directories);135 const contentTarget = getTargetByType(contentItem.type, directories);
136136
137 if (!contentTarget) {137 if (!contentTarget) {
138 console.log(`Content file ${contentItem.filename} has unknown type ${contentItem.type}`);138 console.warn(`Content file ${contentItem.filename} has unknown type ${contentItem.type}`);
139 continue;139 continue;
140 }140 }
141141
@@ -144,12 +144,12 @@ async function seedContentForUser(contentIndex, directories, forceCategories) {
144 contentLog.push(contentItem.filename);144 contentLog.push(contentItem.filename);
145145
146 if (fs.existsSync(targetPath)) {146 if (fs.existsSync(targetPath)) {
147 console.log(`Content file ${contentItem.filename} already exists in ${contentTarget}`);147 console.warn(`Content file ${contentItem.filename} already exists in ${contentTarget}`);
148 continue;148 continue;
149 }149 }
150150
151 fs.cpSync(contentPath, targetPath, { recursive: true, force: false });151 fs.cpSync(contentPath, targetPath, { recursive: true, force: false });
152 console.log(`Content file ${contentItem.filename} copied to ${contentTarget}`);152 console.info(`Content file ${contentItem.filename} copied to ${contentTarget}`);
153 anyContentAdded = true;153 anyContentAdded = true;
154 }154 }
155155
@@ -182,12 +182,12 @@ export async function checkForNewContent(directoriesList, forceCategories = [])
182 }182 }
183183
184 if (anyContentAdded && !contentCheckSkip && forceCategories?.length === 0) {184 if (anyContentAdded && !contentCheckSkip && forceCategories?.length === 0) {
185 console.log();185 console.info();
186 console.log(`${color.blue('If you don\'t want to receive content updates in the future, set')} ${color.yellow('skipContentCheck')} ${color.blue('to true in the config.yaml file.')}`);186 console.info(`${color.blue('If you don\'t want to receive content updates in the future, set')} ${color.yellow('skipContentCheck')} ${color.blue('to true in the config.yaml file.')}`);
187 console.log();187 console.info();
188 }188 }
189 } catch (err) {189 } catch (err) {
190 console.log('Content check failed', err);190 console.error('Content check failed', err);
191 }191 }
192}192}
193193
@@ -331,7 +331,7 @@ async function downloadChubLorebook(id) {
331331
332 if (!result.ok) {332 if (!result.ok) {
333 const text = await result.text();333 const text = await result.text();
334 console.log('Chub returned error', result.statusText, text);334 console.error('Chub returned error', result.statusText, text);
335 throw new Error('Failed to download lorebook');335 throw new Error('Failed to download lorebook');
336 }336 }
337337
@@ -355,7 +355,7 @@ async function downloadChubCharacter(id) {
355355
356 if (!result.ok) {356 if (!result.ok) {
357 const text = await result.text();357 const text = await result.text();
358 console.log('Chub returned error', result.statusText, text);358 console.error('Chub returned error', result.statusText, text);
359 throw new Error('Failed to download character');359 throw new Error('Failed to download character');
360 }360 }
361361
@@ -376,7 +376,7 @@ async function downloadPygmalionCharacter(id) {
376376
377 if (!result.ok) {377 if (!result.ok) {
378 const text = await result.text();378 const text = await result.text();
379 console.log('Pygsite returned error', result.status, text);379 console.error('Pygsite returned error', result.status, text);
380 throw new Error('Failed to download character');380 throw new Error('Failed to download character');
381 }381 }
382382
@@ -485,7 +485,7 @@ async function downloadJannyCharacter(uuid) {
485 }485 }
486 }486 }
487487
488 console.log('Janny returned error', result.statusText, await result.text());488 console.error('Janny returned error', result.statusText, await result.text());
489 throw new Error('Failed to download character');489 throw new Error('Failed to download character');
490}490}
491491
@@ -577,7 +577,7 @@ async function downloadRisuCharacter(uuid) {
577577
578 if (!result.ok) {578 if (!result.ok) {
579 const text = await result.text();579 const text = await result.text();
580 console.log('RisuAI returned error', result.statusText, text);580 console.error('RisuAI returned error', result.statusText, text);
581 throw new Error('Failed to download character');581 throw new Error('Failed to download character');
582 }582 }
583583
@@ -673,11 +673,11 @@ router.post('/importURL', jsonParser, async (request, response) => {
673 type = chubParsed?.type;673 type = chubParsed?.type;
674674
675 if (chubParsed?.type === 'character') {675 if (chubParsed?.type === 'character') {
676 console.log('Downloading chub character:', chubParsed.id);676 console.info('Downloading chub character:', chubParsed.id);
677 result = await downloadChubCharacter(chubParsed.id);677 result = await downloadChubCharacter(chubParsed.id);
678 }678 }
679 else if (chubParsed?.type === 'lorebook') {679 else if (chubParsed?.type === 'lorebook') {
680 console.log('Downloading chub lorebook:', chubParsed.id);680 console.info('Downloading chub lorebook:', chubParsed.id);
681 result = await downloadChubLorebook(chubParsed.id);681 result = await downloadChubLorebook(chubParsed.id);
682 }682 }
683 else {683 else {
@@ -692,7 +692,7 @@ router.post('/importURL', jsonParser, async (request, response) => {
692 type = 'character';692 type = 'character';
693 result = await downloadRisuCharacter(uuid);693 result = await downloadRisuCharacter(uuid);
694 } else if (isGeneric) {694 } else if (isGeneric) {
695 console.log('Downloading from generic url.');695 console.info('Downloading from generic url.');
696 type = 'character';696 type = 'character';
697 result = await downloadGenericPng(url);697 result = await downloadGenericPng(url);
698 } else {698 } else {
@@ -708,7 +708,7 @@ router.post('/importURL', jsonParser, async (request, response) => {
708 response.set('X-Custom-Content-Type', type);708 response.set('X-Custom-Content-Type', type);
709 return response.send(result.buffer);709 return response.send(result.buffer);
710 } catch (error) {710 } catch (error) {
711 console.log('Importing custom content failed', error);711 console.error('Importing custom content failed', error);
712 return response.sendStatus(500);712 return response.sendStatus(500);
713 }713 }
714});714});
@@ -728,22 +728,22 @@ router.post('/importUUID', jsonParser, async (request, response) => {
728 const uuidType = uuid.includes('lorebook') ? 'lorebook' : 'character';728 const uuidType = uuid.includes('lorebook') ? 'lorebook' : 'character';
729729
730 if (isPygmalion) {730 if (isPygmalion) {
731 console.log('Downloading Pygmalion character:', uuid);731 console.info('Downloading Pygmalion character:', uuid);
732 result = await downloadPygmalionCharacter(uuid);732 result = await downloadPygmalionCharacter(uuid);
733 } else if (isJannny) {733 } else if (isJannny) {
734 console.log('Downloading Janitor character:', uuid.split('_')[0]);734 console.info('Downloading Janitor character:', uuid.split('_')[0]);
735 result = await downloadJannyCharacter(uuid.split('_')[0]);735 result = await downloadJannyCharacter(uuid.split('_')[0]);
736 } else if (isAICC) {736 } else if (isAICC) {
737 const [, author, card] = uuid.split('/');737 const [, author, card] = uuid.split('/');
738 console.log('Downloading AICC character:', `${author}/${card}`);738 console.info('Downloading AICC character:', `${author}/${card}`);
739 result = await downloadAICCCharacter(`${author}/${card}`);739 result = await downloadAICCCharacter(`${author}/${card}`);
740 } else {740 } else {
741 if (uuidType === 'character') {741 if (uuidType === 'character') {
742 console.log('Downloading chub character:', uuid);742 console.info('Downloading chub character:', uuid);
743 result = await downloadChubCharacter(uuid);743 result = await downloadChubCharacter(uuid);
744 }744 }
745 else if (uuidType === 'lorebook') {745 else if (uuidType === 'lorebook') {
746 console.log('Downloading chub lorebook:', uuid);746 console.info('Downloading chub lorebook:', uuid);
747 result = await downloadChubLorebook(uuid);747 result = await downloadChubLorebook(uuid);
748 }748 }
749 else {749 else {
@@ -756,7 +756,7 @@ router.post('/importUUID', jsonParser, async (request, response) => {
756 response.set('X-Custom-Content-Type', uuidType);756 response.set('X-Custom-Content-Type', uuidType);
757 return response.send(result.buffer);757 return response.send(result.buffer);
758 } catch (error) {758 } catch (error) {
759 console.log('Importing custom content failed', error);759 console.error('Importing custom content failed', error);
760 return response.sendStatus(500);760 return response.sendStatus(500);
761 }761 }
762});762});
src/endpoints/extensions.js+16 -16
@@ -80,7 +80,7 @@ router.post('/install', jsonParser, async (request, response) => {
80 const { url, global } = request.body;80 const { url, global } = request.body;
8181
82 if (global && !request.user.profile.admin) {82 if (global && !request.user.profile.admin) {
83 console.warn(`User ${request.user.profile.handle} does not have permission to install global extensions.`);83 console.error(`User ${request.user.profile.handle} does not have permission to install global extensions.`);
84 return response.status(403).send('Forbidden: No permission to install global extensions.');84 return response.status(403).send('Forbidden: No permission to install global extensions.');
85 }85 }
8686
@@ -92,13 +92,13 @@ router.post('/install', jsonParser, async (request, response) => {
92 }92 }
9393
94 await git.clone(url, extensionPath, { '--depth': 1 });94 await git.clone(url, extensionPath, { '--depth': 1 });
95 console.log(`Extension has been cloned at ${extensionPath}`);95 console.info(`Extension has been cloned at ${extensionPath}`);
9696
97 const { version, author, display_name } = await getManifest(extensionPath);97 const { version, author, display_name } = await getManifest(extensionPath);
9898
99 return response.send({ version, author, display_name, extensionPath });99 return response.send({ version, author, display_name, extensionPath });
100 } catch (error) {100 } catch (error) {
101 console.log('Importing custom content failed', error);101 console.error('Importing custom content failed', error);
102 return response.status(500).send(`Server Error: ${error.message}`);102 return response.status(500).send(`Server Error: ${error.message}`);
103 }103 }
104});104});
@@ -124,7 +124,7 @@ router.post('/update', jsonParser, async (request, response) => {
124 const { extensionName, global } = request.body;124 const { extensionName, global } = request.body;
125125
126 if (global && !request.user.profile.admin) {126 if (global && !request.user.profile.admin) {
127 console.warn(`User ${request.user.profile.handle} does not have permission to update global extensions.`);127 console.error(`User ${request.user.profile.handle} does not have permission to update global extensions.`);
128 return response.status(403).send('Forbidden: No permission to update global extensions.');128 return response.status(403).send('Forbidden: No permission to update global extensions.');
129 }129 }
130130
@@ -139,9 +139,9 @@ router.post('/update', jsonParser, async (request, response) => {
139 const currentBranch = await git.cwd(extensionPath).branch();139 const currentBranch = await git.cwd(extensionPath).branch();
140 if (!isUpToDate) {140 if (!isUpToDate) {
141 await git.cwd(extensionPath).pull('origin', currentBranch.current);141 await git.cwd(extensionPath).pull('origin', currentBranch.current);
142 console.log(`Extension has been updated at ${extensionPath}`);142 console.info(`Extension has been updated at ${extensionPath}`);
143 } else {143 } else {
144 console.log(`Extension is up to date at ${extensionPath}`);144 console.info(`Extension is up to date at ${extensionPath}`);
145 }145 }
146 await git.cwd(extensionPath).fetch('origin');146 await git.cwd(extensionPath).fetch('origin');
147 const fullCommitHash = await git.cwd(extensionPath).revparse(['HEAD']);147 const fullCommitHash = await git.cwd(extensionPath).revparse(['HEAD']);
@@ -150,7 +150,7 @@ router.post('/update', jsonParser, async (request, response) => {
150 return response.send({ shortCommitHash, extensionPath, isUpToDate, remoteUrl });150 return response.send({ shortCommitHash, extensionPath, isUpToDate, remoteUrl });
151151
152 } catch (error) {152 } catch (error) {
153 console.log('Updating custom content failed', error);153 console.error('Updating custom content failed', error);
154 return response.status(500).send(`Server Error: ${error.message}`);154 return response.status(500).send(`Server Error: ${error.message}`);
155 }155 }
156});156});
@@ -164,7 +164,7 @@ router.post('/move', jsonParser, async (request, response) => {
164 }164 }
165165
166 if (!request.user.profile.admin) {166 if (!request.user.profile.admin) {
167 console.warn(`User ${request.user.profile.handle} does not have permission to move extensions.`);167 console.error(`User ${request.user.profile.handle} does not have permission to move extensions.`);
168 return response.status(403).send('Forbidden: No permission to move extensions.');168 return response.status(403).send('Forbidden: No permission to move extensions.');
169 }169 }
170170
@@ -190,11 +190,11 @@ router.post('/move', jsonParser, async (request, response) => {
190190
191 fs.cpSync(sourcePath, destinationPath, { recursive: true, force: true });191 fs.cpSync(sourcePath, destinationPath, { recursive: true, force: true });
192 fs.rmSync(sourcePath, { recursive: true, force: true });192 fs.rmSync(sourcePath, { recursive: true, force: true });
193 console.log(`Extension has been moved from ${sourcePath} to ${destinationPath}`);193 console.info(`Extension has been moved from ${sourcePath} to ${destinationPath}`);
194194
195 return response.sendStatus(204);195 return response.sendStatus(204);
196 } catch (error) {196 } catch (error) {
197 console.log('Moving extension failed', error);197 console.error('Moving extension failed', error);
198 return response.status(500).send('Internal Server Error. Try again later.');198 return response.status(500).send('Internal Server Error. Try again later.');
199 }199 }
200});200});
@@ -237,13 +237,13 @@ router.post('/version', jsonParser, async (request, response) => {
237 // get only the working branch237 // get only the working branch
238 const currentBranchName = currentBranch.current;238 const currentBranchName = currentBranch.current;
239 await git.cwd(extensionPath).fetch('origin');239 await git.cwd(extensionPath).fetch('origin');
240 console.log(extensionName, currentBranchName, currentCommitHash);240 console.debug(extensionName, currentBranchName, currentCommitHash);
241 const { isUpToDate, remoteUrl } = await checkIfRepoIsUpToDate(extensionPath);241 const { isUpToDate, remoteUrl } = await checkIfRepoIsUpToDate(extensionPath);
242242
243 return response.send({ currentBranchName, currentCommitHash, isUpToDate, remoteUrl });243 return response.send({ currentBranchName, currentCommitHash, isUpToDate, remoteUrl });
244244
245 } catch (error) {245 } catch (error) {
246 console.log('Getting extension version failed', error);246 console.error('Getting extension version failed', error);
247 return response.status(500).send(`Server Error: ${error.message}`);247 return response.status(500).send(`Server Error: ${error.message}`);
248 }248 }
249});249});
@@ -265,7 +265,7 @@ router.post('/delete', jsonParser, async (request, response) => {
265 const { extensionName, global } = request.body;265 const { extensionName, global } = request.body;
266266
267 if (global && !request.user.profile.admin) {267 if (global && !request.user.profile.admin) {
268 console.warn(`User ${request.user.profile.handle} does not have permission to delete global extensions.`);268 console.error(`User ${request.user.profile.handle} does not have permission to delete global extensions.`);
269 return response.status(403).send('Forbidden: No permission to delete global extensions.');269 return response.status(403).send('Forbidden: No permission to delete global extensions.');
270 }270 }
271271
@@ -277,12 +277,12 @@ router.post('/delete', jsonParser, async (request, response) => {
277 }277 }
278278
279 await fs.promises.rm(extensionPath, { recursive: true });279 await fs.promises.rm(extensionPath, { recursive: true });
280 console.log(`Extension has been deleted at ${extensionPath}`);280 console.info(`Extension has been deleted at ${extensionPath}`);
281281
282 return response.send(`Extension has been deleted at ${extensionPath}`);282 return response.send(`Extension has been deleted at ${extensionPath}`);
283283
284 } catch (error) {284 } catch (error) {
285 console.log('Deleting custom content failed', error);285 console.error('Deleting custom content failed', error);
286 return response.status(500).send(`Server Error: ${error.message}`);286 return response.status(500).send(`Server Error: ${error.message}`);
287 }287 }
288});288});
@@ -323,7 +323,7 @@ router.get('/discover', jsonParser, function (request, response) {
323323
324 // Combine all extensions324 // Combine all extensions
325 const allExtensions = [...builtInExtensions, ...userExtensions, ...globalExtensions];325 const allExtensions = [...builtInExtensions, ...userExtensions, ...globalExtensions];
326 console.log('Extensions available for', request.user.profile.handle, allExtensions);326 console.info('Extensions available for', request.user.profile.handle, allExtensions);
327327
328 return response.send(allExtensions);328 return response.send(allExtensions);
329});329});
src/endpoints/files.js+7 -7
@@ -21,7 +21,7 @@ router.post('/sanitize-filename', jsonParser, async (request, response) => {
21 const sanitizedFilename = sanitize(fileName);21 const sanitizedFilename = sanitize(fileName);
22 return response.send({ fileName: sanitizedFilename });22 return response.send({ fileName: sanitizedFilename });
23 } catch (error) {23 } catch (error) {
24 console.log(error);24 console.error(error);
25 return response.sendStatus(500);25 return response.sendStatus(500);
26 }26 }
27});27});
@@ -44,10 +44,10 @@ router.post('/upload', jsonParser, async (request, response) => {
44 const pathToUpload = path.join(request.user.directories.files, request.body.name);44 const pathToUpload = path.join(request.user.directories.files, request.body.name);
45 writeFileSyncAtomic(pathToUpload, request.body.data, 'base64');45 writeFileSyncAtomic(pathToUpload, request.body.data, 'base64');
46 const url = clientRelativePath(request.user.directories.root, pathToUpload);46 const url = clientRelativePath(request.user.directories.root, pathToUpload);
47 console.log(`Uploaded file: ${url} from ${request.user.profile.handle}`);47 console.info(`Uploaded file: ${url} from ${request.user.profile.handle}`);
48 return response.send({ path: url });48 return response.send({ path: url });
49 } catch (error) {49 } catch (error) {
50 console.log(error);50 console.error(error);
51 return response.sendStatus(500);51 return response.sendStatus(500);
52 }52 }
53});53});
@@ -68,10 +68,10 @@ router.post('/delete', jsonParser, async (request, response) => {
68 }68 }
6969
70 fs.rmSync(pathToDelete);70 fs.rmSync(pathToDelete);
71 console.log(`Deleted file: ${request.body.path} from ${request.user.profile.handle}`);71 console.info(`Deleted file: ${request.body.path} from ${request.user.profile.handle}`);
72 return response.sendStatus(200);72 return response.sendStatus(200);
73 } catch (error) {73 } catch (error) {
74 console.log(error);74 console.error(error);
75 return response.sendStatus(500);75 return response.sendStatus(500);
76 }76 }
77});77});
@@ -87,7 +87,7 @@ router.post('/verify', jsonParser, async (request, response) => {
87 for (const url of request.body.urls) {87 for (const url of request.body.urls) {
88 const pathToVerify = path.join(request.user.directories.root, url);88 const pathToVerify = path.join(request.user.directories.root, url);
89 if (!pathToVerify.startsWith(request.user.directories.files)) {89 if (!pathToVerify.startsWith(request.user.directories.files)) {
90 console.debug(`File verification: Invalid path: ${pathToVerify}`);90 console.warn(`File verification: Invalid path: ${pathToVerify}`);
91 continue;91 continue;
92 }92 }
93 const fileExists = fs.existsSync(pathToVerify);93 const fileExists = fs.existsSync(pathToVerify);
@@ -96,7 +96,7 @@ router.post('/verify', jsonParser, async (request, response) => {
9696
97 return response.send(verified);97 return response.send(verified);
98 } catch (error) {98 } catch (error) {
99 console.log(error);99 console.error(error);
100 return response.sendStatus(500);100 return response.sendStatus(500);
101 }101 }
102});102});
src/endpoints/google.js+3 -3
@@ -34,7 +34,7 @@ router.post('/caption-image', jsonParser, async (request, response) => {
34 generationConfig: { maxOutputTokens: 1000 },34 generationConfig: { maxOutputTokens: 1000 },
35 };35 };
3636
37 console.log('Multimodal captioning request', model, body);37 console.debug('Multimodal captioning request', model, body);
3838
39 const result = await fetch(url, {39 const result = await fetch(url, {
40 body: JSON.stringify(body),40 body: JSON.stringify(body),
@@ -46,13 +46,13 @@ router.post('/caption-image', jsonParser, async (request, response) => {
4646
47 if (!result.ok) {47 if (!result.ok) {
48 const error = await result.json();48 const error = await result.json();
49 console.log(`Google AI Studio API returned error: ${result.status} ${result.statusText}`, error);49 console.error(`Google AI Studio API returned error: ${result.status} ${result.statusText}`, error);
50 return response.status(result.status).send({ error: true });50 return response.status(result.status).send({ error: true });
51 }51 }
5252
53 /** @type {any} */53 /** @type {any} */
54 const data = await result.json();54 const data = await result.json();
55 console.log('Multimodal captioning response', data);55 console.info('Multimodal captioning response', data);
5656
57 const candidates = data?.candidates;57 const candidates = data?.candidates;
58 if (!candidates) {58 if (!candidates) {
src/endpoints/groups.js+1 -1
@@ -114,7 +114,7 @@ router.post('/delete', jsonParser, async (request, response) => {
114114
115 if (group && Array.isArray(group.chats)) {115 if (group && Array.isArray(group.chats)) {
116 for (const chat of group.chats) {116 for (const chat of group.chats) {
117 console.log('Deleting group chat', chat);117 console.info('Deleting group chat', chat);
118 const pathToFile = path.join(request.user.directories.groupChats, `${id}.jsonl`);118 const pathToFile = path.join(request.user.directories.groupChats, `${id}.jsonl`);
119119
120 if (fs.existsSync(pathToFile)) {120 if (fs.existsSync(pathToFile)) {
src/endpoints/horde.js+15 -15
@@ -155,7 +155,7 @@ router.post('/cancel-task', jsonParser, async (request, response) => {
155 });155 });
156156
157 const data = await fetchResult.json();157 const data = await fetchResult.json();
158 console.log(`Cancelled Horde task ${taskId}`);158 console.info(`Cancelled Horde task ${taskId}`);
159 return response.send(data);159 return response.send(data);
160 } catch (error) {160 } catch (error) {
161 console.error(error);161 console.error(error);
@@ -174,7 +174,7 @@ router.post('/task-status', jsonParser, async (request, response) => {
174 });174 });
175175
176 const data = await fetchResult.json();176 const data = await fetchResult.json();
177 console.log(`Horde task ${taskId} status:`, data);177 console.info(`Horde task ${taskId} status:`, data);
178 return response.send(data);178 return response.send(data);
179 } catch (error) {179 } catch (error) {
180 console.error(error);180 console.error(error);
@@ -187,7 +187,7 @@ router.post('/generate-text', jsonParser, async (request, response) => {
187 const url = 'https://aihorde.net/api/v2/generate/text/async';187 const url = 'https://aihorde.net/api/v2/generate/text/async';
188 const agent = await getClientAgent();188 const agent = await getClientAgent();
189189
190 console.log(request.body);190 console.debug(request.body);
191 try {191 try {
192 const result = await fetch(url, {192 const result = await fetch(url, {
193 method: 'POST',193 method: 'POST',
@@ -201,14 +201,14 @@ router.post('/generate-text', jsonParser, async (request, response) => {
201201
202 if (!result.ok) {202 if (!result.ok) {
203 const message = await result.text();203 const message = await result.text();
204 console.log('Horde returned an error:', message);204 console.error('Horde returned an error:', message);
205 return response.send({ error: { message } });205 return response.send({ error: { message } });
206 }206 }
207207
208 const data = await result.json();208 const data = await result.json();
209 return response.send(data);209 return response.send(data);
210 } catch (error) {210 } catch (error) {
211 console.log(error);211 console.error(error);
212 return response.send({ error: true });212 return response.send({ error: true });
213 }213 }
214});214});
@@ -254,7 +254,7 @@ router.post('/caption-image', jsonParser, async (request, response) => {
254 for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt++) {254 for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt++) {
255 await delay(CHECK_INTERVAL);255 await delay(CHECK_INTERVAL);
256 const status = await ai_horde.getInterrogationStatus(result.id);256 const status = await ai_horde.getInterrogationStatus(result.id);
257 console.log(status);257 console.info(status);
258258
259 if (status.state === HordeAsyncRequestStates.done) {259 if (status.state === HordeAsyncRequestStates.done) {
260260
@@ -263,7 +263,7 @@ router.post('/caption-image', jsonParser, async (request, response) => {
263 return response.sendStatus(500);263 return response.sendStatus(500);
264 }264 }
265265
266 console.log('Image interrogation result:', status);266 console.debug('Image interrogation result:', status);
267 const caption = status?.forms[0]?.result?.caption || '';267 const caption = status?.forms[0]?.result?.caption || '';
268268
269 if (!caption) {269 if (!caption) {
@@ -275,7 +275,7 @@ router.post('/caption-image', jsonParser, async (request, response) => {
275 }275 }
276276
277 if (status.state === HordeAsyncRequestStates.faulted || status.state === HordeAsyncRequestStates.cancelled) {277 if (status.state === HordeAsyncRequestStates.faulted || status.state === HordeAsyncRequestStates.cancelled) {
278 console.log('Image interrogation request is not successful.');278 console.error('Image interrogation request is not successful.');
279 return response.sendStatus(503);279 return response.sendStatus(503);
280 }280 }
281 }281 }
@@ -315,7 +315,7 @@ router.post('/generate-image', jsonParser, async (request, response) => {
315 try {315 try {
316 const maxLength = PROMPT_THRESHOLD - String(request.body.negative_prompt).length - 5;316 const maxLength = PROMPT_THRESHOLD - String(request.body.negative_prompt).length - 5;
317 if (String(request.body.prompt).length > maxLength) {317 if (String(request.body.prompt).length > maxLength) {
318 console.log('Stable Horde prompt is too long, truncating...');318 console.warn('Stable Horde prompt is too long, truncating...');
319 request.body.prompt = String(request.body.prompt).substring(0, maxLength);319 request.body.prompt = String(request.body.prompt).substring(0, maxLength);
320 }320 }
321321
@@ -324,14 +324,14 @@ router.post('/generate-image', jsonParser, async (request, response) => {
324 const sanitized = sanitizeHordeImagePrompt(request.body.prompt);324 const sanitized = sanitizeHordeImagePrompt(request.body.prompt);
325325
326 if (request.body.prompt !== sanitized) {326 if (request.body.prompt !== sanitized) {
327 console.log('Stable Horde prompt was sanitized.');327 console.info('Stable Horde prompt was sanitized.');
328 }328 }
329329
330 request.body.prompt = sanitized;330 request.body.prompt = sanitized;
331 }331 }
332332
333 const api_key_horde = readSecret(request.user.directories, SECRET_KEYS.HORDE) || ANONYMOUS_KEY;333 const api_key_horde = readSecret(request.user.directories, SECRET_KEYS.HORDE) || ANONYMOUS_KEY;
334 console.log('Stable Horde request:', request.body);334 console.debug('Stable Horde request:', request.body);
335335
336 const ai_horde = await getHordeClient();336 const ai_horde = await getHordeClient();
337 // noinspection JSCheckFunctionSignatures -- see @ts-ignore - use_gfpgan337 // noinspection JSCheckFunctionSignatures -- see @ts-ignore - use_gfpgan
@@ -360,16 +360,16 @@ router.post('/generate-image', jsonParser, async (request, response) => {
360 { token: api_key_horde });360 { token: api_key_horde });
361361
362 if (!generation.id) {362 if (!generation.id) {
363 console.error('Image generation request is not satisfyable:', generation.message || 'unknown error');363 console.warn('Image generation request is not satisfyable:', generation.message || 'unknown error');
364 return response.sendStatus(400);364 return response.sendStatus(400);
365 }365 }
366366
367 console.log('Horde image generation request:', generation);367 console.info('Horde image generation request:', generation);
368368
369 const controller = new AbortController();369 const controller = new AbortController();
370 request.socket.removeAllListeners('close');370 request.socket.removeAllListeners('close');
371 request.socket.on('close', function () {371 request.socket.on('close', function () {
372 console.log('Horde image generation request aborted.');372 console.warn('Horde image generation request aborted.');
373 controller.abort();373 controller.abort();
374 if (generation.id) ai_horde.deleteImageGenerationRequest(generation.id);374 if (generation.id) ai_horde.deleteImageGenerationRequest(generation.id);
375 });375 });
@@ -378,7 +378,7 @@ router.post('/generate-image', jsonParser, async (request, response) => {
378 controller.signal.throwIfAborted();378 controller.signal.throwIfAborted();
379 await delay(CHECK_INTERVAL);379 await delay(CHECK_INTERVAL);
380 const check = await ai_horde.getImageGenerationCheck(generation.id);380 const check = await ai_horde.getImageGenerationCheck(generation.id);
381 console.log(check);381 console.info(check);
382382
383 if (check.done) {383 if (check.done) {
384 const result = await ai_horde.getImageGenerationStatus(generation.id);384 const result = await ai_horde.getImageGenerationStatus(generation.id);
src/endpoints/images.js+1 -1
@@ -71,7 +71,7 @@ router.post('/upload', jsonParser, async (request, response) => {
71 await fs.promises.writeFile(pathToNewFile, new Uint8Array(imageBuffer));71 await fs.promises.writeFile(pathToNewFile, new Uint8Array(imageBuffer));
72 response.send({ path: clientRelativePath(request.user.directories.root, pathToNewFile) });72 response.send({ path: clientRelativePath(request.user.directories.root, pathToNewFile) });
73 } catch (error) {73 } catch (error) {
74 console.log(error);74 console.error(error);
75 response.status(500).send({ error: 'Failed to save the image' });75 response.status(500).send({ error: 'Failed to save the image' });
76 }76 }
77});77});
src/endpoints/novelai.js+16 -16
@@ -120,7 +120,7 @@ router.post('/status', jsonParser, async function (req, res) {
120 const api_key_novel = readSecret(req.user.directories, SECRET_KEYS.NOVEL);120 const api_key_novel = readSecret(req.user.directories, SECRET_KEYS.NOVEL);
121121
122 if (!api_key_novel) {122 if (!api_key_novel) {
123 console.log('NovelAI Access Token is missing.');123 console.warn('NovelAI Access Token is missing.');
124 return res.sendStatus(400);124 return res.sendStatus(400);
125 }125 }
126126
@@ -137,15 +137,15 @@ router.post('/status', jsonParser, async function (req, res) {
137 const data = await response.json();137 const data = await response.json();
138 return res.send(data);138 return res.send(data);
139 } else if (response.status == 401) {139 } else if (response.status == 401) {
140 console.log('NovelAI Access Token is incorrect.');140 console.error('NovelAI Access Token is incorrect.');
141 return res.send({ error: true });141 return res.send({ error: true });
142 }142 }
143 else {143 else {
144 console.log('NovelAI returned an error:', response.statusText);144 console.warn('NovelAI returned an error:', response.statusText);
145 return res.send({ error: true });145 return res.send({ error: true });
146 }146 }
147 } catch (error) {147 } catch (error) {
148 console.log(error);148 console.error(error);
149 return res.send({ error: true });149 return res.send({ error: true });
150 }150 }
151});151});
@@ -156,7 +156,7 @@ router.post('/generate', jsonParser, async function (req, res) {
156 const api_key_novel = readSecret(req.user.directories, SECRET_KEYS.NOVEL);156 const api_key_novel = readSecret(req.user.directories, SECRET_KEYS.NOVEL);
157157
158 if (!api_key_novel) {158 if (!api_key_novel) {
159 console.log('NovelAI Access Token is missing.');159 console.warn('NovelAI Access Token is missing.');
160 return res.sendStatus(400);160 return res.sendStatus(400);
161 }161 }
162162
@@ -241,7 +241,7 @@ router.post('/generate', jsonParser, async function (req, res) {
241 }241 }
242 }242 }
243243
244 console.log(util.inspect(data, { depth: 4 }));244 console.debug(util.inspect(data, { depth: 4 }));
245245
246 const args = {246 const args = {
247 body: JSON.stringify(data),247 body: JSON.stringify(data),
@@ -261,7 +261,7 @@ router.post('/generate', jsonParser, async function (req, res) {
261 if (!response.ok) {261 if (!response.ok) {
262 const text = await response.text();262 const text = await response.text();
263 let message = text;263 let message = text;
264 console.log(`Novel API returned error: ${response.status} ${response.statusText} ${text}`);264 console.warn(`Novel API returned error: ${response.status} ${response.statusText} ${text}`);
265265
266 try {266 try {
267 const data = JSON.parse(text);267 const data = JSON.parse(text);
@@ -276,7 +276,7 @@ router.post('/generate', jsonParser, async function (req, res) {
276276
277 /** @type {any} */277 /** @type {any} */
278 const data = await response.json();278 const data = await response.json();
279 console.log('NovelAI Output', data?.output);279 console.info('NovelAI Output', data?.output);
280 return res.send(data);280 return res.send(data);
281 }281 }
282 } catch (error) {282 } catch (error) {
@@ -292,12 +292,12 @@ router.post('/generate-image', jsonParser, async (request, response) => {
292 const key = readSecret(request.user.directories, SECRET_KEYS.NOVEL);292 const key = readSecret(request.user.directories, SECRET_KEYS.NOVEL);
293293
294 if (!key) {294 if (!key) {
295 console.log('NovelAI Access Token is missing.');295 console.warn('NovelAI Access Token is missing.');
296 return response.sendStatus(400);296 return response.sendStatus(400);
297 }297 }
298298
299 try {299 try {
300 console.log('NAI Diffusion request:', request.body);300 console.debug('NAI Diffusion request:', request.body);
301 const generateUrl = `${IMAGE_NOVELAI}/ai/generate-image`;301 const generateUrl = `${IMAGE_NOVELAI}/ai/generate-image`;
302 const generateResult = await fetch(generateUrl, {302 const generateResult = await fetch(generateUrl, {
303 method: 'POST',303 method: 'POST',
@@ -358,7 +358,7 @@ router.post('/generate-image', jsonParser, async (request, response) => {
358358
359 if (!generateResult.ok) {359 if (!generateResult.ok) {
360 const text = await generateResult.text();360 const text = await generateResult.text();
361 console.log('NovelAI returned an error.', generateResult.statusText, text);361 console.warn('NovelAI returned an error.', generateResult.statusText, text);
362 return response.sendStatus(500);362 return response.sendStatus(500);
363 }363 }
364364
@@ -366,7 +366,7 @@ router.post('/generate-image', jsonParser, async (request, response) => {
366 const imageBuffer = await extractFileFromZipBuffer(archiveBuffer, '.png');366 const imageBuffer = await extractFileFromZipBuffer(archiveBuffer, '.png');
367367
368 if (!imageBuffer) {368 if (!imageBuffer) {
369 console.warn('NovelAI generated an image, but the PNG file was not found.');369 console.error('NovelAI generated an image, but the PNG file was not found.');
370 return response.sendStatus(500);370 return response.sendStatus(500);
371 }371 }
372372
@@ -378,7 +378,7 @@ router.post('/generate-image', jsonParser, async (request, response) => {
378 }378 }
379379
380 try {380 try {
381 console.debug('Upscaling image...');381 console.info('Upscaling image...');
382 const upscaleUrl = `${API_NOVELAI}/ai/upscale`;382 const upscaleUrl = `${API_NOVELAI}/ai/upscale`;
383 const upscaleResult = await fetch(upscaleUrl, {383 const upscaleResult = await fetch(upscaleUrl, {
384 method: 'POST',384 method: 'POST',
@@ -413,7 +413,7 @@ router.post('/generate-image', jsonParser, async (request, response) => {
413 return response.send(originalBase64);413 return response.send(originalBase64);
414 }414 }
415 } catch (error) {415 } catch (error) {
416 console.log(error);416 console.error(error);
417 return response.sendStatus(500);417 return response.sendStatus(500);
418 }418 }
419});419});
@@ -422,7 +422,7 @@ router.post('/generate-voice', jsonParser, async (request, response) => {
422 const token = readSecret(request.user.directories, SECRET_KEYS.NOVEL);422 const token = readSecret(request.user.directories, SECRET_KEYS.NOVEL);
423423
424 if (!token) {424 if (!token) {
425 console.log('NovelAI Access Token is missing.');425 console.error('NovelAI Access Token is missing.');
426 return response.sendStatus(400);426 return response.sendStatus(400);
427 }427 }
428428
@@ -445,7 +445,7 @@ router.post('/generate-voice', jsonParser, async (request, response) => {
445445
446 if (!result.ok) {446 if (!result.ok) {
447 const errorText = await result.text();447 const errorText = await result.text();
448 console.log('NovelAI returned an error.', result.statusText, errorText);448 console.error('NovelAI returned an error.', result.statusText, errorText);
449 return response.sendStatus(500);449 return response.sendStatus(500);
450 }450 }
451451
src/endpoints/openai.js+16 -16
@@ -63,7 +63,7 @@ router.post('/caption-image', jsonParser, async (request, response) => {
63 }63 }
6464
65 if (!key && !request.body.reverse_proxy && ['custom', 'ooba', 'koboldcpp', 'vllm'].includes(request.body.api) === false) {65 if (!key && !request.body.reverse_proxy && ['custom', 'ooba', 'koboldcpp', 'vllm'].includes(request.body.api) === false) {
66 console.log('No key found for API', request.body.api);66 console.warn('No key found for API', request.body.api);
67 return response.sendStatus(400);67 return response.sendStatus(400);
68 }68 }
6969
@@ -93,7 +93,7 @@ router.post('/caption-image', jsonParser, async (request, response) => {
93 excludeKeysByYaml(body, request.body.custom_exclude_body);93 excludeKeysByYaml(body, request.body.custom_exclude_body);
94 }94 }
9595
96 console.log('Multimodal captioning request', body);96 console.debug('Multimodal captioning request', body);
9797
98 let apiUrl = '';98 let apiUrl = '';
9999
@@ -158,13 +158,13 @@ router.post('/caption-image', jsonParser, async (request, response) => {
158158
159 if (!result.ok) {159 if (!result.ok) {
160 const text = await result.text();160 const text = await result.text();
161 console.log('Multimodal captioning request failed', result.statusText, text);161 console.warn('Multimodal captioning request failed', result.statusText, text);
162 return response.status(500).send(text);162 return response.status(500).send(text);
163 }163 }
164164
165 /** @type {any} */165 /** @type {any} */
166 const data = await result.json();166 const data = await result.json();
167 console.log('Multimodal captioning response', data);167 console.info('Multimodal captioning response', data);
168 const caption = data?.choices[0]?.message?.content;168 const caption = data?.choices[0]?.message?.content;
169169
170 if (!caption) {170 if (!caption) {
@@ -184,17 +184,17 @@ router.post('/transcribe-audio', urlencodedParser, async (request, response) =>
184 const key = readSecret(request.user.directories, SECRET_KEYS.OPENAI);184 const key = readSecret(request.user.directories, SECRET_KEYS.OPENAI);
185185
186 if (!key) {186 if (!key) {
187 console.log('No OpenAI key found');187 console.warn('No OpenAI key found');
188 return response.sendStatus(400);188 return response.sendStatus(400);
189 }189 }
190190
191 if (!request.file) {191 if (!request.file) {
192 console.log('No audio file found');192 console.warn('No audio file found');
193 return response.sendStatus(400);193 return response.sendStatus(400);
194 }194 }
195195
196 const formData = new FormData();196 const formData = new FormData();
197 console.log('Processing audio file', request.file.path);197 console.info('Processing audio file', request.file.path);
198 formData.append('file', fs.createReadStream(request.file.path), { filename: 'audio.wav', contentType: 'audio/wav' });198 formData.append('file', fs.createReadStream(request.file.path), { filename: 'audio.wav', contentType: 'audio/wav' });
199 formData.append('model', request.body.model);199 formData.append('model', request.body.model);
200200
@@ -213,13 +213,13 @@ router.post('/transcribe-audio', urlencodedParser, async (request, response) =>
213213
214 if (!result.ok) {214 if (!result.ok) {
215 const text = await result.text();215 const text = await result.text();
216 console.log('OpenAI request failed', result.statusText, text);216 console.warn('OpenAI request failed', result.statusText, text);
217 return response.status(500).send(text);217 return response.status(500).send(text);
218 }218 }
219219
220 fs.rmSync(request.file.path);220 fs.rmSync(request.file.path);
221 const data = await result.json();221 const data = await result.json();
222 console.log('OpenAI transcription response', data);222 console.debug('OpenAI transcription response', data);
223 return response.json(data);223 return response.json(data);
224 } catch (error) {224 } catch (error) {
225 console.error('OpenAI transcription failed', error);225 console.error('OpenAI transcription failed', error);
@@ -232,7 +232,7 @@ router.post('/generate-voice', jsonParser, async (request, response) => {
232 const key = readSecret(request.user.directories, SECRET_KEYS.OPENAI);232 const key = readSecret(request.user.directories, SECRET_KEYS.OPENAI);
233233
234 if (!key) {234 if (!key) {
235 console.log('No OpenAI key found');235 console.warn('No OpenAI key found');
236 return response.sendStatus(400);236 return response.sendStatus(400);
237 }237 }
238238
@@ -253,7 +253,7 @@ router.post('/generate-voice', jsonParser, async (request, response) => {
253253
254 if (!result.ok) {254 if (!result.ok) {
255 const text = await result.text();255 const text = await result.text();
256 console.log('OpenAI request failed', result.statusText, text);256 console.warn('OpenAI request failed', result.statusText, text);
257 return response.status(500).send(text);257 return response.status(500).send(text);
258 }258 }
259259
@@ -271,11 +271,11 @@ router.post('/generate-image', jsonParser, async (request, response) => {
271 const key = readSecret(request.user.directories, SECRET_KEYS.OPENAI);271 const key = readSecret(request.user.directories, SECRET_KEYS.OPENAI);
272272
273 if (!key) {273 if (!key) {
274 console.log('No OpenAI key found');274 console.warn('No OpenAI key found');
275 return response.sendStatus(400);275 return response.sendStatus(400);
276 }276 }
277277
278 console.log('OpenAI request', request.body);278 console.debug('OpenAI request', request.body);
279279
280 const result = await fetch('https://api.openai.com/v1/images/generations', {280 const result = await fetch('https://api.openai.com/v1/images/generations', {
281 method: 'POST',281 method: 'POST',
@@ -288,7 +288,7 @@ router.post('/generate-image', jsonParser, async (request, response) => {
288288
289 if (!result.ok) {289 if (!result.ok) {
290 const text = await result.text();290 const text = await result.text();
291 console.log('OpenAI request failed', result.statusText, text);291 console.warn('OpenAI request failed', result.statusText, text);
292 return response.status(500).send(text);292 return response.status(500).send(text);
293 }293 }
294294
@@ -308,7 +308,7 @@ custom.post('/generate-voice', jsonParser, async (request, response) => {
308 const { input, provider_endpoint, response_format, voice, speed, model } = request.body;308 const { input, provider_endpoint, response_format, voice, speed, model } = request.body;
309309
310 if (!provider_endpoint) {310 if (!provider_endpoint) {
311 console.log('No OpenAI-compatible TTS provider endpoint provided');311 console.warn('No OpenAI-compatible TTS provider endpoint provided');
312 return response.sendStatus(400);312 return response.sendStatus(400);
313 }313 }
314314
@@ -329,7 +329,7 @@ custom.post('/generate-voice', jsonParser, async (request, response) => {
329329
330 if (!result.ok) {330 if (!result.ok) {
331 const text = await result.text();331 const text = await result.text();
332 console.log('OpenAI request failed', result.statusText, text);332 console.warn('OpenAI request failed', result.statusText, text);
333 return response.status(500).send(text);333 return response.status(500).send(text);
334 }334 }
335335
src/endpoints/openrouter.js+25 -0
@@ -4,6 +4,31 @@ import { jsonParser } from '../express-common.js';
4export const router = express.Router();4export const router = express.Router();
5const API_OPENROUTER = 'https://openrouter.ai/api/v1';5const API_OPENROUTER = 'https://openrouter.ai/api/v1';
66
7router.post('/models/providers', jsonParser, async (req, res) => {
8 try {
9 const { model } = req.body;
10 const response = await fetch(`${API_OPENROUTER}/models/${model}/endpoints`, {
11 method: 'GET',
12 headers: {
13 'Accept': 'application/json',
14 },
15 });
16
17 if (!response.ok) {
18 return res.json([]);
19 }
20
21 const data = await response.json();
22 const endpoints = data?.data?.endpoints || [];
23 const providerNames = endpoints.map(e => e.provider_name);
24
25 return res.json(providerNames);
26 } catch (error) {
27 console.error(error);
28 return res.sendStatus(500);
29 }
30});
31
7router.post('/models/multimodal', jsonParser, async (_req, res) => {32router.post('/models/multimodal', jsonParser, async (_req, res) => {
8 try {33 try {
9 // The endpoint is available without authentication34 // The endpoint is available without authentication
src/endpoints/presets.js+1 -1
@@ -96,7 +96,7 @@ router.post('/restore', jsonParser, function (request, response) {
9696
97 return response.send(result);97 return response.send(result);
98 } catch (error) {98 } catch (error) {
99 console.log(error);99 console.error(error);
100 return response.sendStatus(500);100 return response.sendStatus(500);
101 }101 }
102});102});
src/endpoints/search.js+66 -21
@@ -96,25 +96,26 @@ router.post('/serpapi', jsonParser, async (request, response) => {
96 const key = readSecret(request.user.directories, SECRET_KEYS.SERPAPI);96 const key = readSecret(request.user.directories, SECRET_KEYS.SERPAPI);
9797
98 if (!key) {98 if (!key) {
99 console.log('No SerpApi key found');99 console.error('No SerpApi key found');
100 return response.sendStatus(400);100 return response.sendStatus(400);
101 }101 }
102102
103 const { query } = request.body;103 const { query } = request.body;
104 const result = await fetch(`https://serpapi.com/search.json?q=${encodeURIComponent(query)}&api_key=${key}`);104 const result = await fetch(`https://serpapi.com/search.json?q=${encodeURIComponent(query)}&api_key=${key}`);
105105
106 console.log('SerpApi query', query);106 console.debug('SerpApi query', query);
107107
108 if (!result.ok) {108 if (!result.ok) {
109 const text = await result.text();109 const text = await result.text();
110 console.log('SerpApi request failed', result.statusText, text);110 console.error('SerpApi request failed', result.statusText, text);
111 return response.status(500).send(text);111 return response.status(500).send(text);
112 }112 }
113113
114 const data = await result.json();114 const data = await result.json();
115 console.debug('SerpApi response', data);
115 return response.json(data);116 return response.json(data);
116 } catch (error) {117 } catch (error) {
117 console.log(error);118 console.error(error);
118 return response.sendStatus(500);119 return response.sendStatus(500);
119 }120 }
120});121});
@@ -130,7 +131,7 @@ router.post('/transcript', jsonParser, async (request, response) => {
130 const json = request.body.json;131 const json = request.body.json;
131132
132 if (!id) {133 if (!id) {
133 console.log('Id is required for /transcript');134 console.error('Id is required for /transcript');
134 return response.sendStatus(400);135 return response.sendStatus(400);
135 }136 }
136137
@@ -155,7 +156,7 @@ router.post('/transcript', jsonParser, async (request, response) => {
155 throw error;156 throw error;
156 }157 }
157 } catch (error) {158 } catch (error) {
158 console.log(error);159 console.error(error);
159 return response.sendStatus(500);160 return response.sendStatus(500);
160 }161 }
161});162});
@@ -165,17 +166,17 @@ router.post('/searxng', jsonParser, async (request, response) => {
165 const { baseUrl, query, preferences, categories } = request.body;166 const { baseUrl, query, preferences, categories } = request.body;
166167
167 if (!baseUrl || !query) {168 if (!baseUrl || !query) {
168 console.log('Missing required parameters for /searxng');169 console.error('Missing required parameters for /searxng');
169 return response.sendStatus(400);170 return response.sendStatus(400);
170 }171 }
171172
172 console.log('SearXNG query', baseUrl, query);173 console.debug('SearXNG query', baseUrl, query);
173174
174 const mainPageUrl = new URL(baseUrl);175 const mainPageUrl = new URL(baseUrl);
175 const mainPageRequest = await fetch(mainPageUrl, { headers: visitHeaders });176 const mainPageRequest = await fetch(mainPageUrl, { headers: visitHeaders });
176177
177 if (!mainPageRequest.ok) {178 if (!mainPageRequest.ok) {
178 console.log('SearXNG request failed', mainPageRequest.statusText);179 console.error('SearXNG request failed', mainPageRequest.statusText);
179 return response.sendStatus(500);180 return response.sendStatus(500);
180 }181 }
181182
@@ -202,14 +203,14 @@ router.post('/searxng', jsonParser, async (request, response) => {
202203
203 if (!searchResult.ok) {204 if (!searchResult.ok) {
204 const text = await searchResult.text();205 const text = await searchResult.text();
205 console.log('SearXNG request failed', searchResult.statusText, text);206 console.error('SearXNG request failed', searchResult.statusText, text);
206 return response.sendStatus(500);207 return response.sendStatus(500);
207 }208 }
208209
209 const data = await searchResult.text();210 const data = await searchResult.text();
210 return response.send(data);211 return response.send(data);
211 } catch (error) {212 } catch (error) {
212 console.log('SearXNG request failed', error);213 console.error('SearXNG request failed', error);
213 return response.sendStatus(500);214 return response.sendStatus(500);
214 }215 }
215});216});
@@ -219,7 +220,7 @@ router.post('/tavily', jsonParser, async (request, response) => {
219 const apiKey = readSecret(request.user.directories, SECRET_KEYS.TAVILY);220 const apiKey = readSecret(request.user.directories, SECRET_KEYS.TAVILY);
220221
221 if (!apiKey) {222 if (!apiKey) {
222 console.log('No Tavily key found');223 console.error('No Tavily key found');
223 return response.sendStatus(400);224 return response.sendStatus(400);
224 }225 }
225226
@@ -246,18 +247,19 @@ router.post('/tavily', jsonParser, async (request, response) => {
246 body: JSON.stringify(body),247 body: JSON.stringify(body),
247 });248 });
248249
249 console.log('Tavily query', query);250 console.debug('Tavily query', query);
250251
251 if (!result.ok) {252 if (!result.ok) {
252 const text = await result.text();253 const text = await result.text();
253 console.log('Tavily request failed', result.statusText, text);254 console.error('Tavily request failed', result.statusText, text);
254 return response.status(500).send(text);255 return response.status(500).send(text);
255 }256 }
256257
257 const data = await result.json();258 const data = await result.json();
259 console.debug('Tavily response', data);
258 return response.json(data);260 return response.json(data);
259 } catch (error) {261 } catch (error) {
260 console.log(error);262 console.error(error);
261 return response.sendStatus(500);263 return response.sendStatus(500);
262 }264 }
263});265});
@@ -290,6 +292,49 @@ router.post('/koboldcpp', jsonParser, async (request, response) => {
290 }292 }
291293
292 const data = await result.json();294 const data = await result.json();
295 console.debug('KoboldCpp search response', data);
296 return response.json(data);
297 } catch (error) {
298 console.error(error);
299 return response.sendStatus(500);
300 }
301});
302
303router.post('/serper', jsonParser, async (request, response) => {
304 try {
305 const key = readSecret(request.user.directories, SECRET_KEYS.SERPER);
306
307 if (!key) {
308 console.error('No Serper key found');
309 return response.sendStatus(400);
310 }
311
312 const { query, images } = request.body;
313
314 const url = images
315 ? 'https://google.serper.dev/images'
316 : 'https://google.serper.dev/search';
317
318 const result = await fetch(url, {
319 method: 'POST',
320 headers: {
321 'X-API-KEY': key,
322 'Content-Type': 'application/json',
323 },
324 redirect: 'follow',
325 body: JSON.stringify({ q: query }),
326 });
327
328 console.debug('Serper query', query);
329
330 if (!result.ok) {
331 const text = await result.text();
332 console.warn('Serper request failed', result.statusText, text);
333 return response.status(500).send(text);
334 }
335
336 const data = await result.json();
337 console.debug('Serper response', data);
293 return response.json(data);338 return response.json(data);
294 } catch (error) {339 } catch (error) {
295 console.error(error);340 console.error(error);
@@ -303,7 +348,7 @@ router.post('/visit', jsonParser, async (request, response) => {
303 const html = Boolean(request.body.html ?? true);348 const html = Boolean(request.body.html ?? true);
304349
305 if (!url) {350 if (!url) {
306 console.log('No url provided for /visit');351 console.error('No url provided for /visit');
307 return response.sendStatus(400);352 return response.sendStatus(400);
308 }353 }
309354
@@ -330,16 +375,16 @@ router.post('/visit', jsonParser, async (request, response) => {
330 throw new Error('Invalid hostname');375 throw new Error('Invalid hostname');
331 }376 }
332 } catch (error) {377 } catch (error) {
333 console.log('Invalid url provided for /visit', url);378 console.error('Invalid url provided for /visit', url);
334 return response.sendStatus(400);379 return response.sendStatus(400);
335 }380 }
336381
337 console.log('Visiting web URL', url);382 console.info('Visiting web URL', url);
338383
339 const result = await fetch(url, { headers: visitHeaders });384 const result = await fetch(url, { headers: visitHeaders });
340385
341 if (!result.ok) {386 if (!result.ok) {
342 console.log(`Visit failed ${result.status} ${result.statusText}`);387 console.error(`Visit failed ${result.status} ${result.statusText}`);
343 return response.sendStatus(500);388 return response.sendStatus(500);
344 }389 }
345390
@@ -347,7 +392,7 @@ router.post('/visit', jsonParser, async (request, response) => {
347392
348 if (html) {393 if (html) {
349 if (!contentType.includes('text/html')) {394 if (!contentType.includes('text/html')) {
350 console.log(`Visit failed, content-type is ${contentType}, expected text/html`);395 console.error(`Visit failed, content-type is ${contentType}, expected text/html`);
351 return response.sendStatus(500);396 return response.sendStatus(500);
352 }397 }
353398
@@ -359,7 +404,7 @@ router.post('/visit', jsonParser, async (request, response) => {
359 const buffer = await result.arrayBuffer();404 const buffer = await result.arrayBuffer();
360 return response.send(Buffer.from(buffer));405 return response.send(Buffer.from(buffer));
361 } catch (error) {406 } catch (error) {
362 console.log(error);407 console.error(error);
363 return response.sendStatus(500);408 return response.sendStatus(500);
364 }409 }
365});410});
src/endpoints/secrets.js+3 -1
@@ -50,8 +50,10 @@ export const SECRET_KEYS = {
50 TAVILY: 'api_key_tavily',50 TAVILY: 'api_key_tavily',
51 NANOGPT: 'api_key_nanogpt',51 NANOGPT: 'api_key_nanogpt',
52 BFL: 'api_key_bfl',52 BFL: 'api_key_bfl',
53 FALAI: 'api_key_falai',
53 GENERIC: 'api_key_generic',54 GENERIC: 'api_key_generic',
54 DEEPSEEK: 'api_key_deepseek',55 DEEPSEEK: 'api_key_deepseek',
56 SERPER: 'api_key_serper',
55};57};
5658
57// These are the keys that are safe to expose, even if allowKeysExposure is false59// These are the keys that are safe to expose, even if allowKeysExposure is false
@@ -151,7 +153,7 @@ export function getAllSecrets(directories) {
151 const filePath = path.join(directories.root, SECRETS_FILE);153 const filePath = path.join(directories.root, SECRETS_FILE);
152154
153 if (!fs.existsSync(filePath)) {155 if (!fs.existsSync(filePath)) {
154 console.log('Secrets file does not exist');156 console.error('Secrets file does not exist');
155 return undefined;157 return undefined;
156 }158 }
157159
src/endpoints/settings.js+7 -7
@@ -105,7 +105,7 @@ function readPresetsFromDirectory(directoryPath, options = {}) {
105 fileNames.push(removeFileExtension ? item.replace(/\.[^/.]+$/, '') : item);105 fileNames.push(removeFileExtension ? item.replace(/\.[^/.]+$/, '') : item);
106 } catch {106 } catch {
107 // skip107 // skip
108 console.log(`${item} is not a valid JSON`);108 console.warn(`${item} is not a valid JSON`);
109 }109 }
110 });110 });
111111
@@ -120,7 +120,7 @@ async function backupSettings() {
120 backupUserSettings(handle, true);120 backupUserSettings(handle, true);
121 }121 }
122 } catch (err) {122 } catch (err) {
123 console.log('Could not backup settings file', err);123 console.error('Could not backup settings file', err);
124 }124 }
125}125}
126126
@@ -202,7 +202,7 @@ router.post('/save', jsonParser, function (request, response) {
202 triggerAutoSave(request.user.profile.handle);202 triggerAutoSave(request.user.profile.handle);
203 response.send({ result: 'ok' });203 response.send({ result: 'ok' });
204 } catch (err) {204 } catch (err) {
205 console.log(err);205 console.error(err);
206 response.send(err);206 response.send(err);
207 }207 }
208});208});
@@ -292,7 +292,7 @@ router.post('/get-snapshots', jsonParser, async (request, response) => {
292292
293 response.json(result);293 response.json(result);
294 } catch (error) {294 } catch (error) {
295 console.log(error);295 console.error(error);
296 response.sendStatus(500);296 response.sendStatus(500);
297 }297 }
298});298});
@@ -316,7 +316,7 @@ router.post('/load-snapshot', jsonParser, getFileNameValidationFunction('name'),
316316
317 response.send(content);317 response.send(content);
318 } catch (error) {318 } catch (error) {
319 console.log(error);319 console.error(error);
320 response.sendStatus(500);320 response.sendStatus(500);
321 }321 }
322});322});
@@ -326,7 +326,7 @@ router.post('/make-snapshot', jsonParser, async (request, response) => {
326 backupUserSettings(request.user.profile.handle, false);326 backupUserSettings(request.user.profile.handle, false);
327 response.sendStatus(204);327 response.sendStatus(204);
328 } catch (error) {328 } catch (error) {
329 console.log(error);329 console.error(error);
330 response.sendStatus(500);330 response.sendStatus(500);
331 }331 }
332});332});
@@ -352,7 +352,7 @@ router.post('/restore-snapshot', jsonParser, getFileNameValidationFunction('name
352352
353 response.sendStatus(204);353 response.sendStatus(204);
354 } catch (error) {354 } catch (error) {
355 console.log(error);355 console.error(error);
356 response.sendStatus(500);356 response.sendStatus(500);
357 }357 }
358});358});
src/endpoints/speech.js+3 -3
@@ -43,8 +43,8 @@ router.post('/recognize', jsonParser, async (req, res) => {
43 const start = performance.now();43 const start = performance.now();
44 const result = await pipe(wav, { language: lang || null, task: 'transcribe' });44 const result = await pipe(wav, { language: lang || null, task: 'transcribe' });
45 const end = performance.now();45 const end = performance.now();
46 console.log(`Execution duration: ${(end - start) / 1000} seconds`);46 console.info(`Execution duration: ${(end - start) / 1000} seconds`);
47 console.log('Transcribed audio:', result.text);47 console.info('Transcribed audio:', result.text);
4848
49 return res.json({ text: result.text });49 return res.json({ text: result.text });
50 } catch (error) {50 } catch (error) {
@@ -64,7 +64,7 @@ router.post('/synthesize', jsonParser, async (req, res) => {
64 const start = performance.now();64 const start = performance.now();
65 const result = await pipe(text, { speaker_embeddings: speaker_embeddings });65 const result = await pipe(text, { speaker_embeddings: speaker_embeddings });
66 const end = performance.now();66 const end = performance.now();
67 console.log(`Execution duration: ${(end - start) / 1000} seconds`);67 console.debug(`Execution duration: ${(end - start) / 1000} seconds`);
6868
69 const wav = new wavefile.WaveFile();69 const wav = new wavefile.WaveFile();
70 wav.fromScratch(1, result.sampling_rate, '32f', result.audio);70 wav.fromScratch(1, result.sampling_rate, '32f', result.audio);
src/endpoints/sprites.js+3 -3
@@ -82,14 +82,14 @@ export function importRisuSprites(directories, data) {
82 return;82 return;
83 }83 }
8484
85 console.log(`RisuAI: Found ${images.length} sprites for ${name}. Writing to disk.`);85 console.info(`RisuAI: Found ${images.length} sprites for ${name}. Writing to disk.`);
86 const files = fs.readdirSync(spritesPath);86 const files = fs.readdirSync(spritesPath);
8787
88 outer: for (const [label, fileBase64] of images) {88 outer: for (const [label, fileBase64] of images) {
89 // Remove existing sprite with the same label89 // Remove existing sprite with the same label
90 for (const file of files) {90 for (const file of files) {
91 if (path.parse(file).name === label) {91 if (path.parse(file).name === label) {
92 console.log(`RisuAI: The sprite ${label} for ${name} already exists. Skipping.`);92 console.warn(`RisuAI: The sprite ${label} for ${name} already exists. Skipping.`);
93 continue outer;93 continue outer;
94 }94 }
95 }95 }
@@ -139,7 +139,7 @@ router.get('/get', jsonParser, function (request, response) {
139 }139 }
140 }140 }
141 catch (err) {141 catch (err) {
142 console.log(err);142 console.error(err);
143 }143 }
144 return response.send(sprites);144 return response.send(sprites);
145});145});
src/endpoints/stable-diffusion.js+201 -74
@@ -45,7 +45,7 @@ router.post('/ping', jsonParser, async (request, response) => {
4545
46 return response.sendStatus(200);46 return response.sendStatus(200);
47 } catch (error) {47 } catch (error) {
48 console.log(error);48 console.error(error);
49 return response.sendStatus(500);49 return response.sendStatus(500);
50 }50 }
51});51});
@@ -99,7 +99,7 @@ router.post('/upscalers', jsonParser, async (request, response) => {
9999
100 return response.send(upscalers);100 return response.send(upscalers);
101 } catch (error) {101 } catch (error) {
102 console.log(error);102 console.error(error);
103 return response.sendStatus(500);103 return response.sendStatus(500);
104 }104 }
105});105});
@@ -131,7 +131,7 @@ router.post('/vaes', jsonParser, async (request, response) => {
131 const names = data.map(x => x.model_name);131 const names = data.map(x => x.model_name);
132 return response.send(names);132 return response.send(names);
133 } catch (error) {133 } catch (error) {
134 console.log(error);134 console.error(error);
135 return response.sendStatus(500);135 return response.sendStatus(500);
136 }136 }
137});137});
@@ -158,7 +158,7 @@ router.post('/samplers', jsonParser, async (request, response) => {
158 return response.send(names);158 return response.send(names);
159159
160 } catch (error) {160 } catch (error) {
161 console.log(error);161 console.error(error);
162 return response.sendStatus(500);162 return response.sendStatus(500);
163 }163 }
164});164});
@@ -184,7 +184,7 @@ router.post('/schedulers', jsonParser, async (request, response) => {
184 const names = data.map(x => x.name);184 const names = data.map(x => x.name);
185 return response.send(names);185 return response.send(names);
186 } catch (error) {186 } catch (error) {
187 console.log(error);187 console.error(error);
188 return response.sendStatus(500);188 return response.sendStatus(500);
189 }189 }
190});190});
@@ -210,7 +210,7 @@ router.post('/models', jsonParser, async (request, response) => {
210 const models = data.map(x => ({ value: x.title, text: x.title }));210 const models = data.map(x => ({ value: x.title, text: x.title }));
211 return response.send(models);211 return response.send(models);
212 } catch (error) {212 } catch (error) {
213 console.log(error);213 console.error(error);
214 return response.sendStatus(500);214 return response.sendStatus(500);
215 }215 }
216});216});
@@ -230,7 +230,7 @@ router.post('/get-model', jsonParser, async (request, response) => {
230 const data = await result.json();230 const data = await result.json();
231 return response.send(data['sd_model_checkpoint']);231 return response.send(data['sd_model_checkpoint']);
232 } catch (error) {232 } catch (error) {
233 console.log(error);233 console.error(error);
234 return response.sendStatus(500);234 return response.sendStatus(500);
235 }235 }
236});236});
@@ -283,13 +283,13 @@ router.post('/set-model', jsonParser, async (request, response) => {
283 break;283 break;
284 }284 }
285285
286 console.log(`Waiting for SD WebUI to finish model loading... Progress: ${progress}; Job count: ${jobCount}`);286 console.info(`Waiting for SD WebUI to finish model loading... Progress: ${progress}; Job count: ${jobCount}`);
287 await delay(CHECK_INTERVAL);287 await delay(CHECK_INTERVAL);
288 }288 }
289289
290 return response.sendStatus(200);290 return response.sendStatus(200);
291 } catch (error) {291 } catch (error) {
292 console.log(error);292 console.error(error);
293 return response.sendStatus(500);293 return response.sendStatus(500);
294 }294 }
295});295});
@@ -309,7 +309,7 @@ router.post('/generate', jsonParser, async (request, response) => {
309 }309 }
310 }310 }
311 } catch (error) {311 } catch (error) {
312 console.log('SD WebUI failed to get options:', error);312 console.error('SD WebUI failed to get options:', error);
313 }313 }
314314
315 const controller = new AbortController();315 const controller = new AbortController();
@@ -323,7 +323,7 @@ router.post('/generate', jsonParser, async (request, response) => {
323 controller.abort();323 controller.abort();
324 });324 });
325325
326 console.log('SD WebUI request:', request.body);326 console.debug('SD WebUI request:', request.body);
327 const txt2imgUrl = new URL(request.body.url);327 const txt2imgUrl = new URL(request.body.url);
328 txt2imgUrl.pathname = '/sdapi/v1/txt2img';328 txt2imgUrl.pathname = '/sdapi/v1/txt2img';
329 const result = await fetch(txt2imgUrl, {329 const result = await fetch(txt2imgUrl, {
@@ -344,7 +344,7 @@ router.post('/generate', jsonParser, async (request, response) => {
344 const data = await result.json();344 const data = await result.json();
345 return response.send(data);345 return response.send(data);
346 } catch (error) {346 } catch (error) {
347 console.log(error);347 console.error(error);
348 return response.sendStatus(500);348 return response.sendStatus(500);
349 }349 }
350});350});
@@ -377,7 +377,7 @@ router.post('/sd-next/upscalers', jsonParser, async (request, response) => {
377377
378 return response.send(names);378 return response.send(names);
379 } catch (error) {379 } catch (error) {
380 console.log(error);380 console.error(error);
381 return response.sendStatus(500);381 return response.sendStatus(500);
382 }382 }
383});383});
@@ -395,7 +395,7 @@ comfy.post('/ping', jsonParser, async (request, response) => {
395395
396 return response.sendStatus(200);396 return response.sendStatus(200);
397 } catch (error) {397 } catch (error) {
398 console.log(error);398 console.error(error);
399 return response.sendStatus(500);399 return response.sendStatus(500);
400 }400 }
401});401});
@@ -413,7 +413,7 @@ comfy.post('/samplers', jsonParser, async (request, response) => {
413 const data = await result.json();413 const data = await result.json();
414 return response.send(data.KSampler.input.required.sampler_name[0]);414 return response.send(data.KSampler.input.required.sampler_name[0]);
415 } catch (error) {415 } catch (error) {
416 console.log(error);416 console.error(error);
417 return response.sendStatus(500);417 return response.sendStatus(500);
418 }418 }
419});419});
@@ -441,7 +441,7 @@ comfy.post('/models', jsonParser, async (request, response) => {
441441
442 return response.send(models);442 return response.send(models);
443 } catch (error) {443 } catch (error) {
444 console.log(error);444 console.error(error);
445 return response.sendStatus(500);445 return response.sendStatus(500);
446 }446 }
447});447});
@@ -459,7 +459,7 @@ comfy.post('/schedulers', jsonParser, async (request, response) => {
459 const data = await result.json();459 const data = await result.json();
460 return response.send(data.KSampler.input.required.scheduler[0]);460 return response.send(data.KSampler.input.required.scheduler[0]);
461 } catch (error) {461 } catch (error) {
462 console.log(error);462 console.error(error);
463 return response.sendStatus(500);463 return response.sendStatus(500);
464 }464 }
465});465});
@@ -477,7 +477,7 @@ comfy.post('/vaes', jsonParser, async (request, response) => {
477 const data = await result.json();477 const data = await result.json();
478 return response.send(data.VAELoader.input.required.vae_name[0]);478 return response.send(data.VAELoader.input.required.vae_name[0]);
479 } catch (error) {479 } catch (error) {
480 console.log(error);480 console.error(error);
481 return response.sendStatus(500);481 return response.sendStatus(500);
482 }482 }
483});483});
@@ -487,7 +487,7 @@ comfy.post('/workflows', jsonParser, async (request, response) => {
487 const data = getComfyWorkflows(request.user.directories);487 const data = getComfyWorkflows(request.user.directories);
488 return response.send(data);488 return response.send(data);
489 } catch (error) {489 } catch (error) {
490 console.log(error);490 console.error(error);
491 return response.sendStatus(500);491 return response.sendStatus(500);
492 }492 }
493});493});
@@ -501,7 +501,7 @@ comfy.post('/workflow', jsonParser, async (request, response) => {
501 const data = fs.readFileSync(filePath, { encoding: 'utf-8' });501 const data = fs.readFileSync(filePath, { encoding: 'utf-8' });
502 return response.send(JSON.stringify(data));502 return response.send(JSON.stringify(data));
503 } catch (error) {503 } catch (error) {
504 console.log(error);504 console.error(error);
505 return response.sendStatus(500);505 return response.sendStatus(500);
506 }506 }
507});507});
@@ -513,7 +513,7 @@ comfy.post('/save-workflow', jsonParser, async (request, response) => {
513 const data = getComfyWorkflows(request.user.directories);513 const data = getComfyWorkflows(request.user.directories);
514 return response.send(data);514 return response.send(data);
515 } catch (error) {515 } catch (error) {
516 console.log(error);516 console.error(error);
517 return response.sendStatus(500);517 return response.sendStatus(500);
518 }518 }
519});519});
@@ -526,7 +526,7 @@ comfy.post('/delete-workflow', jsonParser, async (request, response) => {
526 }526 }
527 return response.sendStatus(200);527 return response.sendStatus(200);
528 } catch (error) {528 } catch (error) {
529 console.log(error);529 console.error(error);
530 return response.sendStatus(500);530 return response.sendStatus(500);
531 }531 }
532});532});
@@ -591,7 +591,7 @@ comfy.post('/generate', jsonParser, async (request, response) => {
591 const imgBuffer = await imgResponse.arrayBuffer();591 const imgBuffer = await imgResponse.arrayBuffer();
592 return response.send(Buffer.from(imgBuffer).toString('base64'));592 return response.send(Buffer.from(imgBuffer).toString('base64'));
593 } catch (error) {593 } catch (error) {
594 console.log('ComfyUI error:', error);594 console.error('ComfyUI error:', error);
595 response.status(500).send(error.message);595 response.status(500).send(error.message);
596 return response;596 return response;
597 }597 }
@@ -604,7 +604,7 @@ together.post('/models', jsonParser, async (request, response) => {
604 const key = readSecret(request.user.directories, SECRET_KEYS.TOGETHERAI);604 const key = readSecret(request.user.directories, SECRET_KEYS.TOGETHERAI);
605605
606 if (!key) {606 if (!key) {
607 console.log('TogetherAI key not found.');607 console.warn('TogetherAI key not found.');
608 return response.sendStatus(400);608 return response.sendStatus(400);
609 }609 }
610610
@@ -616,14 +616,14 @@ together.post('/models', jsonParser, async (request, response) => {
616 });616 });
617617
618 if (!modelsResponse.ok) {618 if (!modelsResponse.ok) {
619 console.log('TogetherAI returned an error.');619 console.warn('TogetherAI returned an error.');
620 return response.sendStatus(500);620 return response.sendStatus(500);
621 }621 }
622622
623 const data = await modelsResponse.json();623 const data = await modelsResponse.json();
624624
625 if (!Array.isArray(data)) {625 if (!Array.isArray(data)) {
626 console.log('TogetherAI returned invalid data.');626 console.warn('TogetherAI returned invalid data.');
627 return response.sendStatus(500);627 return response.sendStatus(500);
628 }628 }
629629
@@ -633,7 +633,7 @@ together.post('/models', jsonParser, async (request, response) => {
633633
634 return response.send(models);634 return response.send(models);
635 } catch (error) {635 } catch (error) {
636 console.log(error);636 console.error(error);
637 return response.sendStatus(500);637 return response.sendStatus(500);
638 }638 }
639});639});
@@ -643,11 +643,11 @@ together.post('/generate', jsonParser, async (request, response) => {
643 const key = readSecret(request.user.directories, SECRET_KEYS.TOGETHERAI);643 const key = readSecret(request.user.directories, SECRET_KEYS.TOGETHERAI);
644644
645 if (!key) {645 if (!key) {
646 console.log('TogetherAI key not found.');646 console.warn('TogetherAI key not found.');
647 return response.sendStatus(400);647 return response.sendStatus(400);
648 }648 }
649649
650 console.log('TogetherAI request:', request.body);650 console.debug('TogetherAI request:', request.body);
651651
652 const result = await fetch('https://api.together.xyz/v1/images/generations', {652 const result = await fetch('https://api.together.xyz/v1/images/generations', {
653 method: 'POST',653 method: 'POST',
@@ -669,13 +669,13 @@ together.post('/generate', jsonParser, async (request, response) => {
669 });669 });
670670
671 if (!result.ok) {671 if (!result.ok) {
672 console.log('TogetherAI returned an error.', { body: await result.text() });672 console.warn('TogetherAI returned an error.', { body: await result.text() });
673 return response.sendStatus(500);673 return response.sendStatus(500);
674 }674 }
675675
676 /** @type {any} */676 /** @type {any} */
677 const data = await result.json();677 const data = await result.json();
678 console.log('TogetherAI response:', data);678 console.debug('TogetherAI response:', data);
679679
680 const choice = data?.data?.[0];680 const choice = data?.data?.[0];
681 let b64_json = choice.b64_json;681 let b64_json = choice.b64_json;
@@ -687,7 +687,7 @@ together.post('/generate', jsonParser, async (request, response) => {
687687
688 return response.send({ format: 'jpg', data: b64_json });688 return response.send({ format: 'jpg', data: b64_json });
689 } catch (error) {689 } catch (error) {
690 console.log(error);690 console.error(error);
691 return response.sendStatus(500);691 return response.sendStatus(500);
692 }692 }
693});693});
@@ -709,7 +709,7 @@ drawthings.post('/ping', jsonParser, async (request, response) => {
709709
710 return response.sendStatus(200);710 return response.sendStatus(200);
711 } catch (error) {711 } catch (error) {
712 console.log(error);712 console.error(error);
713 return response.sendStatus(500);713 return response.sendStatus(500);
714 }714 }
715});715});
@@ -728,7 +728,7 @@ drawthings.post('/get-model', jsonParser, async (request, response) => {
728728
729 return response.send(data['model']);729 return response.send(data['model']);
730 } catch (error) {730 } catch (error) {
731 console.log(error);731 console.error(error);
732 return response.sendStatus(500);732 return response.sendStatus(500);
733 }733 }
734});734});
@@ -747,14 +747,14 @@ drawthings.post('/get-upscaler', jsonParser, async (request, response) => {
747747
748 return response.send(data['upscaler']);748 return response.send(data['upscaler']);
749 } catch (error) {749 } catch (error) {
750 console.log(error);750 console.error(error);
751 return response.sendStatus(500);751 return response.sendStatus(500);
752 }752 }
753});753});
754754
755drawthings.post('/generate', jsonParser, async (request, response) => {755drawthings.post('/generate', jsonParser, async (request, response) => {
756 try {756 try {
757 console.log('SD DrawThings API request:', request.body);757 console.debug('SD DrawThings API request:', request.body);
758758
759 const url = new URL(request.body.url);759 const url = new URL(request.body.url);
760 url.pathname = '/sdapi/v1/txt2img';760 url.pathname = '/sdapi/v1/txt2img';
@@ -781,7 +781,7 @@ drawthings.post('/generate', jsonParser, async (request, response) => {
781 const data = await result.json();781 const data = await result.json();
782 return response.send(data);782 return response.send(data);
783 } catch (error) {783 } catch (error) {
784 console.log(error);784 console.error(error);
785 return response.sendStatus(500);785 return response.sendStatus(500);
786 }786 }
787});787});
@@ -794,21 +794,21 @@ pollinations.post('/models', jsonParser, async (_request, response) => {
794 const result = await fetch(modelsUrl);794 const result = await fetch(modelsUrl);
795795
796 if (!result.ok) {796 if (!result.ok) {
797 console.log('Pollinations returned an error.', result.status, result.statusText);797 console.warn('Pollinations returned an error.', result.status, result.statusText);
798 throw new Error('Pollinations request failed.');798 throw new Error('Pollinations request failed.');
799 }799 }
800800
801 const data = await result.json();801 const data = await result.json();
802802
803 if (!Array.isArray(data)) {803 if (!Array.isArray(data)) {
804 console.log('Pollinations returned invalid data.');804 console.warn('Pollinations returned invalid data.');
805 throw new Error('Pollinations request failed.');805 throw new Error('Pollinations request failed.');
806 }806 }
807807
808 const models = data.map(x => ({ value: x, text: x }));808 const models = data.map(x => ({ value: x, text: x }));
809 return response.send(models);809 return response.send(models);
810 } catch (error) {810 } catch (error) {
811 console.log(error);811 console.error(error);
812 return response.sendStatus(500);812 return response.sendStatus(500);
813 }813 }
814});814});
@@ -829,12 +829,12 @@ pollinations.post('/generate', jsonParser, async (request, response) => {
829 });829 });
830 promptUrl.search = params.toString();830 promptUrl.search = params.toString();
831831
832 console.log('Pollinations request URL:', promptUrl.toString());832 console.info('Pollinations request URL:', promptUrl.toString());
833833
834 const result = await fetch(promptUrl);834 const result = await fetch(promptUrl);
835835
836 if (!result.ok) {836 if (!result.ok) {
837 console.log('Pollinations returned an error.', result.status, result.statusText);837 console.warn('Pollinations returned an error.', result.status, result.statusText);
838 throw new Error('Pollinations request failed.');838 throw new Error('Pollinations request failed.');
839 }839 }
840840
@@ -843,7 +843,7 @@ pollinations.post('/generate', jsonParser, async (request, response) => {
843843
844 return response.send({ image: base64 });844 return response.send({ image: base64 });
845 } catch (error) {845 } catch (error) {
846 console.log(error);846 console.error(error);
847 return response.sendStatus(500);847 return response.sendStatus(500);
848 }848 }
849});849});
@@ -855,13 +855,13 @@ stability.post('/generate', jsonParser, async (request, response) => {
855 const key = readSecret(request.user.directories, SECRET_KEYS.STABILITY);855 const key = readSecret(request.user.directories, SECRET_KEYS.STABILITY);
856856
857 if (!key) {857 if (!key) {
858 console.log('Stability AI key not found.');858 console.warn('Stability AI key not found.');
859 return response.sendStatus(400);859 return response.sendStatus(400);
860 }860 }
861861
862 const { payload, model } = request.body;862 const { payload, model } = request.body;
863863
864 console.log('Stability AI request:', model, payload);864 console.debug('Stability AI request:', model, payload);
865865
866 const formData = new FormData();866 const formData = new FormData();
867 for (const [key, value] of Object.entries(payload)) {867 for (const [key, value] of Object.entries(payload)) {
@@ -896,14 +896,14 @@ stability.post('/generate', jsonParser, async (request, response) => {
896896
897 if (!result.ok) {897 if (!result.ok) {
898 const text = await result.text();898 const text = await result.text();
899 console.log('Stability AI returned an error.', result.status, result.statusText, text);899 console.warn('Stability AI returned an error.', result.status, result.statusText, text);
900 return response.sendStatus(500);900 return response.sendStatus(500);
901 }901 }
902902
903 const buffer = await result.arrayBuffer();903 const buffer = await result.arrayBuffer();
904 return response.send(Buffer.from(buffer).toString('base64'));904 return response.send(Buffer.from(buffer).toString('base64'));
905 } catch (error) {905 } catch (error) {
906 console.log(error);906 console.error(error);
907 return response.sendStatus(500);907 return response.sendStatus(500);
908 }908 }
909});909});
@@ -915,7 +915,7 @@ blockentropy.post('/models', jsonParser, async (request, response) => {
915 const key = readSecret(request.user.directories, SECRET_KEYS.BLOCKENTROPY);915 const key = readSecret(request.user.directories, SECRET_KEYS.BLOCKENTROPY);
916916
917 if (!key) {917 if (!key) {
918 console.log('Block Entropy key not found.');918 console.warn('Block Entropy key not found.');
919 return response.sendStatus(400);919 return response.sendStatus(400);
920 }920 }
921921
@@ -927,21 +927,21 @@ blockentropy.post('/models', jsonParser, async (request, response) => {
927 });927 });
928928
929 if (!modelsResponse.ok) {929 if (!modelsResponse.ok) {
930 console.log('Block Entropy returned an error.');930 console.warn('Block Entropy returned an error.');
931 return response.sendStatus(500);931 return response.sendStatus(500);
932 }932 }
933933
934 const data = await modelsResponse.json();934 const data = await modelsResponse.json();
935935
936 if (!Array.isArray(data)) {936 if (!Array.isArray(data)) {
937 console.log('Block Entropy returned invalid data.');937 console.warn('Block Entropy returned invalid data.');
938 return response.sendStatus(500);938 return response.sendStatus(500);
939 }939 }
940 const models = data.map(x => ({ value: x.name, text: x.name }));940 const models = data.map(x => ({ value: x.name, text: x.name }));
941 return response.send(models);941 return response.send(models);
942942
943 } catch (error) {943 } catch (error) {
944 console.log(error);944 console.error(error);
945 return response.sendStatus(500);945 return response.sendStatus(500);
946 }946 }
947});947});
@@ -951,11 +951,11 @@ blockentropy.post('/generate', jsonParser, async (request, response) => {
951 const key = readSecret(request.user.directories, SECRET_KEYS.BLOCKENTROPY);951 const key = readSecret(request.user.directories, SECRET_KEYS.BLOCKENTROPY);
952952
953 if (!key) {953 if (!key) {
954 console.log('Block Entropy key not found.');954 console.warn('Block Entropy key not found.');
955 return response.sendStatus(400);955 return response.sendStatus(400);
956 }956 }
957957
958 console.log('Block Entropy request:', request.body);958 console.debug('Block Entropy request:', request.body);
959959
960 const result = await fetch('https://api.blockentropy.ai/sdapi/v1/txt2img', {960 const result = await fetch('https://api.blockentropy.ai/sdapi/v1/txt2img', {
961 method: 'POST',961 method: 'POST',
@@ -976,16 +976,16 @@ blockentropy.post('/generate', jsonParser, async (request, response) => {
976 });976 });
977977
978 if (!result.ok) {978 if (!result.ok) {
979 console.log('Block Entropy returned an error.');979 console.warn('Block Entropy returned an error.');
980 return response.sendStatus(500);980 return response.sendStatus(500);
981 }981 }
982982
983 const data = await result.json();983 const data = await result.json();
984 console.log('Block Entropy response:', data);984 console.debug('Block Entropy response:', data);
985985
986 return response.send(data);986 return response.send(data);
987 } catch (error) {987 } catch (error) {
988 console.log(error);988 console.error(error);
989 return response.sendStatus(500);989 return response.sendStatus(500);
990 }990 }
991});991});
@@ -998,11 +998,11 @@ huggingface.post('/generate', jsonParser, async (request, response) => {
998 const key = readSecret(request.user.directories, SECRET_KEYS.HUGGINGFACE);998 const key = readSecret(request.user.directories, SECRET_KEYS.HUGGINGFACE);
999999
1000 if (!key) {1000 if (!key) {
1001 console.log('Hugging Face key not found.');1001 console.warn('Hugging Face key not found.');
1002 return response.sendStatus(400);1002 return response.sendStatus(400);
1003 }1003 }
10041004
1005 console.log('Hugging Face request:', request.body);1005 console.debug('Hugging Face request:', request.body);
10061006
1007 const result = await fetch(`https://api-inference.huggingface.co/models/${request.body.model}`, {1007 const result = await fetch(`https://api-inference.huggingface.co/models/${request.body.model}`, {
1008 method: 'POST',1008 method: 'POST',
@@ -1016,7 +1016,7 @@ huggingface.post('/generate', jsonParser, async (request, response) => {
1016 });1016 });
10171017
1018 if (!result.ok) {1018 if (!result.ok) {
1019 console.log('Hugging Face returned an error.');1019 console.warn('Hugging Face returned an error.');
1020 return response.sendStatus(500);1020 return response.sendStatus(500);
1021 }1021 }
10221022
@@ -1025,7 +1025,7 @@ huggingface.post('/generate', jsonParser, async (request, response) => {
1025 image: Buffer.from(buffer).toString('base64'),1025 image: Buffer.from(buffer).toString('base64'),
1026 });1026 });
1027 } catch (error) {1027 } catch (error) {
1028 console.log(error);1028 console.error(error);
1029 return response.sendStatus(500);1029 return response.sendStatus(500);
1030 }1030 }
1031});1031});
@@ -1037,7 +1037,7 @@ nanogpt.post('/models', jsonParser, async (request, response) => {
1037 const key = readSecret(request.user.directories, SECRET_KEYS.NANOGPT);1037 const key = readSecret(request.user.directories, SECRET_KEYS.NANOGPT);
10381038
1039 if (!key) {1039 if (!key) {
1040 console.log('NanoGPT key not found.');1040 console.warn('NanoGPT key not found.');
1041 return response.sendStatus(400);1041 return response.sendStatus(400);
1042 }1042 }
10431043
@@ -1050,7 +1050,7 @@ nanogpt.post('/models', jsonParser, async (request, response) => {
1050 });1050 });
10511051
1052 if (!modelsResponse.ok) {1052 if (!modelsResponse.ok) {
1053 console.log('NanoGPT returned an error.');1053 console.warn('NanoGPT returned an error.');
1054 return response.sendStatus(500);1054 return response.sendStatus(500);
1055 }1055 }
10561056
@@ -1059,7 +1059,7 @@ nanogpt.post('/models', jsonParser, async (request, response) => {
1059 const imageModels = data?.models?.image;1059 const imageModels = data?.models?.image;
10601060
1061 if (!imageModels || typeof imageModels !== 'object') {1061 if (!imageModels || typeof imageModels !== 'object') {
1062 console.log('NanoGPT returned invalid data.');1062 console.warn('NanoGPT returned invalid data.');
1063 return response.sendStatus(500);1063 return response.sendStatus(500);
1064 }1064 }
10651065
@@ -1067,7 +1067,7 @@ nanogpt.post('/models', jsonParser, async (request, response) => {
1067 return response.send(models);1067 return response.send(models);
1068 }1068 }
1069 catch (error) {1069 catch (error) {
1070 console.log(error);1070 console.error(error);
1071 return response.sendStatus(500);1071 return response.sendStatus(500);
1072 }1072 }
1073});1073});
@@ -1077,11 +1077,11 @@ nanogpt.post('/generate', jsonParser, async (request, response) => {
1077 const key = readSecret(request.user.directories, SECRET_KEYS.NANOGPT);1077 const key = readSecret(request.user.directories, SECRET_KEYS.NANOGPT);
10781078
1079 if (!key) {1079 if (!key) {
1080 console.log('NanoGPT key not found.');1080 console.warn('NanoGPT key not found.');
1081 return response.sendStatus(400);1081 return response.sendStatus(400);
1082 }1082 }
10831083
1084 console.log('NanoGPT request:', request.body);1084 console.debug('NanoGPT request:', request.body);
10851085
1086 const result = await fetch('https://nano-gpt.com/api/generate-image', {1086 const result = await fetch('https://nano-gpt.com/api/generate-image', {
1087 method: 'POST',1087 method: 'POST',
@@ -1093,7 +1093,7 @@ nanogpt.post('/generate', jsonParser, async (request, response) => {
1093 });1093 });
10941094
1095 if (!result.ok) {1095 if (!result.ok) {
1096 console.log('NanoGPT returned an error.');1096 console.warn('NanoGPT returned an error.');
1097 return response.sendStatus(500);1097 return response.sendStatus(500);
1098 }1098 }
10991099
@@ -1102,14 +1102,14 @@ nanogpt.post('/generate', jsonParser, async (request, response) => {
11021102
1103 const image = data?.data?.[0]?.b64_json;1103 const image = data?.data?.[0]?.b64_json;
1104 if (!image) {1104 if (!image) {
1105 console.log('NanoGPT returned invalid data.');1105 console.warn('NanoGPT returned invalid data.');
1106 return response.sendStatus(500);1106 return response.sendStatus(500);
1107 }1107 }
11081108
1109 return response.send({ image });1109 return response.send({ image });
1110 }1110 }
1111 catch (error) {1111 catch (error) {
1112 console.log(error);1112 console.error(error);
1113 return response.sendStatus(500);1113 return response.sendStatus(500);
1114 }1114 }
1115});1115});
@@ -1121,7 +1121,7 @@ bfl.post('/generate', jsonParser, async (request, response) => {
1121 const key = readSecret(request.user.directories, SECRET_KEYS.BFL);1121 const key = readSecret(request.user.directories, SECRET_KEYS.BFL);
11221122
1123 if (!key) {1123 if (!key) {
1124 console.log('BFL key not found.');1124 console.warn('BFL key not found.');
1125 return response.sendStatus(400);1125 return response.sendStatus(400);
1126 }1126 }
11271127
@@ -1173,7 +1173,7 @@ bfl.post('/generate', jsonParser, async (request, response) => {
1173 delete requestBody.guidance;1173 delete requestBody.guidance;
1174 }1174 }
11751175
1176 console.log('BFL request:', requestBody);1176 console.debug('BFL request:', requestBody);
11771177
1178 const result = await fetch(`https://api.bfl.ml/v1/${request.body.model}`, {1178 const result = await fetch(`https://api.bfl.ml/v1/${request.body.model}`, {
1179 method: 'POST',1179 method: 'POST',
@@ -1185,7 +1185,7 @@ bfl.post('/generate', jsonParser, async (request, response) => {
1185 });1185 });
11861186
1187 if (!result.ok) {1187 if (!result.ok) {
1188 console.log('BFL returned an error.');1188 console.warn('BFL returned an error.');
1189 return response.sendStatus(500);1189 return response.sendStatus(500);
1190 }1190 }
11911191
@@ -1201,7 +1201,7 @@ bfl.post('/generate', jsonParser, async (request, response) => {
12011201
1202 if (!statusResult.ok) {1202 if (!statusResult.ok) {
1203 const text = await statusResult.text();1203 const text = await statusResult.text();
1204 console.log('BFL returned an error.', text);1204 console.warn('BFL returned an error.', text);
1205 return response.sendStatus(500);1205 return response.sendStatus(500);
1206 }1206 }
12071207
@@ -1223,11 +1223,137 @@ bfl.post('/generate', jsonParser, async (request, response) => {
1223 throw new Error('BFL failed to generate image.', { cause: statusData });1223 throw new Error('BFL failed to generate image.', { cause: statusData });
1224 }1224 }
1225 } catch (error) {1225 } catch (error) {
1226 console.log(error);1226 console.error(error);
1227 return response.sendStatus(500);1227 return response.sendStatus(500);
1228 }1228 }
1229});1229});
12301230
1231const falai = express.Router();
1232
1233falai.post('/models', jsonParser, async (_request, response) => {
1234 try {
1235 const modelsUrl = new URL('https://fal.ai/api/models?categories=text-to-image');
1236 const result = await fetch(modelsUrl);
1237
1238 if (!result.ok) {
1239 console.warn('FAL.AI returned an error.', result.status, result.statusText);
1240 throw new Error('FAL.AI request failed.');
1241 }
1242
1243 const data = await result.json();
1244
1245 if (!Array.isArray(data)) {
1246 console.warn('FAL.AI returned invalid data.');
1247 throw new Error('FAL.AI request failed.');
1248 }
1249
1250 const models = data
1251 .filter(x => !x.title.toLowerCase().includes('inpainting') &&
1252 !x.title.toLowerCase().includes('control') &&
1253 !x.title.toLowerCase().includes('upscale'))
1254 .sort((a, b) => a.title.localeCompare(b.title))
1255 .map(x => ({ value: x.modelUrl.split('fal-ai/')[1], text: x.title }));
1256 return response.send(models);
1257 } catch (error) {
1258 console.error(error);
1259 return response.sendStatus(500);
1260 }
1261});
1262
1263falai.post('/generate', jsonParser, async (request, response) => {
1264 try {
1265 const key = readSecret(request.user.directories, SECRET_KEYS.FALAI);
1266
1267 if (!key) {
1268 console.warn('FAL.AI key not found.');
1269 return response.sendStatus(400);
1270 }
1271
1272 const requestBody = {
1273 prompt: request.body.prompt,
1274 image_size: { 'width': request.body.width, 'height': request.body.height },
1275 num_inference_steps: request.body.steps,
1276 seed: request.body.seed ?? null,
1277 guidance_scale: request.body.guidance,
1278 enable_safety_checker: false, // Disable general safety checks
1279 safety_tolerance: 6 // Make Flux the least strict
1280 };
1281
1282 console.debug('FAL.AI request:', requestBody);
1283
1284 const result = await fetch(`https://queue.fal.run/fal-ai/${request.body.model}`, {
1285 method: 'POST',
1286 body: JSON.stringify(requestBody),
1287 headers: {
1288 'Content-Type': 'application/json',
1289 'Authorization': `Key ${key}`,
1290 },
1291 });
1292
1293 if (!result.ok) {
1294 console.warn('FAL.AI returned an error.');
1295 return response.sendStatus(500);
1296 }
1297
1298 /** @type {any} */
1299 const taskData = await result.json();
1300 const { status_url } = taskData;
1301
1302 const MAX_ATTEMPTS = 100;
1303 for (let i = 0; i < MAX_ATTEMPTS; i++) {
1304 await delay(2500);
1305
1306 const statusResult = await fetch(status_url, {
1307 headers: {
1308 'Authorization': `Key ${key}`,
1309 },
1310 });
1311
1312 if (!statusResult.ok) {
1313 const text = await statusResult.text();
1314 console.warn('FAL.AI returned an error.', text);
1315 return response.sendStatus(500);
1316 }
1317
1318 /** @type {any} */
1319 const statusData = await statusResult.json();
1320
1321 if (statusData?.status === 'IN_QUEUE' || statusData?.status === 'IN_PROGRESS') {
1322 continue;
1323 }
1324
1325 if (statusData?.status === 'COMPLETED') {
1326 const resultFetch = await fetch(statusData?.response_url, {
1327 method: 'GET',
1328 headers: {
1329 'Authorization': `Key ${key}`,
1330 },
1331 });
1332 const resultData = await resultFetch.json();
1333
1334 if (resultData.detail !== null && resultData.detail !== undefined) {
1335 throw new Error('FAL.AI failed to generate image.', { cause: `${resultData.detail[0].loc[1]}: ${resultData.detail[0].msg}` });
1336 }
1337
1338 const imageFetch = await fetch(resultData?.images[0].url, {
1339 headers: {
1340 'Authorization': `Key ${key}`,
1341 },
1342 });
1343
1344 const fetchData = await imageFetch.arrayBuffer();
1345 const image = Buffer.from(fetchData).toString('base64');
1346 return response.send({ image: image });
1347 }
1348
1349 throw new Error('FAL.AI failed to generate image.', { cause: statusData });
1350 }
1351 } catch (error) {
1352 console.error(error);
1353 return response.status(500).send(error.cause || error.message);
1354 }
1355});
1356
1231router.use('/comfy', comfy);1357router.use('/comfy', comfy);
1232router.use('/together', together);1358router.use('/together', together);
1233router.use('/drawthings', drawthings);1359router.use('/drawthings', drawthings);
@@ -1237,3 +1363,4 @@ router.use('/blockentropy', blockentropy);
1237router.use('/huggingface', huggingface);1363router.use('/huggingface', huggingface);
1238router.use('/nanogpt', nanogpt);1364router.use('/nanogpt', nanogpt);
1239router.use('/bfl', bfl);1365router.use('/bfl', bfl);
1366router.use('/falai', falai);
src/endpoints/stats.js+2 -2
@@ -148,7 +148,7 @@ async function collectAndCreateStats(chatsPath, charactersPath) {
148 * @param {string} charactersPath Path to the directory containing the character files.148 * @param {string} charactersPath Path to the directory containing the character files.
149 */149 */
150export async function recreateStats(handle, chatsPath, charactersPath) {150export async function recreateStats(handle, chatsPath, charactersPath) {
151 console.log('Collecting and creating stats for user:', handle);151 console.info('Collecting and creating stats for user:', handle);
152 const stats = await collectAndCreateStats(chatsPath, charactersPath);152 const stats = await collectAndCreateStats(chatsPath, charactersPath);
153 STATS.set(handle, stats);153 STATS.set(handle, stats);
154 await saveStatsToFile();154 await saveStatsToFile();
@@ -200,7 +200,7 @@ async function saveStatsToFile() {
200 await writeFileAtomic(statsFilePath, JSON.stringify(charStats));200 await writeFileAtomic(statsFilePath, JSON.stringify(charStats));
201 TIMESTAMPS.set(handle, Date.now());201 TIMESTAMPS.set(handle, Date.now());
202 } catch (error) {202 } catch (error) {
203 console.log('Failed to save stats to file.', error);203 console.error('Failed to save stats to file.', error);
204 }204 }
205 }205 }
206 }206 }
src/endpoints/thumbnails.js+3 -4
@@ -89,7 +89,6 @@ async function generateThumbnail(directories, type, file) {
89 let thumbnailFolder = getThumbnailFolder(directories, type);89 let thumbnailFolder = getThumbnailFolder(directories, type);
90 let originalFolder = getOriginalFolder(directories, type);90 let originalFolder = getOriginalFolder(directories, type);
91 if (thumbnailFolder === undefined || originalFolder === undefined) throw new Error('Invalid thumbnail type');91 if (thumbnailFolder === undefined || originalFolder === undefined) throw new Error('Invalid thumbnail type');
92
93 const pathToCachedFile = path.join(thumbnailFolder, file);92 const pathToCachedFile = path.join(thumbnailFolder, file);
94 const pathToOriginalFile = path.join(originalFolder, file);93 const pathToOriginalFile = path.join(originalFolder, file);
9594
@@ -104,7 +103,7 @@ async function generateThumbnail(directories, type, file) {
104 const cachedStat = fs.statSync(pathToCachedFile);103 const cachedStat = fs.statSync(pathToCachedFile);
105104
106 if (originalStat.mtimeMs > cachedStat.ctimeMs) {105 if (originalStat.mtimeMs > cachedStat.ctimeMs) {
107 //console.log('Original file changed. Regenerating thumbnail...');106 //console.warn('Original file changed. Regenerating thumbnail...');
108 shouldRegenerate = true;107 shouldRegenerate = true;
109 }108 }
110 }109 }
@@ -157,7 +156,7 @@ export async function ensureThumbnailCache() {
157 return;156 return;
158 }157 }
159158
160 console.log('Generating thumbnails cache. Please wait...');159 console.info('Generating thumbnails cache. Please wait...');
161160
162 const bgFiles = fs.readdirSync(directories.backgrounds);161 const bgFiles = fs.readdirSync(directories.backgrounds);
163 const tasks = [];162 const tasks = [];
@@ -167,7 +166,7 @@ export async function ensureThumbnailCache() {
167 }166 }
168167
169 await Promise.all(tasks);168 await Promise.all(tasks);
170 console.log(`Done! Generated: ${bgFiles.length} preview images`);169 console.info(`Done! Generated: ${bgFiles.length} preview images`);
171 }170 }
172}171}
173172
src/endpoints/tokenizers.js+17 -17
@@ -96,7 +96,7 @@ async function getPathToTokenizer(model, fallbackModel) {
96 throw new Error('Downloading tokenizers is disabled, the model is not cached');96 throw new Error('Downloading tokenizers is disabled, the model is not cached');
97 }97 }
9898
99 console.log('Downloading tokenizer model:', model);99 console.info('Downloading tokenizer model:', model);
100 const response = await fetch(model);100 const response = await fetch(model);
101 if (!response.ok) {101 if (!response.ok) {
102 throw new Error(`Failed to fetch the model: ${response.status} ${response.statusText}`);102 throw new Error(`Failed to fetch the model: ${response.status} ${response.statusText}`);
@@ -108,7 +108,7 @@ async function getPathToTokenizer(model, fallbackModel) {
108 } catch (error) {108 } catch (error) {
109 const getLastSegment = str => str?.split('/')?.pop() || '';109 const getLastSegment = str => str?.split('/')?.pop() || '';
110 if (fallbackModel) {110 if (fallbackModel) {
111 console.log(`Could not get a tokenizer from ${getLastSegment(model)}. Reason: ${error.message}. Using a fallback model: ${getLastSegment(fallbackModel)}.`);111 console.error(`Could not get a tokenizer from ${getLastSegment(model)}. Reason: ${error.message}. Using a fallback model: ${getLastSegment(fallbackModel)}.`);
112 return fallbackModel;112 return fallbackModel;
113 }113 }
114114
@@ -156,7 +156,7 @@ class SentencePieceTokenizer {
156 const pathToModel = await getPathToTokenizer(this.#model, this.#fallbackModel);156 const pathToModel = await getPathToTokenizer(this.#model, this.#fallbackModel);
157 this.#instance = new SentencePieceProcessor();157 this.#instance = new SentencePieceProcessor();
158 await this.#instance.load(pathToModel);158 await this.#instance.load(pathToModel);
159 console.log('Instantiated the tokenizer for', path.parse(pathToModel).name);159 console.info('Instantiated the tokenizer for', path.parse(pathToModel).name);
160 return this.#instance;160 return this.#instance;
161 } catch (error) {161 } catch (error) {
162 console.error('Sentencepiece tokenizer failed to load: ' + this.#model, error);162 console.error('Sentencepiece tokenizer failed to load: ' + this.#model, error);
@@ -205,7 +205,7 @@ class WebTokenizer {
205 const pathToModel = await getPathToTokenizer(this.#model, this.#fallbackModel);205 const pathToModel = await getPathToTokenizer(this.#model, this.#fallbackModel);
206 const arrayBuffer = fs.readFileSync(pathToModel).buffer;206 const arrayBuffer = fs.readFileSync(pathToModel).buffer;
207 this.#instance = await Tokenizer.fromJSON(arrayBuffer);207 this.#instance = await Tokenizer.fromJSON(arrayBuffer);
208 console.log('Instantiated the tokenizer for', path.parse(pathToModel).name);208 console.info('Instantiated the tokenizer for', path.parse(pathToModel).name);
209 return this.#instance;209 return this.#instance;
210 } catch (error) {210 } catch (error) {
211 console.error('Web tokenizer failed to load: ' + this.#model, error);211 console.error('Web tokenizer failed to load: ' + this.#model, error);
@@ -484,7 +484,7 @@ export function getTiktokenTokenizer(model) {
484 }484 }
485485
486 const tokenizer = tiktoken.encoding_for_model(model);486 const tokenizer = tiktoken.encoding_for_model(model);
487 console.log('Instantiated the tokenizer for', model);487 console.info('Instantiated the tokenizer for', model);
488 tokenizersCache[model] = tokenizer;488 tokenizersCache[model] = tokenizer;
489 return tokenizer;489 return tokenizer;
490}490}
@@ -531,7 +531,7 @@ function createSentencepieceEncodingHandler(tokenizer) {
531 const chunks = instance?.encodePieces(text);531 const chunks = instance?.encodePieces(text);
532 return response.send({ ids, count, chunks });532 return response.send({ ids, count, chunks });
533 } catch (error) {533 } catch (error) {
534 console.log(error);534 console.error(error);
535 return response.send({ ids: [], count: 0, chunks: [] });535 return response.send({ ids: [], count: 0, chunks: [] });
536 }536 }
537 };537 };
@@ -562,7 +562,7 @@ function createSentencepieceDecodingHandler(tokenizer) {
562 const text = chunks.join('');562 const text = chunks.join('');
563 return response.send({ text, chunks });563 return response.send({ text, chunks });
564 } catch (error) {564 } catch (error) {
565 console.log(error);565 console.error(error);
566 return response.send({ text: '', chunks: [] });566 return response.send({ text: '', chunks: [] });
567 }567 }
568 };568 };
@@ -591,7 +591,7 @@ function createTiktokenEncodingHandler(modelId) {
591 const chunks = await getTiktokenChunks(tokenizer, tokens);591 const chunks = await getTiktokenChunks(tokenizer, tokens);
592 return response.send({ ids: tokens, count: tokens.length, chunks });592 return response.send({ ids: tokens, count: tokens.length, chunks });
593 } catch (error) {593 } catch (error) {
594 console.log(error);594 console.error(error);
595 return response.send({ ids: [], count: 0, chunks: [] });595 return response.send({ ids: [], count: 0, chunks: [] });
596 }596 }
597 };597 };
@@ -620,7 +620,7 @@ function createTiktokenDecodingHandler(modelId) {
620 const text = new TextDecoder().decode(textBytes);620 const text = new TextDecoder().decode(textBytes);
621 return response.send({ text });621 return response.send({ text });
622 } catch (error) {622 } catch (error) {
623 console.log(error);623 console.error(error);
624 return response.send({ text: '' });624 return response.send({ text: '' });
625 }625 }
626 };626 };
@@ -650,7 +650,7 @@ function createWebTokenizerEncodingHandler(tokenizer) {
650 const chunks = getWebTokenizersChunks(instance, tokens);650 const chunks = getWebTokenizersChunks(instance, tokens);
651 return response.send({ ids: tokens, count: tokens.length, chunks });651 return response.send({ ids: tokens, count: tokens.length, chunks });
652 } catch (error) {652 } catch (error) {
653 console.log(error);653 console.error(error);
654 return response.send({ ids: [], count: 0, chunks: [] });654 return response.send({ ids: [], count: 0, chunks: [] });
655 }655 }
656 };656 };
@@ -681,7 +681,7 @@ function createWebTokenizerDecodingHandler(tokenizer) {
681 const text = instance.decode(new Int32Array(ids));681 const text = instance.decode(new Int32Array(ids));
682 return response.send({ text, chunks });682 return response.send({ text, chunks });
683 } catch (error) {683 } catch (error) {
684 console.log(error);684 console.error(error);
685 return response.send({ text: '', chunks: [] });685 return response.send({ text: '', chunks: [] });
686 }686 }
687 };687 };
@@ -781,7 +781,7 @@ router.post('/openai/encode', jsonParser, async function (req, res) {
781 const handler = createTiktokenEncodingHandler(model);781 const handler = createTiktokenEncodingHandler(model);
782 return handler(req, res);782 return handler(req, res);
783 } catch (error) {783 } catch (error) {
784 console.log(error);784 console.error(error);
785 return res.send({ ids: [], count: 0, chunks: [] });785 return res.send({ ids: [], count: 0, chunks: [] });
786 }786 }
787});787});
@@ -849,7 +849,7 @@ router.post('/openai/decode', jsonParser, async function (req, res) {
849 const handler = createTiktokenDecodingHandler(model);849 const handler = createTiktokenDecodingHandler(model);
850 return handler(req, res);850 return handler(req, res);
851 } catch (error) {851 } catch (error) {
852 console.log(error);852 console.error(error);
853 return res.send({ text: '' });853 return res.send({ text: '' });
854 }854 }
855});855});
@@ -988,7 +988,7 @@ router.post('/remote/kobold/count', jsonParser, async function (request, respons
988 const result = await fetch(url, args);988 const result = await fetch(url, args);
989989
990 if (!result.ok) {990 if (!result.ok) {
991 console.log(`API returned error: ${result.status} ${result.statusText}`);991 console.warn(`API returned error: ${result.status} ${result.statusText}`);
992 return response.send({ error: true });992 return response.send({ error: true });
993 }993 }
994994
@@ -997,7 +997,7 @@ router.post('/remote/kobold/count', jsonParser, async function (request, respons
997 const ids = data['ids'] ?? [];997 const ids = data['ids'] ?? [];
998 return response.send({ count, ids });998 return response.send({ count, ids });
999 } catch (error) {999 } catch (error) {
1000 console.log(error);1000 console.error(error);
1001 return response.send({ error: true });1001 return response.send({ error: true });
1002 }1002 }
1003});1003});
@@ -1052,7 +1052,7 @@ router.post('/remote/textgenerationwebui/encode', jsonParser, async function (re
1052 const result = await fetch(url, args);1052 const result = await fetch(url, args);
10531053
1054 if (!result.ok) {1054 if (!result.ok) {
1055 console.log(`API returned error: ${result.status} ${result.statusText}`);1055 console.warn(`API returned error: ${result.status} ${result.statusText}`);
1056 return response.send({ error: true });1056 return response.send({ error: true });
1057 }1057 }
10581058
@@ -1062,7 +1062,7 @@ router.post('/remote/textgenerationwebui/encode', jsonParser, async function (re
10621062
1063 return response.send({ count, ids });1063 return response.send({ count, ids });
1064 } catch (error) {1064 } catch (error) {
1065 console.log(error);1065 console.error(error);
1066 return response.send({ error: true });1066 return response.send({ error: true });
1067 }1067 }
1068});1068});
src/endpoints/translate.js+39 -39
@@ -35,7 +35,7 @@ function decodeBuffer(buffer) {
35 try {35 try {
36 return iconv.decode(Buffer.from(buffer), 'utf-8');36 return iconv.decode(Buffer.from(buffer), 'utf-8');
37 } catch (error) {37 } catch (error) {
38 console.log('Failed to decode buffer:', error);38 console.error('Failed to decode buffer:', error);
39 return Buffer.from(buffer).toString('utf-8');39 return Buffer.from(buffer).toString('utf-8');
40 }40 }
41}41}
@@ -46,7 +46,7 @@ router.post('/libre', jsonParser, async (request, response) => {
46 const url = readSecret(request.user.directories, SECRET_KEYS.LIBRE_URL);46 const url = readSecret(request.user.directories, SECRET_KEYS.LIBRE_URL);
4747
48 if (!url) {48 if (!url) {
49 console.log('LibreTranslate URL is not configured.');49 console.warn('LibreTranslate URL is not configured.');
50 return response.sendStatus(400);50 return response.sendStatus(400);
51 }51 }
5252
@@ -69,7 +69,7 @@ router.post('/libre', jsonParser, async (request, response) => {
69 return response.sendStatus(400);69 return response.sendStatus(400);
70 }70 }
7171
72 console.log('Input text: ' + text);72 console.debug('Input text: ' + text);
7373
74 const result = await fetch(url, {74 const result = await fetch(url, {
75 method: 'POST',75 method: 'POST',
@@ -85,17 +85,17 @@ router.post('/libre', jsonParser, async (request, response) => {
8585
86 if (!result.ok) {86 if (!result.ok) {
87 const error = await result.text();87 const error = await result.text();
88 console.log('LibreTranslate error: ', result.statusText, error);88 console.warn('LibreTranslate error: ', result.statusText, error);
89 return response.sendStatus(500);89 return response.sendStatus(500);
90 }90 }
9191
92 /** @type {any} */92 /** @type {any} */
93 const json = await result.json();93 const json = await result.json();
94 console.log('Translated text: ' + json.translatedText);94 console.debug('Translated text: ' + json.translatedText);
9595
96 return response.send(json.translatedText);96 return response.send(json.translatedText);
97 } catch (error) {97 } catch (error) {
98 console.log('Translation error: ' + error.message);98 console.error('Translation error: ' + error.message);
99 return response.sendStatus(500);99 return response.sendStatus(500);
100 }100 }
101});101});
@@ -109,14 +109,14 @@ router.post('/google', jsonParser, async (request, response) => {
109 return response.sendStatus(400);109 return response.sendStatus(400);
110 }110 }
111111
112 console.log('Input text: ' + text);112 console.debug('Input text: ' + text);
113113
114 const { generateRequestUrl, normaliseResponse } = getGoogleTranslateClient();114 const { generateRequestUrl, normaliseResponse } = getGoogleTranslateClient();
115 const requestUrl = generateRequestUrl(text, { to: lang });115 const requestUrl = generateRequestUrl(text, { to: lang });
116 const result = await fetch(requestUrl);116 const result = await fetch(requestUrl);
117117
118 if (!result.ok) {118 if (!result.ok) {
119 console.log('Google Translate error: ', result.statusText);119 console.warn('Google Translate error: ', result.statusText);
120 return response.sendStatus(500);120 return response.sendStatus(500);
121 }121 }
122122
@@ -125,10 +125,10 @@ router.post('/google', jsonParser, async (request, response) => {
125 const translatedText = translateResponse.text;125 const translatedText = translateResponse.text;
126126
127 response.setHeader('Content-Type', 'text/plain; charset=utf-8');127 response.setHeader('Content-Type', 'text/plain; charset=utf-8');
128 console.log('Translated text: ' + translatedText);128 console.debug('Translated text: ' + translatedText);
129 return response.send(translatedText);129 return response.send(translatedText);
130 } catch (error) {130 } catch (error) {
131 console.log('Translation error', error);131 console.error('Translation error', error);
132 return response.sendStatus(500);132 return response.sendStatus(500);
133 }133 }
134});134});
@@ -161,7 +161,7 @@ router.post('/yandex', jsonParser, async (request, response) => {
161 params.append('lang', lang);161 params.append('lang', lang);
162 const ucid = uuidv4().replaceAll('-', '');162 const ucid = uuidv4().replaceAll('-', '');
163163
164 console.log('Input text: ' + inputText);164 console.debug('Input text: ' + inputText);
165165
166 const result = await fetch(`https://translate.yandex.net/api/v1/tr.json/translate?ucid=${ucid}&srv=android&format=text`, {166 const result = await fetch(`https://translate.yandex.net/api/v1/tr.json/translate?ucid=${ucid}&srv=android&format=text`, {
167 method: 'POST',167 method: 'POST',
@@ -173,18 +173,18 @@ router.post('/yandex', jsonParser, async (request, response) => {
173173
174 if (!result.ok) {174 if (!result.ok) {
175 const error = await result.text();175 const error = await result.text();
176 console.log('Yandex error: ', result.statusText, error);176 console.warn('Yandex error: ', result.statusText, error);
177 return response.sendStatus(500);177 return response.sendStatus(500);
178 }178 }
179179
180 /** @type {any} */180 /** @type {any} */
181 const json = await result.json();181 const json = await result.json();
182 const translated = json.text.join();182 const translated = json.text.join();
183 console.log('Translated text: ' + translated);183 console.debug('Translated text: ' + translated);
184184
185 return response.send(translated);185 return response.send(translated);
186 } catch (error) {186 } catch (error) {
187 console.log('Translation error: ' + error.message);187 console.error('Translation error: ' + error.message);
188 return response.sendStatus(500);188 return response.sendStatus(500);
189 }189 }
190});190});
@@ -195,7 +195,7 @@ router.post('/lingva', jsonParser, async (request, response) => {
195 const baseUrl = secretUrl || LINGVA_DEFAULT;195 const baseUrl = secretUrl || LINGVA_DEFAULT;
196196
197 if (!secretUrl && baseUrl === LINGVA_DEFAULT) {197 if (!secretUrl && baseUrl === LINGVA_DEFAULT) {
198 console.log('Lingva URL is using default value.', LINGVA_DEFAULT);198 console.warn('Lingva URL is using default value.', LINGVA_DEFAULT);
199 }199 }
200200
201 if (request.body.lang === 'zh-CN' || request.body.lang === 'zh-TW') {201 if (request.body.lang === 'zh-CN' || request.body.lang === 'zh-TW') {
@@ -213,22 +213,22 @@ router.post('/lingva', jsonParser, async (request, response) => {
213 return response.sendStatus(400);213 return response.sendStatus(400);
214 }214 }
215215
216 console.log('Input text: ' + text);216 console.debug('Input text: ' + text);
217217
218 const url = urlJoin(baseUrl, 'auto', lang, encodeURIComponent(text));218 const url = urlJoin(baseUrl, 'auto', lang, encodeURIComponent(text));
219 const result = await fetch(url);219 const result = await fetch(url);
220220
221 if (!result.ok) {221 if (!result.ok) {
222 const error = await result.text();222 const error = await result.text();
223 console.log('Lingva error: ', result.statusText, error);223 console.warn('Lingva error: ', result.statusText, error);
224 }224 }
225225
226 /** @type {any} */226 /** @type {any} */
227 const data = await result.json();227 const data = await result.json();
228 console.log('Translated text: ' + data.translation);228 console.debug('Translated text: ' + data.translation);
229 return response.send(data.translation);229 return response.send(data.translation);
230 } catch (error) {230 } catch (error) {
231 console.log('Translation error', error);231 console.error('Translation error', error);
232 return response.sendStatus(500);232 return response.sendStatus(500);
233 }233 }
234});234});
@@ -238,7 +238,7 @@ router.post('/deepl', jsonParser, async (request, response) => {
238 const key = readSecret(request.user.directories, SECRET_KEYS.DEEPL);238 const key = readSecret(request.user.directories, SECRET_KEYS.DEEPL);
239239
240 if (!key) {240 if (!key) {
241 console.log('DeepL key is not configured.');241 console.warn('DeepL key is not configured.');
242 return response.sendStatus(400);242 return response.sendStatus(400);
243 }243 }
244244
@@ -254,7 +254,7 @@ router.post('/deepl', jsonParser, async (request, response) => {
254 return response.sendStatus(400);254 return response.sendStatus(400);
255 }255 }
256256
257 console.log('Input text: ' + text);257 console.debug('Input text: ' + text);
258258
259 const params = new URLSearchParams();259 const params = new URLSearchParams();
260 params.append('text', text);260 params.append('text', text);
@@ -280,17 +280,17 @@ router.post('/deepl', jsonParser, async (request, response) => {
280280
281 if (!result.ok) {281 if (!result.ok) {
282 const error = await result.text();282 const error = await result.text();
283 console.log('DeepL error: ', result.statusText, error);283 console.warn('DeepL error: ', result.statusText, error);
284 return response.sendStatus(500);284 return response.sendStatus(500);
285 }285 }
286286
287 /** @type {any} */287 /** @type {any} */
288 const json = await result.json();288 const json = await result.json();
289 console.log('Translated text: ' + json.translations[0].text);289 console.debug('Translated text: ' + json.translations[0].text);
290290
291 return response.send(json.translations[0].text);291 return response.send(json.translations[0].text);
292 } catch (error) {292 } catch (error) {
293 console.log('Translation error: ' + error.message);293 console.error('Translation error: ' + error.message);
294 return response.sendStatus(500);294 return response.sendStatus(500);
295 }295 }
296});296});
@@ -301,12 +301,12 @@ router.post('/onering', jsonParser, async (request, response) => {
301 const url = secretUrl || ONERING_URL_DEFAULT;301 const url = secretUrl || ONERING_URL_DEFAULT;
302302
303 if (!url) {303 if (!url) {
304 console.log('OneRing URL is not configured.');304 console.warn('OneRing URL is not configured.');
305 return response.sendStatus(400);305 return response.sendStatus(400);
306 }306 }
307307
308 if (!secretUrl && url === ONERING_URL_DEFAULT) {308 if (!secretUrl && url === ONERING_URL_DEFAULT) {
309 console.log('OneRing URL is using default value.', ONERING_URL_DEFAULT);309 console.info('OneRing URL is using default value.', ONERING_URL_DEFAULT);
310 }310 }
311311
312 if (request.body.lang === 'pt-BR' || request.body.lang === 'pt-PT') {312 if (request.body.lang === 'pt-BR' || request.body.lang === 'pt-PT') {
@@ -326,7 +326,7 @@ router.post('/onering', jsonParser, async (request, response) => {
326 params.append('from_lang', from_lang);326 params.append('from_lang', from_lang);
327 params.append('to_lang', to_lang);327 params.append('to_lang', to_lang);
328328
329 console.log('Input text: ' + text);329 console.debug('Input text: ' + text);
330330
331 const fetchUrl = new URL(url);331 const fetchUrl = new URL(url);
332 fetchUrl.search = params.toString();332 fetchUrl.search = params.toString();
@@ -337,17 +337,17 @@ router.post('/onering', jsonParser, async (request, response) => {
337337
338 if (!result.ok) {338 if (!result.ok) {
339 const error = await result.text();339 const error = await result.text();
340 console.log('OneRing error: ', result.statusText, error);340 console.warn('OneRing error: ', result.statusText, error);
341 return response.sendStatus(500);341 return response.sendStatus(500);
342 }342 }
343343
344 /** @type {any} */344 /** @type {any} */
345 const data = await result.json();345 const data = await result.json();
346 console.log('Translated text: ' + data.result);346 console.debug('Translated text: ' + data.result);
347347
348 return response.send(data.result);348 return response.send(data.result);
349 } catch (error) {349 } catch (error) {
350 console.log('Translation error: ' + error.message);350 console.error('Translation error: ' + error.message);
351 return response.sendStatus(500);351 return response.sendStatus(500);
352 }352 }
353});353});
@@ -358,12 +358,12 @@ router.post('/deeplx', jsonParser, async (request, response) => {
358 const url = secretUrl || DEEPLX_URL_DEFAULT;358 const url = secretUrl || DEEPLX_URL_DEFAULT;
359359
360 if (!url) {360 if (!url) {
361 console.log('DeepLX URL is not configured.');361 console.warn('DeepLX URL is not configured.');
362 return response.sendStatus(400);362 return response.sendStatus(400);
363 }363 }
364364
365 if (!secretUrl && url === DEEPLX_URL_DEFAULT) {365 if (!secretUrl && url === DEEPLX_URL_DEFAULT) {
366 console.log('DeepLX URL is using default value.', DEEPLX_URL_DEFAULT);366 console.info('DeepLX URL is using default value.', DEEPLX_URL_DEFAULT);
367 }367 }
368368
369 const text = request.body.text;369 const text = request.body.text;
@@ -376,7 +376,7 @@ router.post('/deeplx', jsonParser, async (request, response) => {
376 return response.sendStatus(400);376 return response.sendStatus(400);
377 }377 }
378378
379 console.log('Input text: ' + text);379 console.debug('Input text: ' + text);
380380
381 const result = await fetch(url, {381 const result = await fetch(url, {
382 method: 'POST',382 method: 'POST',
@@ -393,17 +393,17 @@ router.post('/deeplx', jsonParser, async (request, response) => {
393393
394 if (!result.ok) {394 if (!result.ok) {
395 const error = await result.text();395 const error = await result.text();
396 console.log('DeepLX error: ', result.statusText, error);396 console.warn('DeepLX error: ', result.statusText, error);
397 return response.sendStatus(500);397 return response.sendStatus(500);
398 }398 }
399399
400 /** @type {any} */400 /** @type {any} */
401 const json = await result.json();401 const json = await result.json();
402 console.log('Translated text: ' + json.data);402 console.debug('Translated text: ' + json.data);
403403
404 return response.send(json.data);404 return response.send(json.data);
405 } catch (error) {405 } catch (error) {
406 console.log('DeepLX translation error: ' + error.message);406 console.error('DeepLX translation error: ' + error.message);
407 return response.sendStatus(500);407 return response.sendStatus(500);
408 }408 }
409});409});
@@ -429,14 +429,14 @@ router.post('/bing', jsonParser, async (request, response) => {
429 return response.sendStatus(400);429 return response.sendStatus(400);
430 }430 }
431431
432 console.log('Input text: ' + text);432 console.debug('Input text: ' + text);
433433
434 const result = await bingTranslate(text, null, lang);434 const result = await bingTranslate(text, null, lang);
435 const translatedText = result?.translation;435 const translatedText = result?.translation;
436 console.log('Translated text: ' + translatedText);436 console.debug('Translated text: ' + translatedText);
437 return response.send(translatedText);437 return response.send(translatedText);
438 } catch (error) {438 } catch (error) {
439 console.log('Translation error', error);439 console.error('Translation error', error);
440 return response.sendStatus(500);440 return response.sendStatus(500);
441 }441 }
442});442});
src/endpoints/users-admin.js+19 -19
@@ -53,12 +53,12 @@ router.post('/get', requireAdminMiddleware, jsonParser, async (_request, respons
53router.post('/disable', requireAdminMiddleware, jsonParser, async (request, response) => {53router.post('/disable', requireAdminMiddleware, jsonParser, async (request, response) => {
54 try {54 try {
55 if (!request.body.handle) {55 if (!request.body.handle) {
56 console.log('Disable user failed: Missing required fields');56 console.warn('Disable user failed: Missing required fields');
57 return response.status(400).json({ error: 'Missing required fields' });57 return response.status(400).json({ error: 'Missing required fields' });
58 }58 }
5959
60 if (request.body.handle === request.user.profile.handle) {60 if (request.body.handle === request.user.profile.handle) {
61 console.log('Disable user failed: Cannot disable yourself');61 console.warn('Disable user failed: Cannot disable yourself');
62 return response.status(400).json({ error: 'Cannot disable yourself' });62 return response.status(400).json({ error: 'Cannot disable yourself' });
63 }63 }
6464
@@ -66,7 +66,7 @@ router.post('/disable', requireAdminMiddleware, jsonParser, async (request, resp
66 const user = await storage.getItem(toKey(request.body.handle));66 const user = await storage.getItem(toKey(request.body.handle));
6767
68 if (!user) {68 if (!user) {
69 console.log('Disable user failed: User not found');69 console.error('Disable user failed: User not found');
70 return response.status(404).json({ error: 'User not found' });70 return response.status(404).json({ error: 'User not found' });
71 }71 }
7272
@@ -82,7 +82,7 @@ router.post('/disable', requireAdminMiddleware, jsonParser, async (request, resp
82router.post('/enable', requireAdminMiddleware, jsonParser, async (request, response) => {82router.post('/enable', requireAdminMiddleware, jsonParser, async (request, response) => {
83 try {83 try {
84 if (!request.body.handle) {84 if (!request.body.handle) {
85 console.log('Enable user failed: Missing required fields');85 console.warn('Enable user failed: Missing required fields');
86 return response.status(400).json({ error: 'Missing required fields' });86 return response.status(400).json({ error: 'Missing required fields' });
87 }87 }
8888
@@ -90,7 +90,7 @@ router.post('/enable', requireAdminMiddleware, jsonParser, async (request, respo
90 const user = await storage.getItem(toKey(request.body.handle));90 const user = await storage.getItem(toKey(request.body.handle));
9191
92 if (!user) {92 if (!user) {
93 console.log('Enable user failed: User not found');93 console.error('Enable user failed: User not found');
94 return response.status(404).json({ error: 'User not found' });94 return response.status(404).json({ error: 'User not found' });
95 }95 }
9696
@@ -106,7 +106,7 @@ router.post('/enable', requireAdminMiddleware, jsonParser, async (request, respo
106router.post('/promote', requireAdminMiddleware, jsonParser, async (request, response) => {106router.post('/promote', requireAdminMiddleware, jsonParser, async (request, response) => {
107 try {107 try {
108 if (!request.body.handle) {108 if (!request.body.handle) {
109 console.log('Promote user failed: Missing required fields');109 console.warn('Promote user failed: Missing required fields');
110 return response.status(400).json({ error: 'Missing required fields' });110 return response.status(400).json({ error: 'Missing required fields' });
111 }111 }
112112
@@ -114,7 +114,7 @@ router.post('/promote', requireAdminMiddleware, jsonParser, async (request, resp
114 const user = await storage.getItem(toKey(request.body.handle));114 const user = await storage.getItem(toKey(request.body.handle));
115115
116 if (!user) {116 if (!user) {
117 console.log('Promote user failed: User not found');117 console.error('Promote user failed: User not found');
118 return response.status(404).json({ error: 'User not found' });118 return response.status(404).json({ error: 'User not found' });
119 }119 }
120120
@@ -130,12 +130,12 @@ router.post('/promote', requireAdminMiddleware, jsonParser, async (request, resp
130router.post('/demote', requireAdminMiddleware, jsonParser, async (request, response) => {130router.post('/demote', requireAdminMiddleware, jsonParser, async (request, response) => {
131 try {131 try {
132 if (!request.body.handle) {132 if (!request.body.handle) {
133 console.log('Demote user failed: Missing required fields');133 console.warn('Demote user failed: Missing required fields');
134 return response.status(400).json({ error: 'Missing required fields' });134 return response.status(400).json({ error: 'Missing required fields' });
135 }135 }
136136
137 if (request.body.handle === request.user.profile.handle) {137 if (request.body.handle === request.user.profile.handle) {
138 console.log('Demote user failed: Cannot demote yourself');138 console.warn('Demote user failed: Cannot demote yourself');
139 return response.status(400).json({ error: 'Cannot demote yourself' });139 return response.status(400).json({ error: 'Cannot demote yourself' });
140 }140 }
141141
@@ -143,7 +143,7 @@ router.post('/demote', requireAdminMiddleware, jsonParser, async (request, respo
143 const user = await storage.getItem(toKey(request.body.handle));143 const user = await storage.getItem(toKey(request.body.handle));
144144
145 if (!user) {145 if (!user) {
146 console.log('Demote user failed: User not found');146 console.error('Demote user failed: User not found');
147 return response.status(404).json({ error: 'User not found' });147 return response.status(404).json({ error: 'User not found' });
148 }148 }
149149
@@ -159,7 +159,7 @@ router.post('/demote', requireAdminMiddleware, jsonParser, async (request, respo
159router.post('/create', requireAdminMiddleware, jsonParser, async (request, response) => {159router.post('/create', requireAdminMiddleware, jsonParser, async (request, response) => {
160 try {160 try {
161 if (!request.body.handle || !request.body.name) {161 if (!request.body.handle || !request.body.name) {
162 console.log('Create user failed: Missing required fields');162 console.warn('Create user failed: Missing required fields');
163 return response.status(400).json({ error: 'Missing required fields' });163 return response.status(400).json({ error: 'Missing required fields' });
164 }164 }
165165
@@ -167,12 +167,12 @@ router.post('/create', requireAdminMiddleware, jsonParser, async (request, respo
167 const handle = lodash.kebabCase(String(request.body.handle).toLowerCase().trim());167 const handle = lodash.kebabCase(String(request.body.handle).toLowerCase().trim());
168168
169 if (!handle) {169 if (!handle) {
170 console.log('Create user failed: Invalid handle');170 console.warn('Create user failed: Invalid handle');
171 return response.status(400).json({ error: 'Invalid handle' });171 return response.status(400).json({ error: 'Invalid handle' });
172 }172 }
173173
174 if (handles.some(x => x === handle)) {174 if (handles.some(x => x === handle)) {
175 console.log('Create user failed: User with that handle already exists');175 console.warn('Create user failed: User with that handle already exists');
176 return response.status(409).json({ error: 'User already exists' });176 return response.status(409).json({ error: 'User already exists' });
177 }177 }
178178
@@ -192,7 +192,7 @@ router.post('/create', requireAdminMiddleware, jsonParser, async (request, respo
192 await storage.setItem(toKey(handle), newUser);192 await storage.setItem(toKey(handle), newUser);
193193
194 // Create user directories194 // Create user directories
195 console.log('Creating data directories for', newUser.handle);195 console.info('Creating data directories for', newUser.handle);
196 await ensurePublicDirectoriesExist();196 await ensurePublicDirectoriesExist();
197 const directories = getUserDirectories(newUser.handle);197 const directories = getUserDirectories(newUser.handle);
198 await checkForNewContent([directories]);198 await checkForNewContent([directories]);
@@ -206,17 +206,17 @@ router.post('/create', requireAdminMiddleware, jsonParser, async (request, respo
206router.post('/delete', requireAdminMiddleware, jsonParser, async (request, response) => {206router.post('/delete', requireAdminMiddleware, jsonParser, async (request, response) => {
207 try {207 try {
208 if (!request.body.handle) {208 if (!request.body.handle) {
209 console.log('Delete user failed: Missing required fields');209 console.warn('Delete user failed: Missing required fields');
210 return response.status(400).json({ error: 'Missing required fields' });210 return response.status(400).json({ error: 'Missing required fields' });
211 }211 }
212212
213 if (request.body.handle === request.user.profile.handle) {213 if (request.body.handle === request.user.profile.handle) {
214 console.log('Delete user failed: Cannot delete yourself');214 console.warn('Delete user failed: Cannot delete yourself');
215 return response.status(400).json({ error: 'Cannot delete yourself' });215 return response.status(400).json({ error: 'Cannot delete yourself' });
216 }216 }
217217
218 if (request.body.handle === DEFAULT_USER.handle) {218 if (request.body.handle === DEFAULT_USER.handle) {
219 console.log('Delete user failed: Cannot delete default user');219 console.warn('Delete user failed: Cannot delete default user');
220 return response.status(400).json({ error: 'Sorry, but the default user cannot be deleted. It is required as a fallback.' });220 return response.status(400).json({ error: 'Sorry, but the default user cannot be deleted. It is required as a fallback.' });
221 }221 }
222222
@@ -224,7 +224,7 @@ router.post('/delete', requireAdminMiddleware, jsonParser, async (request, respo
224224
225 if (request.body.purge) {225 if (request.body.purge) {
226 const directories = getUserDirectories(request.body.handle);226 const directories = getUserDirectories(request.body.handle);
227 console.log('Deleting data directories for', request.body.handle);227 console.info('Deleting data directories for', request.body.handle);
228 await fsPromises.rm(directories.root, { recursive: true, force: true });228 await fsPromises.rm(directories.root, { recursive: true, force: true });
229 }229 }
230230
@@ -238,7 +238,7 @@ router.post('/delete', requireAdminMiddleware, jsonParser, async (request, respo
238router.post('/slugify', requireAdminMiddleware, jsonParser, async (request, response) => {238router.post('/slugify', requireAdminMiddleware, jsonParser, async (request, response) => {
239 try {239 try {
240 if (!request.body.text) {240 if (!request.body.text) {
241 console.log('Slugify failed: Missing required fields');241 console.warn('Slugify failed: Missing required fields');
242 return response.status(400).json({ error: 'Missing required fields' });242 return response.status(400).json({ error: 'Missing required fields' });
243 }243 }
244244
src/endpoints/users-private.js+8 -9
@@ -58,18 +58,18 @@ router.get('/me', async (request, response) => {
58router.post('/change-avatar', jsonParser, async (request, response) => {58router.post('/change-avatar', jsonParser, async (request, response) => {
59 try {59 try {
60 if (!request.body.handle) {60 if (!request.body.handle) {
61 console.log('Change avatar failed: Missing required fields');61 console.warn('Change avatar failed: Missing required fields');
62 return response.status(400).json({ error: 'Missing required fields' });62 return response.status(400).json({ error: 'Missing required fields' });
63 }63 }
6464
65 if (request.body.handle !== request.user.profile.handle && !request.user.profile.admin) {65 if (request.body.handle !== request.user.profile.handle && !request.user.profile.admin) {
66 console.log('Change avatar failed: Unauthorized');66 console.error('Change avatar failed: Unauthorized');
67 return response.status(403).json({ error: 'Unauthorized' });67 return response.status(403).json({ error: 'Unauthorized' });
68 }68 }
6969
70 // Avatar is not a data URL or not an empty string70 // Avatar is not a data URL or not an empty string
71 if (!request.body.avatar.startsWith('data:image/') && request.body.avatar !== '') {71 if (!request.body.avatar.startsWith('data:image/') && request.body.avatar !== '') {
72 console.log('Change avatar failed: Invalid data URL');72 console.warn('Change avatar failed: Invalid data URL');
73 return response.status(400).json({ error: 'Invalid data URL' });73 return response.status(400).json({ error: 'Invalid data URL' });
74 }74 }
7575
@@ -77,7 +77,7 @@ router.post('/change-avatar', jsonParser, async (request, response) => {
77 const user = await storage.getItem(toKey(request.body.handle));77 const user = await storage.getItem(toKey(request.body.handle));
7878
79 if (!user) {79 if (!user) {
80 console.log('Change avatar failed: User not found');80 console.error('Change avatar failed: User not found');
81 return response.status(404).json({ error: 'User not found' });81 return response.status(404).json({ error: 'User not found' });
82 }82 }
8383
@@ -93,12 +93,12 @@ router.post('/change-avatar', jsonParser, async (request, response) => {
93router.post('/change-password', jsonParser, async (request, response) => {93router.post('/change-password', jsonParser, async (request, response) => {
94 try {94 try {
95 if (!request.body.handle) {95 if (!request.body.handle) {
96 console.log('Change password failed: Missing required fields');96 console.warn('Change password failed: Missing required fields');
97 return response.status(400).json({ error: 'Missing required fields' });97 return response.status(400).json({ error: 'Missing required fields' });
98 }98 }
9999
100 if (request.body.handle !== request.user.profile.handle && !request.user.profile.admin) {100 if (request.body.handle !== request.user.profile.handle && !request.user.profile.admin) {
101 console.log('Change password failed: Unauthorized');101 console.error('Change password failed: Unauthorized');
102 return response.status(403).json({ error: 'Unauthorized' });102 return response.status(403).json({ error: 'Unauthorized' });
103 }103 }
104104
@@ -106,17 +106,17 @@ router.post('/change-password', jsonParser, async (request, response) => {
106 const user = await storage.getItem(toKey(request.body.handle));106 const user = await storage.getItem(toKey(request.body.handle));
107107
108 if (!user) {108 if (!user) {
109 console.log('Change password failed: User not found');109 console.error('Change password failed: User not found');
110 return response.status(404).json({ error: 'User not found' });110 return response.status(404).json({ error: 'User not found' });
111 }111 }
112112
113 if (!user.enabled) {113 if (!user.enabled) {
114 console.log('Change password failed: User is disabled');114 console.error('Change password failed: User is disabled');
115 return response.status(403).json({ error: 'User is disabled' });115 return response.status(403).json({ error: 'User is disabled' });
116 }116 }
117117
118 if (!request.user.profile.admin && user.password && user.password !== getPasswordHash(request.body.oldPassword, user.salt)) {118 if (!request.user.profile.admin && user.password && user.password !== getPasswordHash(request.body.oldPassword, user.salt)) {
119 console.log('Change password failed: Incorrect password');
src/endpoints/users-public.js+0 -0
src/endpoints/vectors.js+0 -0
src/endpoints/worldinfo.js+0 -0
src/middleware/cacheBuster.js+0 -0
src/middleware/whitelist.js+0 -0
src/plugin-loader.js+0 -0
src/prompt-converters.js+0 -0
src/request-proxy.js+0 -0
src/users.js+0 -0
src/util.js+0 -0
src/vectors/cohere-vectors.js+0 -0
src/vectors/extras-vectors.js+0 -0
src/vectors/makersuite-vectors.js+0 -0
src/vectors/nomicai-vectors.js+0 -0
src/vectors/openai-vectors.js+0 -0
Diff truncated