Merge pull request #5368 from SillyTavern/staging 1.17.0

e3f41666c69db032e17e079fcddcf40cf47e8593

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

Signed
216 files changed, +5515 -1056Ignore whitespace
.eslintrc.cjs+23 -0
@@ -102,5 +102,28 @@ module.exports = {
102 // These rules should eventually be enabled.102 // These rules should eventually be enabled.
103 'no-async-promise-executor': 'off',103 'no-async-promise-executor': 'off',
104 'no-inner-declarations': 'off',104 'no-inner-declarations': 'off',
105 // Additional formatting rules based on codebase conventions
106 'brace-style': ['error', '1tbs', { allowSingleLine: true }],
107 'array-bracket-spacing': ['error', 'never'],
108 'computed-property-spacing': ['error', 'never'],
109 'block-spacing': ['error', 'always'],
110 'keyword-spacing': ['error', { before: true, after: true }],
111 'space-before-blocks': ['error', 'always'],
112 'space-before-function-paren': ['error', { anonymous: 'always', named: 'never', asyncArrow: 'always' }],
113 'space-in-parens': ['error', 'never'],
114 'comma-spacing': ['error', { before: false, after: true }],
115 'key-spacing': ['error', { beforeColon: false, afterColon: true }],
116 'func-call-spacing': ['error', 'never'],
117 'no-multiple-empty-lines': ['error', { max: 2, maxEOF: 1, maxBOF: 0 }],
118 'padded-blocks': ['error', 'never'],
119 'no-whitespace-before-property': 'error',
120 'space-unary-ops': ['error', { words: true, nonwords: false }],
121 'arrow-spacing': ['error', { before: true, after: true }],
122 'template-curly-spacing': ['error', 'never'],
123 'rest-spread-spacing': ['error', 'never'],
124 'generator-star-spacing': ['error', { before: false, after: true }],
125 'yield-star-spacing': ['error', { before: false, after: true }],
126 'template-tag-spacing': ['error', 'never'],
127 'switch-colon-spacing': ['error', { after: true, before: false }],
105 },128 },
106};129};
.github/readme-de_de.md+1 -1
@@ -36,7 +36,7 @@ Angefangen im Februar 2023 als Fork von TavernAI 1.2.8 hat SillyTavern nun über
3636
37## Brauche ich einen leistungsstarken PC, um SillyTavern auszuführen?37## Brauche ich einen leistungsstarken PC, um SillyTavern auszuführen?
3838
39Die Hardwareanforderungen sind minimal: Es läuft auf allem, was NodeJS 18 oder höher ausführen kann. Wenn du LLM-Inferenz auf deinem lokalen Rechner durchführen möchtest, empfehlen wir eine NVIDIA-Grafikkarte der 3000er-Serie mit mindestens 6 GB VRAM, aber die tatsächlichen Anforderungen können je nach Modell und Backend, das du verwendest, variieren.39Die Hardwareanforderungen sind minimal: Es läuft auf allem, was NodeJS 20 oder höher ausführen kann. Wenn du LLM-Inferenz auf deinem lokalen Rechner durchführen möchtest, empfehlen wir eine NVIDIA-Grafikkarte der 3000er-Serie mit mindestens 6 GB VRAM, aber die tatsächlichen Anforderungen können je nach Modell und Backend, das du verwendest, variieren.
4040
41## Fragen oder Vorschläge?41## Fragen oder Vorschläge?
4242
.github/readme-ja_jp.md+1 -1
@@ -36,7 +36,7 @@ SillyTavern(略してST)は、テキスト生成LLM、画像生成エンジ
3636
37## SillyTavernを実行するには強力なPCが必要ですか?37## SillyTavernを実行するには強力なPCが必要ですか?
3838
39ハードウェア要件は最小限です。NodeJS 18以上を実行できるものであれば何でも動作します。ローカルマシンでLLM推論を行う場合は、少なくとも6GBのVRAMを搭載した3000シリーズのNVIDIAグラフィックスカードを推奨しますが、実際の要件は使用するモデルやバックエンドによって異なる場合があります。39ハードウェア要件は最小限です。NodeJS 20以上を実行できるものであれば何でも動作します。ローカルマシンでLLM推論を行う場合は、少なくとも6GBのVRAMを搭載した3000シリーズのNVIDIAグラフィックスカードを推奨しますが、実際の要件は使用するモデルやバックエンドによって異なる場合があります。
4040
41## 質問や提案はありますか?41## 質問や提案はありますか?
4242
.github/readme-ko_kr.md+1 -1
@@ -36,7 +36,7 @@ SillyTavern(짧게는 ST)은 텍스트 생성 LLM, 이미지 생성 엔진, TTS
3636
37## SillyTavern을 위해서 좋은 성능의 PC가 필요한가요?37## SillyTavern을 위해서 좋은 성능의 PC가 필요한가요?
3838
39하드웨어 요구 사항은 거의 없습니다: NodeJS 18 이상을 실행할 수 있는 모든 환경에서 작동합니다. 다만 로컬 LLM 모델을 사용할 경우, 최소 6GB VRAM 이상의 3000번대 NVIDIA 그래픽 카드를 권장하지만, 실제 요구 사항은 사용하는 모델과 백엔드에 따라 달라질 수 있습니다.39하드웨어 요구 사항은 거의 없습니다: NodeJS 20 이상을 실행할 수 있는 모든 환경에서 작동합니다. 다만 로컬 LLM 모델을 사용할 경우, 최소 6GB VRAM 이상의 3000번대 NVIDIA 그래픽 카드를 권장하지만, 실제 요구 사항은 사용하는 모델과 백엔드에 따라 달라질 수 있습니다.
4040
41## 질문이나 제안이 있으신가요?41## 질문이나 제안이 있으신가요?
4242
.github/readme-ru_ru.md+1 -1
@@ -36,7 +36,7 @@ SillyTavern (или сокращенно ST) - это локально уста
3636
37## Нужен ли мне мощный компьютер для запуска SillyTavern?37## Нужен ли мне мощный компьютер для запуска SillyTavern?
3838
39Требования к оборудованию минимальны: он будет работать на всем, что может запустить NodeJS 18 или выше. Если вы собираетесь выполнять инференс LLM на своем локальном компьютере, мы рекомендуем видеокарту NVIDIA 3000-й серии с не менее чем 6 ГБ видеопамяти, но фактические требования могут варьироваться в зависимости от модели и используемого вами бэкенда.39Требования к оборудованию минимальны: он будет работать на всем, что может запустить NodeJS 20 или выше. Если вы собираетесь выполнять инференс LLM на своем локальном компьютере, мы рекомендуем видеокарту NVIDIA 3000-й серии с не менее чем 6 ГБ видеопамяти, но фактические требования могут варьироваться в зависимости от модели и используемого вами бэкенда.
4040
41## Вопросы или предложения?41## Вопросы или предложения?
4242
.github/readme-zh_cn.md+1 -1
@@ -36,7 +36,7 @@ SillyTavern 于 2023 年 2 月作为 TavernAI 1.2.8 的一个分支开始,如
3636
37## 我需要一台性能强大的电脑来运行 SillyTavern 吗?37## 我需要一台性能强大的电脑来运行 SillyTavern 吗?
3838
39硬件要求很低:任何可以运行 NodeJS 18 或更高版本的设备都可以运行它。如果您打算在本地计算机上进行 LLM 推理,我们建议使用至少具有 6GB VRAM 的 3000 系列 NVIDIA 显卡,但实际要求可能会根据模型和您使用的后端而有所不同。39硬件要求很低:任何可以运行 NodeJS 20 或更高版本的设备都可以运行它。如果您打算在本地计算机上进行 LLM 推理,我们建议使用至少具有 6GB VRAM 的 3000 系列 NVIDIA 显卡,但实际要求可能会根据模型和您使用的后端而有所不同。
4040
41## 有问题或建议?41## 有问题或建议?
4242
.github/readme-zh_tw.md+1 -1
@@ -36,7 +36,7 @@ SillyTavern 起源於 2023 年 2 月,作為 TavernAI 1.2.8 的分支版本發
3636
37## 我需要高效能電腦才能運行 SillyTavern 嗎?37## 我需要高效能電腦才能運行 SillyTavern 嗎?
3838
39SillyTavern 的硬體需求相當低。任何能夠運行 NodeJS 18 或更高版本的設備都可以執行。若您打算在本地機器上進行 LLM 推理,我們建議使用擁有至少 6GB VRAM 的 3000 系列 NVIDIA 顯示卡,但實際需求可能因模型和您使用的後端而異。39SillyTavern 的硬體需求相當低。任何能夠運行 NodeJS 20 或更高版本的設備都可以執行。若您打算在本地機器上進行 LLM 推理,我們建議使用擁有至少 6GB VRAM 的 3000 系列 NVIDIA 顯示卡,但實際需求可能因模型和您使用的後端而異。
4040
41## 有任何問題或建議?41## 有任何問題或建議?
4242
.github/readme.md+1 -1
@@ -33,7 +33,7 @@ Beginning in February 2023 as a fork of TavernAI 1.2.8, SillyTavern now has over
3333
34## Do I need a powerful PC to run SillyTavern?34## Do I need a powerful PC to run SillyTavern?
3535
36The hardware requirements are minimal: it will run on anything that can run NodeJS 18 or higher. If you intend to do LLM inference on your local machine, we recommend a 3000-series NVIDIA graphics card with at least 6GB of VRAM, but actual requirements may vary depending on the model and backend you choose to use.36The hardware requirements are minimal: it will run on anything that can run NodeJS 20 or higher. If you intend to do LLM inference on your local machine, we recommend a 3000-series NVIDIA graphics card with at least 6GB of VRAM, but actual requirements may vary depending on the model and backend you choose to use.
3737
38## Questions or suggestions?38## Questions or suggestions?
3939
default/config.yaml+24 -1
@@ -165,6 +165,8 @@ rateLimiting:
165165
166## BACKUP CONFIGURATION166## BACKUP CONFIGURATION
167backups:167backups:
168 # Allow users to create a full backup archive of their data
169 allowFullDataBackup: true
168 # Common settings for all backup types170 # Common settings for all backup types
169 common:171 common:
170 # Number of backups to keep for each chat and settings file172 # Number of backups to keep for each chat and settings file
@@ -202,6 +204,16 @@ performance:
202 memoryCacheCapacity: '100mb'204 memoryCacheCapacity: '100mb'
203 # Enables disk caching for character cards. Improves performances with large card libraries.205 # Enables disk caching for character cards. Improves performances with large card libraries.
204 useDiskCache: true206 useDiskCache: true
207 # Configures gzip compression for client requests with large payloads (e.g. settings or chat saves).
208 requestCompression:
209 # Enable request compression.
210 enabled: false
211 # Minimum payload size to trigger compression. Set to 0 to compress all requests regardless of size.
212 minPayloadSize: '256kb'
213 # Hard upper payload size limit for compression. Set to 0 to allow compression of any size.
214 maxPayloadSize: '8mb'
215 # Timeout for request compression in milliseconds.
216 timeout: 4000
205217
206# CACHE BUSTER CONFIGURATION218# CACHE BUSTER CONFIGURATION
207# IMPORTANT: Requires localhost or a domain with HTTPS, otherwise will not work!219# IMPORTANT: Requires localhost or a domain with HTTPS, otherwise will not work!
@@ -250,6 +262,13 @@ extensions:
250 speechToText: Xenova/whisper-small262 speechToText: Xenova/whisper-small
251 textToSpeech: Xenova/speecht5_tts263 textToSpeech: Xenova/speecht5_tts
252264
265# Git backend for plugin/extension repository operations.
266# - Use "auto" to prefer system git, falling back to the integrated backend
267# - Use "system" to force system backend
268# - Use "builtin" to force integrated backend
269git:
270 backend: auto
271
253# Additional model tokenizers can be downloaded on demand.272# Additional model tokenizers can be downloaded on demand.
254# Disabling will fallback to another locally available tokenizer.273# Disabling will fallback to another locally available tokenizer.
255enableDownloadableTokenizers: true274enableDownloadableTokenizers: true
@@ -293,7 +312,7 @@ claude:
293 # Otherwise, you'll just waste money on cache misses.312 # Otherwise, you'll just waste money on cache misses.
294 enableSystemPromptCache: false313 enableSystemPromptCache: false
295 # Enables caching of the message history at depth (if supported).314 # Enables caching of the message history at depth (if supported).
296 # https://docs.anthropic.com/en/docs/build-with-claude/prompt-caching315 # https://platform.claude.com/docs/en/build-with-claude/prompt-caching
297 # -- IMPORTANT! --316 # -- IMPORTANT! --
298 # Use with caution. Behavior may be unpredictable and no guarantees can or will be made.317 # Use with caution. Behavior may be unpredictable and no guarantees can or will be made.
299 # Set to an integer to specify the desired depth. 0 (which does NOT include the prefill)318 # Set to an integer to specify the desired depth. 0 (which does NOT include the prefill)
@@ -304,6 +323,10 @@ claude:
304 ## 5m: base price x 1.25323 ## 5m: base price x 1.25
305 ## 1h: base price x 2324 ## 1h: base price x 2
306 extendedTTL: false325 extendedTTL: false
326 # Enables adaptive thinking for supported models (Opus 4.6+).
327 # Disable to enforce legacy thinking mode (with thinking budget).
328 # https://platform.claude.com/docs/en/build-with-claude/adaptive-thinking
329 enableAdaptiveThinking: false
307# -- GOOGLE GEMINI API CONFIGURATION --330# -- GOOGLE GEMINI API CONFIGURATION --
308gemini:331gemini:
309 # API endpoint version ("v1beta" or "v1alpha")332 # API endpoint version ("v1beta" or "v1alpha")
default/content/index.json+4 -0
@@ -638,5 +638,9 @@
638 {638 {
639 "filename": "presets/reasoning/OpenAI Harmony.json",639 "filename": "presets/reasoning/OpenAI Harmony.json",
640 "type": "reasoning"640 "type": "reasoning"
641 },
642 {
643 "filename": "presets/reasoning/Think XML.json",
644 "type": "reasoning"
641 }645 }
642]646]
default/content/presets/instruct/KoboldAI.json+1 -1
@@ -1,6 +1,6 @@
1{1{
2 "input_sequence": "{{[INPUT]}}",2 "input_sequence": "{{[INPUT]}}",
3 "output_sequence": "{{[OUPUT]}}",3 "output_sequence": "{{[OUTPUT]}}",
4 "last_output_sequence": "",4 "last_output_sequence": "",
5 "system_sequence": "{{[SYSTEM]}}",5 "system_sequence": "{{[SYSTEM]}}",
6 "stop_sequence": "",6 "stop_sequence": "",
default/content/presets/reasoning/Think XML.json+6 -0
@@ -0,0 +1,6 @@
1{
2 "name": "Think XML",
3 "prefix": "<think>",
4 "suffix": "</think>",
5 "separator": "\n"
6}
default/content/settings.json+1 -1
@@ -191,7 +191,7 @@
191 "custom_stopping_strings_macro": true,191 "custom_stopping_strings_macro": true,
192 "fuzzy_search": true,192 "fuzzy_search": true,
193 "encode_tags": false,193 "encode_tags": false,
194 "experimental_macro_engine": false,194 "experimental_macro_engine": true,
195 "enableLabMode": false,195 "enableLabMode": false,
196 "enableZenSliders": false,196 "enableZenSliders": false,
197 "ui_mode": 1,197 "ui_mode": 1,
index.d.ts+1 -1
@@ -40,7 +40,7 @@ declare global {
40 /**40 /**
41 * Authenticated user handle.41 * Authenticated user handle.
42 */42 */
43 handle: string;43 handle: string | null;
44 /**44 /**
45 * Last time the session was extended.45 * Last time the session was extended.
46 */46 */
package-lock.json+445 -24
@@ -1,12 +1,12 @@
1{1{
2 "name": "sillytavern",2 "name": "sillytavern",
3 "version": "1.16.0",3 "version": "1.17.0",
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.16.0",9 "version": "1.17.0",
10 "hasInstallScript": true,10 "hasInstallScript": true,
11 "license": "AGPL-3.0",11 "license": "AGPL-3.0",
12 "dependencies": {12 "dependencies": {
@@ -58,10 +58,11 @@
58 "droll": "^0.2.1",58 "droll": "^0.2.1",
59 "env-paths": "^3.0.0",59 "env-paths": "^3.0.0",
60 "express": "^4.21.0",60 "express": "^4.21.0",
61 "fflate": "^0.8.2",
61 "form-data": "^4.0.4",62 "form-data": "^4.0.4",
62 "fuse.js": "^7.1.0",63 "fuse.js": "^7.1.0",
63 "google-translate-api-x": "^10.7.2",64 "google-translate-api-x": "^10.7.2",
64 "handlebars": "^4.7.8",65 "handlebars": "^4.7.9",
65 "helmet": "^8.1.0",66 "helmet": "^8.1.0",
66 "highlight.js": "^11.11.1",67 "highlight.js": "^11.11.1",
67 "host-validation-middleware": "^0.1.1",68 "host-validation-middleware": "^0.1.1",
@@ -72,6 +73,7 @@
72 "ip-regex": "^5.0.0",73 "ip-regex": "^5.0.0",
73 "ipaddr.js": "^2.2.0",74 "ipaddr.js": "^2.2.0",
74 "is-docker": "^3.0.0",75 "is-docker": "^3.0.0",
76 "isomorphic-git": "^1.36.3",
75 "localforage": "^1.10.0",77 "localforage": "^1.10.0",
76 "lodash": "^4.17.21",78 "lodash": "^4.17.21",
77 "mime-types": "^3.0.2",79 "mime-types": "^3.0.2",
@@ -99,7 +101,7 @@
99 "webpack": "^5.105.4",101 "webpack": "^5.105.4",
100 "write-file-atomic": "^5.0.1",102 "write-file-atomic": "^5.0.1",
101 "ws": "^8.18.3",103 "ws": "^8.18.3",
102 "yaml": "^2.8.1",104 "yaml": "^2.8.3",
103 "yargs": "^17.7.1",105 "yargs": "^17.7.1",
104 "yauzl": "^3.2.1"106 "yauzl": "^3.2.1"
105 },107 },
@@ -140,7 +142,7 @@
140 "eslint-plugin-playwright": "^2.3.0"142 "eslint-plugin-playwright": "^2.3.0"
141 },143 },
142 "engines": {144 "engines": {
143 "node": ">= 18"145 "node": ">= 20"
144 }146 }
145 },147 },
146 "node_modules/@aashutoshrathi/word-wrap": {148 "node_modules/@aashutoshrathi/word-wrap": {
@@ -2982,12 +2984,33 @@
2982 "resolved": "https://registry.npmjs.org/async/-/async-3.2.5.tgz",2984 "resolved": "https://registry.npmjs.org/async/-/async-3.2.5.tgz",
2983 "integrity": "sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg=="2985 "integrity": "sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg=="
2984 },2986 },
2987 "node_modules/async-lock": {
2988 "version": "1.4.1",
2989 "resolved": "https://registry.npmjs.org/async-lock/-/async-lock-1.4.1.tgz",
2990 "integrity": "sha512-Az2ZTpuytrtqENulXwO3GGv1Bztugx6TT37NIo7imr/Qo0gsYiGtSdBa2B6fsXhTpVZDNfu1Qn3pk531e3q+nQ==",
2991 "license": "MIT"
2992 },
2985 "node_modules/asynckit": {2993 "node_modules/asynckit": {
2986 "version": "0.4.0",2994 "version": "0.4.0",
2987 "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",2995 "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
2988 "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",2996 "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
2989 "license": "MIT"2997 "license": "MIT"
2990 },2998 },
2999 "node_modules/available-typed-arrays": {
3000 "version": "1.0.7",
3001 "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz",
3002 "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==",
3003 "license": "MIT",
3004 "dependencies": {
3005 "possible-typed-array-names": "^1.0.0"
3006 },
3007 "engines": {
3008 "node": ">= 0.4"
3009 },
3010 "funding": {
3011 "url": "https://github.com/sponsors/ljharb"
3012 }
3013 },
2991 "node_modules/await-to-js": {3014 "node_modules/await-to-js": {
2992 "version": "3.0.0",3015 "version": "3.0.0",
2993 "resolved": "https://registry.npmjs.org/await-to-js/-/await-to-js-3.0.0.tgz",3016 "resolved": "https://registry.npmjs.org/await-to-js/-/await-to-js-3.0.0.tgz",
@@ -3334,6 +3357,24 @@
3334 "node": ">=8"3357 "node": ">=8"
3335 }3358 }
3336 },3359 },
3360 "node_modules/call-bind": {
3361 "version": "1.0.8",
3362 "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz",
3363 "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==",
3364 "license": "MIT",
3365 "dependencies": {
3366 "call-bind-apply-helpers": "^1.0.0",
3367 "es-define-property": "^1.0.0",
3368 "get-intrinsic": "^1.2.4",
3369 "set-function-length": "^1.2.2"
3370 },
3371 "engines": {
3372 "node": ">= 0.4"
3373 },
3374 "funding": {
3375 "url": "https://github.com/sponsors/ljharb"
3376 }
3377 },
3337 "node_modules/call-bind-apply-helpers": {3378 "node_modules/call-bind-apply-helpers": {
3338 "version": "1.0.2",3379 "version": "1.0.2",
3339 "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",3380 "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
@@ -3484,6 +3525,12 @@
3484 "node": ">=6.0"3525 "node": ">=6.0"
3485 }3526 }
3486 },3527 },
3528 "node_modules/clean-git-ref": {
3529 "version": "2.0.1",
3530 "resolved": "https://registry.npmjs.org/clean-git-ref/-/clean-git-ref-2.0.1.tgz",
3531 "integrity": "sha512-bLSptAy2P0s6hU4PzuIMKmMJJSE6gLXGH1cntDu7bWJUksvuM+7ReOK61mozULErYvP6a15rnYl0zFDef+pyPw==",
3532 "license": "Apache-2.0"
3533 },
3487 "node_modules/cliui": {3534 "node_modules/cliui": {
3488 "version": "8.0.1",3535 "version": "8.0.1",
3489 "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",3536 "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
@@ -4109,6 +4156,23 @@
4109 "node": ">=10"4156 "node": ">=10"
4110 }4157 }
4111 },4158 },
4159 "node_modules/define-data-property": {
4160 "version": "1.1.4",
4161 "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz",
4162 "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==",
4163 "license": "MIT",
4164 "dependencies": {
4165 "es-define-property": "^1.0.0",
4166 "es-errors": "^1.3.0",
4167 "gopd": "^1.0.1"
4168 },
4169 "engines": {
4170 "node": ">= 0.4"
4171 },
4172 "funding": {
4173 "url": "https://github.com/sponsors/ljharb"
4174 }
4175 },
4112 "node_modules/define-lazy-prop": {4176 "node_modules/define-lazy-prop": {
4113 "version": "3.0.0",4177 "version": "3.0.0",
4114 "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz",4178 "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz",
@@ -4169,6 +4233,12 @@
4169 "integrity": "sha512-IayShXAgj/QMXgB0IWmKx+rOPuGMhqm5w6jvFxmVenXKIzRqTAAsbBPT3kWQeGANj3jGgvcvv4yK6SxqYmikgw==",4233 "integrity": "sha512-IayShXAgj/QMXgB0IWmKx+rOPuGMhqm5w6jvFxmVenXKIzRqTAAsbBPT3kWQeGANj3jGgvcvv4yK6SxqYmikgw==",
4170 "license": "Apache-2.0"4234 "license": "Apache-2.0"
4171 },4235 },
4236 "node_modules/diff3": {
4237 "version": "0.0.3",
4238 "resolved": "https://registry.npmjs.org/diff3/-/diff3-0.0.3.tgz",
4239 "integrity": "sha512-iSq8ngPOt0K53A6eVr4d5Kn6GNrM2nQZtC740pzIriHtn4pOQ2lyzEXQMBeVcWERN0ye7fhBsk9PbLLQOnUx/g==",
4240 "license": "MIT"
4241 },
4172 "node_modules/digest-fetch": {4242 "node_modules/digest-fetch": {
4173 "version": "1.3.0",4243 "version": "1.3.0",
4174 "resolved": "https://registry.npmjs.org/digest-fetch/-/digest-fetch-1.3.0.tgz",4244 "resolved": "https://registry.npmjs.org/digest-fetch/-/digest-fetch-1.3.0.tgz",
@@ -4252,10 +4322,13 @@
4252 }4322 }
4253 },4323 },
4254 "node_modules/dompurify": {4324 "node_modules/dompurify": {
4255 "version": "3.2.6",4325 "version": "3.3.2",
4256 "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.6.tgz",4326 "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.3.2.tgz",
4257 "integrity": "sha512-/2GogDQlohXPZe6D6NOgQvXLPSYBqIWMnZ8zzOhn09REE4eyAzb+Hed3jhoM9OkuaJ8P6ZGTTVWQKAi8ieIzfQ==",4327 "integrity": "sha512-6obghkliLdmKa56xdbLOpUZ43pAR6xFy1uOrxBaIDjT+yaRuuybLjGS9eVBoSR/UPU5fq3OXClEHLJNGvbxKpQ==",
4258 "license": "(MPL-2.0 OR Apache-2.0)",4328 "license": "(MPL-2.0 OR Apache-2.0)",
4329 "engines": {
4330 "node": ">=20"
4331 },
4259 "optionalDependencies": {4332 "optionalDependencies": {
4260 "@types/trusted-types": "^2.0.7"4333 "@types/trusted-types": "^2.0.7"
4261 }4334 }
@@ -5023,6 +5096,12 @@
5023 "node": "^12.20 || >= 14.13"5096 "node": "^12.20 || >= 14.13"
5024 }5097 }
5025 },5098 },
5099 "node_modules/fflate": {
5100 "version": "0.8.2",
5101 "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz",
5102 "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==",
5103 "license": "MIT"
5104 },
5026 "node_modules/file-entry-cache": {5105 "node_modules/file-entry-cache": {
5027 "version": "6.0.1",5106 "version": "6.0.1",
5028 "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz",5107 "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz",
@@ -5123,9 +5202,9 @@
5123 "license": "SEE LICENSE IN LICENSE.txt"5202 "license": "SEE LICENSE IN LICENSE.txt"
5124 },5203 },
5125 "node_modules/flatted": {5204 "node_modules/flatted": {
5126 "version": "3.4.1",5205 "version": "3.4.2",
5127 "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.1.tgz",5206 "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz",
5128 "integrity": "sha512-IxfVbRFVlV8V/yRaGzk0UVIcsKKHMSfYw66T/u4nTwlWteQePsxe//LjudR1AMX4tZW3WFCh3Zqa/sjlqpbURQ==",5207 "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==",
5129 "dev": true,5208 "dev": true,
5130 "license": "ISC"5209 "license": "ISC"
5131 },5210 },
@@ -5149,6 +5228,21 @@
5149 }5228 }
5150 }5229 }
5151 },5230 },
5231 "node_modules/for-each": {
5232 "version": "0.3.5",
5233 "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz",
5234 "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==",
5235 "license": "MIT",
5236 "dependencies": {
5237 "is-callable": "^1.2.7"
5238 },
5239 "engines": {
5240 "node": ">= 0.4"
5241 },
5242 "funding": {
5243 "url": "https://github.com/sponsors/ljharb"
5244 }
5245 },
5152 "node_modules/foreground-child": {5246 "node_modules/foreground-child": {
5153 "version": "3.1.1",5247 "version": "3.1.1",
5154 "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz",5248 "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz",
@@ -5545,9 +5639,9 @@
5545 "license": "ISC"5639 "license": "ISC"
5546 },5640 },
5547 "node_modules/handlebars": {5641 "node_modules/handlebars": {
5548 "version": "4.7.8",5642 "version": "4.7.9",
5549 "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz",5643 "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.9.tgz",
5550 "integrity": "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==",5644 "integrity": "sha512-4E71E0rpOaQuJR2A3xDZ+GM1HyWYv1clR58tC8emQNeQe3RH7MAzSbat+V0wG78LQBo6m6bzSG/L4pBuCsgnUQ==",
5551 "license": "MIT",5645 "license": "MIT",
5552 "dependencies": {5646 "dependencies": {
5553 "minimist": "^1.2.5",5647 "minimist": "^1.2.5",
@@ -5574,6 +5668,18 @@
5574 "node": ">=8"5668 "node": ">=8"
5575 }5669 }
5576 },5670 },
5671 "node_modules/has-property-descriptors": {
5672 "version": "1.0.2",
5673 "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz",
5674 "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==",
5675 "license": "MIT",
5676 "dependencies": {
5677 "es-define-property": "^1.0.0"
5678 },
5679 "funding": {
5680 "url": "https://github.com/sponsors/ljharb"
5681 }
5682 },
5577 "node_modules/has-symbols": {5683 "node_modules/has-symbols": {
5578 "version": "1.1.0",5684 "version": "1.1.0",
5579 "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",5685 "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
@@ -5827,7 +5933,6 @@
5827 "version": "5.2.4",5933 "version": "5.2.4",
5828 "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz",5934 "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz",
5829 "integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==",5935 "integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==",
5830 "dev": true,
5831 "license": "MIT",5936 "license": "MIT",
5832 "engines": {5937 "engines": {
5833 "node": ">= 4"5938 "node": ">= 4"
@@ -5955,6 +6060,18 @@
5955 "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==",6060 "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==",
5956 "license": "MIT"6061 "license": "MIT"
5957 },6062 },
6063 "node_modules/is-callable": {
6064 "version": "1.2.7",
6065 "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz",
6066 "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==",
6067 "license": "MIT",
6068 "engines": {
6069 "node": ">= 0.4"
6070 },
6071 "funding": {
6072 "url": "https://github.com/sponsors/ljharb"
6073 }
6074 },
5958 "node_modules/is-docker": {6075 "node_modules/is-docker": {
5959 "version": "3.0.0",6076 "version": "3.0.0",
5960 "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz",6077 "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz",
@@ -6057,6 +6174,21 @@
6057 "url": "https://github.com/sponsors/sindresorhus"6174 "url": "https://github.com/sponsors/sindresorhus"
6058 }6175 }
6059 },6176 },
6177 "node_modules/is-typed-array": {
6178 "version": "1.1.15",
6179 "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz",
6180 "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==",
6181 "license": "MIT",
6182 "dependencies": {
6183 "which-typed-array": "^1.1.16"
6184 },
6185 "engines": {
6186 "node": ">= 0.4"
6187 },
6188 "funding": {
6189 "url": "https://github.com/sponsors/ljharb"
6190 }
6191 },
6060 "node_modules/is-wsl": {6192 "node_modules/is-wsl": {
6061 "version": "3.1.0",6193 "version": "3.1.0",
6062 "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.0.tgz",6194 "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.0.tgz",
@@ -6114,6 +6246,88 @@
6114 }6246 }
6115 }6247 }
6116 },6248 },
6249 "node_modules/isomorphic-git": {
6250 "version": "1.37.2",
6251 "resolved": "https://registry.npmjs.org/isomorphic-git/-/isomorphic-git-1.37.2.tgz",
6252 "integrity": "sha512-HCQBBKmXIMPdHgYGstSBNp6MNmVcMQBbUqJF8xfywFmlpNseO4KKex59YlXqNxhRxmv3fUZwvNWvMyOdc1VvhA==",
6253 "license": "MIT",
6254 "dependencies": {
6255 "async-lock": "^1.4.1",
6256 "clean-git-ref": "^2.0.1",
6257 "crc-32": "^1.2.0",
6258 "diff3": "0.0.3",
6259 "ignore": "^5.1.4",
6260 "minimisted": "^2.0.0",
6261 "pako": "^1.0.10",
6262 "pify": "^4.0.1",
6263 "readable-stream": "^4.0.0",
6264 "sha.js": "^2.4.12",
6265 "simple-get": "^4.0.1"
6266 },
6267 "bin": {
6268 "isogit": "cli.cjs"
6269 },
6270 "engines": {
6271 "node": ">=14.17"
6272 }
6273 },
6274 "node_modules/isomorphic-git/node_modules/crc-32": {
6275 "version": "1.2.2",
6276 "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz",
6277 "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==",
6278 "license": "Apache-2.0",
6279 "bin": {
6280 "crc32": "bin/crc32.njs"
6281 },
6282 "engines": {
6283 "node": ">=0.8"
6284 }
6285 },
6286 "node_modules/isomorphic-git/node_modules/readable-stream": {
6287 "version": "4.7.0",
6288 "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz",
6289 "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==",
6290 "license": "MIT",
6291 "dependencies": {
6292 "abort-controller": "^3.0.0",
6293 "buffer": "^6.0.3",
6294 "events": "^3.3.0",
6295 "process": "^0.11.10",
6296 "string_decoder": "^1.3.0"
6297 },
6298 "engines": {
6299 "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
6300 }
6301 },
6302 "node_modules/isomorphic-git/node_modules/safe-buffer": {
6303 "version": "5.2.1",
6304 "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
6305 "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
6306 "funding": [
6307 {
6308 "type": "github",
6309 "url": "https://github.com/sponsors/feross"
6310 },
6311 {
6312 "type": "patreon",
6313 "url": "https://www.patreon.com/feross"
6314 },
6315 {
6316 "type": "consulting",
6317 "url": "https://feross.org/support"
6318 }
6319 ],
6320 "license": "MIT"
6321 },
6322 "node_modules/isomorphic-git/node_modules/string_decoder": {
6323 "version": "1.3.0",
6324 "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
6325 "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
6326 "license": "MIT",
6327 "dependencies": {
6328 "safe-buffer": "~5.2.0"
6329 }
6330 },
6117 "node_modules/jackspeak": {6331 "node_modules/jackspeak": {
6118 "version": "3.4.3",6332 "version": "3.4.3",
6119 "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz",6333 "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz",
@@ -6611,6 +6825,15 @@
6611 "url": "https://github.com/sponsors/ljharb"6825 "url": "https://github.com/sponsors/ljharb"
6612 }6826 }
6613 },6827 },
6828 "node_modules/minimisted": {
6829 "version": "2.0.1",
6830 "resolved": "https://registry.npmjs.org/minimisted/-/minimisted-2.0.1.tgz",
6831 "integrity": "sha512-1oPjfuLQa2caorJUM8HV8lGgWCc0qqAO1MNv/k05G4qslmsndV/5WdNZrqCiyqiz3wohia2Ij2B7w2Dr7/IyrA==",
6832 "license": "MIT",
6833 "dependencies": {
6834 "minimist": "^1.2.5"
6835 }
6836 },
6614 "node_modules/minipass": {6837 "node_modules/minipass": {
6615 "version": "7.1.2",6838 "version": "7.1.2",
6616 "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",6839 "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
@@ -7232,9 +7455,9 @@
7232 }7455 }
7233 },7456 },
7234 "node_modules/path-to-regexp": {7457 "node_modules/path-to-regexp": {
7235 "version": "0.1.12",7458 "version": "0.1.13",
7236 "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz",7459 "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz",
7237 "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==",7460 "integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==",
7238 "license": "MIT"7461 "license": "MIT"
7239 },7462 },
7240 "node_modules/path-type": {7463 "node_modules/path-type": {
@@ -7285,9 +7508,9 @@
7285 "license": "ISC"7508 "license": "ISC"
7286 },7509 },
7287 "node_modules/picomatch": {7510 "node_modules/picomatch": {
7288 "version": "2.3.1",7511 "version": "2.3.2",
7289 "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",7512 "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz",
7290 "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",7513 "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==",
7291 "dev": true,7514 "dev": true,
7292 "license": "MIT",7515 "license": "MIT",
7293 "engines": {7516 "engines": {
@@ -7297,6 +7520,15 @@
7297 "url": "https://github.com/sponsors/jonschlinkert"7520 "url": "https://github.com/sponsors/jonschlinkert"
7298 }7521 }
7299 },7522 },
7523 "node_modules/pify": {
7524 "version": "4.0.1",
7525 "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz",
7526 "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==",
7527 "license": "MIT",
7528 "engines": {
7529 "node": ">=6"
7530 }
7531 },
7300 "node_modules/platform": {7532 "node_modules/platform": {
7301 "version": "1.3.6",7533 "version": "1.3.6",
7302 "resolved": "https://registry.npmjs.org/platform/-/platform-1.3.6.tgz",7534 "resolved": "https://registry.npmjs.org/platform/-/platform-1.3.6.tgz",
@@ -7327,6 +7559,15 @@
7327 "node": ">=14.19.0"7559 "node": ">=14.19.0"
7328 }7560 }
7329 },7561 },
7562 "node_modules/possible-typed-array-names": {
7563 "version": "1.1.0",
7564 "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz",
7565 "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==",
7566 "license": "MIT",
7567 "engines": {
7568 "node": ">= 0.4"
7569 }
7570 },
7330 "node_modules/prelude-ls": {7571 "node_modules/prelude-ls": {
7331 "version": "1.2.1",7572 "version": "1.2.1",
7332 "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",7573 "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
@@ -7987,12 +8228,69 @@
7987 "node": ">= 0.8.0"8228 "node": ">= 0.8.0"
7988 }8229 }
7989 },8230 },
8231 "node_modules/set-function-length": {
8232 "version": "1.2.2",
8233 "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",
8234 "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==",
8235 "license": "MIT",
8236 "dependencies": {
8237 "define-data-property": "^1.1.4",
8238 "es-errors": "^1.3.0",
8239 "function-bind": "^1.1.2",
8240 "get-intrinsic": "^1.2.4",
8241 "gopd": "^1.0.1",
8242 "has-property-descriptors": "^1.0.2"
8243 },
8244 "engines": {
8245 "node": ">= 0.4"
8246 }
8247 },
7990 "node_modules/setprototypeof": {8248 "node_modules/setprototypeof": {
7991 "version": "1.2.0",8249 "version": "1.2.0",
7992 "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",8250 "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
7993 "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",8251 "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
7994 "license": "ISC"8252 "license": "ISC"
7995 },8253 },
8254 "node_modules/sha.js": {
8255 "version": "2.4.12",
8256 "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.12.tgz",
8257 "integrity": "sha512-8LzC5+bvI45BjpfXU8V5fdU2mfeKiQe1D1gIMn7XUlF3OTUrpdJpPPH4EMAnF0DsHHdSZqCdSss5qCmJKuiO3w==",
8258 "license": "(MIT AND BSD-3-Clause)",
8259 "dependencies": {
8260 "inherits": "^2.0.4",
8261 "safe-buffer": "^5.2.1",
8262 "to-buffer": "^1.2.0"
8263 },
8264 "bin": {
8265 "sha.js": "bin.js"
8266 },
8267 "engines": {
8268 "node": ">= 0.10"
8269 },
8270 "funding": {
8271 "url": "https://github.com/sponsors/ljharb"
8272 }
8273 },
8274 "node_modules/sha.js/node_modules/safe-buffer": {
8275 "version": "5.2.1",
8276 "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
8277 "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
8278 "funding": [
8279 {
8280 "type": "github",
8281 "url": "https://github.com/sponsors/feross"
8282 },
8283 {
8284 "type": "patreon",
8285 "url": "https://www.patreon.com/feross"
8286 },
8287 {
8288 "type": "consulting",
8289 "url": "https://feross.org/support"
8290 }
8291 ],
8292 "license": "MIT"
8293 },
7996 "node_modules/shebang-command": {8294 "node_modules/shebang-command": {
7997 "version": "2.0.0",8295 "version": "2.0.0",
7998 "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",8296 "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
@@ -8163,6 +8461,51 @@
8163 "regenerator-runtime": "^0.13.3"8461 "regenerator-runtime": "^0.13.3"
8164 }8462 }
8165 },8463 },
8464 "node_modules/simple-concat": {
8465 "version": "1.0.1",
8466 "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz",
8467 "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==",
8468 "funding": [
8469 {
8470 "type": "github",
8471 "url": "https://github.com/sponsors/feross"
8472 },
8473 {
8474 "type": "patreon",
8475 "url": "https://www.patreon.com/feross"
8476 },
8477 {
8478 "type": "consulting",
8479 "url": "https://feross.org/support"
8480 }
8481 ],
8482 "license": "MIT"
8483 },
8484 "node_modules/simple-get": {
8485 "version": "4.0.1",
8486 "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz",
8487 "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==",
8488 "funding": [
8489 {
8490 "type": "github",
8491 "url": "https://github.com/sponsors/feross"
8492 },
8493 {
8494 "type": "patreon",
8495 "url": "https://www.patreon.com/feross"
8496 },
8497 {
8498 "type": "consulting",
8499 "url": "https://feross.org/support"
8500 }
8501 ],
8502 "license": "MIT",
8503 "dependencies": {
8504 "decompress-response": "^6.0.0",
8505 "once": "^1.3.1",
8506 "simple-concat": "^1.0.0"
8507 }
8508 },
8166 "node_modules/simple-git": {8509 "node_modules/simple-git": {
8167 "version": "3.33.0",8510 "version": "3.33.0",
8168 "resolved": "https://registry.npmjs.org/simple-git/-/simple-git-3.33.0.tgz",8511 "resolved": "https://registry.npmjs.org/simple-git/-/simple-git-3.33.0.tgz",
@@ -8586,6 +8929,46 @@
8586 "integrity": "sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==",8929 "integrity": "sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==",
8587 "license": "MIT"8930 "license": "MIT"
8588 },8931 },
8932 "node_modules/to-buffer": {
8933 "version": "1.2.2",
8934 "resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.2.2.tgz",
8935 "integrity": "sha512-db0E3UJjcFhpDhAF4tLo03oli3pwl3dbnzXOUIlRKrp+ldk/VUxzpWYZENsw2SZiuBjHAk7DfB0VU7NKdpb6sw==",
8936 "license": "MIT",
8937 "dependencies": {
8938 "isarray": "^2.0.5",
8939 "safe-buffer": "^5.2.1",
8940 "typed-array-buffer": "^1.0.3"
8941 },
8942 "engines": {
8943 "node": ">= 0.4"
8944 }
8945 },
8946 "node_modules/to-buffer/node_modules/isarray": {
8947 "version": "2.0.5",
8948 "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz",
8949 "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==",
8950 "license": "MIT"
8951 },
8952 "node_modules/to-buffer/node_modules/safe-buffer": {
8953 "version": "5.2.1",
8954 "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
8955 "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
8956 "funding": [
8957 {
8958 "type": "github",
8959 "url": "https://github.com/sponsors/feross"
8960 },
8961 {
8962 "type": "patreon",
8963 "url": "https://www.patreon.com/feross"
8964 },
8965 {
8966 "type": "consulting",
8967 "url": "https://feross.org/support"
8968 }
8969 ],
8970 "license": "MIT"
8971 },
8589 "node_modules/to-regex-range": {8972 "node_modules/to-regex-range": {
8590 "version": "5.0.1",8973 "version": "5.0.1",
8591 "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",8974 "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
@@ -8734,6 +9117,20 @@
8734 "node": ">= 0.6"9117 "node": ">= 0.6"
8735 }9118 }
8736 },9119 },
9120 "node_modules/typed-array-buffer": {
9121 "version": "1.0.3",
9122 "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz",
9123 "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==",
9124 "license": "MIT",
9125 "dependencies": {
9126 "call-bound": "^1.0.3",
9127 "es-errors": "^1.3.0",
9128 "is-typed-array": "^1.1.14"
9129 },
9130 "engines": {
9131 "node": ">= 0.4"
9132 }
9133 },
8737 "node_modules/typedarray": {9134 "node_modules/typedarray": {
8738 "version": "0.0.6",9135 "version": "0.0.6",
8739 "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz",9136 "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz",
@@ -9082,6 +9479,27 @@
9082 "node": ">= 8"9479 "node": ">= 8"
9083 }9480 }
9084 },9481 },
9482 "node_modules/which-typed-array": {
9483 "version": "1.1.20",
9484 "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.20.tgz",
9485 "integrity": "sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg==",
9486 "license": "MIT",
9487 "dependencies": {
9488 "available-typed-arrays": "^1.0.7",
9489 "call-bind": "^1.0.8",
9490 "call-bound": "^1.0.4",
9491 "for-each": "^0.3.5",
9492 "get-proto": "^1.0.1",
9493 "gopd": "^1.2.0",
9494 "has-tostringtag": "^1.0.2"
9495 },
9496 "engines": {
9497 "node": ">= 0.4"
9498 },
9499 "funding": {
9500 "url": "https://github.com/sponsors/ljharb"
9501 }
9502 },
9085 "node_modules/wordwrap": {9503 "node_modules/wordwrap": {
9086 "version": "1.0.0",9504 "version": "1.0.0",
9087 "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz",9505 "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz",
@@ -9237,15 +9655,18 @@
9237 }9655 }
9238 },9656 },
9239 "node_modules/yaml": {9657 "node_modules/yaml": {
9240 "version": "2.8.1",9658 "version": "2.8.3",
9241 "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.1.tgz",9659 "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.3.tgz",
9242 "integrity": "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==",9660 "integrity": "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==",
9243 "license": "ISC",9661 "license": "ISC",
9244 "bin": {9662 "bin": {
9245 "yaml": "bin.mjs"9663 "yaml": "bin.mjs"
9246 },9664 },
9247 "engines": {9665 "engines": {
9248 "node": ">= 14.6"9666 "node": ">= 14.6"
9667 },
9668 "funding": {
9669 "url": "https://github.com/sponsors/eemeli"
9249 }9670 }
9250 },9671 },
9251 "node_modules/yargs": {9672 "node_modules/yargs": {
package.json+6 -4
@@ -48,10 +48,11 @@
48 "droll": "^0.2.1",48 "droll": "^0.2.1",
49 "env-paths": "^3.0.0",49 "env-paths": "^3.0.0",
50 "express": "^4.21.0",50 "express": "^4.21.0",
51 "fflate": "^0.8.2",
51 "form-data": "^4.0.4",52 "form-data": "^4.0.4",
52 "fuse.js": "^7.1.0",53 "fuse.js": "^7.1.0",
53 "google-translate-api-x": "^10.7.2",54 "google-translate-api-x": "^10.7.2",
54 "handlebars": "^4.7.8",55 "handlebars": "^4.7.9",
55 "helmet": "^8.1.0",56 "helmet": "^8.1.0",
56 "highlight.js": "^11.11.1",57 "highlight.js": "^11.11.1",
57 "host-validation-middleware": "^0.1.1",58 "host-validation-middleware": "^0.1.1",
@@ -62,6 +63,7 @@
62 "ip-regex": "^5.0.0",63 "ip-regex": "^5.0.0",
63 "ipaddr.js": "^2.2.0",64 "ipaddr.js": "^2.2.0",
64 "is-docker": "^3.0.0",65 "is-docker": "^3.0.0",
66 "isomorphic-git": "^1.36.3",
65 "localforage": "^1.10.0",67 "localforage": "^1.10.0",
66 "lodash": "^4.17.21",68 "lodash": "^4.17.21",
67 "mime-types": "^3.0.2",69 "mime-types": "^3.0.2",
@@ -89,12 +91,12 @@
89 "webpack": "^5.105.4",91 "webpack": "^5.105.4",
90 "write-file-atomic": "^5.0.1",92 "write-file-atomic": "^5.0.1",
91 "ws": "^8.18.3",93 "ws": "^8.18.3",
92 "yaml": "^2.8.1",94 "yaml": "^2.8.3",
93 "yargs": "^17.7.1",95 "yargs": "^17.7.1",
94 "yauzl": "^3.2.1"96 "yauzl": "^3.2.1"
95 },97 },
96 "engines": {98 "engines": {
97 "node": ">= 18"99 "node": ">= 20"
98 },100 },
99 "overrides": {101 "overrides": {
100 "vectra": {102 "vectra": {
@@ -111,7 +113,7 @@
111 "type": "git",113 "type": "git",
112 "url": "https://github.com/SillyTavern/SillyTavern.git"114 "url": "https://github.com/SillyTavern/SillyTavern.git"
113 },115 },
114 "version": "1.16.0",116 "version": "1.17.0",
115 "scripts": {117 "scripts": {
116 "start": "node server.js",118 "start": "node server.js",
117 "debug": "node --inspect server.js",119 "debug": "node --inspect server.js",
plugins.js+4 -3
@@ -9,11 +9,13 @@ import process from 'node:process';
9import { fileURLToPath } from 'node:url';9import { fileURLToPath } from 'node:url';
1010
11import { default as git, CheckRepoActions } from 'simple-git';11import { default as git, CheckRepoActions } from 'simple-git';
12import { createGitClient } from './src/git/client.js';
12import { color } from './src/util.js';13import { color } from './src/util.js';
1314
14const __dirname = import.meta.dirname ?? path.dirname(fileURLToPath(import.meta.url));15const __dirname = import.meta.dirname ?? path.dirname(fileURLToPath(import.meta.url));
15process.chdir(__dirname);16process.chdir(__dirname);
16const pluginsPath = './plugins';17const pluginsPath = './plugins';
18const gitBackend = process.env.SILLYTAVERN_GIT_BACKEND || 'auto';
1719
18const command = process.argv[2];20const command = process.argv[2];
1921
@@ -87,10 +89,9 @@ async function installPlugin(pluginName) {
87 return console.log(color.yellow(`Directory already exists at ${pluginPath}`));89 return console.log(color.yellow(`Directory already exists at ${pluginPath}`));
88 }90 }
8991
90 await git().clone(pluginName, pluginPath, { '--depth': 1 });92 await createGitClient({ backend: gitBackend }).clone(pluginName, pluginPath, { depth: 1 });
91 console.log(`Plugin ${color.green(pluginName)} installed to ${color.cyan(pluginPath)}`);93 console.log(`Plugin ${color.green(pluginName)} installed to ${color.cyan(pluginPath)}`);
92 }94 } catch (error) {
93 catch (error) {
94 console.error(color.red(`Failed to install plugin ${pluginName}`), error);95 console.error(color.red(`Failed to install plugin ${pluginName}`), error);
95 }96 }
96}97}
public/css/backgrounds.css+207 -16
@@ -107,7 +107,8 @@
107 opacity: 1;107 opacity: 1;
108}108}
109109
110.bg_example .mobile-only-menu-toggle {110.bg_example .mobile-only-menu-toggle,
111.bg_folder_tile .mobile-only-menu-toggle {
111 display: none;112 display: none;
112}113}
113114
@@ -135,19 +136,32 @@
135 outline-offset: 0;136 outline-offset: 0;
136}137}
137138
138.bg_example.locked-background::after {139.bg_example.locked-background::after,
139 content: '\f023';140.bg_example.selected-background::before,
141#Backgrounds.bg-selection-mode #bg_menu_content .bg_example.folder-group-selected::before {
140 font-family: 'Font Awesome 6 Free';142 font-family: 'Font Awesome 6 Free';
141 font-weight: 900;143 font-weight: 900;
142
143 position: absolute;144 position: absolute;
145 z-index: 4;
146 filter: drop-shadow(0 1px 3px rgba(0, 0, 0, 0.8));
147 pointer-events: none;
148}
149
150.bg_example.selected-background::before,
151#Backgrounds.bg-selection-mode #bg_menu_content .bg_example.folder-group-selected::before {
152 content: '\f00c';
153 top: 5px;
154 color: var(--white100);
155 font-size: calc(var(--mainFontSize) * 0.9);
156}
157
158.bg_example.locked-background::after {
159 content: '\f023';
144 bottom: 5px;160 bottom: 5px;
145 right: 5px;161 right: 5px;
146 z-index: 4;
147 color: var(--golden);162 color: var(--golden);
148 filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.8));163 filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.8));
149 font-size: calc(var(--mainFontSize) * 0.8);164 font-size: calc(var(--mainFontSize) * 0.8);
150 pointer-events: none;
151}165}
152166
153.bg_example:not(.locked-background) .jg-unlock,167.bg_example:not(.locked-background) .jg-unlock,
@@ -161,17 +175,28 @@
161}175}
162176
163.bg_example.selected-background::before {177.bg_example.selected-background::before {
164 content: '\f00c';
165 font-family: 'Font Awesome 6 Free';
166 font-weight: 900;
167 position: absolute;
168 top: 5px;
169 left: 5px;178 left: 5px;
170 z-index: 4;179}
171 color: var(--white100);180
172 filter: drop-shadow(0 1px 3px rgba(0, 0, 0, 0.8));181#bg_selection_mode_button.active {
173 font-size: calc(var(--mainFontSize) * 0.9);182 color: var(--golden);
174 pointer-events: none;183}
184
185#bg_group_select_count {
186 font-weight: 700;
187}
188
189#Backgrounds.bg-selection-mode #bg_menu_content .bg_example .jg-menu {
190 display: none;
191}
192
193#Backgrounds.bg-selection-mode #bg_menu_content .bg_example.folder-group-selected {
194 outline: 2px solid var(--interactable-outline-color);
195 outline-offset: 0;
196}
197
198#Backgrounds.bg-selection-mode #bg_menu_content .bg_example.folder-group-selected::before {
199 right: 5px;
175}200}
176201
177.bg_example .jg-menu {202.bg_example .jg-menu {
@@ -376,3 +401,169 @@
376#bg_custom_content:not(:empty)~#bg_chat_hint {401#bg_custom_content:not(:empty)~#bg_chat_hint {
377 display: none;402 display: none;
378}403}
404
405.bg_folder_grid.bg_list {
406 display: grid;
407 gap: 5px;
408 width: 100%;
409 grid-template-columns: repeat(calc(var(--bg-thumb-columns, 5) + 2), 1fr);
410 margin-bottom: 10px;
411}
412
413.bg_folder_grid:empty {
414 display: none;
415}
416
417.bg_folder_tile {
418 cursor: pointer;
419 position: relative;
420 overflow: hidden;
421 border-radius: 8px;
422 height: auto;
423 aspect-ratio: 1 / 1;
424 outline: 2px solid var(--SmartThemeBorderColor);
425 outline-offset: -1px;
426 box-shadow: 0 0 7px var(--black50a);
427 display: flex;
428 flex-direction: column;
429 align-items: center;
430 justify-content: center;
431 background-color: var(--SmartThemeBlurTintColor);
432 transition: filter var(--animation-duration) ease;
433}
434
435.bg_folder_tile:hover {
436 filter: brightness(1.1);
437}
438
439.bg_folder_tile_cover {
440 position: absolute;
441 top: 0;
442 left: 0;
443 right: 0;
444 bottom: 0;
445 background-size: cover;
446 background-position: center;
447 border-radius: inherit;
448}
449
450.bg_folder_tile_overlay {
451 position: absolute;
452 bottom: 0;
453 left: 0;
454 right: 0;
455 background: linear-gradient(transparent, rgba(0, 0, 0, 0.85));
456 color: var(--SmartThemeBodyColor);
457 padding: 6px 8px 4px;
458 display: flex;
459 align-items: center;
460 gap: 5px;
461 font-size: 0.85em;
462 font-weight: 600;
463 pointer-events: none;
464 border-radius: 0 0 8px 8px;
465}
466
467.bg_folder_tile_overlay i {
468 font-size: 0.9em;
469 opacity: 0.8;
470}
471
472.bg_folder_tile_name {
473 white-space: nowrap;
474 overflow: hidden;
475 text-overflow: ellipsis;
476}
477
478/* Folder tile action menu */
479.bg_folder_tile .bg_folder_tile_menu {
480 display: flex;
481 position: absolute;
482 top: 2px;
483 right: 2px;
484 background-color: rgba(0, 0, 0, 0.5);
485 border-radius: 5px;
486 padding: 3px;
487 z-index: 3;
488 backdrop-filter: blur(4px);
489 border: 1px solid var(--SmartThemeBorderColor);
490 align-items: center;
491
492 opacity: 0;
493 visibility: hidden;
494 transform: scale(0.9);
495 transform-origin: center;
496 transition: opacity var(--animation-duration) ease-out, visibility var(--animation-duration) ease-out, transform var(--animation-duration) ease-out;
497}
498
499.bg_folder_tile:hover .bg_folder_tile_menu,
500.bg_folder_tile:focus-within .bg_folder_tile_menu {
501 opacity: 1;
502 visibility: visible;
503 transform: scale(1);
504}
505
506.bg_folder_tile .jg-button {
507 display: flex;
508 width: 24px;
509 height: 24px;
510 align-items: center;
511 justify-content: center;
512 color: white;
513 padding: 5px;
514 font-size: 1.1em;
515 border-radius: 5px;
516 transition: background-color var(--animation-duration) ease;
517}
518
519.bg_folder_tile .jg-button:hover {
520 background-color: rgba(255, 255, 255, 0.2);
521}
522
523/* New Folder placeholder tile */
524.bg_new_folder_tile {
525 border: 2px dashed var(--SmartThemeBorderColor);
526 outline: none;
527 color: var(--SmartThemeBodyColor);
528 opacity: 0.6;
529 gap: 6px;
530 font-size: 0.85em;
531 font-weight: 600;
532}
533
534.bg_new_folder_tile:hover {
535 opacity: 1;
536 transform: scale(1.03);
537}
538
539/* Breadcrumb bar */
540.bg_folder_breadcrumb {
541 display: flex;
542 align-items: center;
543 gap: 8px;
544 padding: 5px 0;
545 margin-bottom: 5px;
546}
547
548.bg_current_folder_name {
549 font-weight: 600;
550 font-size: 1em;
551 color: var(--SmartThemeBodyColor);
552 white-space: nowrap;
553 overflow: hidden;
554 text-overflow: ellipsis;
555}
556
557#Backgrounds:not(.in-folder-view) .jg-set-cover {
558 display: none !important;
559}
560
561#Backgrounds.in-folder-view .jg-set-cover {
562 display: flex !important;
563}
564
565/* Hide folder actions on custom (chat-specific) backgrounds */
566.bg_example[custom="true"] .jg-folder,
567.bg_example[custom="true"] .jg-set-cover {
568 display: none !important;
569}
public/css/loader.css+57 -0
@@ -29,3 +29,60 @@
29 align-items: center;29 align-items: center;
30 justify-content: center;30 justify-content: center;
31}31}
32
33/* Action loader toast styles */
34.action-loader-toast {
35 display: flex;
36 align-items: center;
37 gap: 10px;
38 width: 100%;
39}
40
41.action-loader-message {
42 flex: 1;
43}
44
45.action-loader-stop {
46 cursor: pointer;
47 font-size: 1.2em;
48 opacity: 0.8;
49 transition: opacity 0.15s ease, color 0.15s ease;
50}
51
52.action-loader-stop:hover {
53 opacity: 1;
54 color: color-mix(in srgb, currentColor 40%, #e74c3c 60%);
55}
56
57/* Splash screen styles for branded loading */
58#loader.splash-screen {
59 display: flex;
60 flex-direction: column;
61 align-items: center;
62 justify-content: center;
63 gap: 4rem;
64 width: 100%;
65 height: 100%;
66 transition: all var(--animation-duration-2x) ease-out;
67}
68
69#loader.splash-screen #load-spinner {
70 position: static;
71 top: unset;
72 left: unset;
73}
74
75.splash-logo {
76 /* max original px, or 50% on mobile */
77 width: min(150px, 50%);
78 height: auto;
79 filter: drop-shadow(0 4px 8px rgba(0, 0, 0, 0.3));
80}
81
82.splash-message {
83 margin: 0;
84 font-size: 1.25rem;
85 font-weight: 500;
86 opacity: 0.9;
87 letter-spacing: 0.02em;
88}
public/css/mobile-styles.css+72 -1
@@ -38,6 +38,56 @@
38 grid-template-columns: repeat(var(--bg-thumb-columns, 3), 1fr);38 grid-template-columns: repeat(var(--bg-thumb-columns, 3), 1fr);
39 }39 }
4040
41 .bg_folder_grid {
42 grid-template-columns: repeat(var(--bg-thumb-columns, 3), 1fr);
43 }
44
45 .bg_folder_tile .bg_folder_tile_menu {
46 opacity: 0;
47 visibility: hidden;
48 transform: scale(0.9);
49 }
50
51 .bg_folder_tile:hover .bg_folder_tile_menu,
52 .bg_folder_tile:focus-within .bg_folder_tile_menu {
53 display: none;
54 }
55
56 .bg_folder_tile.mobile-menu-open .bg_folder_tile_menu {
57 display: flex;
58 opacity: 1;
59 visibility: visible;
60 transform: scale(1);
61 z-index: 4;
62 }
63
64 .bg_folder_tile.mobile-menu-open .mobile-only-menu-toggle {
65 opacity: 0;
66 pointer-events: none;
67 }
68
69 .bg_folder_tile .mobile-only-menu-toggle {
70 display: flex;
71 align-items: center;
72 justify-content: center;
73 position: absolute;
74 top: 5px;
75 right: 5px;
76 width: 30px;
77 height: 30px;
78 background-color: rgba(0, 0, 0, 0.4);
79 color: white;
80 border-radius: 6px;
81 z-index: 3;
82 cursor: pointer;
83 backdrop-filter: blur(2px);
84 }
85
86 .bg_folder_tile .jg-button {
87 width: 30px;
88 height: 30px;
89 }
90
41 .bg_list {91 .bg_list {
42 width: unset;92 width: unset;
43 }93 }
@@ -102,10 +152,31 @@
102 }152 }
103153
104 #add_background_button_top>span,154 #add_background_button_top>span,
105 #auto_background>span {155 #auto_background>span,
156 #bg_add_folder_button>span,
157 #bg_selection_mode_button>span:not(#bg_group_select_count),
158 #bg_group_add_to_folder_button>span,
159 #bg_folder_remove_selected_button>span {
106 display: none;160 display: none;
107 }161 }
108162
163 #bg_group_select_count:not(:empty) {
164 display: inline !important;
165 }
166
167 .bg_folder_breadcrumb {
168 gap: 5px;
169 }
170
171 .bg_folder_breadcrumb .expander {
172 min-width: 0;
173 }
174
175 #bg_current_folder_name {
176 min-width: 0;
177 flex: 1 1 auto;
178 }
179
109 #extensions_settings,180 #extensions_settings,
110 #extensions_settings2 {181 #extensions_settings2 {
111 width: 100% !important;182 width: 100% !important;
public/css/popup.css+4 -0
@@ -161,6 +161,10 @@ body.no-blur .popup[open]::backdrop {
161.popup-inputs {161.popup-inputs {
162 margin-top: 10px;162 margin-top: 10px;
163 font-size: smaller;163 font-size: smaller;
164}
165
166.popup-inputs label span {
167 /* Make the title of the custom inputs a bit darker, but keep their text at default opacity. */
164 opacity: 0.7;168 opacity: 0.7;
165}169}
166170
public/css/toggle-dependent.css+6 -0
@@ -568,3 +568,9 @@ label[for="bind_preset_to_connection"]:has(input:checked) {
568#adaptive_p_block:has([data-tg-samplers="adaptive_target"][style*="display: none"]):has([data-tg-samplers="adaptive_decay"][style*="display: none"]) {568#adaptive_p_block:has([data-tg-samplers="adaptive_target"][style*="display: none"]):has([data-tg-samplers="adaptive_decay"][style*="display: none"]) {
569 display: none;569 display: none;
570}570}
571
572#openrouter_provider_warning_text,
573#openrouter_provider_warning_chat {
574 color: var(--warning);
575 margin-left: 5px;
576}
public/global.d.ts+4 -1
@@ -1,6 +1,6 @@
1import libs from './lib';1import libs from './lib';
2import getContext from './scripts/st-context';2import getContext from './scripts/st-context';
3import { power_user } from './scripts/power-user';3import { power_user, getThemeObject } from './scripts/power-user';
4import { QuickReplyApi } from './scripts/extensions/quick-reply/api/QuickReplyApi';4import { QuickReplyApi } from './scripts/extensions/quick-reply/api/QuickReplyApi';
5import { oai_settings } from './scripts/openai';5import { oai_settings } from './scripts/openai';
6import { textgenerationwebui_settings } from './scripts/textgen-settings';6import { textgenerationwebui_settings } from './scripts/textgen-settings';
@@ -21,6 +21,7 @@ declare global {
21 type MessageTimestamp = string | number | Date;21 type MessageTimestamp = string | number | Date;
22 type Character = import('./scripts/char-data').v1CharData;22 type Character = import('./scripts/char-data').v1CharData;
23 type ChatMessageExtra = BaseMessageExtra & Partial<ReasoningMessageExtra> & Record<string, any>;23 type ChatMessageExtra = BaseMessageExtra & Partial<ReasoningMessageExtra> & Record<string, any>;
24 type Theme = ReturnType<typeof getThemeObject>;
2425
25 interface Group {26 interface Group {
26 id: string;27 id: string;
@@ -138,6 +139,8 @@ declare global {
138 interface ImageGenerationAttachmentProps {139 interface ImageGenerationAttachmentProps {
139 generation_type?: number;140 generation_type?: number;
140 negative?: string;141 negative?: string;
142 width?: number;
143 height?: number;
141 }144 }
142145
143 interface ImageCaptionAttachmentProps {146 interface ImageCaptionAttachmentProps {
public/index.html+131 -9
@@ -1982,7 +1982,7 @@
1982 </span>1982 </span>
1983 </div>1983 </div>
1984 </div>1984 </div>
1985 <div class="range-block" data-source="makersuite,vertexai,aimlapi,openrouter,claude,xai,electronhub,chutes,nanogpt">1985 <div class="range-block" data-source="makersuite,vertexai,aimlapi,openrouter,claude,electronhub,chutes,nanogpt">
1986 <label for="openai_enable_web_search" class="checkbox_label flexWrap widthFreeExpand">1986 <label for="openai_enable_web_search" class="checkbox_label flexWrap widthFreeExpand">
1987 <input id="openai_enable_web_search" type="checkbox" />1987 <input id="openai_enable_web_search" type="checkbox" />
1988 <span data-i18n="Enable web search">Enable web search</span>1988 <span data-i18n="Enable web search">Enable web search</span>
@@ -2011,6 +2011,30 @@
2011 <strong data-i18n="enable_functions_desc_4">Not supported when Prompt Post-Processing with "no tools" is used!</strong>2011 <strong data-i18n="enable_functions_desc_4">Not supported when Prompt Post-Processing with "no tools" is used!</strong>
2012 </div>2012 </div>
2013 </div>2013 </div>
2014 <div class="range-block" data-source="openrouter">
2015 <div class="flex-container flexFlowColumn wide100p textAlignCenter marginTop10">
2016 <div class="flex-container oneline-dropdown">
2017 <label for="tool_reasoning_mode" data-i18n="Interleaved Thinking">
2018 Interleaved Thinking
2019 </label>
2020 <select id="tool_reasoning_mode">
2021 <option data-i18n="Disabled" value="disabled">Disabled</option>
2022 <option data-i18n="Since Last User Message" value="since_last_user">Since Last User Message</option>
2023 <option data-i18n="Active Tool Chain" value="active_chain">Active Tool Chain</option>
2024 </select>
2025 </div>
2026 </div>
2027 <div class="toggle-description justifyLeft">
2028 <span data-i18n="openrouter_interleaved_thinking_hint">
2029 Sends reasoning from preceding assistant turns with tool-call requests to maintain interleaved thinking context.
2030 </span>
2031 </div>
2032 <div id="openrouter_interleaved_thinking_disabled_hint" class="toggle-description justifyLeft" style="display:none;">
2033 <em data-i18n="Enable &quot;Request model reasoning&quot; to use Interleaved Thinking.">
2034 Enable "Request model reasoning" to use Interleaved Thinking.
2035 </em>
2036 </div>
2037 </div>
2014 <div class="range-block" data-source="openai,aimlapi,openrouter,mistralai,makersuite,vertexai,claude,custom,xai,pollinations,moonshot,cohere,cometapi,nanogpt,electronhub,azure_openai,zai,siliconflow,chutes">2038 <div class="range-block" data-source="openai,aimlapi,openrouter,mistralai,makersuite,vertexai,claude,custom,xai,pollinations,moonshot,cohere,cometapi,nanogpt,electronhub,azure_openai,zai,siliconflow,chutes">
2015 <label for="openai_media_inlining" class="checkbox_label flexWrap widthFreeExpand">2039 <label for="openai_media_inlining" class="checkbox_label flexWrap widthFreeExpand">
2016 <input id="openai_media_inlining" type="checkbox" />2040 <input id="openai_media_inlining" type="checkbox" />
@@ -2105,7 +2129,7 @@
2105 <span data-i18n="Allows the model to return its thinking process.">2129 <span data-i18n="Allows the model to return its thinking process.">
2106 Allows the model to return its thinking process.2130 Allows the model to return its thinking process.
2107 </span>2131 </span>
2108 <strong data-i18n="This setting affects visibility only." data-source-mode="except" data-source="zai,moonshot">2132 <strong data-i18n="This setting affects visibility only." data-source-mode="except" data-source="zai,moonshot,openrouter">
2109 This setting affects visibility only.2133 This setting affects visibility only.
2110 </strong>2134 </strong>
2111 </div>2135 </div>
@@ -2127,6 +2151,9 @@
2127 <div class="toggle-description justifyLeft marginBot5" data-source="openai,custom,xai,aimlapi,openrouter,pollinations,perplexity,cometapi,electronhub,azure_openai,chutes" data-i18n="OpenAI-style options: low, medium, high. Minimum and maximum are aliased to low and high. Auto does not send an effort level.">2151 <div class="toggle-description justifyLeft marginBot5" data-source="openai,custom,xai,aimlapi,openrouter,pollinations,perplexity,cometapi,electronhub,azure_openai,chutes" data-i18n="OpenAI-style options: low, medium, high. Minimum and maximum are aliased to low and high. Auto does not send an effort level.">
2128 OpenAI-style options: low, medium, high. Minimum and maximum are aliased to low and high. Auto does not send an effort level.2152 OpenAI-style options: low, medium, high. Minimum and maximum are aliased to low and high. Auto does not send an effort level.
2129 </div>2153 </div>
2154 <strong class="toggle-description justifyLeft marginBot5" data-source="openrouter">
2155 Request model reasoning = Off with Reasoning Effort = Minimum disables reasoning entirely on models that support that, but can cause errors with some models.
2156 </strong>
2130 <div class="toggle-description justifyLeft marginBot5" data-source="claude" data-i18n="Allocates a portion of the response length for thinking (min: 1024 tokens, low: 10%, medium: 25%, high: 50%, max: 95%), but minimum 1024 tokens. Auto does not request thinking.">2157 <div class="toggle-description justifyLeft marginBot5" data-source="claude" data-i18n="Allocates a portion of the response length for thinking (min: 1024 tokens, low: 10%, medium: 25%, high: 50%, max: 95%), but minimum 1024 tokens. Auto does not request thinking.">
2131 Allocates a portion of the response length for thinking (min: 1024 tokens, low: 10%, medium: 25%, high: 50%, max: 95%), but minimum 1024 tokens. Auto does not request thinking.2158 Allocates a portion of the response length for thinking (min: 1024 tokens, low: 10%, medium: 25%, high: 50%, max: 95%), but minimum 1024 tokens. Auto does not request thinking.
2132 </div>2159 </div>
@@ -2155,6 +2182,9 @@
2155 <div class="toggle-description justifyLeft marginBot5" data-i18n="Constrains the verbosity of the model's response.">2182 <div class="toggle-description justifyLeft marginBot5" data-i18n="Constrains the verbosity of the model's response.">
2156 Constrains the verbosity of the model's response.2183 Constrains the verbosity of the model's response.
2157 </div>2184 </div>
2185 <strong class="toggle-description justifyLeft marginBot5" data-source="claude">
2186 On Opus 4.6 / Sonnet 4.6, a non-automatic Reasoning Effort takes precedence over Verbosity.
2187 </strong>
2158 </div>2188 </div>
2159 </div>2189 </div>
2160 <div class="range-block" data-source="claude">2190 <div class="range-block" data-source="claude">
@@ -2433,10 +2463,13 @@
2433 </select>2463 </select>
2434 </div>2464 </div>
2435 <div>2465 <div>
2436 <h4 data-i18n="Model Providers">Model Providers</h4>2466 <h4>
2467 <span data-i18n="Model Providers">Model Providers</span>
2468 <i id="openrouter_provider_warning_text" class="fa-solid fa-circle-exclamation displayNone" title="Deselect inapplicable provider(s) or select an applicable provider to avoid a 404 error."></i>
2469 </h4>
2437 <select id="openrouter_providers_text" class="openrouter_providers" multiple>2470 <select id="openrouter_providers_text" class="openrouter_providers" multiple>
2438 </select>2471 </select>
2439 <label class="checkbox_label" data-tg-samplers="openrouter_allow_fallbacks" data-i18n="[title]Automatically chooses an alternative provider if chosen providers can't serve your request." for="openrouter_allow_fallbacks_textgenerationwebui" title="Automatically chooses an alternative provider if chosen providers can't serve your request.">2472 <label class="checkbox_label" data-i18n="[title]Automatically chooses an alternative provider if chosen providers can't serve your request." for="openrouter_allow_fallbacks_textgenerationwebui" title="Automatically chooses an alternative provider if chosen providers can't serve your request.">
2440 <input id="openrouter_allow_fallbacks_textgenerationwebui" type="checkbox" />2473 <input id="openrouter_allow_fallbacks_textgenerationwebui" type="checkbox" />
2441 <span data-i18n="Allow fallback providers">Allow fallback providers</span>2474 <span data-i18n="Allow fallback providers">Allow fallback providers</span>
2442 </label>2475 </label>
@@ -2995,6 +3028,12 @@
2995 <div>3028 <div>
2996 <h4 data-i18n="OpenAI Model">OpenAI Model</h4>3029 <h4 data-i18n="OpenAI Model">OpenAI Model</h4>
2997 <select id="model_openai_select">3030 <select id="model_openai_select">
3031 <optgroup label="GPT-5.4">
3032 <option value="gpt-5.4">gpt-5.4</option>
3033 </optgroup>
3034 <optgroup label="GPT-5.3">
3035 <option value="gpt-5.3-chat-latest">gpt-5.3-chat-latest</option>
3036 </optgroup>
2998 <optgroup label="GPT-5.2">3037 <optgroup label="GPT-5.2">
2999 <option value="gpt-5.2">gpt-5.2</option>3038 <option value="gpt-5.2">gpt-5.2</option>
3000 <option value="gpt-5.2-2025-12-11">gpt-5.2-2025-12-11</option>3039 <option value="gpt-5.2-2025-12-11">gpt-5.2-2025-12-11</option>
@@ -3107,6 +3146,7 @@
3107 <option value="claude-opus-4-6">claude-opus-4-6</option>3146 <option value="claude-opus-4-6">claude-opus-4-6</option>
3108 <option value="claude-opus-4-5">claude-opus-4-5</option>3147 <option value="claude-opus-4-5">claude-opus-4-5</option>
3109 <option value="claude-opus-4-5-20251101">claude-opus-4-5-20251101</option>3148 <option value="claude-opus-4-5-20251101">claude-opus-4-5-20251101</option>
3149 <option value="claude-sonnet-4-6">claude-sonnet-4-6</option>
3110 <option value="claude-sonnet-4-5">claude-sonnet-4-5</option>3150 <option value="claude-sonnet-4-5">claude-sonnet-4-5</option>
3111 <option value="claude-sonnet-4-5-20250929">claude-sonnet-4-5-20250929</option>3151 <option value="claude-sonnet-4-5-20250929">claude-sonnet-4-5-20250929</option>
3112 <option value="claude-haiku-4-5">claude-haiku-4-5</option>3152 <option value="claude-haiku-4-5">claude-haiku-4-5</option>
@@ -3186,7 +3226,10 @@
3186 </div>3226 </div>
3187 </div>3227 </div>
3188 <div>3228 <div>
3189 <h4 data-i18n="Model Providers">Model Providers</h4>3229 <h4>
3230 <span data-i18n="Model Providers">Model Providers</span>
3231 <i id="openrouter_provider_warning_chat" class="fa-solid fa-circle-exclamation displayNone" title="Deselect inapplicable provider(s) or select an applicable provider to avoid a 404 error."></i>
3232 </h4>
3190 <select id="openrouter_providers_chat" class="openrouter_providers" multiple>3233 <select id="openrouter_providers_chat" class="openrouter_providers" multiple>
3191 </select>3234 </select>
3192 <label class="checkbox_label marginTopBot5" for="openrouter_allow_fallbacks" data-i18n="[title]Automatically chooses an alternative provider if chosen providers can't serve your request." title="Automatically chooses an alternative provider if chosen providers can't serve your request.">3235 <label class="checkbox_label marginTopBot5" for="openrouter_allow_fallbacks" data-i18n="[title]Automatically chooses an alternative provider if chosen providers can't serve your request." title="Automatically chooses an alternative provider if chosen providers can't serve your request.">
@@ -3259,6 +3302,11 @@
3259 <div>3302 <div>
3260 <h4 data-i18n="Google Model">Google Model</h4>3303 <h4 data-i18n="Google Model">Google Model</h4>
3261 <select id="model_google_select">3304 <select id="model_google_select">
3305 <optgroup label="Gemini 3.1">
3306 <option value="gemini-3.1-pro-preview">gemini-3.1-pro-preview</option>
3307 <option value="gemini-3.1-flash-lite-preview">gemini-3.1-flash-lite-preview</option>
3308 <option value="gemini-3.1-flash-image-preview">gemini-3.1-flash-image-preview</option>
3309 </optgroup>
3262 <optgroup label="Gemini 3.0">3310 <optgroup label="Gemini 3.0">
3263 <option value="gemini-3-pro-preview">gemini-3-pro-preview</option>3311 <option value="gemini-3-pro-preview">gemini-3-pro-preview</option>
3264 <option value="gemini-3-pro-image-preview">gemini-3-pro-image-preview</option>3312 <option value="gemini-3-pro-image-preview">gemini-3-pro-image-preview</option>
@@ -3432,6 +3480,11 @@
3432 <h4 data-i18n="Google Model">Google Model</h4>3480 <h4 data-i18n="Google Model">Google Model</h4>
3433 <select id="model_vertexai_select">3481 <select id="model_vertexai_select">
3434 <!-- data-mode="full" is for models that require a service account -->3482 <!-- data-mode="full" is for models that require a service account -->
3483 <optgroup label="Gemini 3.1">
3484 <option value="gemini-3.1-pro-preview">gemini-3.1-pro-preview</option>
3485 <option value="gemini-3.1-flash-lite-preview">gemini-3.1-flash-lite-preview</option>
3486 <option value="gemini-3.1-flash-image-preview">gemini-3.1-flash-image-preview</option>
3487 </optgroup>
3435 <optgroup label="Gemini 3.0">3488 <optgroup label="Gemini 3.0">
3436 <option value="gemini-3-pro-preview">gemini-3-pro-preview</option>3489 <option value="gemini-3-pro-preview">gemini-3-pro-preview</option>
3437 <option value="gemini-3-pro-image-preview">gemini-3-pro-image-preview</option>3490 <option value="gemini-3-pro-image-preview">gemini-3-pro-image-preview</option>
@@ -3503,6 +3556,11 @@
3503 <div data-for="api_key_siliconflow" class="neutral_warning" data-i18n="For privacy reasons, your API key will be hidden after you click 'Connect'.">3556 <div data-for="api_key_siliconflow" class="neutral_warning" data-i18n="For privacy reasons, your API key will be hidden after you click 'Connect'.">
3504 For privacy reasons, your API key will be hidden after you click 'Connect'.3557 For privacy reasons, your API key will be hidden after you click 'Connect'.
3505 </div>3558 </div>
3559 <h4 data-i18n="SiliconFlow Endpoint">SiliconFlow Endpoint</h4>
3560 <select id="siliconflow_endpoint">
3561 <option value="global" data-i18n="Global (siliconflow.com)">Global (siliconflow.com)</option>
3562 <option value="cn" data-i18n="China (siliconflow.cn)">China (siliconflow.cn)</option>
3563 </select>
3506 <h4>SiliconFlow Model</h4>3564 <h4>SiliconFlow Model</h4>
3507 <select id="model_siliconflow_select">3565 <select id="model_siliconflow_select">
3508 <option value="" data-i18n="-- Connect to the API --">-- Connect to the API --</option>3566 <option value="" data-i18n="-- Connect to the API --">-- Connect to the API --</option>
@@ -3865,6 +3923,8 @@
3865 </select>3923 </select>
3866 <h4 data-i18n="Z.AI Model">Z.AI Model</h4>3924 <h4 data-i18n="Z.AI Model">Z.AI Model</h4>
3867 <select id="model_zai_select">3925 <select id="model_zai_select">
3926 <option value="glm-5-turbo">glm-5-turbo</option>
3927 <option value="glm-5.1">glm-5.1</option>
3868 <option value="glm-5">glm-5</option>3928 <option value="glm-5">glm-5</option>
3869 <option value="glm-4.7">glm-4.7</option>3929 <option value="glm-4.7">glm-4.7</option>
3870 <option value="glm-4.7-flash">glm-4.7-flash</option>3930 <option value="glm-4.7-flash">glm-4.7-flash</option>
@@ -5557,6 +5617,10 @@
5557 <i class="fa-solid fa-wand-magic"></i>5617 <i class="fa-solid fa-wand-magic"></i>
5558 <span data-i18n="Auto-select">Auto-select</span>5618 <span data-i18n="Auto-select">Auto-select</span>
5559 </div>5619 </div>
5620 <button id="bg_add_folder_button" class="menu_button menu_button_icon" data-i18n="[title]New Folder" title="New Folder">
5621 <i class="fa-solid fa-folder-plus"></i>
5622 <span data-i18n="New Folder">New Folder</span>
5623 </button>
5560 <label for="add_bg_button" id="add_background_button_top" class="menu_button menu_button_icon" data-i18n="[title]Add a new background" title="Add a new background">5624 <label for="add_bg_button" id="add_background_button_top" class="menu_button menu_button_icon" data-i18n="[title]Add a new background" title="Add a new background">
5561 <i class="fa-solid fa-upload"></i>5625 <i class="fa-solid fa-upload"></i>
5562 <span data-i18n="Add Background">Add Background</span>5626 <span data-i18n="Add Background">Add Background</span>
@@ -5581,6 +5645,14 @@
5581 <a href="#bg_chat_tab" data-i18n="Chat">Chat</a>5645 <a href="#bg_chat_tab" data-i18n="Chat">Chat</a>
5582 </li>5646 </li>
5583 <div class="heading-controls">5647 <div class="heading-controls">
5648 <button id="bg_group_add_to_folder_button" class="menu_button menu_button_icon" style="display:none;" data-i18n="[title]Add selected backgrounds to folder" title="Add selected backgrounds to folder">
5649 <i class="fa-solid fa-folder-tree"></i>
5650 <span data-i18n="Add to Folder">Add to Folder</span>
5651 </button>
5652 <button id="bg_selection_mode_button" class="menu_button menu_button_icon" data-i18n="[title]Select backgrounds for bulk folder actions" title="Select backgrounds for bulk folder actions">
5653 <i class="fa-solid fa-edit"></i>
5654 <span id="bg_group_select_count" style="display:none;"></span>
5655 </button>
5584 <button id="bg_thumb_zoom_out" class="menu_button menu_button_icon" title="Make thumbnails smaller" data-i18n="[title]Make thumbnails smaller">5656 <button id="bg_thumb_zoom_out" class="menu_button menu_button_icon" title="Make thumbnails smaller" data-i18n="[title]Make thumbnails smaller">
5585 <i class="fa-solid fa-minus"></i>5657 <i class="fa-solid fa-minus"></i>
5586 </button>5658 </button>
@@ -5590,6 +5662,19 @@
5590 </div>5662 </div>
5591 </ul>5663 </ul>
5592 <div id="bg_global_tab" class="bg_tab_panel">5664 <div id="bg_global_tab" class="bg_tab_panel">
5665 <div id="bg_folder_breadcrumb" class="bg_folder_breadcrumb" style="display:none;">
5666 <button id="bg_back_to_folders" class="menu_button menu_button_icon">
5667 <i class="fa-solid fa-arrow-left"></i>
5668 <span data-i18n="Back">Back</span>
5669 </button>
5670 <span id="bg_current_folder_name" class="bg_current_folder_name"></span>
5671 <span class="expander"></span>
5672 <button id="bg_folder_remove_selected_button" class="menu_button menu_button_icon" style="display:none;" data-i18n="[title]Remove selected backgrounds from this folder" title="Remove selected backgrounds from this folder">
5673 <i class="fa-solid fa-folder-minus"></i>
5674 <span data-i18n="Remove from Folder">Remove from Folder</span>
5675 </button>
5676 </div>
5677 <div id="bg_folder_grid" class="bg_list bg_folder_grid"></div>
5593 <div id="bg_menu_content" class="bg_list"></div>5678 <div id="bg_menu_content" class="bg_list"></div>
5594 </div>5679 </div>
5595 <div id="bg_chat_tab" class="bg_tab_panel">5680 <div id="bg_chat_tab" class="bg_tab_panel">
@@ -5754,12 +5839,18 @@
5754 <div class="persona_controls_buttons_block buttons_block">5839 <div class="persona_controls_buttons_block buttons_block">
5755 <div id="persona_rename_button" class="menu_button fa-solid fa-pencil" title="Rename Persona" data-i18n="[title]Rename Persona"></div>5840 <div id="persona_rename_button" class="menu_button fa-solid fa-pencil" title="Rename Persona" data-i18n="[title]Rename Persona"></div>
5756 <div id="sync_name_button" class="menu_button fa-solid fa-sync" title="Click to set user name for all messages" data-i18n="[title]Click to set user name for all messages"></div>5841 <div id="sync_name_button" class="menu_button fa-solid fa-sync" title="Click to set user name for all messages" data-i18n="[title]Click to set user name for all messages"></div>
5757 <div id="persona_lore_button" class="menu_button fa-solid fa-globe" title="Persona Lore&#10;Alt+Click to open the lorebook" data-i18n="[title]Persona Lore Alt+Click to open the lorebook"></div>5842 <div id="persona_lore_button" class="menu_button fa-solid fa-globe" title="Persona Lore&#10;&#10;Click to load&#10;Shift/Alt-click or long-press to open 'Link to Persona Lorebook' popup" data-i18n="[title]persona_lore_button_title"></div>
57585843
5759 <div id="persona_set_image_button" class="menu_button fa-solid fa-image" title="Change Persona Image" data-i18n="[title]Change Persona Image"></div>5844 <div id="persona_set_image_button" class="menu_button fa-solid fa-image" title="Change Persona Image" data-i18n="[title]Change Persona Image"></div>
5760 <div id="persona_duplicate_button" class="menu_button fa-solid fa-clone" title="Duplicate Persona" data-i18n="[title]Duplicate Persona"></div>5845 <div id="persona_duplicate_button" class="menu_button fa-solid fa-clone" title="Duplicate Persona" data-i18n="[title]Duplicate Persona"></div>
5761 <div id="persona_delete_button" class="menu_button fa-solid fa-skull red_button" title="Delete Persona" data-i18n="[title]Delete Persona"></div>5846 <div id="persona_delete_button" class="menu_button fa-solid fa-skull red_button" title="Delete Persona" data-i18n="[title]Delete Persona"></div>
5762 </div>5847 </div>
5848 <label class="flex1 height100p" for="persona-management-dropdown">
5849 <select id="persona-management-dropdown" class="text_pole">
5850 <option value="default" disabled selected data-i18n="More...">More...</option>
5851 <option id="persona_lorebook_link" data-i18n="Link to Persona Lorebook">Link to Persona Lorebook</option>
5852 </select>
5853 </label>
5763 </div>5854 </div>
57645855
5765 <h4 class="flex-container alignItemsBaseline">5856 <h4 class="flex-container alignItemsBaseline">
@@ -5925,8 +6016,8 @@
5925 <div id="favorite_button" class="menu_button fa-solid fa-star" title="Add to Favorites" data-i18n="[title]Add to Favorites"></div>6016 <div id="favorite_button" class="menu_button fa-solid fa-star" title="Add to Favorites" data-i18n="[title]Add to Favorites"></div>
5926 <input type="hidden" id="fav_checkbox" name="fav" />6017 <input type="hidden" id="fav_checkbox" name="fav" />
5927 <div id="advanced_div" class="menu_button fa-solid fa-book " title="Advanced Definitions" data-i18n="[title]Advanced Definition"></div>6018 <div id="advanced_div" class="menu_button fa-solid fa-book " title="Advanced Definitions" data-i18n="[title]Advanced Definition"></div>
5928 <div id="world_button" class="menu_button fa-solid fa-globe" title="Character Lore&#10;&#10;Click to load&#10;Shift-click to open 'Link to World Info' popup" data-i18n="[title]world_button_title"></div>6019 <div id="world_button" class="menu_button fa-solid fa-globe" title="Character Lore&#10;&#10;Click to load&#10;Shift/Alt-click or long-press to open 'Link to World Info' popup" data-i18n="[title]world_button_title"></div>
5929 <div class="chat_lorebook_button menu_button fa-solid fa-passport" title="Chat Lore&#10;Alt+Click to open the lorebook" data-i18n="[title]Chat Lore Alt+Click to open the lorebook"></div>6020 <div class="chat_lorebook_button menu_button fa-solid fa-passport" title="Chat Lore&#10;&#10;Click to load&#10;Shift/Alt-click or long-press to open 'Link to Chat Lorebook' popup" data-i18n="[title]chat_lorebook_button_title"></div>
5930 <div id="char_connections_button" class="menu_button fa-solid fa-face-smile" title="Connected Personas" data-i18n="[title]Connected Personas"></div>6021 <div id="char_connections_button" class="menu_button fa-solid fa-face-smile" title="Connected Personas" data-i18n="[title]Connected Personas"></div>
5931 <div id="export_button" class="menu_button fa-solid fa-file-export " title="Export and Download" data-i18n="[title]Export and Download"></div>6022 <div id="export_button" class="menu_button fa-solid fa-file-export " title="Export and Download" data-i18n="[title]Export and Download"></div>
5932 <!-- <div id="set_chat_character_settings" class="menu_button fa-solid fa-scroll" title="Set a chat scenario override"></div> -->6023 <!-- <div id="set_chat_character_settings" class="menu_button fa-solid fa-scroll" title="Set a chat scenario override"></div> -->
@@ -6077,8 +6168,14 @@
6077 <div id="group-metadata-controls" class="marginTopBot5">6168 <div id="group-metadata-controls" class="marginTopBot5">
6078 <div class="flex-container wide100p">6169 <div class="flex-container wide100p">
6079 <input id="rm_group_chat_name" class="text_pole flex1" type="text" name="chat_name" data-i18n="[placeholder]Chat Name (Optional)" placeholder="Chat Name (Optional)" />6170 <input id="rm_group_chat_name" class="text_pole flex1" type="text" name="chat_name" data-i18n="[placeholder]Chat Name (Optional)" placeholder="Chat Name (Optional)" />
6080 <div class="chat_lorebook_button menu_button fa-solid fa-passport" title="Chat Lore" data-i18n="[title]Chat Lore"></div>6171 <div class="chat_lorebook_button menu_button fa-solid fa-passport" title="Chat Lore&#10;&#10;Click to load&#10;Shift/Alt-click or long-press to open 'Link to Chat Lorebook' popup" data-i18n="[title]chat_lorebook_button_title"></div>
6081 </div>6172 </div>
6173 <label class="flex1 height100p" for="group-chat-lorebook-dropdown">
6174 <select id="group-chat-lorebook-dropdown" class="text_pole">
6175 <option value="default" disabled selected data-i18n="More...">More...</option>
6176 <option id="group_chat_lorebook_link" data-i18n="Link to Chat Lorebook">Link to Chat Lorebook</option>
6177 </select>
6178 </label>
6082 <div id="group_tags_div" class="wide100p">6179 <div id="group_tags_div" class="wide100p">
6083 <div class="tag_controls">6180 <div class="tag_controls">
6084 <input id="groupTagInput" class="text_pole textarea_compact tag_input flex1 margin0" data-i18n="[placeholder]Search / Create Tags" placeholder="Search / Create tags" />6181 <input id="groupTagInput" class="text_pole textarea_compact tag_input flex1 margin0" data-i18n="[placeholder]Search / Create Tags" placeholder="Search / Create tags" />
@@ -6558,10 +6655,34 @@
6558 <div data-action="copy" class="jg-button jg-copy fa-solid fa-file-arrow-up" data-i18n="[title]Copy to global backgrounds" title="Copy to global backgrounds"></div>6655 <div data-action="copy" class="jg-button jg-copy fa-solid fa-file-arrow-up" data-i18n="[title]Copy to global backgrounds" title="Copy to global backgrounds"></div>
6559 <div data-action="edit" class="jg-button jg-edit fa-solid fa-pen-to-square fa-fw pointer" data-i18n="[title]Rename Background" title="Rename Background"></div>6656 <div data-action="edit" class="jg-button jg-edit fa-solid fa-pen-to-square fa-fw pointer" data-i18n="[title]Rename Background" title="Rename Background"></div>
6560 <div data-action="delete" class="jg-button jg-delete fa-solid fa-trash-can fa-fw pointer" data-i18n="[title]Delete Background" title="Delete Background"></div>6657 <div data-action="delete" class="jg-button jg-delete fa-solid fa-trash-can fa-fw pointer" data-i18n="[title]Delete Background" title="Delete Background"></div>
6658 <div data-action="folder" class="jg-button jg-folder fa-solid fa-folder fa-fw pointer" data-i18n="[title]Add to folder" title="Add to folder"></div>
6659 <div data-action="set-cover" class="jg-button jg-set-cover fa-solid fa-image fa-fw pointer" data-i18n="[title]Set as folder cover" title="Set as folder cover"></div>
6561 </div>6660 </div>
6562 <div class="BGSampleTitle"></div>6661 <div class="BGSampleTitle"></div>
6563 </div>6662 </div>
6564 </div>6663 </div>
6664 <div id="bg_folder_tile_template" class="template_element">
6665 <div class="bg_folder_tile" data-folder-id="">
6666 <div class="bg_folder_tile_cover"></div>
6667 <div class="bg_folder_tile_overlay">
6668 <i class="fa-solid fa-folder"></i>
6669 <span class="bg_folder_tile_name"></span>
6670 </div>
6671 <div class="mobile-only-menu-toggle">
6672 <i class="fa-solid fa-ellipsis-vertical"></i>
6673 </div>
6674 <div class="bg_folder_tile_menu jg-menu">
6675 <div data-action="rename-folder" class="jg-button fa-solid fa-pen-to-square fa-fw pointer" data-i18n="[title]Rename folder" title="Rename folder"></div>
6676 <div data-action="delete-folder" class="jg-button fa-solid fa-trash-can fa-fw pointer" data-i18n="[title]Delete folder" title="Delete folder"></div>
6677 </div>
6678 </div>
6679 </div>
6680 <div id="bg_new_folder_template" class="template_element">
6681 <div class="bg_folder_tile bg_new_folder_tile">
6682 <i class="fa-solid fa-folder-plus fa-2x"></i>
6683 <span data-i18n="New Folder">New Folder</span>
6684 </div>
6685 </div>
6565 <!-- templates for JS to reuse when needed -->6686 <!-- templates for JS to reuse when needed -->
6566 <div id="character_world_template" class="template_element">6687 <div id="character_world_template" class="template_element">
6567 <div class="character_world range-block flexFlowColumn flex-container">6688 <div class="character_world range-block flexFlowColumn flex-container">
@@ -7245,6 +7366,7 @@
7245 <div title="Toggle media display style" class="mes_button mes_media_gallery fa-solid fa-photo-film" data-i18n="[title]Toggle media display style"></div>7366 <div title="Toggle media display style" class="mes_button mes_media_gallery fa-solid fa-photo-film" data-i18n="[title]Toggle media display style"></div>
7246 <div title="Toggle media display style" class="mes_button mes_media_list fa-solid fa-table-cells-large" data-i18n="[title]Toggle media display style"></div>7367 <div title="Toggle media display style" class="mes_button mes_media_list fa-solid fa-table-cells-large" data-i18n="[title]Toggle media display style"></div>
7247 <div title="Embed file or image" class="mes_button mes_embed fa-solid fa-paperclip" data-i18n="[title]Embed file or image"></div>7368 <div title="Embed file or image" class="mes_button mes_embed fa-solid fa-paperclip" data-i18n="[title]Embed file or image"></div>
7369 <div title="Jump to swipe history" class="mes_button mes_swipe_picker fa-solid fa-bookmark" data-i18n="[title]Jump to swipe history" style="display: none;"></div>
7248 <div title="Create checkpoint" class="mes_button mes_create_bookmark fa-regular fa-solid fa-flag-checkered" data-i18n="[title]Create checkpoint"></div>7370 <div title="Create checkpoint" class="mes_button mes_create_bookmark fa-regular fa-solid fa-flag-checkered" data-i18n="[title]Create checkpoint"></div>
7249 <div title="Create branch" class="mes_button mes_create_branch fa-regular fa-code-branch" data-i18n="[title]Create Branch"></div>7371 <div title="Create branch" class="mes_button mes_create_branch fa-regular fa-code-branch" data-i18n="[title]Create Branch"></div>
7250 <div title="Copy" class="mes_button mes_copy fa-solid fa-copy " data-i18n="[title]Copy"></div>7372 <div title="Copy" class="mes_button mes_copy fa-solid fa-copy " data-i18n="[title]Copy"></div>
public/lib.js+5 -0
@@ -23,6 +23,7 @@ import { toggle as slideToggle } from 'slidetoggle';
23import chalk from 'chalk';23import chalk from 'chalk';
24import yaml from 'yaml';24import yaml from 'yaml';
25import * as chevrotain from 'chevrotain';25import * as chevrotain from 'chevrotain';
26import { gzipSync, gzip } from 'fflate';
2627
27/**28/**
28 * Expose the libraries to the 'window' object.29 * Expose the libraries to the 'window' object.
@@ -102,6 +103,8 @@ export default {
102 chalk,103 chalk,
103 yaml,104 yaml,
104 chevrotain,105 chevrotain,
106 gzipSync,
107 gzip,
105};108};
106109
107export {110export {
@@ -127,4 +130,6 @@ export {
127 chalk,130 chalk,
128 yaml,131 yaml,
129 chevrotain,132 chevrotain,
133 gzipSync,
134 gzip,
130};135};
public/locales/ar-sa.json+17 -1
@@ -337,6 +337,8 @@
337 "koboldcpp API key (optional)": "مفتاح koboldcpp API (اختياري)",337 "koboldcpp API key (optional)": "مفتاح koboldcpp API (اختياري)",
338 "Example: http://127.0.0.1:5001": "مثال: http://127.0.0.1:5001",338 "Example: http://127.0.0.1:5001": "مثال: http://127.0.0.1:5001",
339 "Authorize": "تفويض",339 "Authorize": "تفويض",
340 "OpenRouter API key already exists": "مفتاح API OpenRouter موجود بالفعل",
341 "Do you really wish to create a new OpenRouter key? Your existing key will not be deleted.": "هل تريد حقاً إنشاء مفتاح OpenRouter جديد؟ لن يتم حذف مفتاحك الموجود.",
340 "Get your OpenRouter API token using OAuth flow. You will be redirected to openrouter.ai": "احصل على رمز واجهة برمجة التطبيقات الخاص بك لموزع الاتصالات باستخدام تدفق OAuth. سيتم توجيهك إلى openrouter.ai",342 "Get your OpenRouter API token using OAuth flow. You will be redirected to openrouter.ai": "احصل على رمز واجهة برمجة التطبيقات الخاص بك لموزع الاتصالات باستخدام تدفق OAuth. سيتم توجيهك إلى openrouter.ai",
341 "Bypass status check": "تجاوز فحص الحالة",343 "Bypass status check": "تجاوز فحص الحالة",
342 "Chat Completion Source": "مصدر استكمال الدردشة",344 "Chat Completion Source": "مصدر استكمال الدردشة",
@@ -1438,5 +1440,19 @@
1438 "Still have questions?": "هل ما زالت لديك اسئلة؟",1440 "Still have questions?": "هل ما زالت لديك اسئلة؟",
1439 "Join the SillyTavern Discord": "انضم إلى ديسكورد SillyTavern",1441 "Join the SillyTavern Discord": "انضم إلى ديسكورد SillyTavern",
1440 "Post a GitHub issue": "نشر مشكلة على GitHub",1442 "Post a GitHub issue": "نشر مشكلة على GitHub",
1441 "Contact the developers": "الاتصال بالمطورين"1443 "Contact the developers": "الاتصال بالمطورين",
1444 "Interleaved Thinking": "التفكير المتداخل",
1445 "Since Last User Message": "منذ آخر رسالة من المستخدم",
1446 "Active Tool Chain": "سلسلة الأدوات النشطة",
1447 "openrouter_interleaved_thinking_hint": "يرسل الاستدلال من رسائل المساعد السابقة مع طلبات استدعاء الأدوات للحفاظ على سياق التفكير المتداخل.",
1448 "SillyTavern Logo": "شعار SillyTavern",
1449 "Initializing…": "جارٍ التهيئة…",
1450 "Name not accepted, as it is the same as before (ignoring case and accents).": "الاسم غير مقبول، لأنه نفسه كما كان من قبل (تجاهل حالة الأحرف والنبرات).",
1451 "Rename Chat": "إعادة تسمية المحادثة",
1452 "Renaming chat…": "جارٍ إعادة تسمية المحادثة…",
1453 "Delete Chat": "حذف المحادثة",
1454 "Deleting chat…": "جارٍ حذف المحادثة…",
1455 "Loading chat…": "جارٍ تحميل المحادثة…",
1456 "Bulk Delete": "حذف جماعي",
1457 "Deleting ${0} character(s)…": "جارٍ حذف ${0} شخصية (شخصيات)…"
1442}1458}
public/locales/de-de.json+17 -1
@@ -337,6 +337,8 @@
337 "koboldcpp API key (optional)": "koboldcpp API-Schlüssel (optional)",337 "koboldcpp API key (optional)": "koboldcpp API-Schlüssel (optional)",
338 "Example: http://127.0.0.1:5001": "Beispiel: http://127.0.0.1:5001",338 "Example: http://127.0.0.1:5001": "Beispiel: http://127.0.0.1:5001",
339 "Authorize": "Autorisieren",339 "Authorize": "Autorisieren",
340 "OpenRouter API key already exists": "OpenRouter API-Schlüssel existiert bereits",
341 "Do you really wish to create a new OpenRouter key? Your existing key will not be deleted.": "Möchten Sie wirklich einen neuen OpenRouter API-Schlüssel erstellen? Ihr vorhandener Schlüssel wird nicht gelöscht.",
340 "Get your OpenRouter API token using OAuth flow. You will be redirected to openrouter.ai": "Hole dein OpenRouter-API-Token mit OAuth-Fluss. Du wirst zu openrouter.ai weitergeleitet",342 "Get your OpenRouter API token using OAuth flow. You will be redirected to openrouter.ai": "Hole dein OpenRouter-API-Token mit OAuth-Fluss. Du wirst zu openrouter.ai weitergeleitet",
341 "Bypass status check": "Umgehe Statusüberprüfung",343 "Bypass status check": "Umgehe Statusüberprüfung",
342 "Chat Completion Source": "Quelle für Chat-Vervollständigung",344 "Chat Completion Source": "Quelle für Chat-Vervollständigung",
@@ -1438,5 +1440,19 @@
1438 "Still have questions?": "Hast du immer noch Fragen?",1440 "Still have questions?": "Hast du immer noch Fragen?",
1439 "Join the SillyTavern Discord": "Trete dem SillyTavern Discord bei",1441 "Join the SillyTavern Discord": "Trete dem SillyTavern Discord bei",
1440 "Post a GitHub issue": "Veröffentliche ein GitHub-Problem",1442 "Post a GitHub issue": "Veröffentliche ein GitHub-Problem",
1441 "Contact the developers": "Kontaktiere die Entwickler"1443 "Contact the developers": "Kontaktiere die Entwickler",
1444 "Interleaved Thinking": "Verschränktes Denken",
1445 "Since Last User Message": "Seit letzter Nutzernachricht",
1446 "Active Tool Chain": "Aktive Tool-Kette",
1447 "openrouter_interleaved_thinking_hint": "Sendet Begründungen aus vorherigen Assistentennachrichten mit Tool-Aufrufen, um den Kontext für verschränktes Denken zu erhalten.",
1448 "SillyTavern Logo": "SillyTavern-Logo",
1449 "Initializing…": "Initialisierung…",
1450 "Name not accepted, as it is the same as before (ignoring case and accents).": "Name nicht akzeptiert, da er derselbe wie zuvor ist (Groß-/Kleinschreibung und Akzente ignoriert).",
1451 "Rename Chat": "Chat umbenennen",
1452 "Renaming chat…": "Chat wird umbenannt…",
1453 "Delete Chat": "Chat löschen",
1454 "Deleting chat…": "Chat wird gelöscht…",
1455 "Loading chat…": "Chat wird geladen…",
1456 "Bulk Delete": "Massenlöschung",
1457 "Deleting ${0} character(s)…": "${0} Charakter(e) werden gelöscht…"
1442}1458}
public/locales/es-es.json+17 -1
@@ -337,6 +337,8 @@
337 "koboldcpp API key (optional)": "Clave API de koboldcpp (opcional)",337 "koboldcpp API key (optional)": "Clave API de koboldcpp (opcional)",
338 "Example: http://127.0.0.1:5001": "Ejemplo: http://127.0.0.1:5001",338 "Example: http://127.0.0.1:5001": "Ejemplo: http://127.0.0.1:5001",
339 "Authorize": "Autorizar",339 "Authorize": "Autorizar",
340 "OpenRouter API key already exists": "La clave de API de OpenRouter ya existe",
341 "Do you really wish to create a new OpenRouter key? Your existing key will not be deleted.": "¿Realmente deseas crear una nueva clave de API de OpenRouter? Tu clave existente no será eliminada.",
340 "Get your OpenRouter API token using OAuth flow. You will be redirected to openrouter.ai": "Obtenga su token de API de OpenRouter utilizando el flujo OAuth. Será redirigido a openrouter.ai",342 "Get your OpenRouter API token using OAuth flow. You will be redirected to openrouter.ai": "Obtenga su token de API de OpenRouter utilizando el flujo OAuth. Será redirigido a openrouter.ai",
341 "Bypass status check": "Saltar la verificación del estado",343 "Bypass status check": "Saltar la verificación del estado",
342 "Chat Completion Source": "Fuente de Completado de Chat",344 "Chat Completion Source": "Fuente de Completado de Chat",
@@ -1545,5 +1547,19 @@
1545 "Toggles sharing world tint with character sprites (This will override Character Tint)": "Alterna el tinte mundial compartido con los sprites de personajes (esto anulará el tinte de personajes)",1547 "Toggles sharing world tint with character sprites (This will override Character Tint)": "Alterna el tinte mundial compartido con los sprites de personajes (esto anulará el tinte de personajes)",
1546 "Sets the expression of the user sprite": "Establece la expresión del sprite de usuario",1548 "Sets the expression of the user sprite": "Establece la expresión del sprite de usuario",
1547 "Sets the user sprite set to use for the user sprite": "Establece el conjunto de sprites de usuario para usar en el sprite de usuario",1549 "Sets the user sprite set to use for the user sprite": "Establece el conjunto de sprites de usuario para usar en el sprite de usuario",
1548 "Toggles the user sprite on the VN UI": "Alterna el sprite de usuario en la interfaz VN"1550 "Toggles the user sprite on the VN UI": "Alterna el sprite de usuario en la interfaz VN",
1551 "Interleaved Thinking": "Razonamiento intercalado",
1552 "Since Last User Message": "Desde el último mensaje del usuario",
1553 "Active Tool Chain": "Cadena de herramientas activa",
1554 "openrouter_interleaved_thinking_hint": "Envía razonamiento de turnos anteriores del asistente junto con solicitudes de herramientas para mantener el contexto del razonamiento intercalado.",
1555 "SillyTavern Logo": "Logo de SillyTavern",
1556 "Initializing…": "Inicializando…",
1557 "Name not accepted, as it is the same as before (ignoring case and accents).": "Nombre no aceptado, ya que es el mismo que antes (ignorando mayúsculas y acentos).",
1558 "Rename Chat": "Renombrar chat",
1559 "Renaming chat…": "Renombrando chat…",
1560 "Delete Chat": "Eliminar chat",
1561 "Deleting chat…": "Eliminando chat…",
1562 "Loading chat…": "Cargando chat…",
1563 "Bulk Delete": "Eliminación masiva",
1564 "Deleting ${0} character(s)…": "Eliminando ${0} personaje(s)…"
1549}1565}
public/locales/fr-fr.json+21 -5
@@ -320,6 +320,8 @@
320 "koboldcpp API key (optional)": "Clé API koboldcpp (facultatif)",320 "koboldcpp API key (optional)": "Clé API koboldcpp (facultatif)",
321 "Example: http://127.0.0.1:5001": "Exemple : http://127.0.0.1:5001",321 "Example: http://127.0.0.1:5001": "Exemple : http://127.0.0.1:5001",
322 "Authorize": "Autoriser",322 "Authorize": "Autoriser",
323 "OpenRouter API key already exists": "La clé API OpenRouter existe déjà",
324 "Do you really wish to create a new OpenRouter key? Your existing key will not be deleted.": "Voulez-vous vraiment créer une nouvelle clé OpenRouter ? Votre clé existante ne sera pas supprimée.",
323 "Get your OpenRouter API token using OAuth flow. You will be redirected to openrouter.ai": "Obtenez votre jeton API OpenRouter en utilisant le flux OAuth. Vous serez redirigé vers openrouter.ai",325 "Get your OpenRouter API token using OAuth flow. You will be redirected to openrouter.ai": "Obtenez votre jeton API OpenRouter en utilisant le flux OAuth. Vous serez redirigé vers openrouter.ai",
324 "Bypass status check": "Contourner la vérification de l'état",326 "Bypass status check": "Contourner la vérification de l'état",
325 "Chat Completion Source": "Source de complétion de chat",327 "Chat Completion Source": "Source de complétion de chat",
@@ -1516,10 +1518,10 @@
1516 "Contain": "Contenir",1518 "Contain": "Contenir",
1517 "Stretch": "Étirer",1519 "Stretch": "Étirer",
1518 "Center": "Centrer",1520 "Center": "Centrer",
1519 "Persona Lore Alt+Click to open the lorebook": "Lore de persona\nAlt+Click pour ouvrir le lorebook",1521 "persona_lore_button_title": "Lore de persona\n\nCliquer pour charger\nMajuscule/Alt-clic ou appui long pour ouvrir la fenêtre contextuelle « Lien vers le Lorebook de persona ».",
1520 "None (disabled)": "Aucun (désactivé)",1522 "None (disabled)": "Aucun (désactivé)",
1521 "world_button_title": "Lore de personnage\n\nCliquer pour charger\nMajuscule-clic pour ouvrir la fenêtre contextuelle « Lien vers le Wold Info ».",1523 "world_button_title": "Lore de personnage\n\nCliquer pour charger\nMajuscule/Alt-clic ou appui long pour ouvrir la fenêtre contextuelle « Lien vers le World Info ».",
1522 "Chat Lore Alt+Click to open the lorebook": "Lore du chat\nAlt+Click pour ouvrir le lorebook",1524 "chat_lorebook_button_title": "Lore du chat\n\nCliquer pour charger\nMajuscule/Alt-clic ou appui long pour ouvrir la fenêtre contextuelle « Lien vers le Lorebook du chat ».",
1523 "Manual": "Manuel",1525 "Manual": "Manuel",
1524 "Duplicate persona": "Dupliquer la persona",1526 "Duplicate persona": "Dupliquer la persona",
1525 "popup-button-crop": "Recadrer",1527 "popup-button-crop": "Recadrer",
@@ -2012,7 +2014,7 @@
2012 "Downloader Options": "Options de téléchargement",2014 "Downloader Options": "Options de téléchargement",
2013 "Extra parameters for downloading/HuggingFace API": "Paramètres supplémentaires pour le téléchargement/HuggingFace API.\rSi vous n'êtes pas sûr, laissez ces paramètres vides.",2015 "Extra parameters for downloading/HuggingFace API": "Paramètres supplémentaires pour le téléchargement/HuggingFace API.\rSi vous n'êtes pas sûr, laissez ces paramètres vides.",
2014 "Revision": "Révision",2016 "Revision": "Révision",
2015 "Folder Name": "Nom du dossier de sortie",2017 "Output Folder Name": "Nom du dossier de sortie",
2016 "HF Token": "Token HF",2018 "HF Token": "Token HF",
2017 "Include Patterns": "Inclure les patterns",2019 "Include Patterns": "Inclure les patterns",
2018 "Glob patterns of files to include in the download.": "Patterns globaux de fichiers à inclure dans le téléchargement.\rSéparer chaque pattern par une nouvelle ligne.",2020 "Glob patterns of files to include in the download.": "Patterns globaux de fichiers à inclure dans le téléchargement.\rSéparer chaque pattern par une nouvelle ligne.",
@@ -2046,5 +2048,19 @@
2046 "System Prompt Name": "Nom du prompt système:",2048 "System Prompt Name": "Nom du prompt système:",
2047 "Instruct Mode": "Mode Instruction:",2049 "Instruct Mode": "Mode Instruction:",
2048 "Save and Update": "Sauvegarder et mettre à jour",2050 "Save and Update": "Sauvegarder et mettre à jour",
2049 "Don't ask again for this URL": "Ne plus demander pour cette URL"2051 "Don't ask again for this URL": "Ne plus demander pour cette URL",
2052 "Interleaved Thinking": "Raisonnement intercalé",
2053 "Since Last User Message": "Depuis le dernier message utilisateur",
2054 "Active Tool Chain": "Chaîne d'outils active",
2055 "openrouter_interleaved_thinking_hint": "Envoie le raisonnement des tours précédents de l'assistant avec les requêtes d'outils pour conserver le contexte du raisonnement intercalé.",
2056 "SillyTavern Logo": "Logo SillyTavern",
2057 "Initializing…": "Initialisation…",
2058 "Name not accepted, as it is the same as before (ignoring case and accents).": "Nom non accepté, car il est identique à l'ancien (en ignorant la casse et les accents).",
2059 "Rename Chat": "Renommer la discussion",
2060 "Renaming chat…": "Renommage de la discussion…",
2061 "Delete Chat": "Supprimer la discussion",
2062 "Deleting chat…": "Suppression de la discussion…",
2063 "Loading chat…": "Chargement de la discussion…",
2064 "Bulk Delete": "Suppression groupée",
2065 "Deleting ${0} character(s)…": "Suppression de ${0} personnage(s)…"
2050}2066}
public/locales/is-is.json+17 -1
@@ -335,6 +335,8 @@
335 "koboldcpp API key (optional)": "koboldcpp API lykill (valfrjálst)",335 "koboldcpp API key (optional)": "koboldcpp API lykill (valfrjálst)",
336 "Example: http://127.0.0.1:5001": "Dæmi: http://127.0.0.1:5001",336 "Example: http://127.0.0.1:5001": "Dæmi: http://127.0.0.1:5001",
337 "Authorize": "Heimild",337 "Authorize": "Heimild",
338 "OpenRouter API key already exists": "OpenRouter API lykill er þegar til",
339 "Do you really wish to create a new OpenRouter key? Your existing key will not be deleted.": "Viltu virkilega búa til nýjan OpenRouter lykil? Núverandi lykill þinn verður ekki eytt.",
338 "Get your OpenRouter API token using OAuth flow. You will be redirected to openrouter.ai": "Fáðu API lykilinn þinn fyrir OpenRouter með því að nota OAuth strauminn. Þú verður endurvísað(ð/ur) á openrouter.ai",340 "Get your OpenRouter API token using OAuth flow. You will be redirected to openrouter.ai": "Fáðu API lykilinn þinn fyrir OpenRouter með því að nota OAuth strauminn. Þú verður endurvísað(ð/ur) á openrouter.ai",
339 "Bypass status check": "Hlaupa framhjá stöðutík",341 "Bypass status check": "Hlaupa framhjá stöðutík",
340 "Chat Completion Source": "Heimild að fullvirkni spjalls",342 "Chat Completion Source": "Heimild að fullvirkni spjalls",
@@ -1436,5 +1438,19 @@
1436 "Still have questions?": "Ertu enn með spurningar?",1438 "Still have questions?": "Ertu enn með spurningar?",
1437 "Join the SillyTavern Discord": "Skráðu þig í SillyTavern Discord",1439 "Join the SillyTavern Discord": "Skráðu þig í SillyTavern Discord",
1438 "Post a GitHub issue": "Senda inn GitHub málefni",1440 "Post a GitHub issue": "Senda inn GitHub málefni",
1439 "Contact the developers": "Hafa samband við þróunaraðila"1441 "Contact the developers": "Hafa samband við þróunaraðila",
1442 "Interleaved Thinking": "Fléttuð hugsun",
1443 "Since Last User Message": "Frá síðustu notandaskilaboðum",
1444 "Active Tool Chain": "Virk verkfærakeðja",
1445 "openrouter_interleaved_thinking_hint": "Sendir röksemdir úr fyrri aðstoðarskilaboðum með verkfæraköllum til að viðhalda samhengi fléttaðrar hugsunar.",
1446 "SillyTavern Logo": "SillyTavern merki",
1447 "Initializing…": "Ræsir…",
1448 "Name not accepted, as it is the same as before (ignoring case and accents).": "Nafn ekki samþykkt, því það er það sama og áður (hunsar há-/lágstafi og brottmerki).",
1449 "Rename Chat": "Endurnefna spjall",
1450 "Renaming chat…": "Endurnefni spjall…",
1451 "Delete Chat": "Eyða spjalli",
1452 "Deleting chat…": "Eyði spjalli…",
1453 "Loading chat…": "Hleð spjalli…",
1454 "Bulk Delete": "Fjöldaeyðing",
1455 "Deleting ${0} character(s)…": "Eyði ${0} persónu(m)…"
1440}1456}
public/locales/it-it.json+17 -1
@@ -337,6 +337,8 @@
337 "koboldcpp API key (optional)": "Chiave API koboldcpp (opzionale)",337 "koboldcpp API key (optional)": "Chiave API koboldcpp (opzionale)",
338 "Example: http://127.0.0.1:5001": "Esempio: http://127.0.0.1:5001",338 "Example: http://127.0.0.1:5001": "Esempio: http://127.0.0.1:5001",
339 "Authorize": "Autorizzare",339 "Authorize": "Autorizzare",
340 "OpenRouter API key already exists": "La chiave API di OpenRouter esiste già",
341 "Do you really wish to create a new OpenRouter key? Your existing key will not be deleted.": "Desideri veramente creare una nuova chiave API di OpenRouter? La tua chiave esistente non verrà eliminata.",
340 "Get your OpenRouter API token using OAuth flow. You will be redirected to openrouter.ai": "Ottieni il tuo token API di OpenRouter utilizzando il flusso OAuth. Sarai reindirizzato su openrouter.ai",342 "Get your OpenRouter API token using OAuth flow. You will be redirected to openrouter.ai": "Ottieni il tuo token API di OpenRouter utilizzando il flusso OAuth. Sarai reindirizzato su openrouter.ai",
341 "Bypass status check": "Ignora controllo stato",343 "Bypass status check": "Ignora controllo stato",
342 "Chat Completion Source": "Fonte di Completamento della Chat",344 "Chat Completion Source": "Fonte di Completamento della Chat",
@@ -1438,5 +1440,19 @@
1438 "Still have questions?": "Hai ancora domande?",1440 "Still have questions?": "Hai ancora domande?",
1439 "Join the SillyTavern Discord": "Unisciti al SillyTavern Discord",1441 "Join the SillyTavern Discord": "Unisciti al SillyTavern Discord",
1440 "Post a GitHub issue": "Pubblica un problema su GitHub",1442 "Post a GitHub issue": "Pubblica un problema su GitHub",
1441 "Contact the developers": "Contatta gli sviluppatori"1443 "Contact the developers": "Contatta gli sviluppatori",
1444 "Interleaved Thinking": "Ragionamento intercalato",
1445 "Since Last User Message": "Dall'ultimo messaggio utente",
1446 "Active Tool Chain": "Catena di strumenti attiva",
1447 "openrouter_interleaved_thinking_hint": "Invia il ragionamento dai turni precedenti dell'assistente con le richieste di strumenti per mantenere il contesto del ragionamento intercalato.",
1448 "SillyTavern Logo": "Logo SillyTavern",
1449 "Initializing…": "Inizializzazione…",
1450 "Name not accepted, as it is the same as before (ignoring case and accents).": "Nome non accettato, poiché è lo stesso di prima (ignorando maiuscole e accenti).",
1451 "Rename Chat": "Rinomina chat",
1452 "Renaming chat…": "Rinominazione chat…",
1453 "Delete Chat": "Elimina chat",
1454 "Deleting chat…": "Eliminazione chat…",
1455 "Loading chat…": "Caricamento chat…",
1456 "Bulk Delete": "Eliminazione multipla",
1457 "Deleting ${0} character(s)…": "Eliminazione di ${0} personaggio/i…"
1442}1458}
public/locales/ja-jp.json+17 -1
@@ -337,6 +337,8 @@
337 "koboldcpp API key (optional)": "koboldcpp API キー (オプション)",337 "koboldcpp API key (optional)": "koboldcpp API キー (オプション)",
338 "Example: http://127.0.0.1:5001": "例: http://127.0.0.1:5001",338 "Example: http://127.0.0.1:5001": "例: http://127.0.0.1:5001",
339 "Authorize": "承認",339 "Authorize": "承認",
340 "OpenRouter API key already exists": "OpenRouter API キーは既に存在します",
341 "Do you really wish to create a new OpenRouter key? Your existing key will not be deleted.": "新しい OpenRouter キーを作成してもよろしいですか?既存のキーは削除されません。",
340 "Get your OpenRouter API token using OAuth flow. You will be redirected to openrouter.ai": "OAuthフローを使用してOpenRouter APIトークンを取得します。 openrouter.aiにリダイレクトされます",342 "Get your OpenRouter API token using OAuth flow. You will be redirected to openrouter.ai": "OAuthフローを使用してOpenRouter APIトークンを取得します。 openrouter.aiにリダイレクトされます",
341 "Bypass status check": "ステータスのチェックをバイパスする",343 "Bypass status check": "ステータスのチェックをバイパスする",
342 "Chat Completion Source": "チャット補完ソース",344 "Chat Completion Source": "チャット補完ソース",
@@ -1443,5 +1445,19 @@
1443 "Contact the developers": "開発者に連絡",1445 "Contact the developers": "開発者に連絡",
1444 "Stop Inspecting": "検査を停止",1446 "Stop Inspecting": "検査を停止",
1445 "Inspect Prompts": "プロンプトを検査",1447 "Inspect Prompts": "プロンプトを検査",
1446 "Toggle prompt inspection": "プロンプト検査の切り替え"1448 "Toggle prompt inspection": "プロンプト検査の切り替え",
1449 "Interleaved Thinking": "インターリーブ思考",
1450 "Since Last User Message": "直前のユーザーメッセージ以降",
1451 "Active Tool Chain": "アクティブなツールチェーン",
1452 "openrouter_interleaved_thinking_hint": "インターリーブ思考の文脈を維持するため、直前のアシスタントターンの推論をツール呼び出しリクエストとともに送信します。",
1453 "SillyTavern Logo": "SillyTavernロゴ",
1454 "Initializing…": "初期化中…",
1455 "Name not accepted, as it is the same as before (ignoring case and accents).": "名前が受け入れられませんでした。以前と同じです(大文字小文字とアクセントを無視)。",
1456 "Rename Chat": "チャット名を変更",
1457 "Renaming chat…": "チャット名を変更中…",
1458 "Delete Chat": "チャットを削除",
1459 "Deleting chat…": "チャットを削除中…",
1460 "Loading chat…": "チャットを読み込み中…",
1461 "Bulk Delete": "一括削除",
1462 "Deleting ${0} character(s)…": "${0}人のキャラクターを削除中…"
1447}1463}
public/locales/ko-kr.json+18 -2
@@ -337,6 +337,8 @@
337 "koboldcpp API key (optional)": "koboldcpp API 키(선택사항)",337 "koboldcpp API key (optional)": "koboldcpp API 키(선택사항)",
338 "Example: http://127.0.0.1:5001": "예: http://127.0.0.1:5001",338 "Example: http://127.0.0.1:5001": "예: http://127.0.0.1:5001",
339 "Authorize": "승인하기",339 "Authorize": "승인하기",
340 "OpenRouter API key already exists": "OpenRouter API 키가 이미 존재합니다",
341 "Do you really wish to create a new OpenRouter key? Your existing key will not be deleted.": "정말 새로운 OpenRouter 키를 만들기를 원하십니까? 기존 키는 삭제되지 않습니다.",
340 "Get your OpenRouter API token using OAuth flow. You will be redirected to openrouter.ai": "OAuth 플로우를 사용하여 OpenRouter API 토큰을 가져옵니다. openrouter.ai로 리디렉션됩니다.",342 "Get your OpenRouter API token using OAuth flow. You will be redirected to openrouter.ai": "OAuth 플로우를 사용하여 OpenRouter API 토큰을 가져옵니다. openrouter.ai로 리디렉션됩니다.",
341 "Legacy API (pre-OAI, no streaming)": "레거시 API (OAI 이전, 스트리밍 없음)",343 "Legacy API (pre-OAI, no streaming)": "레거시 API (OAI 이전, 스트리밍 없음)",
342 "Bypass status check": "상태 확인 우회",344 "Bypass status check": "상태 확인 우회",
@@ -1533,7 +1535,7 @@
1533 "Only apply color as accent": "색상은 오직 강조로써만 적용됩니다",1535 "Only apply color as accent": "색상은 오직 강조로써만 적용됩니다",
1534 "qr--colorClear": "색상 지우기",1536 "qr--colorClear": "색상 지우기",
1535 "Color": "색상",1537 "Color": "색상",
1536 "world_button_title": "캐릭터 로어. 클릭하여 로드하세요. Shift를 클릭하면 '월드 인포 링크' 팝업이 열립니다.",1538 "world_button_title": "캐릭터 로어. 클릭하여 로드하세요. Shift/Alt+클릭 또는 길게 누르면 '월드 인포 링크' 팝업이 열립니다.",
1537 "Select TTS Provider": "TTS 공급자 선택",1539 "Select TTS Provider": "TTS 공급자 선택",
1538 "tts_enabled": "활성화",1540 "tts_enabled": "활성화",
1539 "Narrate user messages": "사용자 메시지 나레이션",1541 "Narrate user messages": "사용자 메시지 나레이션",
@@ -1617,5 +1619,19 @@
1617 "to install additional features.": "으로 가세요.",1619 "to install additional features.": "으로 가세요.",
1618 "Master Import": "마스터 불러오기",1620 "Master Import": "마스터 불러오기",
1619 "Master Export": "마스터 내보내기",1621 "Master Export": "마스터 내보내기",
1620 "Chat Quick Reply Sets": "채팅 빠른 답장 세트들"1622 "Chat Quick Reply Sets": "채팅 빠른 답장 세트들",
1623 "Interleaved Thinking": "인터리브드 사고",
1624 "Since Last User Message": "마지막 사용자 메시지 이후",
1625 "Active Tool Chain": "활성 도구 체인",
1626 "openrouter_interleaved_thinking_hint": "인터리브드 사고 컨텍스트를 유지하기 위해 이전 어시스턴트 턴의 추론을 도구 호출 요청과 함께 전송합니다.",
1627 "SillyTavern Logo": "SillyTavern 로고",
1628 "Initializing…": "초기화 중…",
1629 "Name not accepted, as it is the same as before (ignoring case and accents).": "이름이 승인되지 않았습니다. 이전과 동일합니다(대소문자 및 악센트 무시).",
1630 "Rename Chat": "채팅 이름 변경",
1631 "Renaming chat…": "채팅 이름 변경 중…",
1632 "Delete Chat": "채팅 삭제",
1633 "Deleting chat…": "채팅 삭제 중…",
1634 "Loading chat…": "채팅 불러오는 중…",
1635 "Bulk Delete": "대량 삭제",
1636 "Deleting ${0} character(s)…": "${0}개 캐릭터 삭제 중…"
1621}1637}
public/locales/nl-nl.json+17 -1
@@ -333,6 +333,8 @@
333 "koboldcpp API key (optional)": "koboldcpp API-sleutel (optioneel)",333 "koboldcpp API key (optional)": "koboldcpp API-sleutel (optioneel)",
334 "Example: http://127.0.0.1:5001": "Voorbeeld: http://127.0.0.1:5001",334 "Example: http://127.0.0.1:5001": "Voorbeeld: http://127.0.0.1:5001",
335 "Authorize": "Toestemming geven",335 "Authorize": "Toestemming geven",
336 "OpenRouter API key already exists": "OpenRouter API-sleutel bestaat al",
337 "Do you really wish to create a new OpenRouter key? Your existing key will not be deleted.": "Wilt u echt een nieuwe OpenRouter API-sleutel maken? Uw bestaande sleutel wordt niet verwijderd.",
336 "Get your OpenRouter API token using OAuth flow. You will be redirected to openrouter.ai": "Haal uw OpenRouter API-token op met behulp van OAuth-flow. U wordt doorgestuurd naar openrouter.ai",338 "Get your OpenRouter API token using OAuth flow. You will be redirected to openrouter.ai": "Haal uw OpenRouter API-token op met behulp van OAuth-flow. U wordt doorgestuurd naar openrouter.ai",
337 "Bypass status check": "Omzeil statuscontrole",339 "Bypass status check": "Omzeil statuscontrole",
338 "Chat Completion Source": "Bron voor Chatvoltooiing",340 "Chat Completion Source": "Bron voor Chatvoltooiing",
@@ -1434,5 +1436,19 @@
1434 "Still have questions?": "Heb je nog vragen?",1436 "Still have questions?": "Heb je nog vragen?",
1435 "Join the SillyTavern Discord": "Word lid van de SillyTavern Discord",1437 "Join the SillyTavern Discord": "Word lid van de SillyTavern Discord",
1436 "Post a GitHub issue": "Plaats een GitHub-probleem",1438 "Post a GitHub issue": "Plaats een GitHub-probleem",
1437 "Contact the developers": "Neem contact op met de ontwikkelaars"1439 "Contact the developers": "Neem contact op met de ontwikkelaars",
1440 "Interleaved Thinking": "Verweven redenering",
1441 "Since Last User Message": "Sinds laatste gebruikersbericht",
1442 "Active Tool Chain": "Actieve toolketen",
1443 "openrouter_interleaved_thinking_hint": "Verstuurt redenering uit eerdere assistentbeurten met tool-aanvragen om de context van verweven redenering te behouden.",
1444 "SillyTavern Logo": "SillyTavern-logo",
1445 "Initializing…": "Initialiseren…",
1446 "Name not accepted, as it is the same as before (ignoring case and accents).": "Naam niet geaccepteerd, omdat deze hetzelfde is als voorheen (hoofdletters en accenten genegeerd).",
1447 "Rename Chat": "Chat hernoemen",
1448 "Renaming chat…": "Chat hernoemen…",
1449 "Delete Chat": "Chat verwijderen",
1450 "Deleting chat…": "Chat verwijderen…",
1451 "Loading chat…": "Chat laden…",
1452 "Bulk Delete": "Bulkverwijdering",
1453 "Deleting ${0} character(s)…": "${0} personage(s) verwijderen…"
1438}1454}
public/locales/pt-pt.json+17 -1
@@ -335,6 +335,8 @@
335 "koboldcpp API key (optional)": "Chave API koboldcpp (opcional)",335 "koboldcpp API key (optional)": "Chave API koboldcpp (opcional)",
336 "Example: http://127.0.0.1:5001": "Exemplo: http://127.0.0.1:5001",336 "Example: http://127.0.0.1:5001": "Exemplo: http://127.0.0.1:5001",
337 "Authorize": "Autorizar",337 "Authorize": "Autorizar",
338 "OpenRouter API key already exists": "A chave de API do OpenRouter já existe",
339 "Do you really wish to create a new OpenRouter key? Your existing key will not be deleted.": "Você realmente deseja criar uma nova chave OpenRouter? Sua chave existente não será excluída.",
338 "Get your OpenRouter API token using OAuth flow. You will be redirected to openrouter.ai": "Obtenha seu token da API do OpenRouter usando o fluxo OAuth. Você será redirecionado para openrouter.ai",340 "Get your OpenRouter API token using OAuth flow. You will be redirected to openrouter.ai": "Obtenha seu token da API do OpenRouter usando o fluxo OAuth. Você será redirecionado para openrouter.ai",
339 "Bypass status check": "Ignorar verificação de status",341 "Bypass status check": "Ignorar verificação de status",
340 "Chat Completion Source": "Fonte de conclusão de chat",342 "Chat Completion Source": "Fonte de conclusão de chat",
@@ -1436,5 +1438,19 @@
1436 "Still have questions?": "Ainda tem perguntas?",1438 "Still have questions?": "Ainda tem perguntas?",
1437 "Join the SillyTavern Discord": "Junte-se ao Discord do SillyTavern",1439 "Join the SillyTavern Discord": "Junte-se ao Discord do SillyTavern",
1438 "Post a GitHub issue": "Publicar um problema no GitHub",1440 "Post a GitHub issue": "Publicar um problema no GitHub",
1439 "Contact the developers": "Contatar os desenvolvedores"1441 "Contact the developers": "Contatar os desenvolvedores",
1442 "Interleaved Thinking": "Raciocínio intercalado",
1443 "Since Last User Message": "Desde a última mensagem do utilizador",
1444 "Active Tool Chain": "Cadeia de ferramentas ativa",
1445 "openrouter_interleaved_thinking_hint": "Envia o raciocínio de turnos anteriores do assistente com pedidos de ferramentas para manter o contexto de raciocínio intercalado.",
1446 "SillyTavern Logo": "Logótipo do SillyTavern",
1447 "Initializing…": "A inicializar…",
1448 "Name not accepted, as it is the same as before (ignoring case and accents).": "Nome não aceite, pois é o mesmo que antes (ignorando maiúsculas e acentos).",
1449 "Rename Chat": "Renomear chat",
1450 "Renaming chat…": "A renomear chat…",
1451 "Delete Chat": "Eliminar chat",
1452 "Deleting chat…": "A eliminar chat…",
1453 "Loading chat…": "A carregar chat…",
1454 "Bulk Delete": "Eliminação em massa",
1455 "Deleting ${0} character(s)…": "A eliminar ${0} personagem(ns)…"
1440}1456}
public/locales/ru-ru.json+20 -4
@@ -758,6 +758,8 @@
758 "Doesn't work? Try adding": "Не работает? Попробуйте добавить в конце",758 "Doesn't work? Try adding": "Не работает? Попробуйте добавить в конце",
759 "at the end!": "!",759 "at the end!": "!",
760 "Authorize": "Авторизоваться",760 "Authorize": "Авторизоваться",
761 "OpenRouter API key already exists": "Ключ API OpenRouter уже существует",
762 "Do you really wish to create a new OpenRouter key? Your existing key will not be deleted.": "Вы действительно хотите создать новый ключ OpenRouter? Ваш существующий ключ не будет удален.",
761 "No persona description": "[Нет описания]",763 "No persona description": "[Нет описания]",
762 "Not connected to API!": "Нет соединения с API!",764 "Not connected to API!": "Нет соединения с API!",
763 "Type a message, or /? for help": "Введите сообщение, или /? для получения справки",765 "Type a message, or /? for help": "Введите сообщение, или /? для получения справки",
@@ -1902,7 +1904,7 @@
1902 "Save": "Сохранить",1904 "Save": "Сохранить",
1903 "Chat Lorebook": "Лорбук для чата",1905 "Chat Lorebook": "Лорбук для чата",
1904 "chat_world_template_txt": "Выбранный мир будет привязан к этому чату. Будет работать наряду с глобальным лорбуком и лором персонажа.",1906 "chat_world_template_txt": "Выбранный мир будет привязан к этому чату. Будет работать наряду с глобальным лорбуком и лором персонажа.",
1905 "world_button_title": "Лор персонажа\n\nНажмите, чтобы загрузить\nShift + ЛКМ, чтобы открыть диалог привязки мира",1907 "world_button_title": "Лор персонажа\n\nНажмите, чтобы загрузить\nShift / Alt + ЛКМ или долгое нажатие, чтобы открыть диалог привязки мира",
1906 "No auxillary Lorebooks set. Click here to select.": "Вспомогательный лорбук не выбран. Нажмите, чтобы выбрать.",1908 "No auxillary Lorebooks set. Click here to select.": "Вспомогательный лорбук не выбран. Нажмите, чтобы выбрать.",
1907 "ext_regex_user_input_desc": "Отправленные вами сообщения.",1909 "ext_regex_user_input_desc": "Отправленные вами сообщения.",
1908 "ext_regex_ai_input_desc": "Полученные от API ответы.",1910 "ext_regex_ai_input_desc": "Полученные от API ответы.",
@@ -2128,7 +2130,7 @@
2128 "openai_reasoning_effort_low": "Поверхностные",2130 "openai_reasoning_effort_low": "Поверхностные",
2129 "openai_reasoning_effort_medium": "Обычные",2131 "openai_reasoning_effort_medium": "Обычные",
2130 "openai_reasoning_effort_high": "Подробные",2132 "openai_reasoning_effort_high": "Подробные",
2131 "Persona Lore Alt+Click to open the lorebook": "Лорбук данной персоны\nAlt + ЛКМ чтобы открыть лорбук",2133 "persona_lore_button_title": "Лорбук данной персоны\n\nНажмите, чтобы открыть\nShift / Alt + ЛКМ или долгое нажатие, чтобы открыть диалог привязки лорбука",
2132 "Persona Lorebook for": "Лорбук для персоны",2134 "Persona Lorebook for": "Лорбук для персоны",
2133 "persona_world_template_txt": "Выбранный мир будет привязан к этой персоне. Будет работать вместе с глобальным лорбуком и лорбуками персонажа и чата.",2135 "persona_world_template_txt": "Выбранный мир будет привязан к этой персоне. Будет работать вместе с глобальным лорбуком и лорбуками персонажа и чата.",
2134 "Global list": "Глобальный список",2136 "Global list": "Глобальный список",
@@ -2143,7 +2145,7 @@
2143 "Enter a valid API URL": "Введите корректный адрес API",2145 "Enter a valid API URL": "Введите корректный адрес API",
2144 "No Ollama model selected.": "Не выбрана модель Ollama",2146 "No Ollama model selected.": "Не выбрана модель Ollama",
2145 "Background Fitting": "Способ подгонки фона под разрешение",2147 "Background Fitting": "Способ подгонки фона под разрешение",
2146 "Chat Lore Alt+Click to open the lorebook": "Лорбук данного чата\nAlt + ЛКМ чтобы открыть лорбук",2148 "chat_lorebook_button_title": "Лорбук данного чата\n\nНажмите, чтобы открыть\nShift / Alt + ЛКМ или долгое нажатие, чтобы открыть диалог привязки лорбука",
2147 "Token Counter": "Подсчитать токены",2149 "Token Counter": "Подсчитать токены",
2148 "Type / paste in the box below to see the number of tokens in the text.": "Введите или вставьте текст в окошко ниже, чтобы подсчитать количество токенов в нём.",2150 "Type / paste in the box below to see the number of tokens in the text.": "Введите или вставьте текст в окошко ниже, чтобы подсчитать количество токенов в нём.",
2149 "Selected tokenizer:": "Выбранный токенайзер:",2151 "Selected tokenizer:": "Выбранный токенайзер:",
@@ -2738,5 +2740,19 @@
2738 "Uses the voices provided by your operating system": "Используем стандартные голоса вашей ОС",2740 "Uses the voices provided by your operating system": "Используем стандартные голоса вашей ОС",
2739 "Your browser or operating system doesn't support speech synthesis": "Ваш браузер либо ОС не поддерживают синтез речи",2741 "Your browser or operating system doesn't support speech synthesis": "Ваш браузер либо ОС не поддерживают синтез речи",
2740 "Works best when: Pass Asterisks to TTS Engine is enabled, and both Only narrate quotes and Ignore *text, even 'quotes', inside asterisks* are disabled.": "Работает лучше всего, когда: одновременно включена опция \"Передавать звёздочки TTS-движку\", а также отключены обе опции: \"Не озвучивать *текст, даже \"в кавычках\", если он внутри звёздочек*\", и \"Озвучивать только текст \"в кавычках\"\"",2742 "Works best when: Pass Asterisks to TTS Engine is enabled, and both Only narrate quotes and Ignore *text, even 'quotes', inside asterisks* are disabled.": "Работает лучше всего, когда: одновременно включена опция \"Передавать звёздочки TTS-движку\", а также отключены обе опции: \"Не озвучивать *текст, даже \"в кавычках\", если он внутри звёздочек*\", и \"Озвучивать только текст \"в кавычках\"\"",
2741 "Available voices": "Доступные голоса"2743 "Available voices": "Доступные голоса",
2744 "Interleaved Thinking": "Чередующееся рассуждение",
2745 "Since Last User Message": "С момента последнего сообщения пользователя",
2746 "Active Tool Chain": "Активная цепочка инструментов",
2747 "openrouter_interleaved_thinking_hint": "Отправляет рассуждение из предыдущих ходов ассистента вместе с запросами вызова инструментов, чтобы сохранять контекст чередующегося рассуждения.",
2748 "SillyTavern Logo": "Логотип SillyTavern",
2749 "Initializing…": "Инициализация…",
2750 "Name not accepted, as it is the same as before (ignoring case and accents).": "Имя не принято, так как оно совпадает с предыдущим (игнорируя регистр и акценты).",
2751 "Rename Chat": "Переименовать чат",
2752 "Renaming chat…": "Переименование чата…",
2753 "Delete Chat": "Удалить чат",
2754 "Deleting chat…": "Удаление чата…",
2755 "Loading chat…": "Загрузка чата…",
2756 "Bulk Delete": "Массовое удаление",
2757 "Deleting ${0} character(s)…": "Удаление ${0} персонажа(ей)…"
2742}2758}
public/locales/th-th.json+17 -1
@@ -339,6 +339,8 @@
339 "koboldcpp API key (optional)": "คีย์ API ของ koboldcpp (เพิ่มเติม)",339 "koboldcpp API key (optional)": "คีย์ API ของ koboldcpp (เพิ่มเติม)",
340 "Example: http://127.0.0.1:5001": "ตัวอย่าง: http://127.0.0.1:5001",340 "Example: http://127.0.0.1:5001": "ตัวอย่าง: http://127.0.0.1:5001",
341 "Authorize": "อนุญาต",341 "Authorize": "อนุญาต",
342 "OpenRouter API key already exists": "คีย์ API ของ OpenRouter มีอยู่แล้ว",
343 "Do you really wish to create a new OpenRouter key? Your existing key will not be deleted.": "คุณต้องการสร้างคีย์ OpenRouter ใหม่จริง ๆ หรือไม่ คีย์ที่มีอยู่ของคุณจะไม่ถูกลบ",
342 "Get your OpenRouter API token using OAuth flow. You will be redirected to openrouter.ai": "โปรดเข้าสู่ระบบเพื่อรับ OpenRouter API Token โดยใช้ OAuth Flow จากนั้นคุณจะถูกเปลี่ยนเส้นทางไปยังเว็บไซต์ของ openrouter.ai",344 "Get your OpenRouter API token using OAuth flow. You will be redirected to openrouter.ai": "โปรดเข้าสู่ระบบเพื่อรับ OpenRouter API Token โดยใช้ OAuth Flow จากนั้นคุณจะถูกเปลี่ยนเส้นทางไปยังเว็บไซต์ของ openrouter.ai",
343 "Bypass status check": "ข้ามการตรวจสอบสถานะ",345 "Bypass status check": "ข้ามการตรวจสอบสถานะ",
344 "Chat Completion Source": "แหล่งที่มาของ Chat Completion",346 "Chat Completion Source": "แหล่งที่มาของ Chat Completion",
@@ -1455,5 +1457,19 @@
1455 "Contact the developers": "ติดต่อผู้พัฒนา",1457 "Contact the developers": "ติดต่อผู้พัฒนา",
1456 "Stop Inspecting": "Stop Inspecting",1458 "Stop Inspecting": "Stop Inspecting",
1457 "Inspect Prompts": "ตรวจสอบ Prompt",1459 "Inspect Prompts": "ตรวจสอบ Prompt",
1458 "Toggle prompt inspection": "สลับการตรวจสอบ Prompt"1460 "Toggle prompt inspection": "สลับการตรวจสอบ Prompt",
1461 "Interleaved Thinking": "การคิดแบบสลับขั้น",
1462 "Since Last User Message": "ตั้งแต่ข้อความล่าสุดของผู้ใช้",
1463 "Active Tool Chain": "สายโซ่เครื่องมือที่ใช้งานอยู่",
1464 "openrouter_interleaved_thinking_hint": "ส่งเหตุผลจากเทิร์นก่อนหน้าของผู้ช่วยไปพร้อมกับคำขอเรียกใช้เครื่องมือ เพื่อคงบริบทการคิดแบบสลับขั้นไว้",
1465 "SillyTavern Logo": "โลโก้ SillyTavern",
1466 "Initializing…": "กำลังเริ่มต้น…",
1467 "Name not accepted, as it is the same as before (ignoring case and accents).": "ชื่อไม่ได้รับการยอมรับ เนื่องจากเหมือนกับก่อนหน้านี้ (ไม่สนใจตัวพิมพ์ใหญ่เล็กและเครื่องหมายเสียง)",
1468 "Rename Chat": "เปลี่ยนชื่อแชท",
1469 "Renaming chat…": "กำลังเปลี่ยนชื่อแชท…",
1470 "Delete Chat": "ลบแชท",
1471 "Deleting chat…": "กำลังลบแชท…",
1472 "Loading chat…": "กำลังโหลดแชท…",
1473 "Bulk Delete": "ลบจำนวนมาก",
1474 "Deleting ${0} character(s)…": "กำลังลบ ${0} ตัวละคร…"
1459}1475}
public/locales/uk-ua.json+17 -1
@@ -335,6 +335,8 @@
335 "koboldcpp API key (optional)": "API-ключ koboldcpp (необов’язково)",335 "koboldcpp API key (optional)": "API-ключ koboldcpp (необов’язково)",
336 "Example: http://127.0.0.1:5001": "Приклад: http://127.0.0.1:5001",336 "Example: http://127.0.0.1:5001": "Приклад: http://127.0.0.1:5001",
337 "Authorize": "Авторизувати",337 "Authorize": "Авторизувати",
338 "OpenRouter API key already exists": "API ключ OpenRouter вже існує",
339 "Do you really wish to create a new OpenRouter key? Your existing key will not be deleted.": "Ви дійсно бажаєте створити новий ключ OpenRouter API? Ваш існуючий ключ не буде видалено.",
338 "Get your OpenRouter API token using OAuth flow. You will be redirected to openrouter.ai": "Отримайте свій токен API OpenRouter за допомогою OAuth. Вас буде перенаправлено на openrouter.ai",340 "Get your OpenRouter API token using OAuth flow. You will be redirected to openrouter.ai": "Отримайте свій токен API OpenRouter за допомогою OAuth. Вас буде перенаправлено на openrouter.ai",
339 "Bypass status check": "Обійти перевірку статусу",341 "Bypass status check": "Обійти перевірку статусу",
340 "Chat Completion Source": "Джерело Chat Completion",342 "Chat Completion Source": "Джерело Chat Completion",
@@ -1436,5 +1438,19 @@
1436 "Still have questions?": "Все ще є питання?",1438 "Still have questions?": "Все ще є питання?",
1437 "Join the SillyTavern Discord": "Приєднуйтесь до SillyTavern Discord",1439 "Join the SillyTavern Discord": "Приєднуйтесь до SillyTavern Discord",
1438 "Post a GitHub issue": "Опублікуйте проблему на GitHub",1440 "Post a GitHub issue": "Опублікуйте проблему на GitHub",
1439 "Contact the developers": "Зв'яжіться з розробниками"1441 "Contact the developers": "Зв'яжіться з розробниками",
1442 "Interleaved Thinking": "Черговане мислення",
1443 "Since Last User Message": "Від останнього повідомлення користувача",
1444 "Active Tool Chain": "Активний ланцюжок інструментів",
1445 "openrouter_interleaved_thinking_hint": "Надсилає міркування з попередніх ходів асистента разом із запитами виклику інструментів, щоб зберегти контекст чергованого мислення.",
1446 "SillyTavern Logo": "Логотип SillyTavern",
1447 "Initializing…": "Ініціалізація…",
1448 "Name not accepted, as it is the same as before (ignoring case and accents).": "Ім'я не прийнято, оскільки воно збігається з попереднім (ігноруючи регістр та акценти).",
1449 "Rename Chat": "Перейменувати чат",
1450 "Renaming chat…": "Перейменування чату…",
1451 "Delete Chat": "Видалити чат",
1452 "Deleting chat…": "Видалення чату…",
1453 "Loading chat…": "Завантаження чату…",
1454 "Bulk Delete": "Масове видалення",
1455 "Deleting ${0} character(s)…": "Видалення ${0} персонажа(ів)…"
1440}1456}
public/locales/vi-vn.json+17 -1
@@ -335,6 +335,8 @@
335 "koboldcpp API key (optional)": "Key API koboldcpp (tùy chọn)",335 "koboldcpp API key (optional)": "Key API koboldcpp (tùy chọn)",
336 "Example: http://127.0.0.1:5001": "Ví dụ: http://127.0.0.1:5001",336 "Example: http://127.0.0.1:5001": "Ví dụ: http://127.0.0.1:5001",
337 "Cho phép": "Ủy quyền",337 "Cho phép": "Ủy quyền",
338 "OpenRouter API key already exists": "Key API OpenRouter đã tồn tại",
339 "Do you really wish to create a new OpenRouter key? Your existing key will not be deleted.": "Bạn có thực sự muốn tạo khóa OpenRouter mới không? Khóa hiện có của bạn sẽ không bị xóa.",
338 "Get your OpenRouter API token using OAuth flow. You will be redirected to openrouter.ai": "Nhận mã thông báo API OpenRouter của bạn bằng cách sử dụng luồng OAuth. Bạn sẽ được chuyển hướng đến openrouter.ai",340 "Get your OpenRouter API token using OAuth flow. You will be redirected to openrouter.ai": "Nhận mã thông báo API OpenRouter của bạn bằng cách sử dụng luồng OAuth. Bạn sẽ được chuyển hướng đến openrouter.ai",
339 "Bypass status check": "Bỏ qua check trạng thái",341 "Bypass status check": "Bỏ qua check trạng thái",
340 "Chat Completion Source": "Nguồn cho Chat Completion",342 "Chat Completion Source": "Nguồn cho Chat Completion",
@@ -1436,5 +1438,19 @@
1436 "Still have questions?": "Bạn còn thắc mắc?",1438 "Still have questions?": "Bạn còn thắc mắc?",
1437 "Join the SillyTavern Discord": "Tham gia Kênh Discord của SillyTavern",1439 "Join the SillyTavern Discord": "Tham gia Kênh Discord của SillyTavern",
1438 "Post a GitHub issue": "Đăng một vấn đề trên GitHub",1440 "Post a GitHub issue": "Đăng một vấn đề trên GitHub",
1439 "Contact the developers": "Liên hệ với Devs"1441 "Contact the developers": "Liên hệ với Devs",
1442 "Interleaved Thinking": "Lập luận xen kẽ",
1443 "Since Last User Message": "Kể từ tin nhắn người dùng gần nhất",
1444 "Active Tool Chain": "Chuỗi công cụ đang hoạt động",
1445 "openrouter_interleaved_thinking_hint": "Gửi lập luận từ các lượt trợ lý trước đó cùng với yêu cầu gọi công cụ để duy trì ngữ cảnh lập luận xen kẽ.",
1446 "SillyTavern Logo": "Logo SillyTavern",
1447 "Initializing…": "Đang khởi tạo…",
1448 "Name not accepted, as it is the same as before (ignoring case and accents).": "Tên không được chấp nhận vì giống như trước đó (bỏ qua chữ hoa/thường và dấu).",
1449 "Rename Chat": "Đổi tên cuộc trò chuyện",
1450 "Renaming chat…": "Đang đổi tên cuộc trò chuyện…",
1451 "Delete Chat": "Xóa cuộc trò chuyện",
1452 "Deleting chat…": "Đang xóa cuộc trò chuyện…",
1453 "Loading chat…": "Đang tải cuộc trò chuyện…",
1454 "Bulk Delete": "Xóa hàng loạt",
1455 "Deleting ${0} character(s)…": "Đang xóa ${0} nhân vật…"
1440}1456}
public/locales/zh-cn.json+1813 -482
@@ -4,8 +4,8 @@
4 "Duplicate": "复制",4 "Duplicate": "复制",
5 "Persona": "用户设定",5 "Persona": "用户设定",
6 "Delete": "删除",6 "Delete": "删除",
7 "AI Response Configuration": "AI响应配置",7 "AI Response Configuration": "AI 响应配置",
8 "AI Configuration panel will stay open": "AI配置面板将保持打开",8 "AI Configuration panel will stay open": "AI 配置面板将保持打开",
9 "clickslidertips": "单击滑块以手动输入值。",9 "clickslidertips": "单击滑块以手动输入值。",
10 "MAD LAB MODE ON": "疯狂实验室模式开启",10 "MAD LAB MODE ON": "疯狂实验室模式开启",
11 "Documentation on sampling parameters": "有关采样参数的文档",11 "Documentation on sampling parameters": "有关采样参数的文档",
@@ -23,25 +23,25 @@
23 "openaipresets": "对话补全预设",23 "openaipresets": "对话补全预设",
24 "Bind presets to API connections": "将预设与 API 配置绑定",24 "Bind presets to API connections": "将预设与 API 配置绑定",
25 "Text Completion presets": "文本补全预设",25 "Text Completion presets": "文本补全预设",
26 "response legth(tokens)": "回复长度(以词符数计)",26 "response legth(tokens)": "回复长度 (Token)",
27 "Streaming": "流式传输",27 "Streaming": "流式传输",
28 "Streaming_desc": "逐词显示生成的回复",28 "Streaming_desc": "逐词显示生成的回复",
29 "context size(tokens)": "上下文长度(以词符数计)",29 "context size(tokens)": "上下文长度 (Token)",
30 "unlocked": "解锁",30 "unlocked": "解锁",
31 "Only enable this if your model supports context sizes greater than 8192 tokens": "仅在您的模型支持大于8192个词符的上下文长度时启用此选项",31 "Only enable this if your model supports context sizes greater than 8192 tokens": "仅在您的模型支持大于 8192 个 Token 的上下文长度时启用此选项",
32 "Max prompt cost:": "最大提示词费用:",32 "Max prompt cost:": "最大提示词开销:",
33 "AI Module": "AI 模块",33 "AI Module": "AI 模块",
34 "Changes the style of the generated text.": "更改生成文本的样式。",34 "Changes the style of the generated text.": "更改生成文本的样式。",
35 "No Module": "无模块",35 "No Module": "无模块",
36 "Instruct": "指导",36 "Instruct": "格式指引",
37 "Prose Augmenter": "散文增强器",37 "Prose Augmenter": "文风润色",
38 "Text Adventure": "文字冒险",38 "Text Adventure": "文字冒险",
39 "Temperature": "温度",39 "Temperature": "温度",
40 "rep.pen": "重复惩罚",40 "rep.pen": "重复惩罚",
41 "Rep. Pen. Range.": "重复惩罚范围。",41 "Rep. Pen. Range.": "重复惩罚范围",
42 "Rep. Pen. Slope": "重复惩罚斜率",42 "Rep. Pen. Slope": "重复惩罚斜率",
43 "Rep. Pen. Freq.": "频率重复惩罚",43 "Rep. Pen. Freq.": "频率惩罚",
44 "Rep. Pen. Presence": "存在重复惩罚",44 "Rep. Pen. Presence": "存在惩罚",
45 "Min P": "Min P",45 "Min P": "Min P",
46 "TFS": "无尾采样",46 "TFS": "无尾采样",
47 "Top P": "Top P",47 "Top P": "Top P",
@@ -49,53 +49,53 @@
49 "Top K": "Top K",49 "Top K": "Top K",
50 "Mirostat Tau": "Mirostat τ",50 "Mirostat Tau": "Mirostat τ",
51 "Mirostat LR": "Mirostat 学习率",51 "Mirostat LR": "Mirostat 学习率",
52 "Typical P": "典型P",52 "Typical P": "Typical P",
53 "Linear": "Linear",53 "Linear": "线性",
54 "Quad": "Quad",54 "Quad": "二次",
55 "Conf": "Conf",55 "Conf": "置信度",
56 "Min Length": "最小长度",56 "Min Length": "最小长度",
57 "Phrase Repetition Penalty": "短语重复惩罚",57 "Phrase Repetition Penalty": "短语重复惩罚",
58 "Off": "关闭",58 "Off": "关闭",
59 "Very light": "非常轻",59 "Very light": "极轻",
60 "Light": "轻",60 "Light": "轻",
61 "Medium": "中",61 "Medium": "中",
62 "Aggressive": "激进",62 "Aggressive": "激进",
63 "Very aggressive": "非常激进",63 "Very aggressive": "极激进",
64 "Preamble": "序文",64 "Preamble": "序言",
65 "Restore default prompt": "恢复默认提示词",65 "Restore default prompt": "恢复默认提示词",
66 "Use style tags to modify the writing style of the output.": "使用样式标签修改输出的写作风格。",66 "Use style tags to modify the writing style of the output.": "使用样式标签 (Style Tags) 修改输出的写作风格。",
67 "Banned Tokens": "禁用的词符",67 "Banned Tokens": "黑名单 Token",
68 "Sequences you don't want to appear in the output. One per line.": "您不希望出现在输出中的字符串。 每行一个。",68 "Sequences you don't want to appear in the output. One per line.": "您不希望出现在输出中的字符串。每行一个。",
69 "Logit Bias": "Logit 偏置",69 "Logit Bias": "Logit 偏置",
70 "Add": "添加",70 "Add": "添加",
71 "Helps to ban or reenforce the usage of certain words": "有助于禁止或加强某些单词的使用",71 "Helps to ban or reenforce the usage of certain words": "有助于禁止或加强特定词语的使用",
72 "Unlocked Context Size": "解锁上下文长度",72 "Unlocked Context Size": "解锁上下文上限",
73 "Unrestricted maximum value for the context slider": "AI可见的最大上下文长度",73 "Unrestricted maximum value for the context slider": "移除上下文滑块的最大值限制",
74 "Context Size (tokens)": "上下文长度(以词符数计)",74 "Context Size (tokens)": "上下文长度 (Token)",
75 "Max Response Length (tokens)": "最大回复长度(以词符数计)",75 "Max Response Length (tokens)": "最大回复长度 (Token)",
76 "Multiple swipes per generation": "每次生成多个备选回复",76 "Multiple swipes per generation": "每次生成多个备选回复",
77 "Allow compressing requests by removing messages from the middle of the prompt.": "允许通过移除提示词中间的消息来压缩请求。",77 "Allow compressing requests by removing messages from the middle of the prompt.": "允许通过移除提示词中间的消息来压缩请求。",
78 "Middle-out Transform": "中心向外算法",78 "Middle-out Transform": "中间向外变换",
79 "Auto": "自动",79 "Auto": "自动",
80 "Allow": "允许",80 "Allow": "允许",
81 "Forbid": "禁止",81 "Forbid": "禁止",
82 "Unknown": "未知",82 "Unknown": "未知",
83 "Enable OpenAI completion streaming": "启用OpenAI文本补全流式传输",83 "Enable OpenAI completion streaming": "启用 OpenAI 文本补全流式传输",
84 "Display the response bit by bit as it is generated.": "随着回复的生成,逐词逐句地显示结果。",84 "Display the response bit by bit as it is generated.": "随着回复生成逐字显示结果。",
85 "When this is off, responses will be displayed all at once when they are complete.": "当此选项关闭时,回复将在完成后一次性显示。",85 "When this is off, responses will be displayed all at once when they are complete.": "关闭此项时,回复将在完成后一次性显示。",
86 "Frequency Penalty": "频率惩罚",86 "Frequency Penalty": "频率惩罚",
87 "Presence Penalty": "存在惩罚",87 "Presence Penalty": "存在惩罚",
88 "Repetition Penalty": "重复惩罚",88 "Repetition Penalty": "重复惩罚",
89 "Quick Prompts Edit": "快速提示词编辑",89 "Quick Prompts Edit": "快速提示词编辑",
90 "Main": "主要",90 "Main": "主要",
91 "Auxiliary": "辅助的",91 "Auxiliary": "辅助",
92 "Post-History Instructions": "后续历史指令",92 "Post-History Instructions": "历史后置指令",
93 "Utility Prompts": "实用提示词",93 "Utility Prompts": "实用提示词",
94 "Impersonation prompt": "AI帮答提示词",94 "Impersonation prompt": "AI 帮答提示词",
95 "Prompt that is used for Impersonation function": "用于AI帮答功能的提示词",95 "Prompt that is used for Impersonation function": "用于 AI 帮答功能的提示词",
96 "World Info Format Template": "世界信息格式模板",96 "World Info Format Template": "世界书格式模板",
97 "Restore default format": "恢复默认格式",97 "Restore default format": "恢复默认格式",
98 "Wraps activated World Info entries before inserting into the prompt.": "在插入提示词之前包裹激活的世界信息条目。",98 "Wraps activated World Info entries before inserting into the prompt.": "在插入提示词之前包裹已激活的世界书条目。",
99 "scenario_format_template_part_1": "使用",99 "scenario_format_template_part_1": "使用",
100 "scenario_format_template_part_2": "标记插入内容的位置。",100 "scenario_format_template_part_2": "标记插入内容的位置。",
101 "Scenario Format Template": "场景格式模板",101 "Scenario Format Template": "场景格式模板",
@@ -103,124 +103,127 @@
103 "Group Nudge Prompt Template": "群聊推进提示词模板",103 "Group Nudge Prompt Template": "群聊推进提示词模板",
104 "Sent at the end of the group chat history to force reply from a specific character.": "在群聊记录的末尾发送,以强制特定角色回复。",104 "Sent at the end of the group chat history to force reply from a specific character.": "在群聊记录的末尾发送,以强制特定角色回复。",
105 "New Chat": "新聊天",105 "New Chat": "新聊天",
106 "Restore new chat prompt": "恢复新的聊天提示词",106 "Restore new chat prompt": "恢复新聊天提示词",
107 "Set at the beginning of the chat history to indicate that a new chat is about to start.": "设置在聊天历史的开头,表示新的聊天即将开始。",107 "Set at the beginning of the chat history to indicate that a new chat is about to start.": "设置在聊天记录的开头,表示新的聊天即将开始。",
108 "New Group Chat": "新群聊",108 "New Group Chat": "新群聊",
109 "Restore new group chat prompt": "恢复默认提示词",109 "Restore new group chat prompt": "恢复新群聊提示词",
110 "Set at the beginning of the chat history to indicate that a new group chat is about to start.": "设置在聊天记录的开头,表示新的群聊即将开始。",110 "Set at the beginning of the chat history to indicate that a new group chat is about to start.": "设置在聊天记录的开头,表示新的群聊即将开始。",
111 "New Example Chat": "新示例聊天",111 "New Example Chat": "新示例聊天",
112 "Set at the beginning of Dialogue examples to indicate that a new example chat is about to start.": "设置在对话示例的开头,以表明新的示例聊天即将开始。",112 "Set at the beginning of Dialogue examples to indicate that a new example chat is about to start.": "设置在对话示例的开头,以表明新的示例聊天即将开始。",
113 "Continue nudge": "继续推进",113 "Continue nudge": "继续推进",
114 "Set at the end of the chat history when the continue button is pressed.": "当按下继续按钮时在聊天记录的末尾设置。",114 "Set at the end of the chat history when the continue button is pressed.": "当按下“续写”按钮时,在聊天记录末尾添加的内容。",
115 "Replace empty message": "替换空消息",115 "Replace empty message": "替换空消息",
116 "Send this text instead of nothing when the text box is empty.": "当文本框为空时,发送此文本而不是空白。",116 "Send this text instead of nothing when the text box is empty.": "当输入框为空时发送此文本。",
117 "Seed": "种子",117 "Seed": "种子",
118 "Set to get deterministic results. Use -1 for random seed.": "设置此值以得到确定性结果。使用 -1 表示随机种子。",118 "Set to get deterministic results. Use -1 for random seed.": "设置此值以获得确定性结果。使用 -1 代表随机种子。",
119 "Temperature controls the randomness in token selection": "温度控制词符选择中的随机性:\n- 低温(<1.0)导致更可预测的文本,优先选择高概率的词符。\n- 高温(>1.0)鼓励创造性和输出的多样性,更多地选择低概率的词符。\n将值设置为 1.0 以使用原始概率。",119 "Temperature controls the randomness in token selection": "温度控制 Token 选择的随机性:\n- 低温(<1.0)会生成更可预测的文本,优先选择高概率的 Token。\n- 高温(>1.0)鼓励创造性和多样性,增加低概率 Token 被选中的机会。\n设为 1.0 即使用原始概率。",
120 "Top_K_desc": "Top K 设定了可以选择的最高概率词符的最大数量。",120 "Top_K_desc": "Top K 设定了仅保留概率最高的 K 个 Token 用于选择。",
121 "Top_P_desc": "Top P(又称核采样)将所有高概率词符聚集在一起,直到达到特定的百分比。\n换句话说,如果前两个词符分别都有 25% 的概率,而 Top-P 为 0.50,那么只有这两个词符会被考虑。\n将这个值设置为 1.0 就相当于关闭了这个功能。",121 "Top_P_desc": "Top P 会选取累积概率达到特定百分比的高概率 Token 集合。\n换句话说,如果前两个 Token 的概率均为 25%,且 Top P 设为 0.50,则仅这两个 Token 会被考虑。\n设为 1.0 即关闭此功能。",
122 "Typical_P_desc": "典型P采样会根据词符与整体熵的平均差异来优先选择词符。\n那些累积概率接近特定阈值(比如 0.5)的词符会被保留,这样就能区分出那些含有平均信息量的词符。\n将这个值设置为 1.0 就相当于关闭了这个功能。",122 "Typical_P_desc": "Typical P 根据 Token 概率与预期熵的接近程度来选择 Token。\n它会保留累积概率接近特定阈值(如 0.5)的 Token,从而筛选出信息量接近平均水平的候选词。\n设为 1.0 即关闭此功能。",
123 "Min_P_desc": "Min P 设定了一个基础的最小概率,它会根据最高词符概率来进行优化。\n如果最高词符概率是 80%,而Min P设定为 0.1,那么只有那些概率高于8%的词符会被考虑。\n将这个值设置为 0 就相当于关闭了这个功能。",123 "Min_P_desc": "Min P 设定了一个相对于最高概率 Token 的最小阈值。\n如果最高概率 Token 为 80%,且 Min P 设为 0.1,则只有概率高于 8% 的 Token 会被保留。\n设为 0 即关闭此功能。",
124 "Top_A_desc": "Top A 设定了一个阈值,用于根据最高词符概率的平方来选择词符。\n如果 Top A 设定为 0.2,而最高词符概率是 50%,那么概率低于 5% 的词符会被排除(0.2 * 0.5^2)。\n将这个值设置为 0 就相当于关闭了这个功能。",124 "Top_A_desc": "Top A 根据最高概率 Token 的平方设定动态阈值。\n若 Top A 设为 0.2,最高概率 Token 为 50%,则概率低于 5%(即 0.2 * 0.5²)的 Token 会被排除。\n设为 0 即关闭此功能。",
125 "Tail_Free_Sampling_desc": "无尾采样(TFS)通过分析词符概率变化率以及二阶导数来搜索分布中概率较低的尾部词符,\n词符会被保留到某个阈值(例如 0.3),这取决于统一的二阶导数。\n这个值越接近 0,被拒绝的词符数量就越多。将这个值设置为 1.0 就相当于关闭了这个功能。",125 "Tail_Free_Sampling_desc": "无尾采样 (TFS) 通过分析 Token 概率的二阶导数(变化率)来寻找并截断概率分布的“尾部”。\nToken 会被保留直到达到取决于二阶导数的某个阈值(例如 0.3)。\n该值越接近 0,被截断的 Token 越多。设为 1.0 即关闭此功能。",
126 "rep.pen range": "重复惩罚范围",126 "rep.pen range": "重复惩罚范围",
127 "Mirostat": "Mirostat",127 "Mirostat": "Mirostat",
128 "Mode": "模式",128 "Mode": "模式",
129 "Mirostat_Mode_desc": "值为 0 表示完全禁用 Mirostat。1 表示 Mirostat 1.0,2 表示 Mirostat 2.0",129 "Mirostat_Mode_desc": "设为 0 禁用 Mirostat。1 代表 Mirostat 1.0,2 代表 Mirostat 2.0。",
130 "Tau": "τ",130 "Tau": "τ (Tau)",
131 "Mirostat_Tau_desc": "控制 Mirostat 输出的变化",131 "Mirostat_Tau_desc": "控制 Mirostat 的目标熵和输出连贯性。",
132 "Eta": "η",132 "Eta": "η (Eta)",
133 "Mirostat_Eta_desc": "控制 Mirostat 的学习率",133 "Mirostat_Eta_desc": "控制 Mirostat 的学习率。",
134 "Ban EOS Token": "禁止 EOS 词符",134 "Ban EOS Token": "禁止 EOS Token",
135 "Ban_EOS_Token_desc": "禁止使用 KoboldCpp 的序列结束 (EOS) 词符(可能还禁止使用 KoboldAI 的其他词符)。适合故事写作,但不应用于聊天和指导模式。",135 "Ban_EOS_Token_desc": "禁止生成 KoboldCpp 的序列结束 (EOS) Token(可能还会禁止 KoboldAI 的其他 Token)。适合故事续写,但不建议用于聊天或格式指引模式。",
136 "GBNF Grammar": "GBNF 语法",136 "GBNF Grammar": "GBNF 语法",
137 "Type in the desired custom grammar": "输入所需的自定义语法",137 "Type in the desired custom grammar": "输入所需的自定义语法",
138 "Samplers Order": "采样器顺序",138 "Samplers Order": "采样器顺序",
139 "Samplers will be applied in a top-down order. Use with caution.": "采样器将按自上而下的顺序应用。请谨慎使用。",139 "Samplers will be applied in a top-down order. Use with caution.": "采样器将按自上而下的顺序应用。请谨慎使用。",
140 "Tail Free Sampling": "无尾采样",140 "Tail Free Sampling": "无尾采样",
141 "Load koboldcpp order": "加载koboldcpp顺序",141 "Load koboldcpp order": "加载 KoboldCpp 默认顺序",
142 "Top K Sampling": "Top K 采样",142 "Top K Sampling": "Top K 采样",
143 "Nucleus Sampling": "核采样",143 "Nucleus Sampling": "核采样",
144 "Top A Sampling": "Top A 采样",144 "Top A Sampling": "Top A 采样",
145 "Unified Sampling": "Unified Sampling",145 "Unified Sampling": "统一采样",
146 "Neutralize Samplers": "置采样器参数为失效值",146 "Neutralize Samplers": "重置采样器",
147 "Set all samplers to their neutral/disabled state.": "将所有采样器设置为失效/禁用状态。",147 "Set all samplers to their neutral/disabled state.": "将所有采样器设置为失效/禁用状态。",
148 "Sampler Select": "采样器选择",148 "Sampler Select": "选择采样器",
149 "Customize displayed samplers or add custom samplers.": "自定义显示的采样器或添加自定义采样器。",149 "Customize displayed samplers or add custom samplers.": "自定义显示的采样器或添加自定义采样器。",
150 "Epsilon Cutoff": "ε 截断",150 "Epsilon Cutoff": "Epsilon 截断",
151 "Epsilon cutoff sets a probability floor below which tokens are excluded from being sampled": "ε 截断设置了一个概率下限,低于该下限的词符将被排除在采样之外。\n以 1e-4 单位;合适的值为 3。将其设置为 0 以禁用。",151 "Epsilon cutoff sets a probability floor below which tokens are excluded from being sampled": "Epsilon 截断设置了一个概率底线,低于该值的 Token 将被排除。\n单位为 1e-4;建议值为 3。设为 0 以禁用。",
152 "Top nsigma": "Top nsigma",152 "Top nsigma": "Top n-sigma",
153 "Min Keep": "Min Keep",153 "Min Keep": "Min Keep",
154 "Eta Cutoff": "η 截断",154 "Eta Cutoff": "η 截断",
155 "Eta_Cutoff_desc": "η截断是特殊η采样技术的主要参数。以1e-4为单位;合理的值为3。设置为0以禁用。详细请参阅《Truncation Sampling as Language Model Desmoothing》(Hewitt et al., 2022)。",155 "Eta_Cutoff_desc": "Eta 截断是特殊 Eta 采样技术的主要参数。单位为 1e-4;建议值为 3。设为 0 以禁用。详情参阅 Hewitt 等人 2022 年的论文《Truncation Sampling as Language Model Desmoothing》。",
156 "rep.pen decay": "重复惩罚衰减",156 "rep.pen decay": "重复惩罚衰减",
157 "Encoder Rep. Pen.": "编码器重复惩罚",157 "Encoder Rep. Pen.": "编码器重复惩罚",
158 "No Repeat Ngram Size": "无重复n-gram大小",158 "No Repeat Ngram Size": "禁止重复 N-Gram 大小",
159 "Skew": "倾斜",159 "Skew": "Skew (偏斜)",
160 "Max Tokens Second": "每秒最大词符数",160 "Max Tokens Second": "每秒最大 Token 数",
161 "Adaptive-P": "Adaptive-P",
162 "Target": "目标值",
163 "Decay": "衰减值",
161 "Smooth Sampling": "平滑采样",164 "Smooth Sampling": "平滑采样",
162 "Smooth_Sampling_desc": "允许您使用二次/三次变换来调整分布。较低的平滑因子值将更具创造性,通常在 0.2-0.3 之间是最佳点(假设曲线 = 1)。较高的平滑曲线值将使曲线更陡峭,这将更积极地惩罚低概率选择。1.0 曲线相当于仅使用平滑因子。",165 "Smooth_Sampling_desc": "允许使用二次/三次变换来调整概率分布。较低的平滑因子(通常在 0.2-0.3 之间)能增加创造性。较高的平滑曲线值会使曲线更陡峭,从而更积极地惩罚低概率选项。1.0 的曲线值相当于仅使用平滑因子。",
163 "Smoothing Factor": "平滑系数",166 "Smoothing Factor": "平滑系数",
164 "Smoothing Curve": "平滑曲线",167 "Smoothing Curve": "平滑曲线",
165 "Exclude Top Choices (XTC)": "Exclude Top Choices (XTC)",168 "Exclude Top Choices (XTC)": "排除首选项 (XTC)",
166 "Threshold": "阈值",169 "Threshold": "阈值",
167 "Probability": "可能性",170 "Probability": "概率",
168 "DRY_Repetition_Penalty_desc": "DRY 会对那些会将输入末尾扩展为输入中之前出现过的序列的词符进行惩罚。将乘数设置为 0 以禁用。",171 "DRY_Repetition_Penalty_desc": "DRY 采样器会对那些可能导致文本重复先前序列的 Token 进行惩罚。将乘数设为 0 以禁用。",
169 "DRY Repetition Penalty": "DRY 重复惩罚",172 "DRY Repetition Penalty": "DRY 重复惩罚",
170 "DRY_Multiplier_desc": "将值设置为 > 0 以启用 DRY。控制最短惩罚序列的惩罚幅度。",173 "DRY_Multiplier_desc": "设为 > 0 的值以启用 DRY。该值控制对最短重复序列的惩罚力度。",
171 "Multiplier": "乘数",174 "Multiplier": "乘数",
172 "DRY_Base_desc": "控制惩罚随序列长度的增加而增长的速度。",175 "DRY_Base_desc": "控制惩罚力度随重复序列长度增加而增长的速率。",
173 "Base": "基数",176 "Base": "基数",
174 "DRY_Allowed_Length_desc": "可以重复而不会受到惩罚的最长序列长度。",177 "DRY_Allowed_Length_desc": "允许重复出现而不受惩罚的最长序列长度。",
175 "Allowed Length": "允许长度",178 "Allowed Length": "允许长度",
176 "Penalty Range": "惩罚范围",179 "Penalty Range": "惩罚范围",
177 "DRY_Sequence_Breakers_desc": "序列匹配不再继续的词符。用逗号分隔的带引号的字符串列表指定。",180 "DRY_Sequence_Breakers_desc": "用于打断序列匹配的特殊 Token。请指定为逗号分隔的带引号字符串列表。",
178 "Sequence Breakers": "序列打断符",181 "Sequence Breakers": "序列打断符",
179 "JSON-serialized array of strings.": "JSON 序列化的字符串数组。",182 "JSON-serialized array of strings.": "JSON 序列化的字符串数组。",
180 "Dynamic Temperature": "动态温度",183 "Dynamic Temperature": "动态温度",
181 "Scale Temperature dynamically per token, based on the variation of probabilities": "根据概率的变化动态地缩放每个词符的温度。",184 "Scale Temperature dynamically per token, based on the variation of probabilities": "根据概率的变化动态调整每个 Token 的温度。",
182 "Minimum Temp": "最小温度",185 "Minimum Temp": "最小温度",
183 "Maximum Temp": "最大温度",186 "Maximum Temp": "最大温度",
184 "Exponent": "指数",187 "Exponent": "指数",
185 "Mirostat (mode=1 is only for llama.cpp)": "Mirostat(mode=1 仅用于 llama.cpp)",188 "Mirostat (mode=1 is only for llama.cpp)": "Mirostat(mode=1 仅可用于 llama.cpp)",
186 "Mirostat_desc": "Mirostat 是一个用于控制输出困惑度的恒温器",189 "Mirostat_desc": "Mirostat 是一种用于控制输出困惑度 (Perplexity) 的算法,类似于恒温器。",
187 "Mirostat Mode": "Mirostat 模式",190 "Mirostat Mode": "Mirostat 模式",
188 "Mirostat Eta": "Mirostat η",191 "Mirostat Eta": "Mirostat η",
189 "Beam search": "束搜索",192 "Beam search": "束搜索",
190 "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采样中使用的贪婪暴力算法,用于找到最可能的单词或标记序列。它一次扩展多个候选序列,在每一步保留固定数量(光束宽度)的最佳序列。",193 "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 采样的贪婪搜索算法,旨在找到概率最高的单词或 Token 序列。它一次扩展多个候选序列,并在每一步保留固定数量(即束宽 Beam Width)的最佳序列。",
191 "# of Beams": "光束数量",194 "# of Beams": "束宽",
192 "The number of sequences generated at each step with Beam Search.": "束搜索每步生成的序列数量。",195 "The number of sequences generated at each step with Beam Search.": "束搜索每步生成的序列数量。",
193 "Length Penalty": "长度惩罚",196 "Length Penalty": "长度惩罚",
194 "Penalize sequences based on their length.": "根据序列长度对其进行惩罚。",197 "Penalize sequences based on their length.": "根据序列长度对其进行惩罚。",
195 "Early Stopping": "提前停止",198 "Early Stopping": "提前停止",
196 "Controls the stopping condition for beam search. If checked, the generation stops as soon as there are '# of Beams' sequences. If not checked, a heuristic is applied and the generation is stopped when it's very unlikely to find better candidates.": "控制光束搜索的停止条件。勾选时,当生成到达‘光束数量’的序列时停止。如果未勾选,则采用启发式方法,当几乎不可能找到更好的候选项时停止生成。",199 "Controls the stopping condition for beam search. If checked, the generation stops as soon as there are '# of Beams' sequences. If not checked, a heuristic is applied and the generation is stopped when it's very unlikely to find better candidates.": "控制束搜索的停止条件。若勾选,当生成了“束宽”数量的完整序列时立即停止。若未勾选,则应用启发式方法,仅在几乎不可能找到更好候选序列时才停止。",
197 "Contrastive search": "对比搜索",200 "Contrastive search": "对比搜索",
198 "Contrastive_search_txt": "通过利用大多数 LLM 表示空间的各向同性以鼓励多样性同时保持一致性的采样器。详见 Su 等人在 2022 年发表的论文 《A Contrastive Framework for Neural Text Generation》。",201 "Contrastive_search_txt": "一种通过利用 LLM 表示空间的各向同性,在鼓励多样性的同时保持连贯性的采样器。详见 Su 等人 2022 年的论文《A Contrastive Framework for Neural Text Generation》。",
199 "Penalty Alpha": "惩罚系数 α",202 "Penalty Alpha": "惩罚系数 α",
200 "Strength of the Contrastive Search regularization term. Set to 0 to disable CS": "对比搜索正则化项的强度。 将值设置为 0 以禁用对比搜索。",203 "Strength of the Contrastive Search regularization term. Set to 0 to disable CS": "对比搜索正则化项的强度。设为 0 以禁用对比搜索。",
201 "Do Sample": "进行采样",204 "Do Sample": "执行采样",
202 "Add BOS Token": "添加序列开始词符",205 "Add BOS Token": "添加 BOS Token",
203 "Add the bos_token to the beginning of prompts. Disabling this can make the replies more creative": "在提示词的开头添加序列开始词符。 禁用此功能可以使回复更具创意",206 "Add the bos_token to the beginning of prompts. Disabling this can make the replies more creative": "在 Prompt 开头添加序列开始 BOS Token。禁用此项可能会让回复更具创造性。",
204 "Ban the eos_token. This forces the model to never end the generation prematurely": "禁止序列结束词符。 这将强制模型永远不会提前结束生成",207 "Ban the eos_token. This forces the model to never end the generation prematurely": "禁止生成 EOS Token。这将强制模型永远不会提前结束生成。",
205 "Ignore EOS Token": "忽略序列结束词符",208 "Ignore EOS Token": "忽略 EOS Token",
206 "Ignore the EOS Token even if it generates.": "即使生成了序列结束词符,也忽略它。",209 "Ignore the EOS Token even if it generates.": "即使模型生成了 EOS Token 也将其忽略(继续生成)。",
207 "Skip Special Tokens": "跳过特殊词符",210 "Skip Special Tokens": "跳过特殊 Token",
208 "Request Model Reasoning": "请求模型推理",211 "Request Model Reasoning": "请求模型推理",
209 "Temperature Last": "温度放最后",212 "Temperature Last": "最后应用温度",
210 "Temperature_Last_desc": "温度采样器放到最后使用。这通常是合理的。\n当启用时:首先进行潜在词符的选择,然后应用温度来修正它们的相对概率(技术上是对数似然)。\n当禁用时:首先应用温度来修正所有词符的相对概率,然后从中选择潜在词符。\n禁用此项可以增大分布在尾部的词符概率,这可能加大得到不相关回复的几率。",213 "Temperature_Last_desc": "将温度采样放到最后一步执行。\n启用时:先筛选潜在 Token,再应用温度调整相对概率。\n禁用时(默认):先应用温度调整所有 Token 的概率,再从中筛选。\n禁用此项会增加尾部(低概率)Token 的权重,可能增加生成不相关内容的风险。",
211 "Speculative Ngram": "推测性 Ngram",214 "Speculative Ngram": "推测性 N-Gram",
212 "Use a different speculative decoding method without a draft model": "使用不同的推测解码方法(不采用草稿模型)。最好使用草稿模型。推测性 Ngram 的效果不太好。",215 "Use a different speculative decoding method without a draft model": "使用一种无需草稿模型的推测解码方法。注意:使用真正的草稿模型效果更好,推测性 N-Gram 的效果通常一般。",
213 "Spaces Between Special Tokens": "特殊词符之间的空格",216 "Spaces Between Special Tokens": "特殊 Token 之间的空格",
214 "Seed_desc": "一个用于生成确定性和可复现的输出的随机种子。设置为 -1 时会使用随机种子。",217 "Seed_desc": "一个用于生成确定性和可复现的输出的随机种子。设置为 -1 时会使用随机种子。",
215 "LLaMA / Mistral / Yi models only": "LLaMA / Mistral / Yi模型专用。首先确保您选择了适当的词符化器。\n这项设置决定了你不想在结果中看到的字符串。\n每行一个字符串。可以是文本或者[词符id]。\n许多词符以空格开头。如果不确定,请使用词符计数器。",218 "LLaMA / Mistral / Yi models only": "仅限 LLaMA / Mistral / Yi 模型。请首先确保您选择了适当的分词器。\n这项设置决定了你不想在结果中看到的字符串。\n每行一个字符串。可以是文本或者 [Token ID]。\n许多 Token 以空格开头。如果不确定,请使用 Token 计数器。",
216 "Global list": "全局列表",219 "Global list": "全局列表",
217 "Example: some text [42, 69, 1337]": "例如:\n一些文本\n[42, 69, 1337]",220 "Example: some text [42, 69, 1337]": "例如:\n一些文本\n[42, 69, 1337]",
218 "Preset-specific list": "预设特有的列表",221 "Preset-specific list": "当前预设专属列表",
219 "CFG": "CFG",222 "CFG": "CFG",
220 "Classifier Free Guidance. More helpful tip coming soon": "无分类器指导(CFG)。更多有用的提示敬请期待。",223 "Classifier Free Guidance. More helpful tip coming soon": "无分类器引导 (CFG)。更多说明敬请期待。",
221 "Scale": "缩放比例",224 "Scale": "缩放比例",
222 "Negative Prompt": "负面提示词",225 "Negative Prompt": "负面提示词",
223 "Used if CFG Scale is unset globally, per chat or character": "如果CFG缩放比例未被全局设置,它将作用于所有聊天或角色",226 "Used if CFG Scale is unset globally, per chat or character": "若未设置全局 CFG 比例,则此设置将应用于当前聊天或角色。",
224 "Add text here that would make the AI generate things you don't want in your outputs.": "请在此处添加文本,以避免生成您不希望出现在输出中的内容。",227 "Add text here that would make the AI generate things you don't want in your outputs.": "请在此处添加文本,以避免生成您不希望出现在输出中的内容。",
225 "Grammar String": "语法字符串",228 "Grammar String": "语法字符串",
226 "GBNF or EBNF, depends on the backend in use. If you're using this you should know which.": "GBNF 或 EBNF,取决于使用的后端。如果您使用这个,您应该知道该用哪一个。",229 "GBNF or EBNF, depends on the backend in use. If you're using this you should know which.": "GBNF 或 EBNF,取决于使用的后端。如果您使用这个,您应该知道该用哪一个。",
@@ -229,10 +232,10 @@
229 "Allow empty schema objects": "允许空结构对象",232 "Allow empty schema objects": "允许空结构对象",
230 "Top P & Min P": "Top P 和 Min P",233 "Top P & Min P": "Top P 和 Min P",
231 "Load default order": "加载默认顺序",234 "Load default order": "加载默认顺序",
232 "Sampler Order": "取样器顺序",235 "Sampler Order": "采样器顺序",
233 "llama.cpp only. Determines the order of samplers. If Mirostat mode is not 0, sampler order is ignored.": "仅限 llama.cpp。确定采样器的顺序。如果 Mirostat 模式不为 0,则忽略采样器顺序。",236 "llama.cpp only. Determines the order of samplers. If Mirostat mode is not 0, sampler order is ignored.": "仅限 llama.cpp。确定采样器的顺序。如果 Mirostat 模式不为 0,则忽略采样器顺序。",
234 "Sampler Priority": "采样器优先级",237 "Sampler Priority": "采样器优先级",
235 "Ooba only. Determines the order of samplers.": "确定采样器的顺序(仅适用于Ooba)",238 "Ooba only. Determines the order of samplers.": "仅适用于 Ooba。用于确定采样器的顺序。",
236 "Aphrodite only. Determines the order of samplers. Skew is always applied post-softmax, so it's not included here.": "仅适用于 Aphrodite,用于确定采样器的顺序。偏移(Skew)始终在 softmax 后应用,因此不包含在这里。",239 "Aphrodite only. Determines the order of samplers. Skew is always applied post-softmax, so it's not included here.": "仅适用于 Aphrodite,用于确定采样器的顺序。偏移(Skew)始终在 softmax 后应用,因此不包含在这里。",
237 "Aphrodite only. Determines the order of samplers.": "仅适用于 Aphrodite。用于确定采样器的顺序。",240 "Aphrodite only. Determines the order of samplers.": "仅适用于 Aphrodite。用于确定采样器的顺序。",
238 "Character Names Behavior": "角色名称行为",241 "Character Names Behavior": "角色名称行为",
@@ -244,20 +247,20 @@
244 "character_names_completion": "适用限制:仅限拉丁字母数字和下划线。不适用于所有补全源,尤其是:Claude、MistralAI、Google。",247 "character_names_completion": "适用限制:仅限拉丁字母数字和下划线。不适用于所有补全源,尤其是:Claude、MistralAI、Google。",
245 "Message Content": "消息内容",248 "Message Content": "消息内容",
246 "Prepend character names to message contents.": "在消息内容中添加角色名称。",249 "Prepend character names to message contents.": "在消息内容中添加角色名称。",
247 "Continue Postfix": "继续后缀",250 "Continue Postfix": "续写后缀",
248 "The next chunk of the continued message will be appended using this as a separator.": "将以此为分隔符附加后续消息的下一块。",251 "The next chunk of the continued message will be appended using this as a separator.": "将以此分隔续写消息和原消息。",
249 "Space": "空格",252 "Space": "空格",
250 "Newline": "换行",253 "Newline": "换行",
251 "Double Newline": "双换行",254 "Double Newline": "双换行",
252 "Continue prefill": "继续预填充",255 "Continue prefill": "续写预填充",
253 "Continue sends the last message as assistant role instead of system message with instruction.": "继续发送的是作为助手角色的最后一条消息,而不是带有指示的系统消息。",256 "Continue sends the last message as assistant role instead of system message with instruction.": "“续写”将以助手身份发送最后一条消息,而不是发送带有指令的系统消息。",
254 "Squash system messages": "压缩系统消息",257 "Squash system messages": "压缩系统消息",
255 "Combines consecutive system messages into one (excluding example dialogues). May improve coherence for some models.": "将连续的系统消息合并为一条(不包括示例对话),可能会提高一些模型的连贯性。",258 "Combines consecutive system messages into one (excluding example dialogues). May improve coherence for some models.": "将连续的系统消息合并为一条(不包括示例对话),可能会提高一些模型的连贯性。",
256 "Use system prompt": "使用系统提示词",259 "Use system prompt": "使用系统提示词",
257 "Send the system prompt for supported models. If disabled, the user message is added to the beginning of the prompt.": "为支持的模型发送系统提示词。如果禁用,则用户消息将添加到提示词的开头。",260 "Send the system prompt for supported models. If disabled, the user message is added to the beginning of the prompt.": "为支持的模型发送系统提示词。如果禁用,则用户消息将添加到提示词的开头。",
258 "Enable web search": "启用联网搜索",261 "Enable web search": "启用联网搜索",
259 "Use search capabilities provided by the backend.": "使用后端提供的联网搜索功能。",262 "Use search capabilities provided by the backend.": "使用后端提供的联网搜索功能。",
260 "openrouter_web_search_fee": "收费,每个提示词会多收 $0.02。",263 "openrouter_web_search_fee": "收费,每个提示词多收 0.02 美元。",
261 "Enable function calling": "启用函数调用",264 "Enable function calling": "启用函数调用",
262 "Supported by the current model": "当前模型支持",265 "Supported by the current model": "当前模型支持",
263 "Unsupported by the current model": "当前模型不支持",266 "Unsupported by the current model": "当前模型不支持",
@@ -267,8 +270,8 @@
267 "enable_functions_desc_4": "当提示词后处理没有选择工具时不支持。",270 "enable_functions_desc_4": "当提示词后处理没有选择工具时不支持。",
268 "Send inline media": "发送内联媒体",271 "Send inline media": "发送内联媒体",
269 "image_inlining_hint_1": "如果模型支持,就可以在提示词中发送媒体文件。",272 "image_inlining_hint_1": "如果模型支持,就可以在提示词中发送媒体文件。",
270 "video_inlining_hint_4": "视频必须在 20MB 以下且时长不超过1分钟。",273 "video_inlining_hint_4": "视频必须在 20MB 以下且时长不超过 1 分钟。",
271 "audio_inlining_hint_2": "音频必须小于 20 MB。",274 "audio_inlining_hint_2": "音频必须小于 20MB。",
272 "Inline Image Quality": "图片画质",275 "Inline Image Quality": "图片画质",
273 "openai_inline_image_quality_auto": "自动",276 "openai_inline_image_quality_auto": "自动",
274 "openai_inline_image_quality_low": "低",277 "openai_inline_image_quality_low": "低",
@@ -281,7 +284,7 @@
281 "Request model reasoning": "请求思维链",284 "Request model reasoning": "请求思维链",
282 "Allows the model to return its thinking process.": "允许模型返回其思维过程。",285 "Allows the model to return its thinking process.": "允许模型返回其思维过程。",
283 "This setting affects visibility only.": "此设置只影响思维链是否可见。",286 "This setting affects visibility only.": "此设置只影响思维链是否可见。",
284 "Constrains effort on reasoning for reasoning models.": "限定模型推理的强度。\n当前支持低、中、高三种强度。\n降低推理强度可以让模型更快回复,并节省推理所用的词符数。。",287 "Constrains effort on reasoning for reasoning models.": "限定模型推理的强度。\n当前支持低、中、高三种强度。\n降低推理强度可以让模型更快回复,并节省推理所用的 Token 数。",
285 "Reasoning Effort": "推理强度",288 "Reasoning Effort": "推理强度",
286 "openai_reasoning_effort_auto": "自动",289 "openai_reasoning_effort_auto": "自动",
287 "openai_reasoning_effort_minimum": "极低",290 "openai_reasoning_effort_minimum": "极低",
@@ -290,28 +293,28 @@
290 "openai_reasoning_effort_high": "高",293 "openai_reasoning_effort_high": "高",
291 "openai_reasoning_effort_maximum": "极高",294 "openai_reasoning_effort_maximum": "极高",
292 "OpenAI-style options: low, medium, high. Minimum and maximum are aliased to low and high. Auto does not send an effort level.": "OpenAI式选项:低、中、高。极低等于低,极高等于高。选择自动,则不传入推理强度参数。",295 "OpenAI-style options: low, medium, high. Minimum and maximum are aliased to low and high. Auto does not send an effort level.": "OpenAI式选项:低、中、高。极低等于低,极高等于高。选择自动,则不传入推理强度参数。",
293 "Allocates a portion of the response length for thinking (min: 1024 tokens, low: 10%, medium: 25%, high: 50%, max: 95%), but minimum 1024 tokens. Auto does not request thinking.": "将最大回复空间的一部分分配给思维链(极低:1024词符,低:10%,中:25%,高:50%,极高:95%),最低1024词符。选择“自动”不会请求模型思维链。",296 "Allocates a portion of the response length for thinking (min: 1024 tokens, low: 10%, medium: 25%, high: 50%, max: 95%), but minimum 1024 tokens. Auto does not request thinking.": "从回复长度中划拨一部分用于思考(最小:1024 Token,低:10%,中:25%,高:50%,最大:95%),且下限为 1024 Token。设为“自动”则不启用思考功能。",
294 "Sets a dynamic reasoning depth level for thinking (Flash 3/Pro 3). High and low are supported by both, minimal and medium are Flash 3 only. Auto lets the model decide.": "设置动态推理强度等级(3 Flash / 3 Pro)。两者都支持高和低,但极低和中只有 3 Flash 支持。自动会让模型自己选择。",297 "Sets a dynamic reasoning depth level for thinking (Flash 3/Pro 3). High and low are supported by both, minimal and medium are Flash 3 only. Auto lets the model decide.": "设置动态推理强度等级(3 Flash / 3 Pro)。两者都支持“高”和“低”,但“极低”和“中”只有 3 Flash 支持。设为“自动”让模型自己选择。",
295 "Allocates a portion of the response length for thinking (Flash 2.5/Pro 2.5) (min: 0/128 tokens, low: 10%, medium: 25%, high: 50%, max: 24576/32768 tokens). Auto lets the model decide.": "将最大回复空间的一部分分配给思维链(仅 2.5 Flash / 2.5 Pro 模型)(极低:0/128词符,低:10%,中:25%,高:50%,极高:24576/32768词符),最低1024词符。选择“自动”会让模型自己决定。",298 "Allocates a portion of the response length for thinking (Flash 2.5/Pro 2.5) (min: 0/128 tokens, low: 10%, medium: 25%, high: 50%, max: 24576/32768 tokens). Auto lets the model decide.": "将最大回复空间的一部分分配给思维链(仅 2.5 Flash / 2.5 Pro 模型)(极低:0/128 Token,低:10%,中:25%,高:50%,极高:24576/32768 Token)。选择“自动”会让模型自己决定。",
296 "Verbosity": "长度",299 "Verbosity": "长度",
297 "openai_verbosity_auto": "自动",300 "openai_verbosity_auto": "自动",
298 "openai_verbosity_low": "低",301 "openai_verbosity_low": "低",
299 "openai_verbosity_medium": "中",302 "openai_verbosity_medium": "中",
300 "openai_verbosity_high": "高",303 "openai_verbosity_high": "高",
301 "Constrains the verbosity of the model's response.": "限制模型回复的长度。",304 "Constrains the verbosity of the model's response.": "限制模型回复的长度。",
302 "Assistant Prefill": "AI预填",305 "Assistant Prefill": "助手预填",
303 "Expand the editor": "展开编辑器",306 "Expand the editor": "展开编辑器",
304 "Start Claude's answer with...": "以如下内容开始Claude的回答...",307 "Start Claude's answer with...": "以如下内容开始 Claude 的回答...",
305 "Assistant Impersonation Prefill": "AI帮答预填",308 "Assistant Impersonation Prefill": "助手帮答预填",
306 "Confirm token parsing with": "确认使用以下工具进行词符解析",309 "Confirm token parsing with": "确认使用以下工具进行 Token 解析",
307 "Tokenizer": "分词器",310 "Tokenizer": "分词器",
308 "New preset": "新预设",311 "New preset": "新预设",
309 "Delete preset": "删除预设",312 "Delete preset": "删除预设",
310 "View / Edit bias preset": "查看/编辑偏置预设",313 "View / Edit bias preset": "查看 / 编辑偏置预设",
311 "Add bias entry": "添加偏置条目",314 "Add bias entry": "添加偏置条目",
312 "openai_logit_bias_no_items": "没有相关产品",315 "openai_logit_bias_no_items": "没有相关产品",
313 "Most tokens have a leading space.": "大多数词符都有一个前导空格。",316 "Most tokens have a leading space.": "大多数 Token 都有一个前导空格。",
314 "API Connections": "API连接",317 "API Connections": "API 连接",
315 "api_no_connection": "无连接...",318 "api_no_connection": "无连接...",
316 "Text Completion": "文本补全",319 "Text Completion": "文本补全",
317 "Chat Completion": "聊天补全",320 "Chat Completion": "聊天补全",
@@ -322,14 +325,14 @@
322 "Avoid sending sensitive information to the Horde.": "避免向 Horde 发送敏感信息。",325 "Avoid sending sensitive information to the Horde.": "避免向 Horde 发送敏感信息。",
323 "Review the Privacy statement": "查看隐私声明",326 "Review the Privacy statement": "查看隐私声明",
324 "Register a Horde account for faster queue times": "注册 Horde 帐户以加快排队时间",327 "Register a Horde account for faster queue times": "注册 Horde 帐户以加快排队时间",
325 "Learn how to contribute your idle GPU cycles to the Horde": "了解如何将您的空闲GPU时钟周期共享给 Horde",328 "Learn how to contribute your idle GPU cycles to the Horde": "了解如何将您的空闲 GPU 时钟周期共享给 Horde",
326 "Adjust context size to worker capabilities": "根据工作单元能力调整上下文长度",329 "Adjust context size to worker capabilities": "根据工作单元能力调整上下文长度",
327 "Adjust response length to worker capabilities": "根据工作单元能力调整响应长度",330 "Adjust response length to worker capabilities": "根据工作单元能力调整响应长度",
328 "Can help with bad responses by queueing only the approved workers. May slowdown the response time.": "可以通过仅排队认证的工作单元来帮助处理不良回复。这可能会减慢回复速度。",331 "Can help with bad responses by queueing only the approved workers. May slowdown the response time.": "可以通过仅排队认证的工作单元来帮助处理不良回复。这可能会减慢回复速度。",
329 "Trusted workers only": "仅信任的工作单元",332 "Trusted workers only": "仅信任的工作单元",
330 "Context": "上下文",333 "Context": "上下文",
331 "Response": "回复",334 "Response": "回复",
332 "API key": "API密钥",335 "API key": "API 密钥",
333 "Get it here:": "在此获取:",336 "Get it here:": "在此获取:",
334 "Register": "注册",337 "Register": "注册",
335 "View my Kudos": "查看我的荣誉",338 "View my Kudos": "查看我的荣誉",
@@ -342,7 +345,7 @@
342 "Refresh models": "刷新模型",345 "Refresh models": "刷新模型",
343 "-- Horde models not loaded --": "-- Horde 模型未加载 --",346 "-- Horde models not loaded --": "-- Horde 模型未加载 --",
344 "Not connected...": "未连接...",347 "Not connected...": "未连接...",
345 "API url": "API地址",348 "API url": "API 地址",
346 "Example: http://127.0.0.1:5000/api ": "示例:http://127.0.0.1:5000/api",349 "Example: http://127.0.0.1:5000/api ": "示例:http://127.0.0.1:5000/api",
347 "Connect": "连接",350 "Connect": "连接",
348 "Cancel": "取消",351 "Cancel": "取消",
@@ -355,27 +358,36 @@
355 "Generic (OpenAI-compatible) [LM Studio, LiteLLM, etc.]": "通用(兼容 OpenAI)[LM Studio、LiteLLM 等]",358 "Generic (OpenAI-compatible) [LM Studio, LiteLLM, etc.]": "通用(兼容 OpenAI)[LM Studio、LiteLLM 等]",
356 "TogetherAI API Key": "TogetherAI API 密钥",359 "TogetherAI API Key": "TogetherAI API 密钥",
357 "TogetherAI Model": "TogetherAI 模型",360 "TogetherAI Model": "TogetherAI 模型",
358 "-- Connect to the API --": "-- 连接到API --",361 "-- Connect to the API --": "-- 连接到 API --",
359 "OpenRouter API Key": "OpenRouter API 密钥",362 "OpenRouter API Key": "OpenRouter API 密钥",
360 "Click Authorize below or get the key from": "点击下方授权或从以下位置获取密钥",363 "Click Authorize below or get the key from": "点击下方授权或从以下位置获取密钥",
361 "View Remaining Credits": "查看剩余额度",364 "View Remaining Credits": "查看剩余额度",
362 "OpenRouter Model": "OpenRouter 模型",365 "OpenRouter Model": "OpenRouter 模型",
363 "Model Providers": "模型提供商",366 "Model Providers": "模型提供商",
364 "Automatically chooses an alternative provider if chosen providers can't serve your request.": "在当前选择的模型提供商无效时,自动选择备用的提供商。",367 "Automatically chooses an alternative provider if chosen providers can't serve your request.": "在当前选择的模型提供商无效时,自动选择备用的提供商。",
365 "Allow fallback providers": "允许后备提供者",368 "Allow fallback providers": "允许后备提供商",
369 "Model Quantizations": "模型量化",
370 "Integer (4 bit)": "整数 (4 bit)",
371 "Integer (8 bit)": "整数 (8 bit)",
372 "Floating point (4 bit)": "浮点数 (4 bit)",
373 "Floating point (6 bit)": "浮点数 (6 bit)",
374 "Floating point (8 bit)": "浮点数 (8 bit)",
375 "Floating point (16 bit)": "浮点数 (16 bit)",
376 "Brain floating point (16 bit)": "Brain 浮点数 (16 bit)",
377 "Floating point (32 bit)": "浮点数 (32 bit)",
366 "InfermaticAI API Key": "InfermaticAI API 密钥",378 "InfermaticAI API Key": "InfermaticAI API 密钥",
367 "InfermaticAI Model": "InfermaticAI 模型",379 "InfermaticAI Model": "InfermaticAI 模型",
368 "DreamGen API key": "DreamGen API 密钥",380 "DreamGen API key": "DreamGen API 密钥",
369 "DreamGen Model": "DreamGen 模型",381 "DreamGen Model": "DreamGen 模型",
370 "Mancer API key": "Mancer API 密钥",382 "Mancer API key": "Mancer API 密钥",
371 "Mancer Model": "Mancer 模型",383 "Mancer Model": "Mancer 模型",
372 "API key (optional)": "API密钥(可选)",384 "API key (optional)": "API 密钥(选填)",
373 "Server url": "服务器URL",385 "Server url": "服务器 URL",
374 "Example: http://127.0.0.1:5000": "示例:http://127.0.0.1:5000",386 "Example: http://127.0.0.1:5000": "示例:http://127.0.0.1:5000",
375 "Model ID (optional)": "模型 ID(可选)",387 "Model ID (optional)": "模型 ID(选填)",
376 "Make sure you run it with": "确保您在运行时加上",388 "Make sure you run it with": "确保您在运行时加上",
377 "flag": "标志",389 "flag": "标志",
378 "Custom model (optional)": "自定义模型(可选)",390 "Custom model (optional)": "自定义模型(选填)",
379 "Featherless Model Selection": "Featherless 模型选择",391 "Featherless Model Selection": "Featherless 模型选择",
380 "Search...": "搜索...",392 "Search...": "搜索...",
381 "Search": "搜索",393 "Search": "搜索",
@@ -392,10 +404,10 @@
392 "vLLM API key": "vLLM API 密钥",404 "vLLM API key": "vLLM API 密钥",
393 "Example: http://127.0.0.1:8000": "示例:http://127.0.0.1:8000",405 "Example: http://127.0.0.1:8000": "示例:http://127.0.0.1:8000",
394 "vLLM Model": "vLLM 模型",406 "vLLM Model": "vLLM 模型",
395 "HuggingFace Token": "HuggingFace 代币",407 "HuggingFace Token": "HuggingFace 令牌",
396 "Endpoint URL": "端点 URL",408 "Endpoint URL": "端点 URL",
397 "Example: https://****.endpoints.huggingface.cloud": "例如:https://****.endpoints.huggingface.cloud",409 "Example: https://****.endpoints.huggingface.cloud": "例如:https://****.endpoints.huggingface.cloud",
398 "PygmalionAI/aphrodite-engine": "PygmalionAI/aphrodite-engine(用于OpenAI API的包装器)",410 "PygmalionAI/aphrodite-engine": "PygmalionAI/aphrodite-engine(用于 OpenAI API 的包装器)",
399 "Aphrodite API key": "Aphrodite API 密钥",411 "Aphrodite API key": "Aphrodite API 密钥",
400 "Aphrodite Model": "Aphrodite 模型",412 "Aphrodite Model": "Aphrodite 模型",
401 "ggml-org/llama.cpp": "ggml-org/llama.cpp",413 "ggml-org/llama.cpp": "ggml-org/llama.cpp",
@@ -407,14 +419,17 @@
407 "Tabby API key": "Tabby API 密钥",419 "Tabby API key": "Tabby API 密钥",
408 "Tabby Model": "Tabby 模型",420 "Tabby Model": "Tabby 模型",
409 "Experimental feature. Use at your own risk.": "实验性功能,请自行承担可能的风险。",421 "Experimental feature. Use at your own risk.": "实验性功能,请自行承担可能的风险。",
410 "must be set in Tabby's config.yml to switch models.": "必须在Tabby的config.yml内设置以切换模型",422 "Tabby model hint prefix": "Tabby 模型提示前缀",
411 "Use an admin API key.": "使用管理员API密钥。",423 "must be set in Tabby's config.yml to switch models.": "必须在 Tabby 的 config.yml 内设置以切换模型",
412 "koboldcpp API key (optional)": "koboldcpp API 密钥(可选)",424 "Use an admin API key.": "使用管理员 API 密钥。",
425 "koboldcpp API key (optional)": "koboldcpp API 密钥(选填)",
413 "Example: http://127.0.0.1:5001": "示例:http://127.0.0.1:5001",426 "Example: http://127.0.0.1:5001": "示例:http://127.0.0.1:5001",
414 "Bypass status check": "跳过状态检查",427 "Bypass status check": "跳过状态检查",
415 "Derive context size from backend": "从后端获取上下文长度",428 "Derive context size from backend": "从后端获取上下文长度",
416 "Authorize": "授权",429 "Authorize": "授权",
417 "Get your OpenRouter API token using OAuth flow. You will be redirected to openrouter.ai": "使用OAuth流程获取您的OpenRouter API令牌。您将被重定向到openrouter.ai",430 "OpenRouter API key already exists": "OpenRouter API 密钥已存在",
431 "Do you really wish to create a new OpenRouter key? Your existing key will not be deleted.": "您确定要创建新的 OpenRouter API 密钥吗?您现有的密钥不会被删除。",
432 "Get your OpenRouter API token using OAuth flow. You will be redirected to openrouter.ai": "使用 OAuth 流程获取您的 OpenRouter API 令牌。您将被重定向到 openrouter.ai",
418 "Chat Completion Source": "聊天补全来源",433 "Chat Completion Source": "聊天补全来源",
419 "Custom (OpenAI-compatible)": "自定义(兼容 OpenAI)",434 "Custom (OpenAI-compatible)": "自定义(兼容 OpenAI)",
420 "Reverse Proxy": "反向代理",435 "Reverse Proxy": "反向代理",
@@ -428,6 +443,7 @@
428 "Alternative server URL (leave empty to use the default value).": "备用服务器 URL(留空以使用默认值)。",443 "Alternative server URL (leave empty to use the default value).": "备用服务器 URL(留空以使用默认值)。",
429 "Doesn't work? Try adding": "不行?在 URL 末尾添加",444 "Doesn't work? Try adding": "不行?在 URL 末尾添加",
430 "at the end!": "试试!",445 "at the end!": "试试!",
446 "completions note prefix": "完成提示词前缀",
431 "suffix will be added automatically.": "的后缀会被自动补全。",447 "suffix will be added automatically.": "的后缀会被自动补全。",
432 "Proxy Password": "代理密码",448 "Proxy Password": "代理密码",
433 "Will be used as a password for the proxy instead of API key.": "将用作代理的密码,而不是 API 密钥。",449 "Will be used as a password for the proxy instead of API key.": "将用作代理的密码,而不是 API 密钥。",
@@ -436,14 +452,14 @@
436 "ANY support requests will be REFUSED if you are using a proxy.": "如果您使用代理,任何支持请求都将被拒绝。",452 "ANY support requests will be REFUSED if you are using a proxy.": "如果您使用代理,任何支持请求都将被拒绝。",
437 "Do not proceed if you do not agree to this!": "如果您不同意,请不要继续!",453 "Do not proceed if you do not agree to this!": "如果您不同意,请不要继续!",
438 "OpenAI API key": "OpenAI API 密钥",454 "OpenAI API key": "OpenAI API 密钥",
439 "View API Usage Metrics": "查看API使用情况",455 "View API Usage Metrics": "查看 API 使用情况",
440 "Follow": "按照",456 "Follow": "按照",
441 "these directions": "这些步骤",457 "these directions": "这些步骤",
442 "to get your OpenAI API key.": "获取您的 OpenAI API 密钥。",458 "to get your OpenAI API key.": "获取您的 OpenAI API 密钥。",
443 "Use Proxy password field instead. This input will be ignored.": "请使用“代理密码”字段。此输入将被忽略。",459 "Use Proxy password field instead. This input will be ignored.": "请使用“代理密码”字段。此输入将被忽略。",
444 "OpenAI Model": "OpenAI 模型",460 "OpenAI Model": "OpenAI 模型",
445 "Bypass API status check": "绕过API状态检查",461 "Bypass API status check": "绕过 API 状态检查",
446 "Show External models (provided by API)": "显示外部模型(由API提供)",462 "Show External models (provided by API)": "显示外部模型(由 API 提供)",
447 "Claude API Key": "Claude API 密钥",463 "Claude API Key": "Claude API 密钥",
448 "Get your key from": "从以下位置获取您的密钥",464 "Get your key from": "从以下位置获取您的密钥",
449 "Anthropic's developer console": "Anthropic 开发者控制台",465 "Anthropic's developer console": "Anthropic 开发者控制台",
@@ -456,7 +472,7 @@
456 "Context Size": "上下文长度",472 "Context Size": "上下文长度",
457 "Group by vendors": "按厂商分组",473 "Group by vendors": "按厂商分组",
458 "Group by vendors Description": "将 OpenAI 模型放在一组,将 Anthropic 模型放在另一组,等等。可以与排序结合。",474 "Group by vendors Description": "将 OpenAI 模型放在一组,将 Anthropic 模型放在另一组,等等。可以与排序结合。",
459 "To use instruct formatting, switch to OpenRouter under Text Completion API.": "要使用指导格式,请在 文字补全API 下切换到 OpenRouter。",475 "To use instruct formatting, switch to OpenRouter under Text Completion API.": "要使用指引格式,请在 文字补全API 下切换到 OpenRouter。",
460 "AI21 API Key": "AI21 API 密钥",476 "AI21 API Key": "AI21 API 密钥",
461 "AI21 Model": "AI21 模型",477 "AI21 Model": "AI21 模型",
462 "Google AI Studio API Key": "Google AI Studio API 密钥",478 "Google AI Studio API Key": "Google AI Studio API 密钥",
@@ -467,12 +483,12 @@
467 "Full Version (Service Account)": "完整版本(Service Account)",483 "Full Version (Service Account)": "完整版本(Service Account)",
468 "(Express mode)": "(快速模式)",484 "(Express mode)": "(快速模式)",
469 "API Key": "API 密钥",485 "API Key": "API 密钥",
470 "Project ID": "项目ID:",486 "Project ID": "项目 ID:",
471 "Project ID is required when selecting regions other than the default (us-central1). You can find this in a model 404 error message.": "仅当选择非默认区域(us-central1)时才需要 项目ID。\n 您可以在模型 404 错误消息中找到它。",487 "Project ID is required when selecting regions other than the default (us-central1). You can find this in a model 404 error message.": "仅当选择非默认区域(us-central1)时才需要项目 ID。\n您可以在模型 404 错误消息中找到它。",
472 "Service Account Configuration": "服务帐户配置",488 "Service Account Configuration": "Service Account 配置",
473 "Service Account JSON Content": "服务帐户 JSON 内容:",489 "Service Account JSON Content": "Service Account JSON 内容:",
474 "For privacy reasons, your Service Account JSON content will be hidden after you click 'Validate JSON'.": "出于隐私考虑,你的服务账号 JSON 内容将在点击“验证JSON”后隐藏。",490 "For privacy reasons, your Service Account JSON content will be hidden after you click 'Validate JSON'.": "出于隐私考虑,你的 Service Account JSON 内容将在点击“验证 JSON”后隐藏。",
475 "Validate JSON": "验证JSON",491 "Validate JSON": "验证 JSON",
476 "Region": "地区:",492 "Region": "地区:",
477 "View available regions and models": "查看可用地区和模型",493 "View available regions and models": "查看可用地区和模型",
478 "MistralAI API Key": "MistralAI API 密钥",494 "MistralAI API Key": "MistralAI API 密钥",
@@ -503,7 +519,7 @@
503 "Custom Endpoint (Base URL)": "自定义端点(基础 URL)",519 "Custom Endpoint (Base URL)": "自定义端点(基础 URL)",
504 "Example: http://localhost:1234/v1": "例如:http://localhost:1234/v1",520 "Example: http://localhost:1234/v1": "例如:http://localhost:1234/v1",
505 "Custom API Key": "自定义 API 密钥",521 "Custom API Key": "自定义 API 密钥",
506 "(Optional)": "(可选)",522 "(Optional)": "(选填)",
507 "Enter a Model ID": "输入模型名",523 "Enter a Model ID": "输入模型名",
508 "Example: gpt-4o": "例如:gpt-4o",524 "Example: gpt-4o": "例如:gpt-4o",
509 "Available Models": "可用模型",525 "Available Models": "可用模型",
@@ -511,9 +527,8 @@
511 "xAI Model": "xAI 模型",527 "xAI Model": "xAI 模型",
512 "AI/ML API Key": "AI/ML API 密钥",528 "AI/ML API Key": "AI/ML API 密钥",
513 "AI/ML Model": "AI/ML 模型",529 "AI/ML Model": "AI/ML 模型",
530 "Pollinations API Key": "Pollinations API 密钥",
514 "Pollinations Model": "Pollinations 模型",531 "Pollinations Model": "Pollinations 模型",
515 "Provided free of charge by Pollinations.AI": "由 Pollinations.AI 免费提供",
516 "Avoid sending sensitive information. Provider's outputs may include ads.": "请避免发送敏感信息。输出可能有提供商的广告。",
517 "Moonshot AI API Key": "Moonshot AI API 密钥",532 "Moonshot AI API Key": "Moonshot AI API 密钥",
518 "Moonshot AI Model": "Moonshot AI 模型",533 "Moonshot AI Model": "Moonshot AI 模型",
519 "Z.AI API Key": "Z.AI API 密钥",534 "Z.AI API Key": "Z.AI API 密钥",
@@ -545,10 +560,10 @@
545 "Auto-connect to Last Server": "自动连接到上次的服务器",560 "Auto-connect to Last Server": "自动连接到上次的服务器",
546 "Missing key": "❌ 缺少密钥",561 "Missing key": "❌ 缺少密钥",
547 "Key saved": "密钥已保存",562 "Key saved": "密钥已保存",
548 "View hidden API keys": "查看隐藏的API密钥",563 "View hidden API keys": "查看隐藏的 API 密钥",
549 "AI Response Formatting": "AI回复格式化",564 "AI Response Formatting": "AI 回复格式化",
550 "Advanced Formatting": "高级格式化设置",565 "Advanced Formatting": "高级格式化设置",
551 "Import Advanced Formatting settings": "导入高级格式化设置\n\n对于指导和上下文模板,你也可以提供旧版文件。",566 "Import Advanced Formatting settings": "导入高级格式化设置\n\n对于格式指引和上下文模板,你也可以提供旧版文件。",
552 "Master Import": "全局导入",567 "Master Import": "全局导入",
553 "Export Advanced Formatting settings": "导出高级格式化设置",568 "Export Advanced Formatting settings": "导出高级格式化设置",
554 "Master Export": "全局导出",569 "Master Export": "全局导出",
@@ -585,61 +600,61 @@
585 "Separators as Stop Strings": "分隔符作为终止字符串",600 "Separators as Stop Strings": "分隔符作为终止字符串",
586 "Add Character and User names to a list of stopping strings.": "将角色和用户名添加到停止字符串列表中。",601 "Add Character and User names to a list of stopping strings.": "将角色和用户名添加到停止字符串列表中。",
587 "Names as Stop Strings": "名称作为终止字符串",602 "Names as Stop Strings": "名称作为终止字符串",
588 "Instruct Template": "指导模板",603 "Instruct Template": "格式指引模板",
589 "instruct_derived": "如果可能,从模型元数据中获取",604 "instruct_derived": "如果可能,从模型元数据中获取",
590 "instruct_bind_to_context": "如果启用,上下文模板将根据所选的指导模板名称或偏好自动选择。",605 "instruct_bind_to_context": "如果启用,上下文模板将根据所选的格式指引模板名称或偏好自动选择。",
591 "instruct_enabled": "启用指导模板",606 "instruct_enabled": "启用格式指引模板",
592 "Select your current Instruct Template": "选择你当前的指导模板",607 "Select your current Instruct Template": "选择你当前的格式指引模板",
593 "Delete template": "删除模板",608 "Delete template": "删除模板",
594 "Activation Regex": "激活正则表达式",609 "Activation Regex": "激活正则表达式",
595 "instruct_template_activation_regex_desc": "当连接到API或选择模型时,若模型名称与给定的正则表达式匹配,自动启用此指导模板。",610 "instruct_template_activation_regex_desc": "当连接到 API 或选择模型时,若模型名称与给定的正则表达式匹配,自动启用此格式指引模板。",
596 "Wrap Sequences with Newline": "用换行符包裹序列",611 "Wrap Sequences with Newline": "用换行符包裹序列",
597 "Replace Macro in Sequences": "替换序列中的宏",612 "Replace Macro in Sequences": "替换序列中的宏",
598 "Sequences as Stop Strings": "将序列用作终止字符串",613 "Sequences as Stop Strings": "将序列用作终止字符串",
599 "Skip Example Dialogues Formatting": "跳过示例对话格式化",614 "Skip Example Dialogues Formatting": "跳过示例对话格式化",
600 "Include Names": "包括名称",615 "Include Names": "包括名称",
601 "Never": "永不",616 "Never": "永不",
602 "Groups and Past Personas": "群聊和过去的用户设定",617 "Groups and Past Personas": "群聊和历史用户设定",
603 "Always": "永远",618 "Always": "总是",
604 "Instruct Sequences": "指令序列",619 "Instruct Sequences": "格式指引序列",
605 "Story String Sequences": "故事字符串序列",620 "Story String Sequences": "故事字符串序列",
606 "Used in Default position only.": "仅在默认位置使用。",621 "Used in Default position only.": "仅在默认位置使用。",
607 "Inserted before a Story String.": "插入在故事字符串之前。",622 "Inserted before a Story String.": "插入到故事字符串之前。",
608 "Story String Prefix": "故事字符串前缀",623 "Story String Prefix": "故事字符串前缀",
609 "Inserted after a Story String.": "插入在故事字符串之后。",624 "Inserted after a Story String.": "插入到故事字符串之后。",
610 "Story String Suffix": "故事字符串后缀",625 "Story String Suffix": "故事字符串后缀",
611 "User Message Sequences": "用户消息序列",626 "User Message Sequences": "用户消息序列",
612 "Inserted before a User message and as a last prompt line when impersonating.": "插入到用户消息之前并作为模拟时的最后一行提示词。",627 "Inserted before a User message and as a last prompt line when impersonating.": "插入到用户消息之前;或在 AI 帮答时,作为提示词的最后一行。",
613 "User Prefix": "用户消息前缀",628 "User Prefix": "用户消息前缀",
614 "Inserted after a User message.": "插入到用户消息之后。",629 "Inserted after a User message.": "插入到用户消息之后。",
615 "User Suffix": "用户消息后缀",630 "User Suffix": "用户消息后缀",
616 "Assistant Message Sequences": "助手消息序列",631 "Assistant Message Sequences": "助手消息序列",
617 "Inserted before an Assistant message and as a last prompt line when generating an AI reply.": "插入到助手消息之前并作为生成 AI 回复时的最后一行提示词。",632 "Inserted before an Assistant message and as a last prompt line when generating an AI reply.": "插入到助手消息之前;当生成 AI 回复时,作为提示词的最后一行。",
618 "Assistant Prefix": "助手消息前缀",633 "Assistant Prefix": "助手消息前缀",
619 "Inserted after an Assistant message.": "插入于助手消息之后。",634 "Inserted after an Assistant message.": "插入到助手消息之后。",
620 "Assistant Suffix": "助手消息后缀",635 "Assistant Suffix": "助手消息后缀",
621 "System Message Sequences": "系统消息序列",636 "System Message Sequences": "系统消息序列",
622 "Inserted before a System (added by slash commands or extensions) message.": "插入到系统(由快捷命令或扩展添加)消息之前。",637 "Inserted before a System (added by slash commands or extensions) message.": "插入到系统消息(由命令或扩展程序添加)之前。",
623 "System Prefix": "系统消息前缀",638 "System Prefix": "系统消息前缀",
624 "Inserted after a System message.": "插入到系统消息之后。",639 "Inserted after a System message.": "插入到系统消息之后。",
625 "System Suffix": "系统消息后缀",640 "System Suffix": "系统消息后缀",
626 "If enabled, System Sequences will be the same as User Sequences.": "如果启用,系统序列将与用户序列相同。",641 "If enabled, System Sequences will be the same as User Sequences.": "如果启用,系统序列将与用户序列相同。",
627 "System same as User": "系统与用户相同",642 "System same as User": "系统与用户相同",
628 "Misc. Sequences": "杂项序列",643 "Misc. Sequences": "其他序列",
629 "Inserted before the first Assistant's message.": "插入到第一个助理的消息之前。",644 "Inserted before the first Assistant's message.": "插入到第一条助手消息之前。",
630 "First Assistant Prefix": "第一个助理前缀",645 "First Assistant Prefix": "首条助手前缀",
631 "instruct_last_output_sequence": "插入到最后一条助手消息之前或作为生成 AI 回复时的最后一行提示词(中立/系统角色除外)。",646 "instruct_last_output_sequence": "插入到最后一条助手消息之前,或作为生成 AI 回复时的最后一行提示词(中立/系统角色除外)。",
632 "Last Assistant Prefix": "最后一个助理前缀",647 "Last Assistant Prefix": "末条助手前缀",
633 "Inserted before the first User's message.": "插入在第一个用户的消息之前。",648 "Inserted before the first User's message.": "插入到第一条用户消息之前。",
634 "First User Prefix": "第一个用户前缀",649 "First User Prefix": "首条用户前缀",
635 "instruct_last_input_sequence": "插入到最后一条用户消息之前。",650 "instruct_last_input_sequence": "插入到最后一条用户消息之前。",
636 "Last User Prefix": "上次用户前缀",651 "Last User Prefix": "末条用户前缀",
637 "Will be inserted as a last prompt line when using system/neutral generation.": "当使用系统/中性生成时将作为最后的一行提示词插入。",652 "Will be inserted as a last prompt line when using system/neutral generation.": "当使用系统/中立生成时,将作为提示词的最后一行插入。",
638 "System Instruction Prefix": "系统指令前缀",653 "System Instruction Prefix": "系统指令前缀",
639 "If a stop sequence is generated, everything past it will be removed from the output (inclusive).": "如果生成了停止序列,则该序列之后的所有内容都将从输出中删除(包括在内)。",654 "If a stop sequence is generated, everything past it will be removed from the output (inclusive).": "如果输出了停止序列,则该序列及其之后的所有内容都将从回复中被移除。",
640 "Stop Sequence": "停止序列",655 "Stop Sequence": "停止序列",
641 "Will be inserted at the start of the chat history if it doesn't start with a User message.": "如果聊天记录不是以用户消息开头,则会插入到聊天记录的开头。",656 "Will be inserted at the start of the chat history if it doesn't start with a User message.": "如果聊天记录不是以用户消息开头,则会插入到聊天记录的开头。",
642 "User Filler Message": "用户填写信息",657 "User Filler Message": "用户填充消息",
643 "System Prompt": "系统提示词",658 "System Prompt": "系统提示词",
644 "sysprompt_enabled": "启用系统提示词",659 "sysprompt_enabled": "启用系统提示词",
645 "Select your current System Prompt": "选择当前的系统提示词",660 "Select your current System Prompt": "选择当前的系统提示词",
@@ -650,9 +665,9 @@
650 "Delete prompt": "删除提示词",665 "Delete prompt": "删除提示词",
651 "Prompt Content": "提示词内容",666 "Prompt Content": "提示词内容",
652 "Custom Stopping Strings": "自定义停止字符串",667 "Custom Stopping Strings": "自定义停止字符串",
653 "JSON serialized array of strings": "JSON序列化的字符串数组",668 "JSON serialized array of strings": "JSON 序列化的字符串数组",
654 "Replace Macro in Stop Strings": "替换自定义停止字符串中的宏",669 "Replace Macro in Stop Strings": "替换自定义停止字符串中的宏",
655 "Token Padding": "词符填充",670 "Token Padding": "Token 填充",
656 "Reasoning": "推理",671 "Reasoning": "推理",
657 "reasoning_auto_parse": "自动从消息内容中提取由推理块前后缀包裹的推理块。前后缀都必须设置且不为空。",672 "reasoning_auto_parse": "自动从消息内容中提取由推理块前后缀包裹的推理块。前后缀都必须设置且不为空。",
658 "Auto-Parse": "自动解析",673 "Auto-Parse": "自动解析",
@@ -674,23 +689,23 @@
674 "Separator": "分隔符",689 "Separator": "分隔符",
675 "Miscellaneous": "杂项",690 "Miscellaneous": "杂项",
676 "Bind Model to Templates": "将模型与模板绑定",691 "Bind Model to Templates": "将模型与模板绑定",
677 "bind_model_templates_desc": "当连接到一个API或选择一个模型,且它们的名字与当前的指导和上下文模板名字匹配时,自动激活当前的指导和上下文模板。",692 "bind_model_templates_desc": "当连接到一个 API 或选择一个模型,且它们的名字与当前的格式指引和上下文模板名字匹配时,自动激活当前的格式指引和上下文模板。",
678 "Non-markdown strings": "非 Markdown 字符串",693 "Non-markdown strings": "非 Markdown 字符串",
679 "comma delimited,no spaces between": "以逗号分隔,无需空格",694 "comma delimited,no spaces between": "以逗号分隔,无需空格",
680 "Start Reply With": "以...开始回复",695 "Start Reply With": "以...开始回复",
681 "Show reply prefix in chat": "在聊天中显示回复前缀",696 "Show reply prefix in chat": "在聊天中显示回复前缀",
682 "World Info": "世界信息",697 "World Info": "世界书",
683 "Locked = World Editor will stay open": "锁定 = 世界编辑器将保持打开状态",698 "Locked = World Editor will stay open": "锁定 = 世界编辑器将保持打开状态",
684 "Worlds/Lorebooks": "世界/知识书",699 "Worlds/Lorebooks": "世界书",
685 "Active World(s) for all chats": "已启用的世界(全局有效)",700 "Active World(s) for all chats": "已启用的世界书(全局有效)",
686 "-- World Info not found --": "-- 未找到世界信息 --",701 "-- World Info not found --": "-- 未找到世界书 --",
687 "Global World Info/Lorebook activation settings": "全局世界信息/知识书激活设置",702 "Global World Info/Lorebook activation settings": "全局世界书激活设置",
688 "Click to expand": "单击展开",703 "Click to expand": "单击展开",
689 "Scan Depth": "扫描深度",704 "Scan Depth": "扫描深度",
690 "Context %": "上下文百分比",705 "Context %": "上下文百分比",
691 "Budget Cap": "Token预算上限",706 "Budget Cap": "Token 预算上限",
692 "(0 = disabled)": "(“0”为禁用)",707 "(0 = disabled)": "(“0”为禁用)",
693 "Scan chronologically until reached min entries or token budget.": "按时间顺序扫描直到达到最少条目或词符预算。",708 "Scan chronologically until reached min entries or token budget.": "按时间顺序扫描直到达到最少条目或 Token 预算。",
694 "Min Activations": "最小激活数",709 "Min Activations": "最小激活数",
695 "(disabled when max recursion steps are used)": "(当使用最大递归步数时禁用)",710 "(disabled when max recursion steps are used)": "(当使用最大递归步数时禁用)",
696 "Max Depth": "最大深度",711 "Max Depth": "最大深度",
@@ -711,7 +726,7 @@
711 "Match Whole Words": "匹配整个单词",726 "Match Whole Words": "匹配整个单词",
712 "Only the entries with the most number of key matches will be selected for Inclusion Group filtering": "仅选择具有最多关键匹配项的条目进行包含组过滤",727 "Only the entries with the most number of key matches will be selected for Inclusion Group filtering": "仅选择具有最多关键匹配项的条目进行包含组过滤",
713 "Use Group Scoring": "使用群组评分",728 "Use Group Scoring": "使用群组评分",
714 "Alert if your world info is greater than the allocated budget.": "如果您的世界信息大于分配的预算,则会发出警报。",729 "Alert if your world info is greater than the allocated budget.": "如果您的世界书大于分配的预算,则会发出警报。",
715 "Alert On Overflow": "溢出警报",730 "Alert On Overflow": "溢出警报",
716 "or": "或",731 "or": "或",
717 "--- Pick to Edit ---": "--- 选择以编辑 ---",732 "--- Pick to Edit ---": "--- 选择以编辑 ---",
@@ -729,8 +744,8 @@
729 "Custom": "自定义",744 "Custom": "自定义",
730 "Title A-Z": "标题 A 到 Z",745 "Title A-Z": "标题 A 到 Z",
731 "Title Z-A": "标题 Z 到 A",746 "Title Z-A": "标题 Z 到 A",
732 "Tokens ↗": "词符 ↗",747 "Tokens ↗": "Token ↗",
733 "Tokens ↘": "词符 ↘",748 "Tokens ↘": "Token ↘",
734 "Depth ↗": "深度 ↗",749 "Depth ↗": "深度 ↗",
735 "Depth ↘": "深度 ↘",750 "Depth ↘": "深度 ↘",
736 "Order ↗": "顺序 ↗",751 "Order ↗": "顺序 ↗",
@@ -746,7 +761,7 @@
746 "Admin Panel": "管理员面板",761 "Admin Panel": "管理员面板",
747 "Logout": "登出",762 "Logout": "登出",
748 "Search Settings": "搜索设置",763 "Search Settings": "搜索设置",
749 "UI Theme": "UI主题",764 "UI Theme": "UI 主题",
750 "Import a theme file": "导入主题文件",765 "Import a theme file": "导入主题文件",
751 "Export a theme file": "导出主题文件",766 "Export a theme file": "导出主题文件",
752 "Delete a theme": "删除主题",767 "Delete a theme": "删除主题",
@@ -799,7 +814,7 @@
799 "No Blur Effect": "禁用模糊效果",814 "No Blur Effect": "禁用模糊效果",
800 "Remove text shadow effect": "移除文本阴影效果",815 "Remove text shadow effect": "移除文本阴影效果",
801 "No Text Shadows": "禁用文本阴影",816 "No Text Shadows": "禁用文本阴影",
802 "Reduce chat height, and put a static sprite behind the chat window": "缩小聊天窗口的高度,并在聊天窗口后面放置一个固定的表情图。",817 "Reduce chat height, and put a static sprite behind the chat window": "缩小聊天窗口的高度,并在聊天窗口后面放置一个固定的立绘。",
803 "Waifu Mode": "视觉小说模式",818 "Waifu Mode": "视觉小说模式",
804 "Always show the full list of the Message Actions context items for chat messages, instead of hiding them behind '...'": "始终显示聊天消息的操作菜单完整列表,而不是将它们隐藏在“...”后面",819 "Always show the full list of the Message Actions context items for chat messages, instead of hiding them behind '...'": "始终显示聊天消息的操作菜单完整列表,而不是将它们隐藏在“...”后面",
805 "Auto-Expand Message Actions": "自动展开消息操作菜单",820 "Auto-Expand Message Actions": "自动展开消息操作菜单",
@@ -808,21 +823,21 @@
808 "Entirely unrestrict all numeric sampling parameters": "完全解除所有数字采样参数的限制",823 "Entirely unrestrict all numeric sampling parameters": "完全解除所有数字采样参数的限制",
809 "Mad Lab Mode": "疯狂实验室模式",824 "Mad Lab Mode": "疯狂实验室模式",
810 "Time the AI's message generation, and show the duration in the chat log": "对 AI 生成消息的时间进行计时,并在聊天记录中显示。",825 "Time the AI's message generation, and show the duration in the chat log": "对 AI 生成消息的时间进行计时,并在聊天记录中显示。",
811 "Message Timer": "AI回复计时器",826 "Message Timer": "AI 回复计时器",
812 "Show a timestamp for each message in the chat log": "在聊天日志中显示每条消息的时间戳",827 "Show a timestamp for each message in the chat log": "在聊天日志中显示每条消息的时间戳",
813 "Chat Timestamps": "聊天时间戳",828 "Chat Timestamps": "聊天时间戳",
814 "Show an icon for the API that generated the message": "显示此消息所用API的图标",829 "Show an icon for the API that generated the message": "显示此消息所用 API 的图标",
815 "Model Icon": "模型图标",830 "Model Icon": "模型图标",
816 "Show sequential message numbers in the chat log": "在聊天记录中显示消息楼层",831 "Show sequential message numbers in the chat log": "在聊天记录中显示消息楼层",
817 "Message IDs": "显示消息楼层",832 "Message IDs": "显示消息楼层",
818 "Hide avatars, only in chat messages.": "在聊天记录中隐藏头像。",833 "Hide avatars, only in chat messages.": "在聊天记录中隐藏头像。",
819 "Hide Chat Avatars": "隐藏头像",834 "Hide Chat Avatars": "隐藏头像",
820 "Show the number of tokens in each message in the chat log": "在聊天记录中显示每条消息的词符数",835 "Show the number of tokens in each message in the chat log": "在聊天记录中显示每条消息的 Token 数",
821 "Show Message Token Count": "显示消息词符数",836 "Show Message Token Count": "显示消息 Token 数",
822 "Single-row message input area. Mobile only, no effect on PC": "将输入框限制为一行。仅适用于移动设备,对PC无影响",837 "Single-row message input area. Mobile only, no effect on PC": "将输入框限制为一行。仅适用于移动设备,对PC无影响",
823 "Compact Input Area (Mobile)": "紧凑输入区域(移动端)",838 "Compact Input Area (Mobile)": "紧凑输入区域(移动端)",
824 "Display swipe numbers for all messages, not just the last.": "显示所有信息的滑动编号,而非仅限最后一条。",839 "Display swipe numbers for all messages, not just the last.": "显示所有信息的备选编号,而非仅限最后一条。",
825 "Swipe # for All Messages": "给所有信息分配滑动编号 #",840 "Swipe # for All Messages": "给所有消息分配备选编号 #",
826 "In the Character Management panel, show quick selection buttons for favorited characters": "在角色管理面板中,显示快速选择按钮以选择收藏的角色",841 "In the Character Management panel, show quick selection buttons for favorited characters": "在角色管理面板中,显示快速选择按钮以选择收藏的角色",
827 "Characters Hotswap": "角色卡热切换",842 "Characters Hotswap": "角色卡热切换",
828 "Enable magnification for zoomed avatar display.": "启用放大功能以放大头像显示。",843 "Enable magnification for zoomed avatar display.": "启用放大功能以放大头像显示。",
@@ -848,8 +863,8 @@
848 "Advanced Character Search": "高级角色搜索",863 "Advanced Character Search": "高级角色搜索",
849 "If checked and the character card contains a prompt override (System Prompt), use that instead": "开启后,如果角色卡已包含系统提示词,则覆盖当前的系统提示词。",864 "If checked and the character card contains a prompt override (System Prompt), use that instead": "开启后,如果角色卡已包含系统提示词,则覆盖当前的系统提示词。",
850 "Prefer Character Card Prompt": "角色卡提示词优先",865 "Prefer Character Card Prompt": "角色卡提示词优先",
851 "If checked and the character card contains a Post-History Instructions override, use that instead": "开启后,如果角色卡包含后历史指令覆盖,则使用它。",866 "If checked and the character card contains a Post-History Instructions override, use that instead": "开启后,如果角色卡包含历史后置指令覆盖,则使用它。",
852 "Prefer Character Card Instructions": "首选角色卡说明",867 "Prefer Character Card Instructions": "首选角色卡指令",
853 "never_resize_avatars_tooltip": "避免裁剪和调整导入的角色图像的大小。关闭时,裁剪/调整大小为 512x768。",868 "never_resize_avatars_tooltip": "避免裁剪和调整导入的角色图像的大小。关闭时,裁剪/调整大小为 512x768。",
854 "Never resize avatars": "永不调整头像大小",869 "Never resize avatars": "永不调整头像大小",
855 "Allow animations for WEBP backgrounds. This is only a change for the selection menu.": "允许WEBP格式的背景动画。此更改仅对选择菜单生效",870 "Allow animations for WEBP backgrounds. This is only a change for the selection menu.": "允许WEBP格式的背景动画。此更改仅对选择菜单生效",
@@ -873,12 +888,12 @@
873 "Experimental feature. May not work for all backends.": "实验性功能。可能不适用于所有后端。",888 "Experimental feature. May not work for all backends.": "实验性功能。可能不适用于所有后端。",
874 "Play a sound when a message generation finishes": "当消息生成完毕时播放声音",889 "Play a sound when a message generation finishes": "当消息生成完毕时播放声音",
875 "Message Sound": "消息声音",890 "Message Sound": "消息声音",
876 "Only play a sound when ST's browser tab is unfocused": "仅在ST的浏览器标签页未被打开时播放声音",891 "Only play a sound when ST's browser tab is unfocused": "仅在 ST 的浏览器标签页未被打开时播放声音",
877 "Background Sound Only": "仅背景声音",892 "Background Sound Only": "仅背景声音",
878 "Reduce the formatting requirements on API URLs": "减少API URL的格式化要求",893 "Reduce the formatting requirements on API URLs": "减少 API URL 的格式化要求",
879 "Relaxed API URLS": "宽松的API URL",894 "Relaxed API URLS": "宽松的 API URL",
880 "Ask to import the World Info/Lorebook for every new character with embedded lorebook. If unchecked, a brief message will be shown instead": "询问是否为每个具有嵌入的知识书的新角色导入世界信息/知识书。如果未选中,则会显示简短的消息",895 "Ask to import the World Info/Lorebook for every new character with embedded lorebook. If unchecked, a brief message will be shown instead": "询问是否为每个具有嵌入的世界书的新角色导入世界书。如果未选中,则会显示简短的消息",
881 "Lorebook Import Dialog": "知识书导入对话框",896 "Lorebook Import Dialog": "世界书导入对话框",
882 "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.": "启用在某些文本字段中单击/选择时自动选中文本的功能。适用于弹出输入框以及可能的其他自定义输入字段。",897 "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.": "启用在某些文本字段中单击/选择时自动选中文本的功能。适用于弹出输入框以及可能的其他自定义输入字段。",
883 "Auto-select Input Text": "自动选择输入文本",898 "Auto-select Input Text": "自动选择输入文本",
884 "markdown_hotkeys_desc": "在特定文本输入框中,启用插入 Markdown 格式的快捷键。详情输入:“/help hotkeys”。",899 "markdown_hotkeys_desc": "在特定文本输入框中,启用插入 Markdown 格式的快捷键。详情输入:“/help hotkeys”。",
@@ -888,11 +903,11 @@
888 "Allow repositioning certain UI elements by dragging them. PC only, no effect on mobile": "允许通过拖动重新定位某些UI元素。仅适用于PC,对移动设备无影响",903 "Allow repositioning certain UI elements by dragging them. PC only, no effect on mobile": "允许通过拖动重新定位某些UI元素。仅适用于PC,对移动设备无影响",
889 "Movable UI Panels": "可移动 UI 面板",904 "Movable UI Panels": "可移动 UI 面板",
890 "Reset MovingUI panel sizes/locations.": "重置 可移动UI 面板大小/位置。",905 "Reset MovingUI panel sizes/locations.": "重置 可移动UI 面板大小/位置。",
891 "mui_reset": "Reset",906 "mui_reset": "重置",
892 "MovingUI preset. Predefined/saved draggable positions": "可移动UI预设。预定义/保存的可拖动位置",907 "MovingUI preset. Predefined/saved draggable positions": "可移动UI 预设。预定义/保存的可拖动位置",
893 "MUI Preset": "可移动 UI 预设",908 "MUI Preset": "可移动 UI 预设",
894 "Save movingUI changes to a new file": "将可移动UI更改保存到新文件中",909 "Save movingUI changes to a new file": "将 可移动UI 更改保存到新文件中",
895 "Apply a custom CSS style to all of the ST GUI": "将自定义CSS样式应用于所有ST GUI",910 "Apply a custom CSS style to all of the ST GUI": "将自定义 CSS 样式应用于所有 ST GUI",
896 "Custom CSS": "自定义 CSS",911 "Custom CSS": "自定义 CSS",
897 "Chat/Message Handling": "聊天/消息处理",912 "Chat/Message Handling": "聊天/消息处理",
898 "# Messages to Load": "要加载 # 条消息",913 "# Messages to Load": "要加载 # 条消息",
@@ -904,21 +919,21 @@
904 "Gradual push-out": "逐渐推出",919 "Gradual push-out": "逐渐推出",
905 "Always include examples": "始终包含示例",920 "Always include examples": "始终包含示例",
906 "Never include examples": "永不包含示例",921 "Never include examples": "永不包含示例",
907 "Image Swipe Behavior:": "图片滑动刷新行为:",922 "Image Swipe Behavior:": "图片备选生成行为:",
908 "Generate new": "生成新的",923 "Generate new": "生成新的",
909 "Roll over": "循环现有",924 "Roll over": "循环现有",
910 "Send on Enter": "按 Enter 发送",925 "Send on Enter": "按 Enter 发送",
911 "Disabled": "已禁用",926 "Disabled": "已禁用",
912 "Automatic (PC)": "自动(PC)",927 "Automatic (PC)": "自动(PC)",
913 "Enabled": "已启用",928 "Enabled": "已启用",
914 "Press Send to continue": "按发送键以继续",929 "Press Send to continue": "按发送键以续写",
915 "Show a button in the input area to ask the AI to continue (extend) its last message": "在输入区域中显示一个按钮,要求AI继续(延长)其上一条消息",930 "Show a button in the input area to ask the AI to continue (extend) its last message": "在输入区域中显示一个按钮,要求 AI 续写(延长)其上一条消息",
916 "Quick 'Continue' button": "快速“继续”按钮",931 "Quick 'Continue' button": "快速“续写”按钮",
917 "Show a button in the input area to ask the AI to impersonate your character for a single message": "在输入区域中显示一个按钮,让 AI 模仿你的角色发送一条消息。",932 "Show a button in the input area to ask the AI to impersonate your character for a single message": "在输入区域中显示一个按钮,让 AI 模仿你的人设发送一条消息。",
918 "Quick 'Impersonate' button": "快速“模仿”按钮",933 "Quick 'Impersonate' button": "快速“模仿”按钮",
919 "Show arrow buttons on the last in-chat message to generate alternative AI responses. Both PC and mobile": "在聊天窗口的最后一条信息上显示箭头按钮,以生成AI的其他回复选项。适用于电脑和手机端。",934 "Show arrow buttons on the last in-chat message to generate alternative AI responses. Both PC and mobile": "在聊天窗口的最后一条信息上显示箭头按钮,以生成 AI 的其他备选回复。适用于电脑和手机端。",
920 "Swipes": "刷新回复按钮",935 "Swipes": "备选回复",
921 "Allow using swiping gestures on the last in-chat message to trigger swipe generation. Mobile only, no effect on PC": "允许在最后一条聊天消息上使用滑动手势触发滑动生成。仅适用于移动设备,对PC无影响",936 "Allow using swiping gestures on the last in-chat message to trigger swipe generation. Mobile only, no effect on PC": "允许在最后一条聊天消息上使用滑动手势触发备选回复生成。仅适用于移动设备,对 PC 无影响",
922 "Gestures": "手势",937 "Gestures": "手势",
923 "Auto-load Last Chat": "自动加载上次聊天",938 "Auto-load Last Chat": "自动加载上次聊天",
924 "Auto-scroll Chat": "自动滚动聊天",939 "Auto-scroll Chat": "自动滚动聊天",
@@ -930,38 +945,41 @@
930 "Forbid External Media": "禁止外部媒体",945 "Forbid External Media": "禁止外部媒体",
931 "Allow {{char}}: in bot messages": "在机器人消息中允许 {{char}}: ",946 "Allow {{char}}: in bot messages": "在机器人消息中允许 {{char}}: ",
932 "Allow {{user}}: in bot messages": "在机器人消息中允许 {{user}}: ",947 "Allow {{user}}: in bot messages": "在机器人消息中允许 {{user}}: ",
933 "Skip encoding and characters in message text, allowing a subset of HTML markup as well as Markdown": "跳过消息文本中的编码和字符,允许一部分HTML标记以及Markdown",948 "Skip encoding and characters in message text, allowing a subset of HTML markup as well as Markdown": "跳过消息文本中的编码和字符,允许一部分 HTML 标记以及 Markdown",
934 "Show tags in responses": "在响应中显示标签",949 "Show tags in responses": "在响应中显示标签",
935 "Experimental Macro Engine": "实验性宏引擎",950 "Experimental Macro Engine": "实验性宏引擎",
936 "Experimental feature. Currently in development to test.": "实验性功能。目前正在开发测试中。",951 "Experimental feature. Currently in development to test.": "实验性功能。目前正在开发测试中。",
937 "Allow AI messages in groups to contain lines spoken by other group members": "允许群聊中的AI输出群中其他成员说的话",952 "Allow AI messages in groups to contain lines spoken by other group members": "允许群聊中的 AI 输出群中其他成员说的话",
938 "Relax message trim in Groups": "减轻群聊中的消息修剪",953 "Relax message trim in Groups": "减轻群聊中的消息修剪",
939 "Log prompts to console": "将提示词输出到控制台",954 "Log prompts to console": "将提示词记录到控制台",
940 "Requests logprobs from the API for the Token Probabilities feature": "从API请求对数概率数据,用于实现词符概率功能。",955 "Requests logprobs from the API for the Token Probabilities feature": "从 API 请求对数概率数据,用于实现 Token 概率功能。",
941 "Request token probabilities": "请求词符概率",956 "Request token probabilities": "请求 Token 概率",
942 "In group chat, highlight the character(s) that are currently queued to generate responses and the order in which they will respond.": "在群聊中,突出显示当前排队等待生成响应的角色以及他们响应的顺序。",957 "In group chat, highlight the character(s) that are currently queued to generate responses and the order in which they will respond.": "在群聊中,突出显示当前排队等待生成响应的角色以及他们响应的顺序。",
943 "Show group chat queue": "显示群聊队列",958 "Show group chat queue": "显示群聊队列",
944 "Always render style tags from greetings, even if the message is unloaded due to lazy loading.": "始终渲染问候消息里的样式标签,即便消息因懒加载策略还未被加载。",959 "Always render style tags from greetings, even if the message is unloaded due to lazy loading.": "始终渲染问候消息里的样式标签,即便消息因懒加载策略还未被加载。",
945 "Pin greeting message styles": "固定问候消息样式",960 "Pin greeting message styles": "固定问候消息样式",
946 "Automatically reject and re-generate AI message based on configurable criteria": "根据可配置的条件自动拒绝并重新生成AI消息",961 "Automatically reject and re-generate AI message based on configurable criteria": "根据可配置的条件自动拒绝并重新生成 AI 消息",
947 "Auto-swipe": "自动滑动",962 "Auto-swipe": "自动生成备选回复",
948 "Enable the auto-swipe function. Settings in this section only have an effect when auto-swipe is enabled": "启用自动滑动功能。仅当启用自动滑动时,本节中的设置才会生效",963 "Enable the auto-swipe function. Settings in this section only have an effect when auto-swipe is enabled": "启用自动生成备选回复功能。仅当启用此功能时,本节中的设置才会生效",
949 "Minimum generated message length": "生成的消息的最小长度",964 "Minimum generated message length": "生成的消息的最小长度",
950 "If the generated message is shorter than these many characters, trigger an auto-swipe": "如果生成的消息短于此长度,则触发自动滑动",965 "If the generated message is shorter than these many characters, trigger an auto-swipe": "如果生成的消息短于此长度,则自动生成备选回复",
951 "Blacklisted words": "屏蔽词",966 "Blacklisted words": "屏蔽词",
952 "words you dont want generated separated by comma ','": "不想生成的词语,用半角逗号“,”分隔",967 "words you dont want generated separated by comma ','": "不想生成的词语,用半角逗号“,”分隔",
953 "Blacklisted word count to swipe": "触发滑动的黑名单词语数量",968 "Blacklisted word count to swipe": "触发自动生成备选回复的黑名单词语数量",
954 "Minimum number of blacklisted words detected to trigger an auto-swipe": "触发自动滑动刷新回复所需检测到的最少违禁词数量。",969 "Minimum number of blacklisted words detected to trigger an auto-swipe": "触发自动生成备选回复所需检测到的最少违禁词数量。",
955 "Automatically 'continue' a response if the model stopped before reaching a certain amount of tokens.": "当回复没有达到特定词符数时,自动让模型“继续”这个回复。",970 "Automatically 'continue' a response if the model stopped before reaching a certain amount of tokens.": "当回复没有达到特定 Token 数时,自动让模型“续写”这个回复。",
956 "Auto-Continue": "自动继续",971 "Auto-Continue": "自动续写",
957 "Allow for Chat Completion APIs": "允许使用聊天补全API",972 "Allow for Chat Completion APIs": "允许使用 聊天补全API",
958 "Target length (tokens)": "目标长度(以词符数计)",973 "Target length (tokens)": "目标长度(Token)",
959 "AutoComplete Settings": "自动补全设置",974 "AutoComplete Settings": "自动补全设置",
975 "When to show the autocomplete for slash commands and macros.": "何时显示快捷命令和宏的自动补全。",
960 "Visibility": "可见性",976 "Visibility": "可见性",
961 "Don't show": "不显示",977 "Don't show": "不显示",
962 "Input length > 1": "输入长度 > 1",978 "Input length > 1": "输入长度 > 1",
963 "Always show": "始终显示",979 "Always show": "始终显示",
964 "Automatically hide details": "自动隐藏详细信息",980 "Automatically hide details": "自动隐藏详细信息",
981 "Show macro autocomplete in all macro-enabled fields. When off, autocomplete only shows in expanded editors or when pressing Ctrl+Space.": "在所有启用宏的字段中显示宏自动补全。关闭时,仅在展开的编辑器中或按 Ctrl+Space 时显示自动补全。",
982 "Show in all macro fields": "在所有宏字段中显示",
965 "Determines how entries are found for autocomplete.": "确定如何找到自动补全的条目。",983 "Determines how entries are found for autocomplete.": "确定如何找到自动补全的条目。",
966 "Autocomplete Matching": "匹配",984 "Autocomplete Matching": "匹配",
967 "Starts with": "以此开始:",985 "Starts with": "以此开始:",
@@ -981,13 +999,13 @@
981 "chat input box": "聊天输入框",999 "chat input box": "聊天输入框",
982 "entire chat width": "整个聊天宽度",1000 "entire chat width": "整个聊天宽度",
983 "full window width": "全窗口宽度",1001 "full window width": "全窗口宽度",
984 "STscript Settings": "STscript设置",1002 "STscript Settings": "ST脚本设置",
985 "Sets default flags for the STscript parser.": "为 STscript 解析器设置默认标志。",1003 "Sets default flags for the STscript parser.": "为 ST脚本 解析器设置默认标志。",
986 "Parser Flags": "解析器标志",1004 "Parser Flags": "解析器标志",
987 "Switch to stricter escaping, allowing all delimiting characters to be escaped with a backslash, and backslashes to be escaped as well.": "切换到更严格的转义,允许所有分隔字符用反斜杠转义,并且反斜杠也可以转义。",1005 "Switch to stricter escaping, allowing all delimiting characters to be escaped with a backslash, and backslashes to be escaped as well.": "切换到更严格的转义,允许所有分隔字符用反斜杠转义,并且反斜杠也可以转义。",
988 "STRICT_ESCAPING": "严格转义",1006 "STRICT_ESCAPING": "严格转义",
989 "stscript_parser_flag_replace_getvar_label": "防止 {{getvar::}} {{getglobalvar::}} 宏具有自动评估的文字宏类值。\n例如,“{{newline}}”保留为文字字符串“{{newline}}”\n\n(这是通过在内部用范围变量替换 {{getvar::}} {{getglobalvar::}} 宏来实现的。)",1007 "stscript_parser_flag_replace_getvar_label": "防止 {{getvar::}} {{getglobalvar::}} 宏具有自动评估的文字宏类值。\n例如,“{{newline}}”保留为文字字符串“{{newline}}”\n\n(这是通过在内部用范围变量替换 {{getvar::}} {{getglobalvar::}} 宏来实现的。)",
990 "REPLACE_GETVAR": "替换GETVAR",1008 "REPLACE_GETVAR": "替换 GETVAR",
991 "Change Background Image": "更改背景图片",1009 "Change Background Image": "更改背景图片",
992 "Backgrounds": "背景",1010 "Backgrounds": "背景",
993 "Background Fitting": "背景图片尺寸",1011 "Background Fitting": "背景图片尺寸",
@@ -1000,6 +1018,11 @@
1000 "Auto-select": "自动选择",1018 "Auto-select": "自动选择",
1001 "Add a new background": "添加新背景",1019 "Add a new background": "添加新背景",
1002 "Add Background": "添加背景",1020 "Add Background": "添加背景",
1021 "Sort backgrounds": "背景顺序",
1022 "A-Z": "A-Z",
1023 "Z-A": "Z-A",
1024 "Newest": "最新",
1025 "Oldest": "最旧",
1003 "Global": "全局",1026 "Global": "全局",
1004 "Chat": "聊天",1027 "Chat": "聊天",
1005 "Make thumbnails smaller": "缩小缩略图",1028 "Make thumbnails smaller": "缩小缩略图",
@@ -1007,16 +1030,16 @@
1007 "bg_chat_hint_1": "使用生成的聊天背景",1030 "bg_chat_hint_1": "使用生成的聊天背景",
1008 "bg_chat_hint_2": "扩展名将出现在这里。",1031 "bg_chat_hint_2": "扩展名将出现在这里。",
1009 "Scroll backgrounds to top": "回顶",1032 "Scroll backgrounds to top": "回顶",
1010 "Extensions": "扩展",1033 "Extensions": "扩展程序",
1011 "Notify on extension updates": "在扩展更新时通知",1034 "Notify on extension updates": "在扩展程序更新时通知",
1012 "Manage extensions": "管理扩展",1035 "Manage extensions": "管理扩展程序",
1013 "Import Extension From Git Repo": "从 Git 仓库导入扩展",1036 "Import Extension From Git Repo": "从 Git 仓库导入扩展程序",
1014 "Install extension": "安装扩展",1037 "Install extension": "安装扩展程序",
1015 "(DEPRECATED)": "(已弃用)",1038 "(DEPRECATED)": "(已弃用)",
1016 "Extras API:": "扩展API:",1039 "Extras API:": "扩展 API:",
1017 "Auto-connect": "自动连接",1040 "Auto-connect": "自动连接",
1018 "Extras API URL": "附加 API URL",1041 "Extras API URL": "扩展 API URL",
1019 "Extras API key (optional)": "扩展API密钥(可选)",1042 "Extras API key (optional)": "扩展 API 密钥(选填)",
1020 "Persona Management": "用户设定管理",1043 "Persona Management": "用户设定管理",
1021 "Click for stats!": "点击即可查看统计~",1044 "Click for stats!": "点击即可查看统计~",
1022 "Usage Stats": "使用统计",1045 "Usage Stats": "使用统计",
@@ -1030,23 +1053,23 @@
1030 "Current Persona": "当前人设",1053 "Current Persona": "当前人设",
1031 "Rename Persona": "重命名人设",1054 "Rename Persona": "重命名人设",
1032 "Click to set user name for all messages": "点击为所有消息设置用户名",1055 "Click to set user name for all messages": "点击为所有消息设置用户名",
1033 "Persona Lore Alt+Click to open the lorebook": "Persona Lore\nAlt+Click to open the lorebook",1056 "persona_lore_button_title": "人设世界书\n\n单击加载\nShift/Alt+单击或长按打开“链接到人设世界书”弹出窗口",
1034 "Change Persona Image": "更改人设图",1057 "Change Persona Image": "更改人设图",
1035 "Duplicate Persona": "复制人设",1058 "Duplicate Persona": "复制人设",
1036 "Delete Persona": "删除人设",1059 "Delete Persona": "删除人设",
1037 "Persona Description": "用户设定描述",1060 "Persona Description": "用户设定描述",
1038 "Example: [{{user}} is a 28-year-old Romanian cat girl.]": "示例:[{{user}}是一个28岁的罗马尼亚猫娘。]",1061 "Example: [{{user}} is a 28-year-old Romanian cat girl.]": "示例:[{{user}}是一个28岁的罗马尼亚猫娘。]",
1039 "Position": "插入位置",1062 "Position": "插入位置",
1040 "Tokens persona description": "人设词符数",1063 "Tokens persona description": "人设 Token 数",
1041 "None (disabled)": "无(已禁用)",1064 "None (disabled)": "无(已禁用)",
1042 "In Story String / Prompt Manager": "在故事字符串/提示词管理器中",1065 "In Story String / Prompt Manager": "在故事字符串/提示词管理器中",
1043 "Top of Author's Note": "作者注的顶部",1066 "Top of Author's Note": "作者注释的顶部",
1044 "Bottom of Author's Note": "作者注的底部",1067 "Bottom of Author's Note": "作者注释的底部",
1045 "Connections": "链接",1068 "Connections": "链接",
1046 "Click to select this as default persona for the new chats. Click again to remove it.": "点击将此人设设置为新聊天的默认人设。再次点击以移除。",1069 "Click to select this as default persona for the new chats. Click again to remove it.": "点击将此人设设置为新聊天的默认用户设定。再次点击以移除。",
1047 "Click to lock your selected persona to the current character. Click again to remove the lock.": "点击将选择的用户设定与当前角色绑定。再次点击以解绑。",1070 "Click to lock your selected persona to the current character. Click again to remove the lock.": "点击将选择的用户设定与当前角色绑定。再次点击以解绑。",
1048 "Character": "角色",1071 "Character": "角色",
1049 "Click to lock your selected persona to the current chat. Click again to remove the lock.": "点击将选择的人设与当前聊天绑定。再次点击以解绑。",1072 "Click to lock your selected persona to the current chat. Click again to remove the lock.": "点击将选择的用户设定与当前聊天绑定。再次点击以解绑。",
1050 "Global Settings": "全局设置",1073 "Global Settings": "全局设置",
1051 "Show notifications on switching personas": "切换用户设定时显示通知",1074 "Show notifications on switching personas": "切换用户设定时显示通知",
1052 "When multiple personas are connected to a character, a popup will appear to select which one to use": "当多个用户设定与一个角色绑定时,会弹出一个弹窗让用户选择使用哪一个。",1075 "When multiple personas are connected to a character, a popup will appear to select which one to use": "当多个用户设定与一个角色绑定时,会弹出一个弹窗让用户选择使用哪一个。",
@@ -1057,21 +1080,21 @@
1057 "Locked = Character Management panel will stay open": "锁定 = 角色管理面板将保持打开状态",1080 "Locked = Character Management panel will stay open": "锁定 = 角色管理面板将保持打开状态",
1058 "Select/Create Characters": "选择/创建角色",1081 "Select/Create Characters": "选择/创建角色",
1059 "Favorite characters to add them to HotSwaps": "收藏角色以将它们添加到快速热切换区",1082 "Favorite characters to add them to HotSwaps": "收藏角色以将它们添加到快速热切换区",
1060 "Token counts may be inaccurate and provided just for reference.": "词符计数可能不准确,仅供参考。",1083 "Token counts may be inaccurate and provided just for reference.": "Token 计数可能不准确,仅供参考。",
1061 "Total tokens": "总词符数",1084 "Total tokens": "总 Token 数",
1062 "Calculating...": "正在计算...",1085 "Calculating...": "正在计算...",
1063 "Tokens": "词符数",1086 "Tokens": "Token 数",
1064 "Permanent tokens": "永久词符",1087 "Permanent tokens": "永久 Token",
1065 "Permanent": "恒定的",1088 "Permanent": "永久的",
1066 "About Token 'Limits'": "关于词符“限制”",1089 "About Token 'Limits'": "关于 Token “限制”",
1067 "Toggle character info panel": "切换角色信息面板",1090 "Toggle character info panel": "切换角色信息面板",
1068 "Name this character": "为这个角色命名",1091 "Name this character": "为这个角色命名",
1069 "extension_token_counter": "词符:",1092 "extension_token_counter": "Token:",
1070 "Click to select a new avatar for this character": "单击以为此角色选择新的头像",1093 "Click to select a new avatar for this character": "单击以为此角色选择新的头像",
1071 "Add to Favorites": "添加到收藏夹",1094 "Add to Favorites": "添加到收藏夹",
1072 "Advanced Definition": "高级定义",1095 "Advanced Definition": "高级定义",
1073 "world_button_title": "角色世界书\n\n单击加载\nShift+单击打开“链接到世界信息”弹出窗口",1096 "world_button_title": "角色世界书\n\n单击加载\nShift/Alt+单击或长按打开“链接到世界书”弹出窗口",
1074 "Chat Lore Alt+Click to open the lorebook": "聊天世界书\nAlt+单击打开世界书",1097 "chat_lorebook_button_title": "聊天世界书\n\n单击加载\nShift/Alt+单击或长按打开“链接到聊天世界书”弹出窗口",
1075 "Connected Personas": "绑定的用户设定",1098 "Connected Personas": "绑定的用户设定",
1076 "Export and Download": "导出并下载",1099 "Export and Download": "导出并下载",
1077 "Duplicate Character": "复制角色",1100 "Duplicate Character": "复制角色",
@@ -1081,12 +1104,12 @@
1081 "Link to World Info": "链接到世界书",1104 "Link to World Info": "链接到世界书",
1082 "Import Card Lore": "导入角色卡的世界书",1105 "Import Card Lore": "导入角色卡的世界书",
1083 "Character Settings Overrides": "角色设置覆盖",1106 "Character Settings Overrides": "角色设置覆盖",
1084 "Convert to Persona": "转换为用户角色",1107 "Convert to Persona": "转换为用户设定",
1085 "Rename": "重命名",1108 "Rename": "重命名",
1086 "Link to Source": "来源链接",1109 "Link to Source": "来源链接",
1087 "Replace / Update": "替换 / 更新",1110 "Replace / Update": "替换 / 更新",
1088 "Import Tags": "导入标签",1111 "Import Tags": "导入标签",
1089 "Set / Unset as Welcome Page Assistant": "设置 / 取消 为欢迎页面助理",1112 "Set / Unset as Welcome Page Assistant": "设为 / 取消设为欢迎页面助理",
1090 "Search / Create Tags": "搜索/创建标签",1113 "Search / Create Tags": "搜索/创建标签",
1091 "View all tags": "查看所有标签",1114 "View all tags": "查看所有标签",
1092 "Creator's Notes": "创作者的注释",1115 "Creator's Notes": "创作者的注释",
@@ -1096,56 +1119,52 @@
1096 "Character details are hidden.": "角色详情已隐藏。",1119 "Character details are hidden.": "角色详情已隐藏。",
1097 "Character Description": "角色描述",1120 "Character Description": "角色描述",
1098 "Click to allow/forbid the use of external media for this character.": "单击以允许/禁止此角色使用外部媒体。",1121 "Click to allow/forbid the use of external media for this character.": "单击以允许/禁止此角色使用外部媒体。",
1099 "Ext. Media": "扩展媒体",1122 "Ext. Media": "外部媒体",
1100 "Describe your character's physical and mental traits here.": "在这里描述您角色的身体和精神特征。",1123 "Describe your character's physical and mental traits here.": "在这里描述您角色的身体和精神特征。",
1101 "First message": "第一条消息",1124 "First message": "开场白",
1102 "Click to set additional greeting messages": "单击以设置其他问候消息",1125 "Click to set additional greeting messages": "单击以设置其他开场白",
1103 "Alt. Greetings": "其他开场",1126 "Alt. Greetings": "其他开场",
1104 "This will be the first message from the character that starts every chat.": "这将是角色在每次聊天开始时发送的第一条消息。",1127 "This will be the first message from the character that starts every chat.": "这将是角色在每次聊天开始时发送的第一条消息。",
1105 "Group Controls": "群聊控制",1128 "Group Controls": "群聊控制",
1106 "Chat Name (Optional)": "聊天名称(可选)",1129 "Chat Name (Optional)": "聊天名称(选填)",
1107 "Chat Lore": "聊天知识",1130 "Chat Lore": "聊天知识",
1108 "Click to select a new avatar for this group": "单击选择该群聊的新头像",1131 "Click to select a new avatar for this group": "单击选择该群聊的新头像",
1109 "Group reply strategy": "群聊发言顺序",1132 "Group reply strategy": "群聊发言顺序",
1110 "Manual": "手动",1133 "Manual": "手动",
1111 "Natural order": "自然顺序",1134 "Natural order": "自然顺序",
1112 "List order": "从上到下",1135 "List order": "列表顺序",
1113 "Pooled order": "随机轮流顺序",1136 "Pooled order": "随机轮流顺序",
1114 "Group generation handling mode": "群组生成处理模式",1137 "Group generation handling mode": "群聊生成处理模式",
1115 "Swap character cards": "交换角色卡",1138 "Swap character cards": "交换角色卡",
1116 "Join character cards (exclude muted)": "加入角色卡(不包括被禁言的)",1139 "Join character cards (exclude muted)": "合并角色卡(排除被禁言角色)",
1117 "Join character cards (include muted)": "加入角色卡(包括被禁言的)",1140 "Join character cards (include muted)": "合并角色卡(包括被禁言角色)",
1118 "Inserted before each part of the joined fields.": "插入到加入字段的每个部分之前。",1141 "Inserted before each part of the joined fields.": "插入到合并字段的每个部分之前。",
1119 "Join Prefix": "加入前缀",1142 "Join Prefix": "合并前缀",
1120 "When 'Join character cards' is selected, all respective fields of the characters are being joined together.This means that in the story string for example all character descriptions will be joined to one big text.If you want those fields to be separated, you can define a prefix or suffix here.This value supports normal macros and will also replace {{char}} with the relevant char's name and <FIELDNAME> with the name of the part (e.g.: description, personality, scenario, etc.)": "当选择“合并角色卡”时,角色的所有相应字段将被合并在一起。这意味着在故事字符串中,例如,所有角色描述都将合并为一个大文本。如果您希望将这些字段分开,可以在此处定义前缀或后缀。此值支持普通宏,还会将 {{char}} 替换为相关角色的名称,将 <FIELDNAME> 替换为部分的名称(例如:描述、个性、场景等)",1143 "When 'Join character cards' is selected, all respective fields of the characters are being joined together.This means that in the story string for example all character descriptions will be joined to one big text.If you want those fields to be separated, you can define a prefix or suffix here.This value supports normal macros and will also replace {{char}} with the relevant char's name and <FIELDNAME> with the name of the part (e.g.: description, personality, scenario, etc.)": "当选择“合并角色卡”时,角色的所有相应字段将被合并在一起。这意味着在故事字符串中,例如,所有角色描述都将合并为一个大文本。如果您希望将这些字段分开,可以在此处定义前缀或后缀。此值支持普通宏,还会将 {{char}} 替换为相关角色的名称,将 <FIELDNAME> 替换为部分的名称(例如:描述、个性、场景等)",
1121 "Inserted after each part of the joined fields.": "插入到加入字段的每个部分之后。",1144 "Inserted after each part of the joined fields.": "插入到合并字段的每个部分之后。",
1122 "Join Suffix": "加入后缀",1145 "Join Suffix": "合并后缀",
1123 "Set group chat character settings overrides": "设置群聊角色设置覆盖",1146 "Set group chat character settings overrides": "设置群聊角色设置覆盖",
1124 "Click to allow/forbid the use of external media for this group.": "单击以允许/禁止该组使用外部媒体。",1147 "Click to allow/forbid the use of external media for this group.": "单击以允许/禁止该群聊使用外部媒体。",
1125 "Restore collage avatar": "恢复拼贴头像",1148 "Restore collage avatar": "恢复拼贴头像",
1126 "Allow self responses": "允许自我回复",1149 "Allow self responses": "允许自我回复",
1127 "Auto Mode": "自动模式",1150 "Auto Mode": "自动模式",
1128 "Auto Mode delay": "自动模式延迟",1151 "Auto Mode delay": "自动模式延迟",
1129 "Hide Muted Member Sprites": "隐藏拼贴头像中被禁言的成员",1152 "Hide Muted Member Sprites": "隐藏被禁言成员的立绘",
1130 "Current Members": "当前成员",1153 "Current Members": "当前成员",
1131 "Group is empty.": "暂无成员",1154 "Group is empty.": "群聊暂无成员。",
1132 "Add Members": "添加成员",1155 "Add Members": "添加成员",
1133 "No characters available": "无可用角色",1156 "No characters available": "无可用角色",
1134 "Create New Character": "新建角色",1157 "Create New Character": "新建角色",
1135 "Import Character from File": "从文件导入角色",1158 "Import Character from File": "从文件导入角色",
1136 "Import content from external URL": "从外部URL导入内容",1159 "Import content from external URL": "从外部 URL 导入内容",
1137 "Create New Chat Group": "创建新的群聊",1160 "Create New Chat Group": "创建新群聊",
1138 "Characters sorting order": "角色排序顺序",1161 "Characters sorting order": "角色排序顺序",
1139 "A-Z": "A-Z",
1140 "Z-A": "Z-A",
1141 "Newest": "最新",
1142 "Oldest": "最旧",
1143 "Favorites": "收藏夹",1162 "Favorites": "收藏夹",
1144 "Recent": "最近",1163 "Recent": "最近",
1145 "Most chats": "最多聊天",1164 "Most chats": "最多聊天",
1146 "Least chats": "最少聊天",1165 "Least chats": "最少聊天",
1147 "Most tokens": "最多词符",1166 "Most tokens": "最多 Token",
1148 "Least tokens": "最少词符",1167 "Least tokens": "最少 Token",
1149 "Random": "随机",1168 "Random": "随机",
1150 "Toggle search bar": "切换搜索栏",1169 "Toggle search bar": "切换搜索栏",
1151 "Toggle character grid view": "切换角色网格视图",1170 "Toggle character grid view": "切换角色网格视图",
@@ -1163,15 +1182,15 @@
1163 "Close popup": "关闭弹出窗口",1182 "Close popup": "关闭弹出窗口",
1164 "Advanced Definitions": "高级定义",1183 "Advanced Definitions": "高级定义",
1165 "Prompt Overrides": "提示词覆盖",1184 "Prompt Overrides": "提示词覆盖",
1166 "(For Chat Completion and Instruct Mode)": "(用于聊天补全和指导模式)",1185 "(For Chat Completion and Instruct Mode)": "(用于聊天补全和格式指引模式)",
1167 "Insert {{original}} into either box to include the respective default prompt from system settings.": "将{{original}}插入到任一框中,以包含系统设置中的相应默认提示词。",1186 "Insert {{original}} into either box to include the respective default prompt from system settings.": "将 {{original}} 插入到任一框中,以包含系统设置中的相应默认提示词。",
1168 "Main Prompt": "主要提示词",1187 "Main Prompt": "主要提示词",
1169 "Any contents here will replace the default Main Prompt used for this character. (v2 spec: system_prompt)": "此处的任何内容都将替换用于此角色的默认主提示词。(v2规范:system_prompt)",1188 "Any contents here will replace the default Main Prompt used for this character. (v2 spec: system_prompt)": "此处的任何内容都将替换用于此角色的默认主提示词。(v2 规范:system_prompt)",
1170 "Any contents here will replace the default Post-History Instructions used for this character. (v2 spec: post_history_instructions)": "此处的任何内容都将替换此角色使用的默认后历史说明。\n(v2 规范:post_history_instructions)",1189 "Any contents here will replace the default Post-History Instructions used for this character. (v2 spec: post_history_instructions)": "此处的任何内容都将替换此角色使用的默认历史后置指令。\n(v2 规范:post_history_instructions)",
1171 "Creator's Metadata (Not sent with the AI prompt)": "创作者的元数据(不与AI提示词一起发送)",1190 "Creator's Metadata (Not sent with the AI prompt)": "创作者的元数据(不与AI提示词一起发送)",
1172 "Creator's Metadata": "创作者的元数据",1191 "Creator's Metadata": "创作者的元数据",
1173 "(Not sent with the AI Prompt)": "(不随 AI 提示词发送)",1192 "(Not sent with the AI Prompt)": "(不随 AI 提示词发送)",
1174 "Everything here is optional": "这里的一切都是可选的",1193 "Everything here is optional": "这里的一切都不是必填的",
1175 "(Botmaker's name / Contact Info)": "(角色制作者的姓名/联系信息)",1194 "(Botmaker's name / Contact Info)": "(角色制作者的姓名/联系信息)",
1176 "(If you want to track character versions)": "(如果您想跟踪角色版本)",1195 "(If you want to track character versions)": "(如果您想跟踪角色版本)",
1177 "(Describe the bot, give use tips, or list the chat models it has been tested on. This will be displayed in the character list.)": "(描述角色,提供使用技巧,或列出已经测试过的聊天模型。这将显示在角色列表中。)",1196 "(Describe the bot, give use tips, or list the chat models it has been tested on. This will be displayed in the character list.)": "(描述角色,提供使用技巧,或列出已经测试过的聊天模型。这将显示在角色列表中。)",
@@ -1182,9 +1201,9 @@
1182 "Scenario": "情景",1201 "Scenario": "情景",
1183 "(Circumstances and context of the interaction)": "(交互的情况和背景)",1202 "(Circumstances and context of the interaction)": "(交互的情况和背景)",
1184 "Character's Note": "角色备注",1203 "Character's Note": "角色备注",
1185 "(Text to be inserted in-chat @ designated depth and role)": "(文本将插入聊天中指定的深度和角色)",1204 "(Text to be inserted in-chat @ designated depth and role)": "(文本将作为指定身份被插入到聊天中指定的深度)",
1186 "@ Depth": "@ 深度",1205 "@ Depth": "@ 深度",
1187 "Role": "角色",1206 "Role": "身份",
1188 "Talkativeness": "发言频率",1207 "Talkativeness": "发言频率",
1189 "How often the character speaks in group chats!": "角色在群聊中发言的频率!",1208 "How often the character speaks in group chats!": "角色在群聊中发言的频率!",
1190 "How often the character speaks in": "角色发言的频率",1209 "How often the character speaks in": "角色发言的频率",
@@ -1194,7 +1213,7 @@
1194 "Chatty": "高",1213 "Chatty": "高",
1195 "Examples of dialogue": "对话示例",1214 "Examples of dialogue": "对话示例",
1196 "Important to set the character's writing style.": "设置角色的写作风格,很重要!",1215 "Important to set the character's writing style.": "设置角色的写作风格,很重要!",
1197 "(Examples of chat dialog. Begin each example with START on a new line.)": "(聊天对话的示例,每个示例都另起一行以<START>开头。)",1216 "(Examples of chat dialog. Begin each example with START on a new line.)": "(聊天对话的示例,每个示例都另起一行并以 <START> 开头。)",
1198 "Save": "保存",1217 "Save": "保存",
1199 "Chat History": "聊天记录",1218 "Chat History": "聊天记录",
1200 "Import Chat": "导入聊天",1219 "Import Chat": "导入聊天",
@@ -1204,15 +1223,15 @@
1204 "Rename Background": "重命名背景",1223 "Rename Background": "重命名背景",
1205 "Delete Background": "删除背景",1224 "Delete Background": "删除背景",
1206 "Select a World Info file for": "选择一个世界书文件给",1225 "Select a World Info file for": "选择一个世界书文件给",
1207 "Primary Lorebook": "主要知识书",1226 "Primary Lorebook": "主要世界书",
1208 "A selected World Info will be bound to this character as its own Lorebook.": "所选的世界信息将会于该角色绑定,作为该角色自己的知识书",1227 "A selected World Info will be bound to this character as its own Lorebook.": "所选的世界书将会与该角色绑定,作为该角色自己的世界书",
1209 "When generating an AI reply, it will be combined with the entries from a global World Info selector.": "在生成AI回复时,它将与全局世界信息选择器中的条目相结合。",1228 "When generating an AI reply, it will be combined with the entries from a global World Info selector.": "在生成AI回复时,它将与全局世界书选择器中的条目相结合。",
1210 "Exporting a character would also export the selected Lorebook file embedded in the JSON data.": "导出角色还将导出嵌入在JSON数据中的所选知识书文件。",1229 "Exporting a character would also export the selected Lorebook file embedded in the JSON data.": "导出角色还将导出嵌入在 JSON 数据中的所选世界书文件。",
1211 "Additional Lorebooks": "附加知识书",1230 "Additional Lorebooks": "附加世界书",
1212 "Associate one or more auxillary Lorebooks with this character.": "将一个或多个辅助知识书与此角色关联。",1231 "Associate one or more auxillary Lorebooks with this character.": "将一个或多个辅助世界书与此角色关联。",
1213 "NOTE: These choices are optional and won't be preserved on character export!": "注意:这些选项是可选的,并且不会在角色导出时被一并导出!",1232 "NOTE: These choices are optional and won't be preserved on character export!": "注意:这些选项是可选的,并且不会在角色导出时被一并导出!",
1214 "Rename chat file": "重命名聊天文件",1233 "Rename chat file": "重命名聊天文件",
1215 "Export JSONL chat file": "导出 .JSONL 聊天文件",1234 "Export JSONL chat file": "导出 JSONL 聊天文件",
1216 "Download chat as plain text document": "将聊天下载为纯文本文档",1235 "Download chat as plain text document": "将聊天下载为纯文本文档",
1217 "Delete chat file": "删除聊天文件",1236 "Delete chat file": "删除聊天文件",
1218 "Drag to reorder tag": "拖动以排序",1237 "Drag to reorder tag": "拖动以排序",
@@ -1233,7 +1252,7 @@
1233 "Optional Filter": "可选过滤器",1252 "Optional Filter": "可选过滤器",
1234 "Keywords or Regexes (ignored if empty)": "关键字或正则表达式(如果为空则忽略)",1253 "Keywords or Regexes (ignored if empty)": "关键字或正则表达式(如果为空则忽略)",
1235 "Comma separated list (ignored if empty)": "逗号分隔列表(如果为空则忽略)",1254 "Comma separated list (ignored if empty)": "逗号分隔列表(如果为空则忽略)",
1236 "wi_outlet_name": "为此世界信息条目设置锚点名称。\n\n位置为“锚点”的世界信息条目不会自动添加到提示词中。相反,它们将被收集并可作为提示词中的宏使用。\n在提示词中任何想要添加此特定锚点的所有世界信息条目的位置添加 {{outlet::YourName}}。",1255 "wi_outlet_name": "为此世界书条目设置锚点名称。\n\n位置为“锚点”的世界书条目不会自动添加到提示词中。相反,它们将被收集并可作为提示词中的宏使用。\n在提示词中任何想要添加此特定锚点的所有世界书条目的位置添加 {{outlet::YourName}}。",
1237 "Outlet Name": "锚点名称",1256 "Outlet Name": "锚点名称",
1238 "Use global setting": "使用全局设置",1257 "Use global setting": "使用全局设置",
1239 "Case-Sensitive": "区分大小写",1258 "Case-Sensitive": "区分大小写",
@@ -1243,8 +1262,8 @@
1243 "Whole Words": "完整单词",1262 "Whole Words": "完整单词",
1244 "Group Scoring": "组评分",1263 "Group Scoring": "组评分",
1245 "Can be used to automatically activate Quick Replies": "可用于自动激活快速回复",1264 "Can be used to automatically activate Quick Replies": "可用于自动激活快速回复",
1246 "Automation ID": "自动化ID",1265 "Automation ID": "自动化 ID",
1247 "( None )": "(没有任何)",1266 "( None )": "(无)",
1248 "delay_until_recursion_level": "定义递归扫描的延迟级别。\n最初,只有第一级(最小数字)会匹配。\n一旦未找到匹配项,下一个级别将变为匹配的候选项。\n这将重复,直到检查所有级别。\n与“延迟到递归”设置相关联。",1267 "delay_until_recursion_level": "定义递归扫描的延迟级别。\n最初,只有第一级(最小数字)会匹配。\n一旦未找到匹配项,下一个级别将变为匹配的候选项。\n这将重复,直到检查所有级别。\n与“延迟到递归”设置相关联。",
1249 "Recursion Level": "递归等级",1268 "Recursion Level": "递归等级",
1250 "Content": "内容",1269 "Content": "内容",
@@ -1256,9 +1275,9 @@
1256 "Delay until recursion": "延迟到递归",1275 "Delay until recursion": "延迟到递归",
1257 "This entry will be included ignoring budget constraints, assuming all other checks pass.": "只要其他检查通过,此条目将无视回复限额,直接加入提示词中",1276 "This entry will be included ignoring budget constraints, assuming all other checks pass.": "只要其他检查通过,此条目将无视回复限额,直接加入提示词中",
1258 "Ignore budget": "无视回复限额",1277 "Ignore budget": "无视回复限额",
1259 "What this keyword should mean to the AI, sent verbatim": "这个关键词对AI的含义,逐字发送",1278 "What this keyword should mean to the AI, sent verbatim": "这个关键词对 AI 的含义,逐字发送",
1260 "Inclusion Group": "包含组",1279 "Inclusion Group": "包含组",
1261 "Inclusion Groups ensure only one entry from a group is activated at a time, if multiple are triggered.Documentation: World Info - Inclusion Group": "包含组可确保每次仅激活组中的一项(如果触发了多项)。支持多个逗号分隔的组。文档:世界信息 - 包含组",1280 "Inclusion Groups ensure only one entry from a group is activated at a time, if multiple are triggered.Documentation: World Info - Inclusion Group": "包含组可确保每次仅激活组中的一项(如果触发了多项)。支持多个逗号分隔的组。文档:世界书 - 包含组",
1262 "Prioritize this entry: When checked, this entry is prioritized out of all selections.If multiple are prioritized, the one with the highest 'Order' is chosen.": "优先考虑此条目:选中后,此条目在所有选择中优先考虑。如果优先考虑多个条目,则选择“顺序”最高的条目。",1281 "Prioritize this entry: When checked, this entry is prioritized out of all selections.If multiple are prioritized, the one with the highest 'Order' is chosen.": "优先考虑此条目:选中后,此条目在所有选择中优先考虑。如果优先考虑多个条目,则选择“顺序”最高的条目。",
1263 "Prioritize": "确定优先级",1282 "Prioritize": "确定优先级",
1264 "Only one entry with the same label will be activated": "只有一个带有相同标签的条目将被激活",1283 "Only one entry with the same label will be activated": "只有一个带有相同标签的条目将被激活",
@@ -1278,9 +1297,9 @@
1278 "Exclude": "排除",1297 "Exclude": "排除",
1279 "-- Characters not found --": "-- 未找到角色 --",1298 "-- Characters not found --": "-- 未找到角色 --",
1280 "Filter to Generation Triggers": "筛选生成触发器",1299 "Filter to Generation Triggers": "筛选生成触发器",
1281 "Continue": "继续",1300 "Continue": "续写",
1282 "Impersonate": "AI 帮答",1301 "Impersonate": "AI 帮答",
1283 "Swipe": "滑动",1302 "Swipe": "备选回复",
1284 "Regenerate": "重新生成",1303 "Regenerate": "重新生成",
1285 "Quiet": "静默",1304 "Quiet": "静默",
1286 "Selective": "选择性",1305 "Selective": "选择性",
@@ -1294,43 +1313,43 @@
1294 "WI_Entry_Status_Constant": "永久",1313 "WI_Entry_Status_Constant": "永久",
1295 "WI_Entry_Status_Normal": "关键词",1314 "WI_Entry_Status_Normal": "关键词",
1296 "WI_Entry_Status_Vectorized": "向量化",1315 "WI_Entry_Status_Vectorized": "向量化",
1297 "T_Position": "↑Char:在角色定义之前\n↓Char:在角色定义之后\n↑AN:在作者注释之前\n↓AN:在作者注释之后\n@D:在深度D处",1316 "T_Position": "↑Char:在角色定义之前\n↓Char:在角色定义之后\n↑AN:在作者注释之前\n↓AN:在作者注释之后\n@D:在指定深度D",
1298 "Before Char Defs": "角色定义之前",1317 "Before Char Defs": "角色定义前(↑Char)",
1299 "After Char Defs": "角色定义之后",1318 "After Char Defs": "角色定义后(↓Char)",
1300 "Before EM": "示例消息前(↑EM)",1319 "Before EM": "示例消息前(↑EM)",
1301 "After EM": "示例消息后(↓EM)",1320 "After EM": "示例消息后(↓EM)",
1302 "Before AN": "作者注释之前",1321 "Before AN": "作者注释前(↑AN)",
1303 "After AN": "作者注释之后",1322 "After AN": "作者注释后(↓AN)",
1304 "at Depth System": "@D ⚙ [系统]在深度​​️",1323 "at Depth System": "[系统⚙] 插入深度 @D",
1305 "at Depth User": "@D 👤 [用户]在深度",1324 "at Depth User": "[用户👤] 插入深度 @D",
1306 "at Depth AI": "@D 🤖 [AI]在深度",1325 "at Depth AI": "[AI🤖] 插入深度 @D",
1307 "Outlet": "➡️ 锚点",1326 "Outlet": "➡️ 锚点",
1308 "Depth": "深度",1327 "Depth": "深度",
1309 "Order:": "顺序:",1328 "Order:": "顺序:",
1310 "Order": "顺序",1329 "Order": "顺序",
1311 "Trigger %:": "触发 %:",1330 "Trigger %:": "触发 %:",
1312 "Move/Copy Entry to Another Lorebook": "移动或复制条目到其他世界书",1331 "Move/Copy Entry to Another Lorebook": "移动或复制条目到其他世界书",
1313 "Duplicate world info entry": "重复的世界信息条目",1332 "Duplicate world info entry": "重复的世界书条目",
1314 "Delete world info entry": "删除世界信息条目",1333 "Delete world info entry": "删除世界书条目",
1315 "This character will be used as a welcome page assistant.": "此角色将作为欢迎页面助理。",1334 "This character will be used as a welcome page assistant.": "此角色将作为欢迎页面助理。",
1316 "Text or token ids": "文本或 [token ID]",1335 "Text or token ids": "文本或 Token ID",
1317 "Type here...": "在此处输入...",1336 "Type here...": "在此处输入...",
1318 "close": "关闭",1337 "close": "关闭",
1319 "prompt_manager_edit": "编辑",1338 "prompt_manager_edit": "编辑",
1320 "prompt_manager_name": "姓名",1339 "prompt_manager_name": "姓名",
1321 "A name for this prompt.": "此提示词的名称。",1340 "A name for this prompt.": "此提示词的名称。",
1322 "AI Assistant": "AI助手",1341 "AI Assistant": "AI 助手",
1323 "To whom this message will be attributed.": "此消息应归于谁。",1342 "To whom this message will be attributed.": "此消息应归于谁。",
1324 "Triggers": "触发器",1343 "Triggers": "触发器",
1325 "Filter to specific generation types.": "筛选到特定的生成类型。",1344 "Filter to specific generation types.": "筛选到特定的生成类型。",
1326 "prompt_manager_position": "位置",1345 "prompt_manager_position": "位置",
1327 "prompt_manager_relative": "相对",1346 "prompt_manager_relative": "相对",
1328 "prompt_manager_in_chat": "聊天中",1347 "prompt_manager_in_chat": "聊天中",
1329 "Relative (to other prompts in prompt manager) or In-chat @ Depth.": "相对(相对于提示管理器中的其他提示)或在聊天中@深度。",1348 "Relative (to other prompts in prompt manager) or In-chat @ Depth.": "相对(相对于提示词管理器中的其他提示词)或在聊天中的指定深度。",
1330 "prompt_manager_depth": "深度",1349 "prompt_manager_depth": "深度",
1331 "0 = after the last message, 1 = before the last message, etc.": "“0”为在最后一条消息之后,“1”为在最后一条消息之前,等等。",1350 "0 = after the last message, 1 = before the last message, etc.": "“0”为在最后一条消息之后,“1”为在最后一条消息之前,等等。",
1332 "prompt_manager_order": "排序",1351 "prompt_manager_order": "排序",
1333 "prompt_manager_order_note": "来自其他来源(世界信息、作者注释等)的提示注入的默认顺序为 100。",1352 "prompt_manager_order_note": "来自其他来源(世界书、作者注释等)的提示词注入的默认顺序为 100。",
1334 "Ordered from low/top to high/bottom, and at same order: Assistant, User, System.": "从低/顶到高/底排序,并按相同顺序:助手、用户、系统。",1353 "Ordered from low/top to high/bottom, and at same order: Assistant, User, System.": "从低/顶到高/底排序,并按相同顺序:助手、用户、系统。",
1335 "The content of this prompt is pulled from elsewhere and cannot be edited here.": "此提示词的内容是从其他地方提取的,无法在此处进行编辑。",1354 "The content of this prompt is pulled from elsewhere and cannot be edited here.": "此提示词的内容是从其他地方提取的,无法在此处进行编辑。",
1336 "Prompt": "提示词",1355 "Prompt": "提示词",
@@ -1340,7 +1359,7 @@
1340 "The prompt to be sent.": "要发送的提示词。",1359 "The prompt to be sent.": "要发送的提示词。",
1341 "reset": "重置",1360 "reset": "重置",
1342 "save": "保存",1361 "save": "保存",
1343 "This message is invisible for the AI": "此消息对AI不可见",1362 "This message is invisible for the AI": "此消息对 AI 不可见",
1344 "Message Actions": "消息操作",1363 "Message Actions": "消息操作",
1345 "Translate message": "翻译消息",1364 "Translate message": "翻译消息",
1346 "Generate Image": "生成图片",1365 "Generate Image": "生成图片",
@@ -1372,14 +1391,14 @@
1372 "welcome_message_part_1": "阅读",1391 "welcome_message_part_1": "阅读",
1373 "welcome_message_part_2": "官方文档",1392 "welcome_message_part_2": "官方文档",
1374 "welcome_message_part_3": "。",1393 "welcome_message_part_3": "。",
1375 "welcome_message_part_4": "类型",1394 "welcome_message_part_4": "输入",
1376 "welcome_message_part_5": "在聊天中输入命令和宏。",1395 "welcome_message_part_5": "以查看命令和宏的教程。",
1377 "welcome_message_part_6": "加入",1396 "welcome_message_part_6": "加入",
1378 "Discord server": "Discord 服务器",1397 "Discord server": "Discord 服务器",
1379 "welcome_message_part_7": "了解信息和公告。",1398 "welcome_message_part_7": "了解信息和公告。",
1380 "Looking for AI characters?": "正在寻找 AI 角色?",1399 "Looking for AI characters?": "正在寻找 AI 角色?",
1381 "onboarding_import": "导入",1400 "onboarding_import": "导入",
1382 "from supported sources or view": "来自受支持的来源或查看",1401 "from supported sources or view": "支持的资源,或查看",
1383 "Sample characters": "示例角色",1402 "Sample characters": "示例角色",
1384 "Your Persona": "您的用户设定",1403 "Your Persona": "您的用户设定",
1385 "Before you get started, you must select a persona name.": "在开始之前,您必须起一个用户名。",1404 "Before you get started, you must select a persona name.": "在开始之前,您必须起一个用户名。",
@@ -1393,15 +1412,15 @@
1393 "Move down": "向下移动",1412 "Move down": "向下移动",
1394 "View character card": "查看角色卡片",1413 "View character card": "查看角色卡片",
1395 "Remove from group": "踢出群聊",1414 "Remove from group": "踢出群聊",
1396 "Add to group": "拉入群聊",1415 "Add to group": "邀请入群",
1397 "in this group": "在此群组中",1416 "in this group": "在此群中",
1398 "Go back": "返回",1417 "Go back": "返回",
1399 "Alternate Greetings": "额外问候语",1418 "Alternate Greetings": "其他开场白",
1400 "Alternate_Greetings_desc": "开始新聊天时,这些按钮将显示为第一条消息的滑动选项。\n群成员可以选择其中之一来发起对话。",1419 "Alternate_Greetings_desc": "开始新聊天时,这些按钮将显示为开场白消息的额外页面。\n群成员可以选择其中之一来发起对话。",
1401 "alternate_greetings_hint_1": "点击",1420 "alternate_greetings_hint_1": "点击",
1402 "alternate_greetings_hint_2": "按钮即可开始!",1421 "alternate_greetings_hint_2": "按钮即可开始!",
1403 "Alternate Greeting #": "额外问候语 #",1422 "Alternate Greeting #": "其他开场白 #",
1404 "(This will be the first message from the character that starts every chat)": "(这是每次聊天开始时角色的第一条消息)",1423 "(This will be the first message from the character that starts every chat)": "(这是每次聊天开始时角色发送的第一条消息)",
1405 "View contents": "查看内容",1424 "View contents": "查看内容",
1406 "Remove the file": "删除文件",1425 "Remove the file": "删除文件",
1407 "Expand and zoom": "展开并缩放",1426 "Expand and zoom": "展开并缩放",
@@ -1412,29 +1431,29 @@
1412 "Mute": "静音",1431 "Mute": "静音",
1413 "Author's Note": "作者注释",1432 "Author's Note": "作者注释",
1414 "Unique to this chat": "仅对此聊天生效",1433 "Unique to this chat": "仅对此聊天生效",
1415 "Checkpoints inherit the Note from their parent, and can be changed individually after that.": "检查点从其父级继承注释,之后可以单独更改。",1434 "Checkpoints inherit the Note from their parent, and can be changed individually after that.": "检查点从其父级继承作者注释,之后可以单独更改。",
1416 "Include in World Info Scanning": "纳入世界信息扫描",1435 "Include in World Info Scanning": "纳入世界书扫描",
1417 "Before Main Prompt / Story String": "主提示词/故事线之前",1436 "Before Main Prompt / Story String": "主提示词/故事线之前",
1418 "After Main Prompt / Story String": "主提示词/故事线之后",1437 "After Main Prompt / Story String": "主提示词/故事线之后",
1419 "as": "作为",1438 "as": "作为",
1420 "Insertion Frequency": "插入频率",1439 "Insertion Frequency": "插入频率",
1421 "(0 = Disable, 1 = Always)": "(“0”为禁用,“1”为始终)",1440 "(0 = Disable, 1 = Always)": "(“0”为禁用,“1”为始终)",
1422 "User inputs until next insertion:": "用户输入直到下一次插入:",1441 "User inputs until next insertion:": "用户输入直到下一次插入:",
1423 "Character Author's Note (Private)": "人物作者注(私密)",1442 "Character Author's Note (Private)": "角色作者注释(私密)",
1424 "Won't be shared with the character card on export.": "导出时不会与角色卡共享。",1443 "Won't be shared with the character card on export.": "导出时不会与角色卡共享。",
1425 "Will be automatically added as the author's note for this character. Will be used in groups, but can't be modified when a group chat is open.": "会自动添加为该角色的作者注解,在群聊中使用,但群聊开启时无法修改。",1444 "Will be automatically added as the author's note for this character. Will be used in groups, but can't be modified when a group chat is open.": "会自动添加为该角色的作者注释,在群聊中使用,但群聊开启时无法修改。",
1426 "Use character author's note": "使用角色作者的注释",1445 "Use character author's note": "使用角色作者的注释",
1427 "Replace Author's Note": "替换作者注",1446 "Replace Author's Note": "替换作者注释",
1428 "Default Author's Note": "默认作者注",1447 "Default Author's Note": "默认作者注释",
1429 "Will be automatically added as the Author's Note for all new chats.": "将自动添加为所有新聊天的作者注释。",1448 "Will be automatically added as the Author's Note for all new chats.": "将自动添加为所有新聊天的作者注释。",
1430 "Chat CFG": "本聊天的CFG缩放",1449 "Chat CFG": "本聊天的 CFG 缩放",
1431 "1 = disabled": "“1”为禁用",1450 "1 = disabled": "“1”为禁用",
1432 "write short replies, write replies using past tense": "写简短的回复,用过去时写回复",1451 "write short replies, write replies using past tense": "写简短的回复,用过去时写回复",
1433 "Positive Prompt": "正面提示词",1452 "Positive Prompt": "正面提示词",
1434 "Use character CFG scales": "单独为各个角色设置CFG缩放",1453 "Use character CFG scales": "单独为各个角色设置 CFG 缩放",
1435 "Character CFG": "角色CFG配置",1454 "Character CFG": "角色 CFG 配置",
1436 "Will be automatically added as the CFG for this character.": "将自动添加到该角色的CFG设置中。",1455 "Will be automatically added as the CFG for this character.": "将自动添加到该角色的 CFG 设置中。",
1437 "Global CFG": "全局CFG",1456 "Global CFG": "全局 CFG",
1438 "Will be used as the default CFG options for every chat unless overridden.": "除非被覆盖,否则将用作每次聊天的默认 CFG 选项。",1457 "Will be used as the default CFG options for every chat unless overridden.": "除非被覆盖,否则将用作每次聊天的默认 CFG 选项。",
1439 "CFG Prompt Cascading": "CFG 提示词级联",1458 "CFG Prompt Cascading": "CFG 提示词级联",
1440 "Combine positive/negative prompts from other boxes.": "结合来自其他框的正面/负面提示词。",1459 "Combine positive/negative prompts from other boxes.": "结合来自其他框的正面/负面提示词。",
@@ -1445,27 +1464,308 @@
1445 "Global Negatives": "全局负面信息",1464 "Global Negatives": "全局负面信息",
1446 "Custom Separator:": "自定义分隔符:",1465 "Custom Separator:": "自定义分隔符:",
1447 "Insertion Depth:": "插入深度:",1466 "Insertion Depth:": "插入深度:",
1448 "Token Probabilities": "词符概率",1467 "Token Probabilities": "Token 概率",
1449 "Select a token to see alternatives considered by the AI.": "选择一个词符来查看 AI 考虑的替代方案。",1468 "Select a token to see alternatives considered by the AI.": "选择一个 Token 来查看 AI 考虑的替代方案。",
1450 "Reroll with the entire prefix": "使用完整前缀重新生成",1469 "Reroll with the entire prefix": "使用完整前缀重新生成",
1451 "Not connected to API!": "未连接到API!",1470 "Not connected to API!": "未连接到 API!",
1452 "Type a message, or /? for help": "输入想发送的消息,或输入 /? 获取帮助",1471 "Type a message, or /? for help": "输入想发送的消息,或输入 /? 获取帮助",
1453 "Continue script execution": "继续执行脚本",1472 "Continue script execution": "继续执行脚本",
1454 "Pause script execution": "暂停执行脚本",1473 "Pause script execution": "暂停执行脚本",
1455 "Abort script execution": "中止执行脚本",1474 "Abort script execution": "中止执行脚本",
1456 "Abort request": "中止请求",1475 "Abort request": "中止请求",
1457 "Ask AI to write your message for you": "让AI为您撰写消息",1476 "Ask AI to write your message for you": "让 AI 为您撰写消息",
1458 "Continue the last message": "继续上一条消息",1477 "Continue the last message": "续写上一条消息",
1459 "Send a message": "发送消息",1478 "Send a message": "发送消息",
1460 "Close chat": "关闭聊天",1479 "Close chat": "关闭聊天",
1461 "Toggle Panels": "切换面板",1480 "Toggle Panels": "切换面板",
1462 "CFG Scale": "CFG缩放",1481 "CFG Scale": "CFG 缩放",
1463 "Back to parent chat": "返回到父级聊天",1482 "Back to parent chat": "返回到父级聊天",
1464 "Save checkpoint": "保存检查点",1483 "Save checkpoint": "保存检查点",
1465 "Convert to group": "转换为群聊",1484 "Convert to group": "转换为群聊",
1466 "Start new chat": "开始新聊天",1485 "Start new chat": "开始新聊天",
1467 "Manage chat files": "管理聊天文件",1486 "Manage chat files": "管理聊天文件",
1468 "Delete messages": "删除消息",1487 "Delete messages": "删除消息",
1488 "Tap to close": "点击关闭",
1489 "Couldn't get CSRF token. Please refresh the page.": "无法获取 CSRF 令牌。请刷新页面。",
1490 "Error": "错误",
1491 "Please wait until the chat is saved before switching characters.": "请在切换角色前等待聊天保存完成。",
1492 "Your chat is still saving...": "您的聊天仍在保存中...",
1493 "Here be dragons": "拔剑四顾心茫然",
1494 "Otterly empty": "风好大,把内容都刮跑了",
1495 "Kiwibunga": "你的数据转生去了异世界",
1496 "Pump-a-Rum": "这里是你小时候的噩梦:404",
1497 "Croak it": "这里除了一只会后空翻的猫,什么都没有。",
1498 "${0} characters hidden.": "${0} 个角色已被隐藏。",
1499 "${0} character hidden.": "${0} 个角色已被隐藏。",
1500 "Character ${0} not found in the list": "列表中不存在角色 ${0}",
1501 "ERROR: The active character is no longer available.": "错误:当前角色已不再可用。",
1502 "The page will be refreshed to prevent data loss. Press \"OK\" to continue.": "页面将刷新以防止数据丢失。点击“确定”继续。",
1503 "Character data length limit reached": "角色数据长度已达上限",
1504 "To resolve this, set \"performance.lazyLoadCharacters\" to \"true\" in config.yaml and restart the server.": "要解决此问题,请在 config.yaml 中将 \"performance.lazyLoadCharacters\" 设置为 \"true\",然后重启服务器。",
1505 "Are you sure you want to delete this message?": "确定删除这条消息吗?",
1506 "Delete Swipe": "删除备选回复",
1507 "Delete Message": "删除消息",
1508 "Confirm the edit to start a generation.": "确认编辑以开始生成。",
1509 "You cannot send a message during a swipe-edit.": "在编辑备选回复期间无法发送消息。",
1510 "Copied!": "已复制~",
1511 "Streaming is enabled, but the version of Kobold used does not support token streaming.": "流式传输已启用,但你使用的 Kobold 版本不支持 Token 流式传输。",
1512 "Verify that the server is running and accessible.": "检查服务器正在运行且可访问。",
1513 "ST Server cannot be reached": "无法访问 ST 服务器",
1514 "API Error": "API 错误",
1515 "Text generation error": "文本生成错误",
1516 "You must first select a character to duplicate!": "没有选择要复制的角色。",
1517 "Character Duplicated": "角色已复制",
1518 "No character name provided.": "未提供角色名称。",
1519 "Rename Character": "重命名角色",
1520 "No character selected.": "未选择角色。",
1521 "New name:": "新名称:",
1522 "Same character name provided, so name did not change.": "角色名称相同,因此名称未更改。",
1523 "Character renamed!": "角色已重命名!",
1524 "Past chats will still contain the old character name. Would you like to update the character name in previous chats as well?": "历史聊天记录内旧的角色名不会自动更新。您是否也想更新之前聊天记录中的角色名?",
1525 "Sprites folder (if any) should be renamed manually.": "立绘文件夹(若有)需要手动重命名。",
1526 "Character renamed and past chats updated!": "角色已重命名并更新了历史聊天记录!",
1527 "Something went wrong. The page will be reloaded.": "代码打结了。页面将被重新加载。",
1528 "Past chat could not be updated: ${0}": "历史聊天无法被更新:${0}",
1529 "Operation was aborted to prevent data corruption.": "操作被中断,以防止数据损坏。",
1530 "saveChat called for a group chat": "不能用 saveChat 保存群聊",
1531 "ERROR: Chat integrity check failed while saving the file.": "错误:聊天完整性检查在保存文件时失败。",
1532 "<p>After you click OK, the page will be reloaded to prevent data corruption.</p>\n <p>To confirm an overwrite (and potentially <b>LOSE YOUR DATA</b>), enter <code>OVERWRITE</code> (in all caps) in the box below before clicking OK.</p>": "<p>点击确定后,页面将被重新加载以防止数据损坏。</p>\n <p>如要确认覆盖(有可能<b>丢失您的数据</b>),请在点击确定之前,在下面的框中输入<code>OVERWRITE</code>(全部大写)。</p>",
1533 "Check the server connection and reload the page to prevent data loss.": "检查服务器连接并刷新页面以防止数据丢失。",
1534 "Chat could not be saved": "聊天无法保存",
1535 "Your messages will now be sent as ${0}": "你的消息现在将以 ${0} 发送",
1536 "Persona Changed": "用户设定已更改",
1537 "Settings could not be loaded after multiple attempts. Please try again later.": "设置无法加载。请稍后再试。",
1538 "Settings could not be saved": "设置无法保存",
1539 "Invalid process (no 'type')": "无效进程(没有 'type')",
1540 "Character Deleted: ${0}": "角色已删除:${0}",
1541 "Character Created: ${0}": "角色已创建:${0}",
1542 "Group Created": "群聊已创建",
1543 "Group Deleted": "群聊已删除",
1544 "Character Imported: ${0}": "角色已导入:${0}",
1545 "Are you sure you want to remove all overrides?": "确定要移除所有覆盖设置吗?",
1546 "This action cannot be undone.": "此操作无法被撤销。",
1547 "Ok": "确定",
1548 "Invalid swipe ID: ${0}": "无效的备选回复 ID: ${0}",
1549 "No messages to delete swipes from.": "没有可删除备选回复的消息。",
1550 "Can't delete the last swipe.": "无法删除最后一个备选回复。",
1551 "No auxiliary Lorebooks set. Click here to select.": "没有设置辅助世界书。点击这里选择。",
1552 "Are you sure you want to delete this alternate greeting?": "确定要删除这个备用问候语吗?",
1553 "Name is required": "名称必填",
1554 "Cannot create characters while generating. Stop the request and try again.": "无法在生成时创建角色。请停止请求并重试。",
1555 "Creation aborted": "创建已中止",
1556 "Failed to create character": "创建角色失败",
1557 "Something went wrong while saving the character, or the image file provided was in an invalid format. Double check that the image is not a webp.": "保存角色时出错,或提供的图像文件格式无效。图像不能是 webp 格式。",
1558 "Cannot swipe while generating. Stop the request and try again.": "无法在生成消息时生成备选回复。请停止请求并重试。",
1559 "Swipe aborted": "备选生成已中止",
1560 "ERROR: <code>syncSwipeToMes</code> has failed to revert the failed ${0} swipe on message #${1}.": "错误:<code>syncSwipeToMes</code> 未能恢复消息 #${1} 上失败的 ${0} 备选。",
1561 "<p>After you click OK, the chat will be reloaded to prevent data corruption.</p>": "<p>点击确定后,聊天将被重新加载以防止数据损坏。</p>",
1562 "When swiping ${0} on message ${1}, syncSwipeToMes has returned false. Attempting to swipe back!": "在消息 ${1} 上生成备选 ${0} 时,syncSwipeToMes 返回了 false。正在尝试恢复!",
1563 "Unsupported file type: ": "不支持的文件类型:",
1564 "Cannot import characters while generating. Stop the request and try again.": "无法在生成时导入角色。停止请求并重试。",
1565 "Import aborted": "导入已中止",
1566 "Character Replaced: ${0}": "角色已替换:${0}",
1567 "The file is likely invalid or corrupted.": "文件可能无效或已损坏。",
1568 "Could not import character": "无法导入角色",
1569 "Name not accepted, as it is the same as before (ignoring case and accents).": "名称不可用,因为它与之前相同(忽略大小写和变音符号)。",
1570 "Rename Chat": "重命名聊天",
1571 "Please stop the message generation first.": "请先停止消息生成。",
1572 "You are currently in a temporary chat.": "您当前处于临时聊天中。",
1573 "Deleting this character will close the chat and you will lose any unsaved messages. Do you want to proceed?": "删除此角色将关闭聊天并丢失所有未保存的消息。是否继续?",
1574 "Character ${0} not found. Skipping deletion.": "未找到角色 ${0}。跳过删除。",
1575 "Failed to delete character": "删除角色失败",
1576 "Delete the Chat File?": "删除聊天文件?",
1577 "Delete the character?": "删除角色?",
1578 "Start new chat?": "开始新聊天?",
1579 "Finish the edit before starting a generation.": "编辑完成后再开始生成。",
1580 "You cannot regenerate the message you are editing.": "您无法重新生成正在编辑的消息。",
1581 "You cannot continue the message you are editing.": "您无法续写正在编辑的消息。",
1582 "Create a copy of this message?": "创建此消息的副本?",
1583 "Only JSON and JSONL files are supported for chat imports.": "仅支持导入 JSON 和 JSONL 格式的聊天。",
1584 "Only SillyTavern's own format is supported for group chat imports. Sorry!": "仅支持 SillyTavern 自身格式用于群聊导入。抱歉!",
1585 "Successfully imported ${0} chat(s).": "成功导入 ${0} 个聊天。",
1586 "Replace Character": "替换角色",
1587 "Choose a new character card to replace this character with.": "选择一个新的角色卡来替换这个角色。",
1588 "You can also replace this character with the one from the online source.": "您也可以用在线资源替换此角色。",
1589 "All chats, assets and group memberships will be preserved, but local changes to the character data will be lost.": "所有聊天、资源和群成员身份将被保留,但角色数据的本地更改将丢失。",
1590 "Proceed?": "是否继续?",
1591 "Replace with URL": "从 URL 替换",
1592 "Replace with File": "从文件替换",
1593 "Replace Character from URL": "从 URL 替换角色",
1594 "Enter the URL of the character card to replace this character with.": "输入要替换此角色的角色卡 URL。",
1595 "This character was downloaded from: <var>${0}</var>": "此角色是从以下地址下载的:<var>${0}</var>",
1596 "Author's Note text updated": "作者注释已更新",
1597 "Not a valid number": "不是有效的数字",
1598 "Author's Note depth updated": "作者注释插入深度已更新",
1599 "Author's Note frequency updated": "作者注释插入频率已更新",
1600 "Not a valid position": "位置无效",
1601 "Author's Note position updated": "作者注释插入位置已更新",
1602 "Not a valid role": "不是有效的角色类型",
1603 "Author's Note role updated": "作者注释的角色类型已更新",
1604 "Something went wrong. Could not save character's author's note.": "出错了。无法保存角色的作者注释。",
1605 "Select a character before trying to use Author's Note": "选择一个角色后再尝试使用作者注释",
1606 "The contents of the Author's Note": "作者注释的内容",
1607 "The contents of the Character Author's Note": "角色作者注释的内容",
1608 "The contents of the Default Author's Note": "默认作者注释的内容",
1609 "Select a chat to lock the background for it": "请先选择一个聊天来锁定背景",
1610 "Enter new background name:": "输入新的背景名称:",
1611 "Also delete file from server": "同时从服务器删除文件",
1612 "Delete the background?": "删除背景?",
1613 "Click here to install the Video Background Loader extension": "点击这里安装视频背景加载器扩展",
1614 "Video background uploads require a downloadable add-on": "视频背景上传需要可下载的附加组件",
1615 "Preparing video for upload. This may take several minutes.": "正在准备视频上传。这可能需要几分钟时间。",
1616 "Please wait": "请等待",
1617 "Error converting video to animated webp": "将视频转换为动态 webp 时出错",
1618 "Select a chat to upload a background for it": "请选择一个聊天来上传背景",
1619 "Convert to group chat": "转换为群聊",
1620 "Are you sure you want to convert this chat to a group chat?": "确定要将此聊天转换为群聊吗?",
1621 "This cannot be reverted.": "此操作无法被撤销。",
1622 "The chat has been successfully converted!": "聊天已成功转换为群聊!",
1623 "Remove all tags from the selected characters": "删除所选角色的所有标签",
1624 "Remove all mutual tags from the selected characters": "删除所选角色的所有共同标签",
1625 "Import all tags from selected characters": "从所选角色导入所有标签",
1626 "Import existing tags from selected characters": "从所选角色导入现有标签",
1627 "Failed to download backup, try again later.": "备份下载失败,请稍后再试。",
1628 "Failed to parse backup content.": "无法解析备份内容。",
1629 "Only .jsonl files are supported for chat imports.": "聊天导入只支持 .jsonl 文件。",
1630 "Failed to import chat backup, try again later.": "聊天备份导入失败,请稍后再试。",
1631 "Are you sure?": "你确定吗?",
1632 "Failed to delete backup, try again later.": "备份删除失败,请稍后再试。",
1633 "Backup deleted successfully.": "成功删除备份。",
1634 "View backup": "查看备份",
1635 "Restore backup": "恢复备份",
1636 "Delete backup": "删除备份",
1637 "Backups": "备份",
1638 "Browse chat backups": "查看聊天备份",
1639 "Context preset for ${0} will use defaults when loaded the next time.": "下次加载时,${0} 的上下文预设将会使用默认预设。",
1640 "Note: Context derivation is disabled. Not including context preset.": "提示:上下文获取已关闭,本次不包含上下文预设。",
1641 "Note: Instruct derivation is disabled. Not including instruct preset.": "提示:指引获取已关闭,本次不包含指引预设。",
1642 "No applicable presets available.": "无可用预设。",
1643 "Bound ${0} to ${1}.": "已将 ${0} 绑定至 ${1}。",
1644 "Could not convert file": "无法转换文件",
1645 "Either the file is corrupted or its format is not supported.": "文件被损坏,或文件格式不支持。",
1646 "Could not upload the file": "无法上传文件",
1647 "Could not upload file": "无法上传文件",
1648 "Could not download file": "无法下载文件",
1649 "File is too big. Maximum size is ${0}.": "文件过大。最大大小为:${0}。",
1650 "Binary files are not supported. Select a text file or image.": "不支持二进制文件。请选择文本文件或图片。",
1651 "File ${0} is not supported.": "文件 ${0} 不被支持。",
1652 "${0} files selected": "已选择 ${0} 个文件",
1653 "To change the global styles preference, please select a character individually.": "要更改全局风格设置,请单独选择一个角色。",
1654 "Just to Creator's Notes": "仅应用到创作者注释",
1655 "Apply to the entire app": "应用到整个页面",
1656 "No character or group selected": "未选择角色或群聊",
1657 "Delete media from message?": "删除消息的媒体文件?",
1658 "This action can't be undone.": "此操作无法撤销。",
1659 "Delete one": "删除一个",
1660 "Delete all": "删除所有",
1661 "Also delete files from server": "同时删除服务器上的文件",
1662 "Could not delete image": "无法删除图片",
1663 "Could not delete file": "无法删除文件",
1664 "No attachments selected.": "未选择附件。",
1665 "Data Bank": "数据库",
1666 "No files were scraped.": "无文件被抓取。",
1667 "Scraped ${0} files from ${1} to ${2}.": "${1} 从 ${2} 抓取了 ${0} 个文件。",
1668 "Check browser console for details.": "详见浏览器控制台。",
1669 "Scraping failed": "抓取失败",
1670 "Use the 'Ext. Media' button to allow it. Click on this message to dismiss.": "使用下方的“外部媒体”按钮以允许外部媒体。点击此消息以忽略。",
1671 "External media has been blocked": "外部媒体被禁用",
1672 "It's either corrupted or not a valid JSONL file.": "文件被损坏或不是有效 JSONL 文件。",
1673 "Failed to import chat": "导入聊天失败",
1674 "Files": "文件",
1675 "Files that are not associated with chat messages or Data Bank. WILL DELETE MANUAL UPLOADS!": "未与消息或数据库关联的文件。会删除手动上传项!",
1676 "Images": "图片",
1677 "Images that are not associated with chat messages. WILL DELETE MANUAL UPLOADS!": "未与消息关联的图片。会删除手动上传项!",
1678 "Chats": "聊天",
1679 "Chat files associated with deleted characters.": "已删除角色的聊天文件。",
1680 "Group Chats": "群聊",
1681 "Chat files associated with deleted groups.": "已删除群聊的聊天文件。",
1682 "Avatar Thumbnails": "头像缩略图",
1683 "Thumbnails for avatars of missing or deleted characters.": "丢失或已删除角色的头像缩略图。",
1684 "Background Thumbnails": "背景缩略图",
1685 "Thumbnails for missing or deleted backgrounds.": "丢失或已删除背景的缩略图。",
1686 "Persona Thumbnails": "用户设定缩略图",
1687 "Thumbnails for missing or deleted personas.": "丢失或已删除用户设定的头像缩略图。",
1688 "Chat Backups": "聊天备份",
1689 "Automatically generated chat backups.": "自动生成的聊天备份。",
1690 "Settings Backups": "设置备份",
1691 "Automatically generated settings backups.": "自动生成的设置备份。",
1692 "The scan is already running. Please wait for it to finish.": "扫描正在运行,请等待扫描完成。",
1693 "An error has occurred. Check the console for details.": "发生了错误。详见控制台。",
1694 "No items found to clean up. Come back later!": "无可清理的项目。之后再说吧!",
1695 "This will permanently delete all files in this category. THIS CANNOT BE UNDONE!": "将永久删除此类型的所有文件。此操作无法被撤销!",
1696 "This will permanently delete the file. THIS CANNOT BE UNDONE!": "将永久删除文件。此操作无法被撤销!",
1697 "Extension \"${0}\" failed to load: ${1}": "扩展程序 \"${0}\" 加载失败:${1}",
1698 "Extension \"${0}\" did not load. Missing required Extras module(s): \"${1}\"": "扩展程序 \"${0}\" 未加载。缺失前置模块:\"${1}\"",
1699 "Extension \"${0}\" did not load. Required extensions exist but are disabled: \"${1}\". Enable them first, then reload.": "扩展程序 \"${0}\" 未加载。前置存在但未启用:\"${1}\"。请先启用前置,再重新加载。",
1700 "Extension \"${0}\" did not load. Missing required extensions: \"${1}\"": "扩展程序 \"${0}\" 未加载。缺失前置扩展程序:\"${1}\"",
1701 "Extension \"${0}\" did not load. Requires ST client version ${1}, but current version is ${2}.": "扩展程序 \"${0}\" 未加载。需要 ST 客户端版本 ${1}。当前版本为 ${2}。",
1702 "Connected to API": "已连接至 API",
1703 "Could not connect to API": "无法连接至 API",
1704 "ext_type_global": "这是一个全局扩展程序,所有用户均可用。",
1705 "ext_type_local": "这是一个本地扩展程序,仅你可用。",
1706 "ext_type_system": "这是一个内置扩展程序,无法被删除,会随应用更新而更新。",
1707 "Click to toggle": "点击切换",
1708 "Move": "移动",
1709 "Switch branch": "切换分支",
1710 "Optional modules:": "可选模块:",
1711 "Not connected to the API!": "未连接至 API!",
1712 "Modules provided by your Extras API:": "扩展API 提供的模块",
1713 "Built-in Extensions:": "内置扩展程序:",
1714 "Installed Extensions:": "已安装扩展程序:",
1715 "Loading third-party extensions... Please wait...": "正在加载第三方扩展程序,请等待……",
1716 "Update all": "更新所有",
1717 "Update enabled": "更新已启用项",
1718 "Bulk toggle third-party extensions.": "批量开关第三方扩展程序",
1719 "Toggle extensions": "开关扩展程序",
1720 "Restore toggled extensions.\n\nIt does not restore extensions toggled individually.": "还原批量开关的第三方扩展程序。\n\n不影响单独开关的扩展程序。",
1721 "Sort: Display Name": "显示名称顺序",
1722 "Sort: Loading Order": "加载顺序",
1723 "Close": "关闭",
1724 "Could not toggle extension ${0}. See console for details.": "无法开关扩展程序 ${0}。详见控制台。",
1725 "The page will be reloaded shortly...": "页面即将刷新……",
1726 "Extensions state changed": "扩展程序状态已更改",
1727 "Error loading extensions. See browser console for details.": "加载扩展程序出错。详见控制台。",
1728 "You don't have permission to update global extensions.": "你没有更新全局扩展程序的权限。",
1729 "Extension update failed": "扩展程序更新失败",
1730 "Extension ${0} updated to ${1}": "扩展程序 ${0} 已更新至 ${1}",
1731 "Reload the page to apply updates": "刷新页面以应用更改",
1732 "You don't have permission to delete global extensions.": "你没有删除全局扩展程序的权限。",
1733 "Are you sure you want to delete ${0}?": "你确定要删除 ${0} 吗?",
1734 "You don't have permission to switch branch.": "你没有切换分支的权限。",
1735 "Switch": "切换",
1736 "You don't have permission to move extensions.": "你没有移动扩展程序的权限。",
1737 "Move extension": "移动扩展程序",
1738 "Are you sure you want to move ${0} to your local extensions? This will make it available only for you.": "你确定要将 ${0} 移动至本地扩展程序吗?此后只有你可使用此扩展。",
1739 "Are you sure you want to move ${0} to the global extensions? This will make it available for all users.": "你确定要将 ${0} 移动至全局扩展程序吗?此后所有人都可使用此扩展。",
1740 "Extension move failed": "扩展程序移动失败",
1741 "Extension ${0} moved.": "已移动扩展程序 ${0}。",
1742 "Extension ${0} deleted": "已删除扩展程序 ${0}",
1743 "Extension branches fetch failed": "扩展程序分支获取失败",
1744 "Extension branch switch failed": "扩展程序分支切换失败",
1745 "Extension ${0} switched to ${1}": "扩展程序 ${0} 已切换至 ${1}",
1746 "Please wait...": "请稍候……",
1747 "Installing extension": "安装扩展程序",
1748 "Extension installation failed": "扩展程序安装失败",
1749 "Extension '${0}' by ${1} (version ${2}) has been installed successfully!": "作者 ${1} 的扩展程序 '${0}'(版本 ${2})已成功安装!",
1750 "Extension installation successful": "扩展程序安装成功",
1751 "Extension updates available": "扩展程序有更新可用",
1752 "Auto-updating extensions. This may take several minutes.": "正在自动更新扩展程序。可能需要几分钟。",
1753 "Install just for me": "只给我安装",
1754 "Install": "安装",
1755 "Install for all users": "给所有人安装",
1756 "Branch or tag name (optional)": "分支或标签名(选填)",
1757 "Characters": "人物",
1758 "Ambient sounds": "环境音",
1759 "Background music": "BGM",
1760 "Blip sounds": "哔哔声",
1761 "Extension repo/guide:": "扩展程序仓库/指南:",
1762 "Preview in browser": "在浏览器中预览",
1763 "Adds a function tool": "添加一个函数工具",
1764 "Tool": "工具",
1765 "Click the flashing button to install extensions.": "点击闪烁的按钮以安装扩展程序。",
1766 "How to install extensions?": "如何安装扩展程序?",
1767 "Loading Asset List": "正在加载资源列表",
1768 "Are you sure you want to connect to the following url?": "你确定要连接到以下 URL 吗?",
1469 "extension_install_1": "若想从此页安装扩展程序,你需要提前安装",1769 "extension_install_1": "若想从此页安装扩展程序,你需要提前安装",
1470 "extension_install_2": "。",1770 "extension_install_2": "。",
1471 "extension_install_3": "点这个图标(",1771 "extension_install_3": "点这个图标(",
@@ -1474,14 +1774,13 @@
1474 "Contest Winners": "比赛获胜者",1774 "Contest Winners": "比赛获胜者",
1475 "These characters are the finalists of character design contests and have remarkable quality.": "这些角色都是角色设计大赛的入围作品,品质十分出色。",1775 "These characters are the finalists of character design contests and have remarkable quality.": "这些角色都是角色设计大赛的入围作品,品质十分出色。",
1476 "Featured Characters": "特色角色",1776 "Featured Characters": "特色角色",
1477 "Download Extensions & Assets": "下载扩展和资源菜单",1777 "Download Extensions & Assets": "下载扩展程序和资源",
1478 "Load a custom asset list or select": "加载自定义资产列表或选择",1778 "Load a custom asset list or select": "加载自定义资源列表或选择",
1479 "to install 3rd party extensions.": "安装第三方扩展。",1779 "to install 3rd party extensions.": "以安装第三方扩展程序。",
1480 "Assets URL": "资产网址",1780 "Assets URL": "资源网址",
1481 "load_asset_list_desc": "根据资产列表文件加载扩展和资产列表。\n\n此字段中的默认资产 URL 指向官方第一方扩展和资产列表。\n如果您有自定义资产列表,可以在此处插入。\n\n要安装单个第三方扩展,请使用右上角的“安装扩展”按钮。",1781 "load_asset_list_desc": "根据资源列表文件加载扩展程序和资源列表。\n\n此字段中的默认资源 URL 指向官方扩展和资源列表。\n如果您有自定义资源列表,可以在此处插入。\n\n要安装单个第三方扩展程序,请使用右上角的“安装扩展程序”按钮。",
1482 "Load an asset list": "加载资产列表",1782 "Load an asset list": "加载资源列表",
1483 "Load Asset List": "加载资产列表",1783 "Load Asset List": "加载资源列表",
1484 "Characters": "人物",
1485 "Attach a file or image to a current chat.": "将文件或图像附加到当前聊天中。",1784 "Attach a file or image to a current chat.": "将文件或图像附加到当前聊天中。",
1486 "Attach a File": "附加文件",1785 "Attach a File": "附加文件",
1487 "Enter a URL or the ID of a Fandom wiki page to scrape:": "输入要抓取的 Fandom wiki 页面的 URL 或 ID:",1786 "Enter a URL or the ID of a Fandom wiki page to scrape:": "输入要抓取的 Fandom wiki 页面的 URL 或 ID:",
@@ -1489,11 +1788,11 @@
1489 "Example:": "例:",1788 "Example:": "例:",
1490 "Single file": "单个文件",1789 "Single file": "单个文件",
1491 "All articles will be concatenated into a single file.": "所有文章将被合并为一个文件。",1790 "All articles will be concatenated into a single file.": "所有文章将被合并为一个文件。",
1492 "File per article": "每篇文章的文件数",1791 "File per article": "每篇文章单独保存为文件",
1493 "Each article will be saved as a separate file.": "不推荐。每篇文章将保存为单独的文件。",1792 "Each article will be saved as a separate file.": "不推荐。每篇文章将保存为单独的文件。",
1793 "View global, character, or data files.": "查看全局、角色或数据文件。",
1494 "Open Data Bank": "打开数据库",1794 "Open Data Bank": "打开数据库",
1495 "Data Bank": "数据库",1795 "These files will be available for extensions that support attachments (e.g. Vector Storage).": "这些文件将可用于支持附件的扩展(如向量存储)。",
1496 "These files will be available for extensions that support attachments (e.g. Vector Storage).": "这些文件将可用于支持附件的扩展(例如 Vector Storage)。",
1497 "Supported file types: Plain Text, PDF, Markdown, HTML, EPUB.": "支持的文件类型:纯文本、PDF、Markdown、HTML、EPUB。",1796 "Supported file types: Plain Text, PDF, Markdown, HTML, EPUB.": "支持的文件类型:纯文本、PDF、Markdown、HTML、EPUB。",
1498 "Drag and drop files here to upload.": "将文件拖放到此处进行上传。",1797 "Drag and drop files here to upload.": "将文件拖放到此处进行上传。",
1499 "Date (Newest First)": "日期(最新在先)",1798 "Date (Newest First)": "日期(最新在先)",
@@ -1517,8 +1816,9 @@
1517 "Enter a base URL of the MediaWiki to scrape.": "输入要抓取的 MediaWiki 的基本 URL。",1816 "Enter a base URL of the MediaWiki to scrape.": "输入要抓取的 MediaWiki 的基本 URL。",
1518 "Don't include the page name!": "不要包含页面名称!",1817 "Don't include the page name!": "不要包含页面名称!",
1519 "Enter web URLs to scrape (one per line):": "输入要抓取的网址(每行一个):",1818 "Enter web URLs to scrape (one per line):": "输入要抓取的网址(每行一个):",
1520 "Enter a video URL to download its transcript.": "输入视频 URL 或 ID 即可下载其文本。",1819 "Enter a video URL to download its transcript.": "输入视频 URL 或 ID 即可下载其字幕。",
1521 "Image Captioning": "图像描述",1820 "Generate Caption": "生成图片描述",
1821 "Image Captioning": "图片描述",
1522 "Source": "来源",1822 "Source": "来源",
1523 "Local": "本地",1823 "Local": "本地",
1524 "Multimodal": "多模态",1824 "Multimodal": "多模态",
@@ -1530,76 +1830,118 @@
1530 "Refresh model list": "刷新模型列表",1830 "Refresh model list": "刷新模型列表",
1531 "currently_selected": "[当前选定]",1831 "currently_selected": "[当前选定]",
1532 "currently_loaded": "[当前正在加载]",1832 "currently_loaded": "[当前正在加载]",
1533 "Custom Model Tag": "自定义模型标签",1833 "Model Id": "模型 ID",
1534 "(for [Custom model] option)": "(自定义模型选项用)",1834 "(for [Custom model] option)": "(自定义模型选项用)",
1835 "Custom Model Tag": "自定义模型标签",
1535 "Allow reverse proxy": "允许反向代理",1836 "Allow reverse proxy": "允许反向代理",
1536 "Hint:": "提示:",1837 "Hint:": "提示:",
1537 "Set your API keys and endpoints in the 'API Connections' tab first.": "首先在“API 连接”选项卡中设置您的 API 密钥和端点。",1838 "Set your API keys and endpoints in the 'API Connections' tab first.": "首先在“API 连接”选项卡中设置您的 API 密钥和端点。",
1538 "Use secondary URL": "使用备用URL",1839 "Use secondary URL": "使用备用 URL",
1539 "Secondary captioning endpoint URL": "备用的描述文字生成端点URL",1840 "Secondary captioning endpoint URL": "备用的图片描述生成端点 URL",
1540 "Caption Prompt": "图像描述提示词",1841 "Caption Prompt": "图片描述提示词",
1541 "Ask every time": "每次都询问",1842 "Ask every time": "每次都询问",
1542 "Message Template": "消息模板",1843 "Message Template": "消息模板",
1543 "(use _space": "(使用",1844 "(use _space": "(使用",
1544 "macro)": "宏指令)",1845 "macro)": "宏指令)",
1545 "Automatically caption images": "自动为图像添加描述文字",1846 "Automatically caption images": "自动为图片添加描述文字",
1546 "Edit captions before saving": "保存前编辑描述文字",1847 "Edit captions before saving": "保存前编辑描述文字",
1547 "Show captions in chat": "在聊天中显示描述文字",1848 "Show captions in chat": "在聊天中显示描述文字",
1548 "Included settings:": "包含的设置:",1849 "Included settings:": "包含的设置:",
1549 "{{@key}}": "{{@key}}:",1850 "{{@key}}": "{{@key}}:",
1550 "Profile name:": "配置名称:",1851 "Profile name:": "配置名称:",
1551 "Creating a Connection Profile": "新建API连接配置",1852 "Are you sure you want to delete the selected profile?": "你确定要删除所选的配置吗?",
1853 "No profile selected": "未选择配置",
1854 "Save and Update": "保存并更新",
1855 "Creating a Connection Profile": "新建 API连接配置",
1552 "Click on the setting name to omit it from the profile.": "点击设置名称以将其从连接配置中删除。",1856 "Click on the setting name to omit it from the profile.": "点击设置名称以将其从连接配置中删除。",
1553 "Enter a name:": "输入名字:",1857 "Enter a name:": "输入名字:",
1554 "Connection Profile": "API连接配置",1858 "Connection Profile": "API 连接配置",
1555 "View connection profile details": "查看API连接配置详情",1859 "View connection profile details": "查看 API 连接配置详情",
1556 "Create a new connection profile": "新建一个API连接配置",1860 "Create a new connection profile": "新建一个 API连接配置",
1557 "Update a connection profile": "更新API连接配置",1861 "Update a connection profile": "更新 API连接配置",
1558 "Edit a connection profile": "编辑API连接配置",1862 "Edit a connection profile": "编辑 API连接配置",
1559 "Reload a connection profile": "重载API连接配置",1863 "Reload a connection profile": "重载 API连接配置",
1560 "Delete a connection profile": "删除API连接配置",1864 "Delete a connection profile": "删除 API连接配置",
1561 "Omitted Settings:": "排除的设置:",1865 "Omitted Settings:": "排除的设置:",
1866 "No expression or sprite name provided": "未提供情绪关键词或表情包名",
1867 "Set Sprite": "表情设置",
1868 "No expression found for search term ${0}": "关键词 ${0} 未搜索到任何情绪",
1869 "No sprite file found for search term ${0}": "关键词 ${0} 未搜索到任何表情文件",
1870 "Expression label is required": "请填写情绪标签",
1871 "Error Uploading Sprite": "表情包上传失败",
1872 "Expression label must contain at least one letter": "情绪标签至少包含一个字母",
1873 "Invalid sprite name. Must follow the naming pattern for expression sprites.": "无效表情包名称。需遵守命名格式。",
1874 "Couldn't find sprite file ${0} for expression ${1}.": "找不到情绪 ${1} 下的表情包 ${0}。",
1875 "Sprite Not Found": "找不到表情包",
1876 "Replace Existing": "替换现有",
1877 "Upload Expression Sprite": "上传表情包",
1878 "The name you entered does not follow the naming schema for the selected expression '${0}'.": "您输入的名称不符合所选情绪 '${0}' 的命名规范。",
1879 "Invalid Expression Sprite Name": "无效表情包名称",
1880 "Cancelled uploading sprite.": "已取消上传表情包。",
1881 "Upload Cancelled": "上传已取消",
1882 "Delete Expression": "删除情绪",
1883 "Are you sure you want to delete this expression? Once deleted, it's gone forever!": "你确定要删除这个情绪吗?一旦删除,它将永久消失!(真的很久!)",
1884 "Expression:": "情绪关键词:",
1885 "In group chats, you must specify a character name.": "在群聊中,你必须指定一个角色名称。",
1886 "No character name specified": "未指定角色名称",
1887 "Couldn't find character ${0}.": "找不到角色 ${0}。",
1888 "Character not found": "未找到角色",
1562 "Character Expressions": "角色表情",1889 "Character Expressions": "角色表情",
1563 "Use the selected API from Chat Translation extension settings.": "使用聊天翻译扩展程序中已选择的API。",1890 "Use the selected API from Chat Translation extension settings.": "使用聊天翻译扩展程序中已选择的 API。",
1564 "Translate text to English before classification": "分类之前将文本翻译成英文",1891 "Translate text to English before classification": "分类之前将文本翻译成英文",
1565 "A single expression can have multiple sprites. Whenever the expression is chosen, a random sprite for this expression will be selected.": "使单个关键词可以有多个表情包。每当出现该关键词时,将随机选择其中一个。",1892 "A single expression can have multiple sprites. Whenever the expression is chosen, a random sprite for this expression will be selected.": "使单个关键词可以有多个表情包。每当出现该关键词时,将随机选择其中一个。",
1566 "Allow multiple sprites per expression": "允许关键词重复",1893 "Allow multiple sprites per expression": "允许关键词重复",
1567 "If the same expression is used again, re-roll the sprite. This only applies to expressions that have multiple available sprites assigned.": "再次使用相同关键词时,将重新刷新表情包。仅适用于关键词重复的表情包。",1894 "If the same expression is used again, re-roll the sprite. This only applies to expressions that have multiple available sprites assigned.": "再次使用相同关键词时,将重新刷新表情包。仅适用于关键词重复的表情包。",
1568 "Re-roll if same expression is used again": "再次使用相同关键词时刷新表情包。",1895 "Re-roll if same expression is used again": "再次使用相同关键词时刷新表情包。",
1569 "Classifier API": "分类器 API",1896 "Classifier API": "分类器 API",
1570 "Select the API for classifying expressions.": "选择用于对表达式进行分类的API。",1897 "Select the API for classifying expressions.": "选择用于对情绪进行分类的API。",
1571 "Main API": "当前连接的 API",1898 "Main API": "当前连接的 API",
1572 "WebLLM Extension": "WebLLM 扩展程序",1899 "WebLLM Extension": "WebLLM 扩展程序",
1573 "When using LLM or WebLLM classifier, only show and use expressions that have sprites assigned to them.": "使用 LLM 或 WebLLM 分类器时,仅显示和使用已分配表情包的关键词。",1900 "When using LLM or WebLLM classifier, only show and use expressions that have sprites assigned to them.": "使用 LLM 或 WebLLM 分类器时,仅显示和使用已分配表情包的关键词。",
1574 "Filter expressions for available sprites": "筛选已有表情包的关键词",1901 "Filter expressions for available sprites": "筛选已有表情包的关键词",
1575 "LLM Prompt": "大语言模型提示词",1902 "LLM Prompt": "大语言模型提示词",
1576 "Used in addition to JSON schemas and function calling.": "可与 JSON结构 和 函数调用 一同使用。",1903 "Used in addition to JSON schemas and function calling.": "可与 JSON 结构和函数调用一同使用。",
1577 "LLM Prompt Strategy": "LLM 提示词策略",1904 "LLM Prompt Strategy": "LLM 提示词策略",
1578 "Limited Context": "限制上下文",1905 "Limited Context": "限制上下文",
1579 "Full Context": "完整上下文",1906 "Full Context": "完整上下文",
1580 "Default / Fallback Expression": "默认/后备表达式",1907 "Default / Fallback Expression": "默认/后备情绪",
1581 "Set the default and fallback expression being used when no matching expression is found.": "设置在未找到匹配表达式时使用的默认表达式和后备表达式。",1908 "Set the default and fallback expression being used when no matching expression is found.": "设置在未找到匹配情绪时使用的默认情绪和后备情绪。",
1582 "Custom Expressions": "自定义表达式",1909 "Custom Expressions": "自定义情绪",
1583 "Can be set manually or with an _space": "可以手动设置或使用",1910 "Can be set manually or with an _space": "可以手动设置或使用",
1584 "space_ slash command.": "快捷命令来设置。",1911 "space_ slash command.": "快捷命令来设置。",
1585 "Open a chat to see the character expressions.": "打开聊天即可查看人物表情。",1912 "Open a chat to see the character expressions.": "打开聊天即可查看人物表情。",
1586 "You are in offline mode. Click on the image below to set the expression.": "您处于离线模式。点击下方图片即可设置表情。",1913 "You are in offline mode. Click on the image below to set the expression.": "您处于离线模式。点击下方图片即可设置表情。",
1587 "Sprite Folder Override": "表情文件夹覆盖",1914 "Sprite Folder Override": "自定义表情包文件夹",
1588 "Use a forward slash to specify a subfolder. Example: _space": "使用正斜杠指定子文件夹。例如:",1915 "Use a forward slash to specify a subfolder. Example: _space": "使用正斜杠指定子文件夹。例如:",
1589 "Upload sprite pack (ZIP)": "上传表情包(ZIP)",1916 "Upload sprite pack (ZIP)": "上传表情图片包(ZIP)",
1590 "Remove all image overrides": "删除所有图片覆盖",1917 "Remove all image overrides": "删除所有图片覆盖",
1591 "Create new folder in the _space": "创建新文件夹到",1918 "Create new folder in the _space": "在您用户数据的目录下,找到 _space",
1592 "folder of your user data directory and name it as the name of the character.": "用户数据目录的文件夹并将其命名为角色的名称。",1919 "folder of your user data directory and name it as the name of the character.": "并在其中创建一个以角色名称为名的文件夹。",
1593 "Put images with expressions there. File names should follow the pattern:": "将带有表情的图像放在那里。文件名应遵循以下模式:",1920 "Put images with expressions there. File names should follow the pattern:": "将表情包图像放在文件夹下。文件名需遵循以下格式:",
1594 "expression_label_pattern": "[表达式标签].[图像格式]",1921 "expression_label_pattern": "[情绪].[图像格式]",
1595 "Sprite set:": "表情集:",1922 "Sprite set:": "表情集:",
1596 "upload_expression_request": "请输入表情名称(不用加后缀)。",1923 "upload_expression_request": "请输入表情名称(不用加后缀)。",
1597 "upload_expression_naming_1": "素材名称必须符合所选表情 {{expression}} 的命名规范",1924 "upload_expression_naming_1": "素材名称必须符合所选表情 {{expression}} 的命名规范",
1598 "upload_expression_naming_2": "当存在多个表情时,名称应由表情名称与合法后缀构成,允许使用横杠'-'或英文句号'.'作为分隔符。",1925 "upload_expression_naming_2": "当存在多个表情时,名称应由表情名称与合法后缀构成,允许使用横杠'-'或英文句号'.'作为分隔符。",
1599 "upload_expression_replace": "点击“替换”以替换当前表情:",1926 "upload_expression_replace": "点击“替换”以替换当前表情:",
1600 "Show Gallery": "展示图库",1927 "Show Gallery": "展示图库",
1928 "Image deleted successfully.": "图片删除成功。",
1929 "File uploaded successfully. Saved at: ${0}": "文件上传成功。已保存至:${0}",
1930 "Failed to upload the file.": "文件上传失败。",
1931 "Image Gallery": "图库",
1932 "Gallery folder changed to ${0}": "图库文件夹已更改为:${0}",
1933 "Unknown error": "未知错误",
1934 "Failed to change gallery folder": "更改图库文件夹失败",
1935 "Failed to restore gallery folder": "还原图库文件夹失败",
1936 "Output Folder Name": "输出文件夹名称",
1937 "Enter a folder name to change the gallery folder": "输入文件夹名称以更改图库文件夹",
1938 "Change gallery folder": "修改图库文件夹",
1939 "Delete mode": "删除模式",
1940 "Delete mode is ON. Click on images you want to delete.": "删除模式已开启。点击您想删除的图片。",
1941 "Restore gallery folder": "还原图库文件夹",
1942 "Are you sure you want to delete this image?": "你确定要删除这个图片吗?",
1601 "ext_sum_title": "总结",1943 "ext_sum_title": "总结",
1602 "ext_sum_with": "总结如下:",1944 "ext_sum_with": "总结来源:",
1603 "ext_sum_main_api": "主要 API",1945 "ext_sum_main_api": "主要 API",
1604 "ext_sum_webllm": "WebLLM 扩展",1946 "ext_sum_webllm": "WebLLM 扩展",
1605 "ext_sum_current_summary": "当前总结:",1947 "ext_sum_current_summary": "当前总结:",
@@ -1610,8 +1952,8 @@
1610 "ext_sum_force_text": "立即总结",1952 "ext_sum_force_text": "立即总结",
1611 "Disable automatic summary updates. While paused, the summary remains as-is. You can still force an update by pressing the Summarize now button (which is only available with the Main API).": "禁用自动总结。暂停时,总结保持原样。您仍然可以通过按“立即总结”按钮(仅适用于主 API)强制更新。",1953 "Disable automatic summary updates. While paused, the summary remains as-is. You can still force an update by pressing the Summarize now button (which is only available with the Main API).": "禁用自动总结。暂停时,总结保持原样。您仍然可以通过按“立即总结”按钮(仅适用于主 API)强制更新。",
1612 "ext_sum_pause": "暂停",1954 "ext_sum_pause": "暂停",
1613 "Omit World Info and Author's Note from text to be summarized. Only has an effect when using the Main API. The Extras API always omits WI/AN.": "从要总结的文本中省略世界信息和作者注释。仅在使用主 API 时有效。附加 API 始终省略世界书/作者注。",1955 "Omit World Info and Author's Note from text to be summarized. Only has an effect when using the Main API. The Extras API always omits WI/AN.": "从要总结的文本中省略世界书和作者注释。仅在使用主 API 时有效。附加 API 始终省略世界书/作者注释。",
1614 "ext_sum_no_wi_an": "无世界书/作者注",1956 "ext_sum_no_wi_an": "无世界书/作者注释",
1615 "ext_sum_settings_tip": "编辑总结提示词、插入位置等。",1957 "ext_sum_settings_tip": "编辑总结提示词、插入位置等。",
1616 "ext_sum_settings": "总结设置",1958 "ext_sum_settings": "总结设置",
1617 "ext_sum_prompt_builder": "提示词生成器",1959 "ext_sum_prompt_builder": "提示词生成器",
@@ -1629,7 +1971,7 @@
1629 "ext_sum_target_length_3": "字)",1971 "ext_sum_target_length_3": "字)",
1630 "ext_sum_api_response_length_1": "API 响应长度",1972 "ext_sum_api_response_length_1": "API 响应长度",
1631 "ext_sum_api_response_length_2": "(",1973 "ext_sum_api_response_length_2": "(",
1632 "ext_sum_api_response_length_3": "个词符)",1974 "ext_sum_api_response_length_3": "个 Token)",
1633 "ext_sum_0_default": "“0”为默认",1975 "ext_sum_0_default": "“0”为默认",
1634 "ext_sum_raw_max_msg": "[原始] 每个请求的最大消息数",1976 "ext_sum_raw_max_msg": "[原始] 每个请求的最大消息数",
1635 "ext_sum_0_unlimited": "“0”为无限制",1977 "ext_sum_0_unlimited": "“0”为无限制",
@@ -1644,10 +1986,10 @@
1644 "ext_sum_injection_template": "插入模板",1986 "ext_sum_injection_template": "插入模板",
1645 "ext_sum_memory_template_placeholder": "{{summary}} 将解析当前摘要内容。",1987 "ext_sum_memory_template_placeholder": "{{summary}} 将解析当前摘要内容。",
1646 "ext_sum_injection_position": "插入位置",1988 "ext_sum_injection_position": "插入位置",
1647 "ext_sum_include_wi_scan_desc": "在世界信息扫描中加入最新总结。",1989 "ext_sum_include_wi_scan_desc": "将最新的总结纳入世界书扫描。",
1648 "ext_sum_include_wi_scan": "纳入世界信息扫描",1990 "ext_sum_include_wi_scan": "纳入世界书扫描",
1649 "None (not injected)": "无(未注入)",1991 "None (not injected)": "无(未注入)",
1650 "ext_sum_injection_position_none": "总结不会被注入到提示中。您仍然可以通过 {{summary}} 宏访问它。",1992 "ext_sum_injection_position_none": "总结不会被注入到提示词中。您仍然可以通过 {{summary}} 宏访问它。",
1651 "How many messages before the current end of the chat.": "当前聊天结束前还有多少条消息。",1993 "How many messages before the current end of the chat.": "当前聊天结束前还有多少条消息。",
1652 "Labels and Message": "标签和信息",1994 "Labels and Message": "标签和信息",
1653 "Label": "标签",1995 "Label": "标签",
@@ -1668,7 +2010,7 @@
1668 "Execute on AI message": "根据 AI 消息执行",2010 "Execute on AI message": "根据 AI 消息执行",
1669 "Execute on chat change": "聊天内容改变时执行",2011 "Execute on chat change": "聊天内容改变时执行",
1670 "Execute on new chat": "在新聊天中执行",2012 "Execute on new chat": "在新聊天中执行",
1671 "Execute on group member draft": "起草群组成员时执行",2013 "Execute on group member draft": "群成员回复起草时执行",
1672 "Execute before message generation": "在消息生成前执行",2014 "Execute before message generation": "在消息生成前执行",
1673 "Automation ID:": "自动化标识",2015 "Automation ID:": "自动化标识",
1674 "Testing": "测试",2016 "Testing": "测试",
@@ -1685,7 +2027,7 @@
1685 "Disable Send (Insert Into Input Field)": "禁用发送(插入输入字段)",2027 "Disable Send (Insert Into Input Field)": "禁用发送(插入输入字段)",
1686 "Place Quick Reply Before Input": "在输入前放置快速回复",2028 "Place Quick Reply Before Input": "在输入前放置快速回复",
1687 "Inject user input automatically": "自动注入用户输入",2029 "Inject user input automatically": "自动注入用户输入",
1688 "(if disabled, use ": "(如果禁用,使用",2030 "(if disabled, use ": "(若禁用,使用",
1689 "macro for manual injection)": "宏用于手动注入)",2031 "macro for manual injection)": "宏用于手动注入)",
1690 "Color": "颜色",2032 "Color": "颜色",
1691 "Only apply color as accent": "仅应用颜色作为强调",2033 "Only apply color as accent": "仅应用颜色作为强调",
@@ -1725,14 +2067,14 @@
1725 "ext_regex_preset_update": "更新已有预设",2067 "ext_regex_preset_update": "更新已有预设",
1726 "ext_regex_preset_apply": "重新应用当前预设",2068 "ext_regex_preset_apply": "重新应用当前预设",
1727 "ext_regex_preset_delete": "删除当前预设",2069 "ext_regex_preset_delete": "删除当前预设",
1728 "ext_regex_global_scripts": "全局正则脚本",2070 "ext_regex_global_scripts": "全局正则",
1729 "ext_regex_global_scripts_desc": "影响所有角色,保存在本地设定中",2071 "ext_regex_global_scripts_desc": "影响所有角色,保存在本地设定中",
1730 "No scripts found": "没有找到脚本",2072 "No scripts found": "没有找到脚本",
1731 "ext_regex_preset_scripts": "预设正则脚本",2073 "ext_regex_preset_scripts": "预设正则",
1732 "ext_regex_disallow_preset": "不允许使用预设正则",2074 "ext_regex_disallow_preset": "不允许使用预设正则",
1733 "ext_regex_allow_preset": "允许使用预设正则",2075 "ext_regex_allow_preset": "允许使用预设正则",
1734 "ext_regex_preset_scripts_desc": "只影响当前预设,保存在预设中",2076 "ext_regex_preset_scripts_desc": "只影响当前预设,保存在预设中",
1735 "ext_regex_scoped_scripts": "局部正则脚本",2077 "ext_regex_scoped_scripts": "局部正则",
1736 "ext_regex_disallow_scoped": "不允许使用局部正则",2078 "ext_regex_disallow_scoped": "不允许使用局部正则",
1737 "ext_regex_allow_scoped": "允许使用局部正则",2079 "ext_regex_allow_scoped": "允许使用局部正则",
1738 "ext_regex_scoped_scripts_desc": "只影响当前角色,保存在角色卡片中",2080 "ext_regex_scoped_scripts_desc": "只影响当前角色,保存在角色卡片中",
@@ -1753,72 +2095,164 @@
1753 "ext_regex_affects": "作用范围",2095 "ext_regex_affects": "作用范围",
1754 "ext_regex_user_input_desc": "用户发送的消息",2096 "ext_regex_user_input_desc": "用户发送的消息",
1755 "ext_regex_user_input": "用户输入",2097 "ext_regex_user_input": "用户输入",
1756 "ext_regex_ai_input_desc": "从生成式API中获取的信息。",2098 "ext_regex_ai_input_desc": "从生成式 API 中获取的信息。",
1757 "ext_regex_ai_output": "AI输出",2099 "ext_regex_ai_output": "AI 输出",
1758 "ext_regex_slash_desc": "通过 STscript 命令发送的消息。",2100 "ext_regex_slash_desc": "通过 ST脚本 命令发送的消息。",
1759 "Slash Commands": "快捷命令",2101 "Slash Commands": "快捷命令",
1760 "ext_regex_wi_desc": "知识书/世界书 条目的内容。需要勾选“仅格式提示词”!",2102 "ext_regex_wi_desc": "知识书/世界书 条目的内容。需要勾选“仅格式提示词”!",
1761 "ext_regex_reasoning_desc": "推理块内容。当'仅格式提示词'被选中时,它会影响提示词里的推理内容。",2103 "ext_regex_reasoning_desc": "推理块内容。当'仅格式提示词'被选中时,它会影响提示词里的推理内容。",
1762 "ext_regex_min_depth_desc": "当应用于提示或显示时,仅影响深度至少为 N 级的消息。“0”为最后一条消息,“1”为倒数第二条消息等。仅计算 WI 条目 @Depth 和可用消息,即非隐藏或系统消息。",2104 "ext_regex_min_depth_desc": "当应用于提示词或显示时,仅影响深度至少为 N 级的消息。“0”为最后一条消息,“1”为倒数第二条消息等。仅计算指定深度的 WI 条目和可用消息,即非隐藏或系统消息。",
1763 "Min Depth": "最小深度",2105 "Min Depth": "最小深度",
1764 "ext_regex_min_depth_placeholder": "无限",2106 "ext_regex_min_depth_placeholder": "无限",
1765 "ext_regex_max_depth_desc": "当应用于提示词或显示时,仅影响深度不超过 N 级的消息。“0”为最后一条消息,“1”为倒数第二条消息等。仅计算世界信息条目 @Depth 和可用消息,即非隐藏或系统消息。",2107 "ext_regex_max_depth_desc": "当应用于提示词或显示时,仅影响深度不超过 N 级的消息。“0”为最后一条消息,“1”为倒数第二条消息等。仅计算指定深度的世界书条目和可用消息,即非隐藏或系统消息。",
1766 "ext_regex_other_options": "其他选项",2108 "ext_regex_other_options": "其他选项",
1767 "ext_regex_run_on_edit_desc": "当指定角色的消息被编辑时运行正则脚本。",2109 "ext_regex_run_on_edit_desc": "当指定角色的消息被编辑时运行正则。",
1768 "Run On Edit": "在编辑时运行",2110 "Run On Edit": "在编辑时运行",
1769 "ext_regex_substitute_regex_desc": "在运行正则表达式查找之前,替换 {{macros}}",2111 "ext_regex_substitute_regex_desc": "在运行正则表达式查找之前,替换 {{macros}}",
1770 "Macro in Find Regex": "正则表达式查找时的宏",2112 "Macro in Find Regex": "正则表达式查找时的宏",
1771 "Don't substitute": "不替换",2113 "Don't substitute": "不替换",
1772 "Substitute (raw)": "替换(原始)",2114 "Substitute (raw)": "替换(原始)",
1773 "Substitute (escaped)": "替换(转义)",2115 "Substitute (escaped)": "替换(转义)",
1774 "Ephemerality": "短暂",2116 "Ephemerality": "表层替换",
1775 "ext_regex_other_options_desc": "默认情况下,正则脚本将直接地、不可逆转地修改聊天文件。\r启用下方任意一或多项可以避免对聊天文件的修改,但仍然修改特定项目。",2117 "ext_regex_other_options_desc": "默认情况下,正则将直接地、不可逆转地修改聊天文件。\r启用下方任意一或多项可以避免对聊天文件的修改,但仍然在特定情况生效。",
1776 "ext_regex_only_format_visual_desc": "正则仅在聊天页面生效,聊天文件内的内容不会被改变。",2118 "ext_regex_only_format_visual_desc": "不改变聊天文件,只影响聊天界面表层显示。",
1777 "Only Format Display": "仅格式显示",2119 "Only Format Display": "仅影响显示",
1778 "ext_regex_only_format_prompt_desc": "聊天记录不会改变,只有在请求发送时(生成时)才会出现提示词。",2120 "ext_regex_only_format_prompt_desc": "不改变聊天文件,只修改请求发送时后端提示词。",
1779 "Only Format Prompt (?)": "仅格式提示词",2121 "Only Format Prompt (?)": "仅影响后端提示词",
1780 "This character has embedded regex script(s).": "此角色含有内置正则脚本。",2122 "This character has embedded regex script(s).": "此角色含有内置正则。",
1781 "Would you like to allow using them?": "你想要启用它们吗?",2123 "Would you like to allow using them?": "你想要启用它们吗?",
1782 "If you want to do it later, select 'Regex' from the extensions menu.": "你可以稍后在扩展栏的 \"正则\" 区域管理它们。",2124 "If you want to do it later, select 'Regex' from the extensions menu.": "你可以稍后在扩展栏的 \"正则\" 区域管理它们。",
1783 "ext_regex_import_target": "导入至:",2125 "ext_regex_import_target": "导入至:",
1784 "This preset has embedded regex script(s).": "此预设包含内置正则脚本。",2126 "Unknown Preset": "未知预设",
2127 "You have unsaved changes to the \"${0}\" preset.": "\"${0}\" 预设有未保存的更改。",
2128 "Do you want to save them before switching?": "要在切换前保存更改吗?",
2129 "Save Changes": "保存更改",
2130 "Discard Changes": "放弃更改",
2131 "Selects a regex preset by name or ID. Gets the current regex preset ID if no argument is provided.": "根据名称或 ID 选择一个正则预设。如果没有提供参数,则获取当前正则预设的 ID。",
2132 "[No presets saved]": "[没有保存的预设]",
2133 "Could not find the selected preset.": "找不到所选的预设。",
2134 "Could not find the preset to update.": "找不到要更新的预设。",
2135 "Enter a name for the new regex preset:": "为新正则预设起个名字:",
2136 "Regex preset updated": "正则预设已更新",
2137 "Regex preset saved": "正则预设已保存",
2138 "Could not find the preset to delete.": "找不到要删除的预设。",
2139 "Are you sure you want to delete this regex preset?": "你确定要删除这个正则预设吗?",
2140 "Regex preset deleted": "正则预设已删除",
2141 "Could not save regex script: The script name was undefined or empty!": "无法保存正则:名称未定义或为空",
2142 "This regex script will not work, but was saved anyway: A find regex isn't present.": "此正则已保存但不会生效:查找表达式未生效。",
2143 "This regex script will not work, but was saved anyway: One \"Affects\" checkbox must be selected!": "此正则已保存但不会生效。要使其生效,必须选择一个 \"作用范围\"。",
2144 "Are you sure you want to move this regex script to global?": "你确定要将此正则作用于全局吗?",
2145 "Cannot edit scoped scripts in group chats.": "无法在群聊内修改局部正则。",
2146 "Are you sure you want to move this regex script to scoped?": "你确定要将此正则绑定至角色吗?",
2147 "Are you sure you want to move this regex script to preset?": "你确定要将此正则绑定至预设吗?",
2148 "Are you sure you want to delete this regex script?": "你确定要删除这个正则吗?",
2149 "No regex rules found.": "找不到正则规则。",
2150 "Scoped": "局部",
2151 "Preset": "预设",
2152 "Global Rules": "全局规则",
2153 "Preset Rules": "预设规则",
2154 "Scoped Rules": "局部规则",
2155 "After:": "此后:",
2156 "Captured:": "捕获",
2157 "Added:": "添加:",
2158 "Removed:": "删除:",
2159 "Total Captured:": "总捕获:",
2160 "Total Added:": "总添加:",
2161 "Total Removed:": "总删除:",
2162 "Regex script order saved!": "正则顺序已保存!",
2163 "Step-by-step Transformation": "逐步转换",
2164 "Final Output": "最终输出",
2165 "Find Regex is empty": "查找正则表达式为空",
2166 "Invalid Find Regex": "无效查找正则表达式",
2167 "Applies to all matches": "应用于所有匹配项",
2168 "Applies to the first match": "仅应用于第一个匹配项",
2169 "Case insensitive": "不区分大小写",
2170 "Case sensitive": "区分大小写",
2171 "Regex script \"${0}\" is disabled.": "已禁用正则 \"${0}\"。",
2172 "Regex script '${0}' not found.": "找不到正则 '${0}'。",
2173 "Regex script '${0}' has been disabled.": "已禁用正则 '${0}'。",
2174 "Regex script '${0}' has been enabled.": "已启用正则 '${0}'。",
2175 "Regex script \"${0}\" imported.": "已导入正则 \"${0}\"。",
2176 "Invalid regex object.": "无效正则对象。",
2177 "Reload the chat for regex to take effect": "重新加载聊天以使正则生效",
2178 "Click here to reload immediately": "点击此处立即重新加载",
2179 "Preset '${0}' contains enabled regex scripts": "预设 '${0}' 包含被启用的正则",
2180 "No regex scripts selected for enabling.": "无选择要启用的正则。",
2181 "No regex scripts selected for disabling.": "无选择要禁用的正则。",
2182 "No regex scripts selected for moving.": "无选择要移动的正则。",
2183 "Are you sure you want to move the selected regex scripts to global?": "你确定要将所选的正则作用于全局吗?",
2184 "Are you sure you want to move the selected regex scripts to scoped?": "你确定要将所选的正则绑定至角色吗?",
2185 "Are you sure you want to move the selected regex scripts to preset?": "你确定要将所选的正则绑定至预设吗?",
2186 "No regex scripts selected for deletion.": "无选择要删除的正则。",
2187 "Are you sure you want to delete the selected regex scripts?": "你确定要删除选择的正则吗?",
2188 "No regex scripts selected for export.": "无选择要导出的正则。",
2189 "This preset has embedded regex script(s).": "此预设包含内置正则。",
1785 "ext_regex_disable_script": "禁用脚本",2190 "ext_regex_disable_script": "禁用脚本",
1786 "ext_regex_enable_script": "启用脚本",2191 "ext_regex_enable_script": "启用脚本",
1787 "Show more options": "展示更多选项",2192 "Show more options": "展示更多选项",
1788 "ext_regex_export_script": "导出脚本",2193 "ext_regex_export_script": "导出脚本",
1789 "ext_regex_edit_script": "编辑脚本",2194 "ext_regex_edit_script": "编辑脚本",
1790 "ext_regex_delete_script": "删除脚本",2195 "ext_regex_delete_script": "删除脚本",
1791 "Trigger Stable Diffusion": "触发Stable Diffusion",2196 "Select a Connection Profile": "选择 API连接配置",
2197 "Trigger Stable Diffusion": "触发 Stable Diffusion",
1792 "Abort current image generation task": "中止当前图像生成",2198 "Abort current image generation task": "中止当前图像生成",
1793 "Stop Image Generation": "停止图像生成",2199 "Stop Image Generation": "停止图像生成",
1794 "Send me a picture of:": "给我发一张……的照片:",2200 "Send me a picture of:": "给我发一张……的照片:",
1795 "sd_Yourself": "你自己",2201 "sd_Yourself": "角色",
1796 "sd_Your_Face": "你的脸",2202 "sd_Your_Face": "角色的脸",
1797 "sd_Me": "我",2203 "sd_Me": "用户",
1798 "sd_The_Whole_Story": "整个故事",2204 "sd_The_Whole_Story": "整个故事",
1799 "sd_The_Last_Message": "最后的信息",2205 "sd_The_Last_Message": "最后一条消息",
1800 "sd_Raw_Last_Message": "原始最后一条消息",2206 "sd_Raw_Last_Message": "最后一条原始消息",
1801 "sd_Background": "背景",2207 "sd_Background": "背景",
2208 "Restore default": "恢复默认",
2209 "Are you sure you want to delete the style \"${0}\"?": "你确定要删除风格 \"${0}\" 吗?",
2210 "Enter style name:": "输入风格名称:",
2211 "Enter new style name:": "输入新风格名称:",
2212 "A style with that name already exists": "风格名称已存在",
2213 "sd_res_512x512": "512x512 (1:1,图标,个人信息图像)",
2214 "sd_res_600x600": "600x600 (1:1,图标,个人信息图像)",
2215 "sd_res_512x768": "512x768 (2:3,纵向角色卡)",
2216 "sd_res_768x512": "768x512 (3:2,横向 35-mm 电影胶片)",
2217 "sd_res_960x540": "960x540 (16:9,横向壁纸)",
2218 "sd_res_540x960": "540x960 (9:16,纵向壁纸)",
2219 "sd_res_1920x1088": "1920x1088 (16:9,1080p,横向壁纸)",
2220 "sd_res_1088x1920": "1088x1920 (9:16,1080p,纵向壁纸)",
2221 "sd_res_1280x720": "1280x720 (16:9,720p,横向壁纸)",
2222 "sd_res_720x1280": "720x1280 (9:16,720p,纵向壁纸)",
2223 "<Enter Model ID above>": "<在上方输入模型 ID>",
2224 "Generating an image": "正在生成图像",
2225 "Delete the workflow? This action is irreversible.": "删除工作流?此操作无法被撤销。",
2226 "Enter new workflow name:": "输入新工作流名称:",
2227 "A workflow with that name already exists": "已存在同名工作流",
2228 "Failed to rename workflow.\n\n${0}": "重命名工作流失败。\n\n${0}",
2229 "Image Generation Notice": "图像生成通知",
2230 "To disable generation on image swipes, change the \"Image Swipe Behavior\" setting in the User Settings panel. This message will not be shown again.": "要关闭生成备选图片,请在用户设置面板中更改“图像备选生成行为”设置。此提示只显示一次。",
2231 "Character's positive Image Generation prompt prefix": "正向角色图像生成提示词前缀",
2232 "Character's negative Image Generation prompt prefix": "负向角色图像生成提示词前缀",
1802 "Image Generation": "图像生成",2233 "Image Generation": "图像生成",
1803 "sd_refine_mode": "允许在将提示词发送到生成 API 之前手动编辑提示词",2234 "sd_refine_mode": "在提示词被发送给图像生成 API 前先手动编辑它",
1804 "sd_refine_mode_txt": "生成之前编辑提示词",2235 "sd_refine_mode_txt": "生成前编辑提示词",
1805 "sd_function_tool": "使用函数工具自动检测生成图像的意图。",2236 "sd_function_tool": "利用函数工具功能,自动识别对话中的绘图需求并生成图像。",
1806 "sd_function_tool_txt": "使用函数工具",2237 "sd_function_tool_txt": "使用函数工具",
1807 "sd_interactive_mode": "发送消息时自动生成图像,例如“给我发一张猫的照片”。",2238 "sd_interactive_mode": "发送消息时自动生成图像,例如“给我发一张猫的照片”。",
1808 "sd_interactive_mode_txt": "交互模式",2239 "sd_interactive_mode_txt": "交互模式",
1809 "sd_multimodal_captioning": "使用多模态字幕根据用户和角色的头像生成提示词。",2240 "sd_multimodal_captioning": "根据用户和角色的头像,使用多模态模型生成图片描述。",
1810 "sd_multimodal_captioning_txt": "使用多模态字幕来描绘肖像",2241 "sd_multimodal_captioning_txt": "对图片使用多模态模型生成描述",
1811 "sd_free_extend": "使用当前选择的 LLM 自动扩展自由模式主题提示(不是肖像或背景)。",2242 "sd_free_extend": "使用当前选定的 LLM 自动扩展自由模式的主题提示词(不包括肖像或背景)。",
1812 "sd_free_extend_txt": "延长自由模式提示",2243 "sd_free_extend_txt": "延长自由模式提示词",
1813 "sd_free_extend_small": "(交互/命令)",2244 "sd_free_extend_small": "(交互/命令)",
1814 "sd_snap": "快照生成请求具有强制纵横比(肖像、背景)到最接近已知分辨率,同时尝试保留绝对像素数(推荐用于 SDXL)。",2245 "sd_snap": "对于具有特定长宽比的生成请求(如肖像、背景),将其调整至最接近的已知分辨率,同时尽量保持绝对像素数(建议用于 SDXL)。",
1815 "sd_snap_txt": "自动调整分辨率",2246 "sd_snap_txt": "自动调整分辨率",
2247 "sd_minimal_prompt_processing": "使 LLM 不要过度处理提示词,以保留原有的 JSON 或其他格式。",
2248 "sd_minimal_prompt_processing_txt": "最小化提示词处理",
1816 "sd_auto_url": "例如:{{auto_url}}",2249 "sd_auto_url": "例如:{{auto_url}}",
1817 "Authentication (optional)": "身份验证(可选)",2250 "Authentication (optional)": "身份验证(选填)",
1818 "Example: username:password": "例:用户名:密码",2251 "Example: username:password": "示例:用户名:密码",
1819 "Important:": "重要:",2252 "Important:": "重要:",
1820 "sd_auto_auth_warning_1": "使用",2253 "sd_auto_auth_warning_1": "使用",
1821 "sd_auto_auth_warning_2": "注意!服务器必须可从 SillyTavern 主机访问。",2254 "sd_auto_auth_warning_2": "注意!服务器必须可从 SillyTavern 主机访问。",
2255 "The server must be accessible from the SillyTavern host machine.": "必须能够从 SillyTavern 主机访问该服务器。",
1822 "sd_drawthings_url": "例如:{{drawthings_url}}",2256 "sd_drawthings_url": "例如:{{drawthings_url}}",
1823 "sd_drawthings_auth_txt": "运行 DrawThings 应用程序并在 UI 中启用 HTTP API 开关!必须可以从 SillyTavern 主机访问服务器。",2257 "sd_drawthings_auth_txt": "运行 DrawThings 应用程序并在 UI 中启用 HTTP API 开关!必须可以从 SillyTavern 主机访问服务器。",
1824 "Hint: Save an API key in the Hugging Face (Text Completion) API settings to use it here.": "提示:在 Hugging Face(文本补全)API 设置中保存一个 API 密钥以在此处使用。",2258 "Hint: Save an API key in the Hugging Face (Text Completion) API settings to use it here.": "提示:在 Hugging Face(文本补全)API 设置中保存一个 API 密钥以在此处使用。",
@@ -1829,7 +2263,6 @@
1829 "Image Quality": "画面质量",2263 "Image Quality": "画面质量",
1830 "Hint: Save an API key in the NanoGPT (Chat Completion) API settings to use it here.": "提示:在 NanoGPT(聊天补全)API 设置中保存一个 API 密钥以在此处使用。",2264 "Hint: Save an API key in the NanoGPT (Chat Completion) API settings to use it here.": "提示:在 NanoGPT(聊天补全)API 设置中保存一个 API 密钥以在此处使用。",
1831 "sd_vlad_url": "例如:{{vlad_url}}",2265 "sd_vlad_url": "例如:{{vlad_url}}",
1832 "The server must be accessible from the SillyTavern host machine.": "必须能够从 SillyTavern 主机访问该服务器。",
1833 "Hint: Save an API key in AI Horde API settings to use it here.": "提示:在 Horde AI API 设置中保存一个 API 密钥以在此处使用。",2266 "Hint: Save an API key in AI Horde API settings to use it here.": "提示:在 Horde AI API 设置中保存一个 API 密钥以在此处使用。",
1834 "Allow NSFW images from Horde": "允许来自 Horde 的 NSFW 图片",2267 "Allow NSFW images from Horde": "允许来自 Horde 的 NSFW 图片",
1835 "Sanitize prompts (recommended)": "净化提示词(推荐)",2268 "Sanitize prompts (recommended)": "净化提示词(推荐)",
@@ -1840,6 +2273,8 @@
1840 "Hint: Save an API key in the NovelAI API settings to use it here.": "提示:在 NovelAI API 设置中保存一个 API 密钥以在此处使用。",2273 "Hint: Save an API key in the NovelAI API settings to use it here.": "提示:在 NovelAI API 设置中保存一个 API 密钥以在此处使用。",
1841 "Click to set": "点击设置",2274 "Click to set": "点击设置",
1842 "Image Style": "图像风格",2275 "Image Style": "图像风格",
2276 "Low": "低",
2277 "High": "高",
1843 "Standard": "标准",2278 "Standard": "标准",
1844 "HD": "高清",2279 "HD": "高清",
1845 "Duration": "持续时间",2280 "Duration": "持续时间",
@@ -1847,12 +2282,13 @@
1847 "Medium (8 seconds)": "中(8秒)",2282 "Medium (8 seconds)": "中(8秒)",
1848 "Long (16 seconds)": "长(16秒)",2283 "Long (16 seconds)": "长(16秒)",
1849 "sd_comfy_url": "例如:{{comfy_url}}",2284 "sd_comfy_url": "例如:{{comfy_url}}",
1850 "sd_comfy_runpod_url": "eg: https://api.runpod.ai/v2/<your endpoint id>",2285 "sd_comfy_runpod_url": "eg: https://api.runpod.ai/v2/<你的端点 ID>",
1851 "Open workflow editor": "打开工作流编辑器",2286 "Open workflow editor": "打开工作流编辑器",
1852 "Create new workflow": "创建新的工作流",2287 "Create new workflow": "创建新的工作流",
2288 "Rename workflow": "重命名工作流",
1853 "Delete workflow": "删除工作流",2289 "Delete workflow": "删除工作流",
1854 "Enables prompt enhancing (passes prompts through an LLM to add detail).": "允许提示词增强(通过大语言模型处理提示词以添加细节)。",2290 "Enables prompt enhancing (passes prompts through an LLM to add detail).": "启用提示词增强(通过大语言模型处理提示词以添加细节)。",
1855 "Enhance": "提高",2291 "Enhance": "增强",
1856 "You can find your API key in the Stability AI dashboard.": "您可以在 Stability AI 仪表板中找到您的 API 密钥。",2292 "You can find your API key in the Stability AI dashboard.": "您可以在 Stability AI 仪表板中找到您的 API 密钥。",
1857 "Style Preset": "风格预设",2293 "Style Preset": "风格预设",
1858 "Whether to perform upsampling on the prompt. If active, automatically modifies the prompt for more creative generation.": "是否对提示词使用提示词增强(Upsampling)。若开启,则会自动修改提示词,使回复更有创造力。",2294 "Whether to perform upsampling on the prompt. If active, automatically modifies the prompt for more creative generation.": "是否对提示词使用提示词增强(Upsampling)。若开启,则会自动修改提示词,使回复更有创造力。",
@@ -1886,44 +2322,50 @@
1886 "Preset for prompt prefix and negative prompt": "提示词前缀和负面提示词的预设",2322 "Preset for prompt prefix and negative prompt": "提示词前缀和负面提示词的预设",
1887 "Style": "风格",2323 "Style": "风格",
1888 "Save style": "保存风格",2324 "Save style": "保存风格",
2325 "Rename style": "重命名风格",
1889 "Delete style": "删除风格",2326 "Delete style": "删除风格",
1890 "Common prompt prefix": "常见提示词前缀",2327 "Common prompt prefix": "常见提示词前缀",
1891 "sd_prompt_prefix_placeholder": "使用 {prompt} 指定生成的提示词插入的位置",2328 "sd_prompt_prefix_placeholder": "使用 {prompt} 指定生成的提示词插入的位置",
1892 "Negative common prompt prefix": "常见负面提示词前缀",2329 "Negative common prompt prefix": "常见负面提示词前缀",
1893 "Character-specific prompt prefix": "特定角色的提示词前缀",2330 "Character-specific prompt prefix": "角色特定的提示词前缀",
1894 "Won't be used in groups.": "不可供群聊使用。",2331 "Won't be used in groups.": "不可供群聊使用。",
1895 "sd_character_prompt_placeholder": "描述当前选定角色的任何特征。将添加在常见提示词前缀后。\n示例:女性、绿眼睛、棕色头发、粉色衬衫",2332 "sd_character_prompt_placeholder": "描述当前选定角色的任何特征。将添加在常见提示词前缀后。\n示例:女性、绿眼睛、棕色头发、粉色衬衫",
1896 "Character-specific negative prompt prefix": "特定角色的负面提示词前缀",2333 "Character-specific negative prompt prefix": "角色特定的负面提示词前缀",
1897 "sd_character_negative_prompt_placeholder": "所选角色不应出现的任何特征。将添加在常见负面提示词前缀后。\n示例:珠宝、鞋子、眼镜",2334 "sd_character_negative_prompt_placeholder": "所选角色不应出现的任何特征。将添加在常见负面提示词前缀后。\n示例:珠宝、鞋子、眼镜",
1898 "Shareable": "可共享",2335 "Shareable": "绑定到角色卡",
2336 "When checked, character-specific prompts will be saved with the character card data.": "勾选时,角色特定的图像生成提示词将与角色卡数据一起保存。",
1899 "Chat Message Visibility (by source)": "聊天消息可见性(按来源)",2337 "Chat Message Visibility (by source)": "聊天消息可见性(按来源)",
1900 "Uncheck to hide the extension's messages in chat prompts.": "取消选中可在聊天提示中隐藏扩展消息。",2338 "Uncheck to hide the extension's messages in chat prompts.": "取消选中可在聊天提示词中隐藏扩展消息。",
1901 "Extensions Menu": "扩展菜单",2339 "Extensions Menu": "扩展菜单",
1902 "Slash Command": "快捷命令",2340 "Slash Command": "快捷命令",
1903 "Interactive Mode": "交互模式",2341 "Interactive Mode": "交互模式",
1904 "Function Tool": "函数工具",2342 "Function Tool": "函数工具",
1905 "Image Prompt Templates": "图像提示模板",2343 "Image Prompt Templates": "图像提示词模板",
1906 "Token Counter": "词符计数器",2344 "Token Counter": "Token 计数器",
1907 "Type / paste in the box below to see the number of tokens in the text.": "在下方框中输入或粘贴你想要统计词符数量的文本。",2345 "Type / paste in the box below to see the number of tokens in the text.": "在下方框中输入或粘贴你想要统计 Token 数量的文本。",
1908 "Selected tokenizer:": "已选分词器:",2346 "Selected tokenizer:": "已选分词器:",
1909 "Input:": "输入:",2347 "Input:": "输入:",
1910 "Tokens:": "词符:",2348 "Tokens:": "Token:",
1911 "Tokenized text:": "词符化文本:",2349 "Tokenized text:": "分词后文本:",
1912 "Token IDs:": "词符ID:",2350 "Token IDs:": "Token IDs:",
1913 "ext_translate_btn_chat": "翻译聊天",2351 "ext_translate_btn_chat": "翻译聊天",
1914 "ext_translate_btn_input": "翻译输入",2352 "ext_translate_btn_input": "翻译输入",
1915 "ext_translate_delete_confirm_1": "你确定吗?",2353 "ext_translate_delete_confirm_1": "你确定吗?",
1916 "ext_translate_delete_confirm_2": "这将从当前聊天中的所有消息中删除翻译文本。此操作无法撤消。",2354 "ext_translate_delete_confirm_2": "这将从当前聊天中的所有消息中删除翻译文本。此操作无法撤消。",
1917 "ext_translate_title": "聊天翻译",2355 "ext_translate_title": "聊天翻译",
1918 "ext_translate_auto_mode": "自动模式",2356 "ext_translate_auto_mode": "自动模式",
1919 "ext_translate_mode_none": "没有任何",2357 "ext_translate_mode_none": "关闭",
1920 "ext_translate_mode_responses": "翻译回复",2358 "ext_translate_mode_responses": "仅翻译回复",
1921 "ext_translate_mode_inputs": "翻译输入",2359 "ext_translate_mode_inputs": "仅翻译输入",
1922 "ext_translate_mode_both": "翻译两者",2360 "ext_translate_mode_both": "翻译两者",
1923 "ext_translate_mode_provider": "提供者",2361 "ext_translate_mode_provider": "提供商",
1924 "ext_translate_target_lang": "目标语言",2362 "ext_translate_target_lang": "目标语言",
1925 "ext_translate_clear": "清空设置",2363 "ext_translate_clear": "清空设置",
1926 "Select TTS Provider": "选择 文本转语音 的服务提供商",2364 "TTS Playback": "TTS 重播",
2365 "Narrate All Chat": "朗读全部聊天",
2366 "TTS play/pause": "TTS 播放/暂停",
2367 "Narrate all messages in the current chat. Includes user messages, excludes hidden comments.": "朗读当前聊天中的所有消息。包括用户消息,不包括隐藏的评论。",
2368 "Select TTS Provider": "选择文本转语音服务商",
1927 "tts_refresh": "刷新",2369 "tts_refresh": "刷新",
1928 "tts_enabled": "已启用",2370 "tts_enabled": "已启用",
1929 "Narrate user messages": "朗读用户消息",2371 "Narrate user messages": "朗读用户消息",
@@ -1935,17 +2377,25 @@
1935 "Ignore text, even quotes, inside asterisk": "不朗读所有*星号内文本*,即使其被引号包裹",2377 "Ignore text, even quotes, inside asterisk": "不朗读所有*星号内文本*,即使其被引号包裹",
1936 "Narrate only the translated text": "只朗读翻译后文本",2378 "Narrate only the translated text": "只朗读翻译后文本",
1937 "Skip codeblocks": "跳过代码块",2379 "Skip codeblocks": "跳过代码块",
1938 "Skip tagged blocks": "跳过标签块里的内容(<标签>跳过这里</标签>)",2380 "Skip tagged blocks": "跳过标签块里的内容(<tag>跳过这里</tag>)",
1939 "Pass Asterisks to TTS Engine": "将星号传递给文本转语音服务",2381 "Pass Asterisks to TTS Engine": "将星号传递给文本转语音服务",
1940 "Works best when: Pass Asterisks to TTS Engine is enabled, and both Only narrate quotes and Ignore *text, even 'quotes', inside asterisks* are disabled.": "最佳效果:启用“将星号传递给文本转语音服务”,同时禁用“只朗读引号内文本”和“忽略*星号内文本*(即使其被引号包裹)”功能。",2382 "Works best when: Pass Asterisks to TTS Engine is enabled, and both Only narrate quotes and Ignore *text, even 'quotes', inside asterisks* are disabled.": "最佳效果:启用“将星号传递给文本转语音服务”,同时禁用“只朗读引号内文本”和“忽略*星号内文本*(即使其被引号包裹)”功能。",
1941 "Different voices for quotes and text inside asterisks": "为“引号内文本”、*星号内文本*使用不同的声音",2383 "Different voices for quotes and text inside asterisks": "为“引号内文本”、*星号内文本*使用不同的声音",
2384 "Apply regex filter": "应用正则过滤",
2385 "Regex Pattern": "正则匹配规则(删除匹配文本)",
2386 "Invalid regex pattern.": "无效的正则匹配规则。",
1942 "Audio Playback Speed": "音频播放速度",2387 "Audio Playback Speed": "音频播放速度",
1943 "Available voices": "可用声音",2388 "Available voices": "可用声音",
2389 "Your browser or operating system doesn't support speech synthesis": "您的浏览器或操作系统不支持语音合成",
2390 "Uses the voices provided by your operating system": "使用操作系统提供的语音",
2391 "Rate:": "速率:",
2392 "Pitch:": "音调:",
1944 "Vector Storage": "向量存储",2393 "Vector Storage": "向量存储",
1945 "Vectorization Source": "向量化源",2394 "Vectorization Source": "向量化源",
1946 "Local (Transformers)": "本地(Transformers)",2395 "Local (Transformers)": "本地(Transformers)",
1947 "Vectorization Model": "向量化模型",2396 "Vectorization Model": "向量化模型",
1948 "Hint: Set your Chutes API key in API Connections.": "提示:在 API 连接设置中设置 Chutes API 密钥。",2397 "Hint: Set your Chutes API key in API Connections.": "提示:在 API 连接设置中设置 Chutes API 密钥。",
2398 "Hint: Set your NanoGPT API key in API Connections.": "提示:在 API 连接设置中设置 NanoGPT API 密钥。",
1949 "Hint: Set your Electron Hub API key in API Connections.": "提示:在 API 连接设置中设置 Electron Hub API 密钥。",2399 "Hint: Set your Electron Hub API key in API Connections.": "提示:在 API 连接设置中设置 Electron Hub API 密钥。",
1950 "Secondary Embedding endpoint URL": "次级向量化端点 URL",2400 "Secondary Embedding endpoint URL": "次级向量化端点 URL",
1951 "Keep model in memory": "将模型保存在内存中",2401 "Keep model in memory": "将模型保存在内存中",
@@ -1956,11 +2406,11 @@
1956 "Query messages": "查询消息",2406 "Query messages": "查询消息",
1957 "Score threshold": "分数阈值",2407 "Score threshold": "分数阈值",
1958 "Chunk boundary": "区块边界",2408 "Chunk boundary": "区块边界",
1959 "World Info settings": "世界信息设置",2409 "World Info settings": "世界书设置",
1960 "Enable for World Info": "启用世界信息",2410 "Enable for World Info": "启用世界书",
1961 "Enabled for all entries": "对所有条目启用",2411 "Enabled for all entries": "对所有条目启用",
1962 "Checked: all entries except ❌ status can be activated.": "勾选:除❌状态外的所有条目均可激活。",2412 "Checked: all entries except ❌ status can be activated.": "勾选时:除 ❌ 状态外的所有条目均可激活。",
1963 "Unchecked: only entries with ❌ status can be activated.": "未选中:只有具有🔗状态的条目才可以被激活。",2413 "Unchecked: only entries with ❌ status can be activated.": "未选中时:只有具有 ❌ 状态的条目才可以被激活。",
1964 "Max Entries": "最大条目数",2414 "Max Entries": "最大条目数",
1965 "File vectorization settings": "文件向量化设置",2415 "File vectorization settings": "文件向量化设置",
1966 "Enable for files": "为文件启用",2416 "Enable for files": "为文件启用",
@@ -1989,6 +2439,765 @@
1989 "Only used when Main API or WebLLM Extension is selected.": "仅在 Main API 或 WebLLM 扩展被选中时使用。",2439 "Only used when Main API or WebLLM Extension is selected.": "仅在 Main API 或 WebLLM 扩展被选中时使用。",
1990 "Old messages are vectorized gradually as you chat. To process all previous messages, click the button below.": "随着您聊天,旧消息会逐渐向量化。\n要处理所有以前的消息,请单击下面的按钮。",2440 "Old messages are vectorized gradually as you chat. To process all previous messages, click the button below.": "随着您聊天,旧消息会逐渐向量化。\n要处理所有以前的消息,请单击下面的按钮。",
1991 "View Stats": "查看统计数据",2441 "View Stats": "查看统计数据",
2442 "Warning: Listed member ${0} does not exist as a character. It will be removed from the group.": "警告:成员 ${0} 不是一个角色。Ta 将被移出群聊。",
2443 "Group Validation": "群聊验证",
2444 "Group Chat could not be saved": "无法保存群聊",
2445 "characters": "角色",
2446 "character": "角色",
2447 "Deleted group member swiped. To get a reply, add them back to the group.": "这个成员已经被移出群聊了,无法再生成 Ta 的备选回复。若有需要,请把 Ta 拉回来。",
2448 "Currently no group selected.": "当前无选中群聊。",
2449 "Not so fast! Wait for the characters to stop typing before deleting the group.": "这也太急了!请等待角色发完消息再解散群聊。",
2450 "Delete the group?": "解散群聊?",
2451 "This will also delete all your chats with that group. If you want to delete a single conversation, select a \"View past chats\" option in the lower left menu.": "此举将同时删除群聊的所有聊天。如果只是想删除一个对话,请在左下的菜单中选择“管理聊天文件”。",
2452 "Can't peek a character while group reply is being generated": "无法在生成群聊回复的时候私聊角色",
2453 "Group: ${0}": "群聊:${0}",
2454 "Group chat could not be deleted": "无法解散群聊",
2455 "Group chat could not be saved": "无法保存群聊",
2456 "Connected": "已连接",
2457 "Horde request timed out. Try again": "Horde 请求超时。请重试",
2458 "GUI Settings preset is not supported for Horde. Please select another preset.": "GUI 设置预设不支持 Horde。请选择另一个预设。",
2459 "Please enter your Horde API key": "请输入您的 Horde API 密钥",
2460 "Select Horde models": "选择 Horde 模型",
2461 "(Unknown)": "(未知)",
2462 "Token probabilities are not available when using Smooth Streaming.": "使用平滑流式传输时,无法获取 Token 概率数据。",
2463 "Generation in progress...": "正在生成中……",
2464 "No token probabilities available for the current message.": "此消息没有可用的 Token 概率数据。",
2465 "Reroll from this point": "从此处开始重新生成",
2466 "Feature unavailable": "功能不可用",
2467 "Due to API limitations, rerolling a token is not supported with OpenAI. Try switching to a different API.": "受限于 API,OpenAI 不支持重新生成一个 Token。请尝试切换到别的 API。",
2468 "You are using experimental macro features that require the new macro engine.": "您正在使用需要新宏引擎的实验性宏功能。",
2469 "Recognized Feature: ": "已识别功能:",
2470 "For more information on the new macro engine, visit the <br />${0}.": "有关新宏引擎的更多信息,请访问 <br />${0}。",
2471 "You can enable the engine any time under:<br />${0} → ${1}": "您可以在以下位置随时启用该引擎:<br />${0} → ${1}",
2472 "Would you like to enable it now?": "您想现在启用它吗?",
2473 "Entered reverse proxy address is not a valid URL": "您输入的反向代理地址不是有效的 URL",
2474 "Connecting To Proxy": "正在连接代理",
2475 "Update or remove your reverse proxy settings.": "更新或删除您的反向代理设置。",
2476 "Mandatory prompts exceed the context size.": "必要的提示词超过了上下文大小",
2477 "Not enough free tokens for mandatory prompts. Raise your token limit or disable custom prompts.": "必要的提示词都没空间放了。请上调您的上下文大小限制或禁用自定义提示词。",
2478 "An error occurred while counting tokens: Invalid character name": "计算 Token 时出错:角色名称无效",
2479 "The name of at least one character contained whitespaces or special characters. Please check your user and character name.": "至少一个角色的名字包含空格或特殊字符。请检查您的用户和角色名称。",
2480 "An unknown error occurred while counting tokens. Further information may be available in console.": "Token 计数时发生错误。详见控制台。",
2481 "${0} + $${1}": "${0} + $${1}",
2482 "Use OpenRouter website setting": "使用 OpenRouter 网站设置",
2483 "API returned an error": "API 返回错误",
2484 "Key saved; press \"Test Message\" to verify.": "密钥已保存,点击 \"发送测试消息\" 以验证。",
2485 "Invalid endpoint URL. Requests may fail.": "无效的端点 URL。请求可能失败。",
2486 "Invalid Azure endpoint URL. Requests may fail.": "无效的 Azure 端点 URL。请求可能失败。",
2487 "Status check bypassed": "已跳过状态检查",
2488 "Valid": "有效的",
2489 "Failed to save preset": "保存预设失败",
2490 "Preset name:": "预设名:",
2491 "Preset name should be unique.": "预设名需唯一。",
2492 "Invalid file": "无效文件",
2493 "No preset selected": "未选择预设",
2494 "Your preset contains proxy and/or custom endpoint settings.": "您的预设包含 代理 和/或 自定义端点设置。",
2495 "Do you want to remove these fields before exporting?": "要在导出前删除它们吗?",
2496 "Invalid logit bias preset file.": "无效的 logit bias 预设文件。",
2497 "Delete the preset? This action is irreversible and your current settings will be overwritten.": "删除该预设?此操作不可逆,且您当前的设置将被覆盖。",
2498 "Preset was not deleted from server": "预设未从服务器删除",
2499 "Preset deleted": "预设已删除",
2500 "Delete the preset?": "删除该预设?",
2501 "Hint: Use a character/group name to bind preset to a specific chat.": "提示:使用角色/群聊名以将预设绑定到特定的聊天。",
2502 "Service Account JSON is required for Vertex AI full version. Please validate and save your Service Account JSON.": "Vertex AI 完整版需要 Service Account JSON。请验证并保存您的 Service Account JSON。",
2503 "Please wait for the previous request to complete.": "请等待上一个请求完成。",
2504 "API connection successful!": "API 连接成功!",
2505 "Could not get a reply from API. Check your connection settings / API key and try again.": "无法从 API 获取回复。检查您的连接设置 或 API 密钥并重试。",
2506 "Proxy preset '${0}' not found in proxies array.": "未在代理数组中找到代理预设 '${0}'",
2507 "Proxy Saved": "代理已保存",
2508 "Proxy Deleted": "代理已删除",
2509 "Could not find proxy with name '${0}'": "找不到名为 '${0}' 的代理",
2510 "Proxy preset '${0}' not found": "未找到代理预设 '${0}'",
2511 "Please enter Service Account JSON content": "请输入 Service Account JSON 内容",
2512 "Missing required fields: ${0}": "缺少必填项:${0}",
2513 "Missing fields: ${0}": "${0} 未填写",
2514 "Invalid service account type. Expected \"service_account\"": "无效的 Service Account 类型。期望 \"service_account\"",
2515 "Invalid service account type": "无效的 Service Account 类型",
2516 "Service Account JSON is valid and saved securely": "Service Account JSON 已验证并安全保存",
2517 "Invalid JSON format": "JSON 格式无效",
2518 "Service Account JSON cleared": "Service Account JSON 已清除",
2519 "JSON appears valid - click \"Validate JSON\" to save": "JSON 格式有效 - 点击“验证 JSON”以保存",
2520 "Incomplete or invalid JSON": "不完整或无效的 JSON",
2521 "Service Account JSON is saved and ready to use": "Service Account JSON 已保存且可被使用",
2522 "Preset updated": "预设已更新",
2523 "Select a model": "选择模型",
2524 "Search models...": "搜索模型……",
2525 "All types (default)": "所有类型(默认)",
2526 "Set the crop position of the avatar image": "设置头像图片的剪裁位置",
2527 "Enter a name for this persona:": "为此用户设定命名:",
2528 "Cancel if you're just uploading an avatar.": "如果只是上传头像,请点击取消。",
2529 "Enter a description for this persona:": "描述这个用户设定:",
2530 "You can always add or change it later.": "你可以随时添加或更改它。",
2531 "You can now pick ${0} as a persona in the Persona Management menu.": "现在,可以在 用户设定管理菜单 选择 ${0} 作为您的用户设定。",
2532 "Persona Created": "用户设定已创建",
2533 "Persona Title (optional, display only)": "用户设定名称(选填,仅显示)",
2534 "Overwrite Existing Persona": "覆盖已存在的用户设定",
2535 "This character exists as a persona already. Do you want to overwrite it?": "当前角色已经是一个用户设定了。要覆盖现有的吗?",
2536 "Persona Description Macros": "用户设定描述宏",
2537 "This character has a description that uses <code>{{char}}</code> or <code>{{user}}</code> macros. Do you want to swap them in the persona description?": "这个角色有一个使用 <code>{{char}}</code> 或 <code>{{user}}</code> 宏的描述。你想在人设描述中交换它们吗?",
2538 "[No character connections. Click one of the buttons above to connect this persona.]": "【未绑定角色。点击上方的按钮以绑定此设定。】",
2539 "[Currently no personas connected]": "【目前无绑定的用户设定】",
2540 "Was used in current chat.": "被当前聊天使用。",
2541 "Is your default persona.": "是您的默认用户设定。",
2542 "Remove All Connections": "解除所有绑定",
2543 "All connections to ${0} have been removed.": "已解除 ${0} 的所有绑定。",
2544 "Personas Unlocked": "用户设定已解绑",
2545 "Enter a new name for this persona:": "输入用户设定的新名称:",
2546 "Persona ${0} selected and auto-locked to current chat": "用户设定 ${0} 已选择,并自动绑定至当前聊天",
2547 "Persona Selected": "用户设定已选择",
2548 "This persona is only temporarily chosen. Click for more info.": "当前用户设定仅被临时选择。点击获取更多信息。",
2549 "Temporary Persona": "临时用户设定",
2550 "Persona ${0} is now unlocked from this chat.": "用户设定 ${0} 已从此聊天解绑。",
2551 "Persona Unlocked": "用户设定已解锁",
2552 "Persona ${0} is now unlocked from character ${1}.": "用户设定 ${0} 已从角色 ${1} 解绑。",
2553 "Creating a new persona for currently selected user name and avatar...": "为当前选择的用户名和人设创建新用户设定……",
2554 "Persona Not Found": "未找到用户设定",
2555 "User persona ${0} is locked to ${1} in this chat": "在此聊天中,用户设定 ${0} 已绑定至 ${1}",
2556 "Persona Locked": "用户设定已绑定",
2557 "Unlinked existing persona${0}: ${1}": "已解绑现有用户设定 ${0}:${1}",
2558 "User persona ${0} is locked to character ${1}${2}": "用户设定 ${0} 已绑定至角色 ${1}${2}",
2559 "Are you sure you want to delete this avatar?": "你确定要删除这个人设吗?",
2560 "All information associated with its linked persona will be lost.": "所有与此设定关联的信息将丢失。",
2561 "The default persona was deleted. You will need to set a new default persona.": "默认用户设定已被删除。你需要设置一个新的默认设定。",
2562 "Default Persona Deleted": "默认用户设定已删除",
2563 "The locked persona was deleted. You will need to set a new persona for this chat.": "此聊天绑定的用户设定已被删除。你需要选择一个新的人设。",
2564 "Persona Deleted": "用户设定已删除",
2565 "You must bind a name to this persona before you can set a lorebook.": "在设置世界书前,你必须给这个用户设定起名。",
2566 "Persona Name Not Set": "用户设定未命名",
2567 "You must bind a name to this persona before you can set it as the default.": "在将其设置为默认前,你必须给这个用户设定起名。",
2568 "Are you sure you want to remove the default persona?": "你确定要删除默认用户设定吗?",
2569 "This persona will no longer be used by default when you open a new chat.": "在新聊天中,此用户设定将不再被默认使用。",
2570 "Default Persona Removed": "默认用户设定已删除",
2571 "Set Default Persona": "设置默认用户设定",
2572 "Are you sure you want to set \"${0}\" as the default persona?": "你确定要将 \"${0}\" 设置为默认用户设定吗?",
2573 "This name and avatar will be used for all new chats, as well as existing chats where the user persona is not locked.": "此名称和头像将会被所有新聊天应用,也同样会应用于所有未绑定用户设定的现有聊天中。",
2574 "Set to ${0}.This persona will be used by default when you open a new chat.": "已设为 ${0}。当你打开新聊天时,将默认使用这个用户设定。",
2575 "Default Persona": "默认用户设定",
2576 "Temporary persona in use.": "正在使用临时用户设定。",
2577 "A different persona is locked to this chat, or you have a different default persona set. The currently selected persona will only be temporary, and resets on reload. Consider locking this persona to the chat if you want to permanently use it.": "已经绑定了一个不同的用户设定至此聊天,或者你设置了一个不同的默认用户设定。当前选择的用户设定只是临时的,将在刷新时重置。如果想永久使用它,请将此用户设定绑定到聊天。",
2578 "Current Persona: ${0}": "当前用户设定:${0}",
2579 "Chat persona: ${0}": "聊天用户设定:${0}",
2580 "Default persona: ${0}": "默认用户设定:${0}",
2581 "Select Persona": "选择用户设定",
2582 "Multiple personas are connected to this character.\nSelect a persona to use for this chat.": "这个角色绑定了多个人设。\n请选择此次聊天中想要使用的人设。",
2583 "Auto-selected persona based on ${0} connection.<br />Your messages will now be sent as ${1}.": "基于 ${0}绑定 选择了人设。<br />你的消息将作为 ${1} 发送。",
2584 "Auto-locked this persona to current chat.": "已自动将此用户设定绑定至当前聊天。",
2585 "Persona Auto Selected": "已自动选择用户设定",
2586 "The following personas are connected to the current character.\n\nClick on a persona to select it for the current character.\nShift + Click to unlink the persona from the character.": "当前角色绑定了以下人设。\n\n点击为当前角色选择一个人设。\nShift + Click 以将人设从角色中解绑。",
2587 "Persona Connections": "用户设定绑定",
2588 "User persona ${0} is now unlocked from the current character ${1}.": "用户设定 ${0} 已解绑当前角色 ${1}。",
2589 "Persona unlocked": "用户设定已解绑",
2590 "Selected persona ${0} for current chat.": "已为当前聊天选择用户设定 ${0}。",
2591 "Connected Persona Selected": "已选择绑定的用户设定",
2592 "Invalid file selected": "选择的文件无效",
2593 "Invalid file format": "无效文件格式",
2594 "Personas restored with warnings. Check console for details.": "用户设定已恢复,但有警告。详见控制台。",
2595 "Personas restored successfully.": "用户设定成功恢复。",
2596 "All user-sent messages in this chat will be attributed to ${0}.": "此聊天中所有用户发送的消息将归属于 ${0}。",
2597 "Are you sure you want to duplicate this persona?": "确定要复制此用户设定吗?",
2598 "Unknown lock type \"${0}\"": "未知绑定类型 \"${0}\"",
2599 "Using /lock without a provided state to toggle the persona is deprecated. Please use /persona-lock instead.\n In the future this command with no state provided will return the current state, instead of toggling it.": "使用 /lock 命令时不提供状态来切换用户设定的方法已被弃用。请改用 /persona-lock 命令。\n 未来,若此命令没有提供状态,其将返回当前状态,而不是切换它。",
2600 "Deprecation Warning": "弃用警告",
2601 "Controlled by your operating system's reduced motion setting": "由您操作系统的“减少动画”设置控制",
2602 "Disable animations and transitions": "禁用动画和过渡效果",
2603 "Reload the chat to apply the changes. Click here to reload.": "重新加载聊天以应用更改。点击此处重新加载。",
2604 "Media Style changed": "媒体样式已更改",
2605 "Click here to reload.": "点击此处重新加载。",
2606 "Toggling the Experimental Macro Engine requires a reload.": "切换实验性宏引擎需要重新加载。",
2607 "Invalid data provided for master import": "全局导入所用数据无效",
2608 "Importing instruct template...": "正在导入格式指引模板……",
2609 "Instruct template detected": "检测到格式指引模板",
2610 "Importing as context template...": "正在导入为上下文模板……",
2611 "Context template detected": "检测到上下文模板",
2612 "Importing as system prompt...": "正在导入为系统提示词……",
2613 "System prompt detected": "检测到系统提示词",
2614 "Importing as settings preset...": "正在导入为设置预设……",
2615 "Text Completion settings detected": "检测到文本补全设置",
2616 "Importing as reasoning template...": "正在导入为推理模板……",
2617 "Reasoning template detected": "检测到推理模板",
2618 "No valid sections found in imported data": "导入数据中未找到有效部分",
2619 "Import": "导入",
2620 "No sections selected for import": "未选择要导入的部分",
2621 "Imported ${0} settings: ${1}": "已导入 ${0} 个设置: ${1}",
2622 "No sections selected for export": "未选择要导出的部分",
2623 "Cannot update GUI preset": "无法更新 GUI 预设",
2624 "Template updated": "模板已更新",
2625 "Template name:": "模板名称:",
2626 "Preset saved": "预设已保存",
2627 "Template saved": "模板已保存",
2628 "Preset could not be saved": "预设无法保存",
2629 "Preset could not be renamed": "预设无法重命名",
2630 "Cannot delete GUI preset": "无法删除 GUI 预设",
2631 "Failed to restore default preset": "恢复默认预设失败",
2632 "Failed to restore default template": "恢复默认模板失败",
2633 "Rename preset": "重命名预设",
2634 "Rename template": "重命名模板",
2635 "Enter a new name:": "输入新名称:",
2636 "Rename Preset": "重命名预设",
2637 "Preset renamed": "预设已重命名",
2638 "Template renamed": "模板已重命名",
2639 "Preset imported": "预设已导入",
2640 "Template imported": "模板已导入",
2641 "Delete this preset?": "删除此预设?",
2642 "Delete this template?": "删除此模板?",
2643 "This action is irreversible and your current settings will be overwritten.": "此操作不可逆,当前设置将被覆盖。",
2644 "Template deleted": "模板已删除",
2645 "Template was not deleted from server": "模板未从服务器删除",
2646 "Cannot restore GUI preset": "无法恢复 GUI 预设",
2647 "Default preset cannot be restored": "默认预设无法恢复",
2648 "Default template cannot be restored": "默认模板无法恢复",
2649 "Resetting a <b>default preset</b> will restore the default settings.": "重置 <b>默认预设</b> 将恢复默认设置。",
2650 "Resetting a <b>default template</b> will restore the default settings.": "重置 <b>默认模板</b> 将恢复默认设置。",
2651 "Default preset restored": "默认预设已恢复",
2652 "Default template restored": "默认模板已恢复",
2653 "Resetting a <b>custom preset</b> will restore to the last saved state.": "重置 <b>自定义预设</b> 将恢复到最后保存的状态。",
2654 "Resetting a <b>custom template</b> will restore to the last saved state.": "重置 <b>自定义模板</b> 将恢复到最后保存的状态。",
2655 "Preset restored": "预设已恢复",
2656 "Template restored": "模板已恢复",
2657 "Character Scenario": "角色场景",
2658 "World Info (↑Char)": "世界书(↑Char)",
2659 "World Info (↓Char)": "世界书(↓Char)",
2660 "Are you sure you want to delete this prompt?": "确定要删除此提示词吗?",
2661 "Existing prompts with the same ID will be overridden. Do you want to proceed?": "具有相同 ID 的现有提示词将被覆盖。是否继续?",
2662 "An error occurred while importing prompts. More info available in console.": "导入提示词时发生错误。详见控制台。",
2663 "This will reset the prompt order for this character. You will not lose any prompts.": "这将重置此角色的提示词顺序。您不会丢失任何提示词。",
2664 "Could not import prompts. Export failed validation.": "无法导入提示词。导出文件验证失败。",
2665 "Prompt import complete.": "提示词导入完成。",
2666 "Hidden reasoning - Add reasoning block": "隐藏推理 - 添加推理块",
2667 "Add reasoning block": "添加推理块",
2668 "Thought for ${0}": "思考了 ${0}",
2669 "Thinking...": "思考中……",
2670 "Get the contents of a reasoning block of a message. Returns an empty string if the message does not have a reasoning block.": "获取消息的推理块内容。若其没有推理块,则返回一个空字符串。",
2671 "Set the reasoning block of a message. Returns the reasoning block content.": "设置消息的推理块内容。返回推理块内容。",
2672 "Extracts the reasoning block from a string using the Reasoning Formatting settings.": "使用推理格式设置从字符串中提取推理块。",
2673 "Both prefix and suffix must be set in the Reasoning Formatting settings.": "推理格式设置中必须同时设置前缀和后缀。",
2674 "Reasoning Parse": "推理解析",
2675 "Invalid return type '${0}', defaulting to 'reasoning'.": "无效的返回类型 '${0}',将更改为默认值 'reasoning'。",
2676 "The prefix string used before reasoning blocks": "推理块开头使用的前缀字符串",
2677 "The suffix string used after reasoning blocks": "推理块结尾使用的后缀字符串",
2678 "The separator between thinking content and response": "思考内容与回复之间的分隔符",
2679 "Reasoning already exists.": "推理已存在。",
2680 "Edit Message": "编辑消息",
2681 "Remove Reasoning": "移除推理",
2682 "Are you sure you want to clear the reasoning?<br />Visible message contents will stay intact.": "确定要清除推理吗?<br />可见的消息内容将保持不变。",
2683 "[No label]": "【无备注】",
2684 "Forbidden": "已禁止",
2685 "To view your API keys here, set the value of allowKeysExposure to true in config.yaml file and restart the SillyTavern server.": "要在此处查看您的 API 密钥,请在 config.yaml 文件中将 allowKeysExposure 的值设置为 true,并重新启动 SillyTavern 服务器。",
2686 "Add Secret": "添加密钥",
2687 "Secret value (can be empty):": "密钥值(可为空):",
2688 "Label (optional):": "备注(选填):",
2689 "No value entered": "未输入值",
2690 "No value was entered for the secret. Do you want to add an empty secret?": "未输入密钥值。是否要添加一个空密钥?",
2691 "Secret ID copied to clipboard.": "密钥 ID 已复制到剪贴板。",
2692 "The key exposure might be disabled by the server config.": "服务器配置可能禁用了密钥显示功能。",
2693 "Failed to copy secret value": "复制密钥值失败",
2694 "Secret value copied to clipboard.": "密钥值已复制到剪贴板。",
2695 "Rename Secret": "重命名密钥",
2696 "Enter new label for the secret:": "输入密钥的新备注:",
2697 "Delete Secret: ${0}": "删除密钥:${0}",
2698 "Are you sure you want to delete this secret? This action cannot be undone.": "确定要删除此密钥吗?此操作无法撤销。",
2699 "Sets the ID of a currently active secret key. Gets the ID of the secret key if no value is provided.": "设置当前密钥的 ID。若留空,则获取当前密钥的 ID。",
2700 "The ID of the secret key that is now active.": "当前活动密钥的 ID。",
2701 "Suppress toast message notifications.": "关闭提示弹窗。",
2702 "The key to get the secret ID for. If not provided, will use the currently active API secrets.": "要获取密钥 ID 的密钥槽位。若留空,则获取当前的 API 密钥。",
2703 "The ID or a label of the secret key to set as active. If not provided, will return the currently active secret ID.": "要设置为活动状态的密钥 ID 或备注。如果未提供,则返回当前密钥的 ID。",
2704 "No secret key provided, and the key can't be resolved for the currently selected API type.": "未提供密钥,且无法解析当前选定 API 类型的密钥。",
2705 "No saved secrets found for the key: ${0}": "没有找到密钥槽位 ${0} 内保存的密钥。",
2706 "No active secret found for the key: ${0}": "没有找到密钥槽位 ${0} 下活动的密钥。",
2707 "No secret found with ID: ${0} for the key: ${1}": "没有找到密钥槽位 ${1} 下 ID 为 ${0} 的密钥。",
2708 "Secret with ID: ${0} is now active for the key: ${1}": "ID 为 ${0} 的密钥现在是密钥槽位 ${1} 下的活动密钥。",
2709 "Deletes a secret key by ID.": "通过 ID 删除一个密钥。",
2710 "The key to delete the secret from. If not provided, will use the currently active API secrets.": "要从中删除密钥的密钥槽位。若留空,则使用当前 API 密钥对应的槽位。",
2711 "The ID or a label of the secret key to delete. If not provided, will delete the active secret.": "要删除的密钥 ID 或备注。若留空,则删除当前活动密钥。",
2712 "Secret with ID: ${0} has been deleted for the key: ${1}": "ID 为 ${0} 的密钥已从密钥槽位 ${1} 中删除。",
2713 "Writes a secret key with a value and an optional label.": "使用值和一个可选的备注写入密钥。",
2714 "The ID of the newly created secret key.": "最近创建的密钥 ID。",
2715 "The key to write the secret to. If not provided, will use the currently active API secrets.": "要将密钥写入的密钥槽位。若留空,则使用当前 API 密钥对应的槽位。",
2716 "The label for the secret key. If not provided, will use the current date and time.": "密钥的备注。若留空,则使用当前日期和时间",
2717 "Whether to allow empty values.": "是否允许值为空。",
2718 "The value of the secret key to write.": "要写入的密钥值。",
2719 "No value provided for the secret key: ${0}": "给密钥 ${0} 提供的值为空",
2720 "Secret has been written for the key: ${0}": "密钥已写入槽位 ${0}",
2721 "Renames a secret key by ID.": "通过 ID 重命名一个密钥。",
2722 "The key to rename the secret in. If not provided, will use the currently active API secrets.": "要重命名密钥的密钥槽位。若留空,则使用当前 API 密钥的槽位。",
2723 "The ID of the secret to rename. If not provided, will rename the active secret.": "要重命名的密钥 ID。若留空,将重命名当前密钥。",
2724 "The new label for the secret key.": "密钥的新备注。",
2725 "No new label provided for the secret key: ${0}": "未提供密钥 ${0} 的新备注。",
2726 "Secret with ID: ${0} has been renamed to \"${1}\" for the key: ${2}": "槽位 ${2} 中ID为【${0}】的密钥已被重命名为【${1}】。",
2727 "Reads a secret key by ID. If key exposure is disabled, this command will not work!": "读取指定ID的密钥值。若已关闭密钥显示,则命令无法生效。",
2728 "The value of the secret key.": "密钥的值。",
2729 "The key to read the secret from. If not provided, will use the currently active API secrets.": "要读取密钥值的槽位。若留空,则使用当前 API 密钥的槽位。",
2730 "The ID or a label of the secret key to read. If not provided, will return the currently active secret value.": "要读取的密钥 ID 或备注。若留空,则返回当前活动密钥的值。",
2731 "Could not retrieve the secret value for key: ${0}. Key exposure might be disabled.": "无法获取密钥 ${0} 的值。可能是关闭了密钥显示。",
2732 "Duplicates the currently selected character.": "复制所选角色。",
2733 "Error: ${0} is not a valid API": "错误:API ${0} 无效",
2734 "API set to ${0}, trying to connect..": "API 已切换至 ${0},重连中……",
2735 "the current API": "当前 API",
2736 "Suppress the toast message on connection": "连接时隐藏弹窗消息",
2737 "API to connect to": "要连接到的 API",
2738 "Connect to an API. If no argument is provided, it will return the currently connected API.": "连接到一个 API。如果没有提供参数,它将返回当前连接的 API。",
2739 "Available APIs:": "可用的 API:",
2740 "Cannot run /impersonate command while the reply is being generated.": "无法在回复生成期间运行 /impersonate 命令。",
2741 "Whether to await for the triggered generation before continuing": "是否在继续之前等待触发的生成完成",
2742 "Calls an impersonation response, with an optional additional prompt.": "调用冒充响应,带有可选的附加提示词。",
2743 "If <code>await=true</code> named argument is passed, the command will wait for the impersonation to end before continuing.": "如果传递了 <code>await=true</code> 命名参数,命令将在继续之前等待冒充结束。",
2744 "Chat deletion timed out. Please try again.": "删除聊天超时,请重试。",
2745 "Deletes the current chat.": "删除当前聊天。",
2746 "Name must be provided as an argument to rename this chat.": "要重命名这个聊天,必须将名字作为参数传入。",
2747 "No chat selected that can be renamed.": "选择的聊天均无法重命名。",
2748 "Successfully renamed chat to: ${0}": "成功将聊天重命名为:${0}",
2749 "new chat name": "新聊天名称",
2750 "Renames the current chat.": "重命名当前聊天。",
2751 "chat file name": "聊天文件名称",
2752 "Returns the name of the current chat file into the pipe.": "将当前聊天文件名返回到管道中。",
2753 "Closes the current chat.": "关闭当前聊天。",
2754 "Not in a temporary chat": "不在临时聊天中",
2755 "Failed to open temporary chat": "开启临时聊天失败",
2756 "Opens a temporary chat with Assistant.": "和助手临时聊天。",
2757 "Toggle UI panels on/off": "切换 UI 面板显示开关",
2758 "Chat and settings saved.": "聊天和设置已保存。",
2759 "Forces a save of the current chat and settings": "强制保存当前聊天和设置",
2760 "Instruct template '${0}' not found": "未找到格式指引模板 '${0}'",
2761 "current template": "当前模板",
2762 "Suppress the toast message on template change": "切换模板时隐藏弹窗消息",
2763 "Force getting a name even if instruct mode is disabled": "即使禁用了格式指引模式也强制获取名称",
2764 "instruct template name": "格式指引模板名称",
2765 "Selects instruct mode template by name. Enables instruct mode if not already enabled.": "按名称选择指令模式。如果尚未启用指令模式,则启用它。",
2766 "Gets the current instruct template if no name is provided and instruct mode is enabled or <code>forceGet=true</code> is passed.": "如果没有提供名称且启用了指令模式,或者传递了 <code>forceGet=true</code>,则获取当前指令模式。",
2767 "Enables instruct mode.": "开启格式指引模式",
2768 "Disables instruct mode": "关闭格式指引模式",
2769 "Gets the current instruct mode state. If an argument is provided, it will set the instruct mode state.": "获取当前格式指引模式的状态。若提供了状态,则将格式指引模式设定为指定状态。",
2770 "instruct mode state": "格式指引模式状态",
2771 "Context template '${0}' not found": "未找到上下文模板 '${0}'",
2772 "template name": "模板名称",
2773 "context template name": "上下文模板名称",
2774 "Selects context template by name. Gets the current template if no name is provided": "根据名字选择上下文模板。若未提供名字,则获取当前模板。",
2775 "Opens the chat manager for the current character/group.": "为当前角色/群聊打开聊天管理",
2776 "help topic": "帮助主题",
2777 "slash commands (STscript)": "快捷命令(ST脚本)",
2778 "{{macros}} (text replacement)": "{{macros}}(文本替换)",
2779 "chat/text formatting": "聊天/文本格式化",
2780 "keyboard shortcuts": "快捷键",
2781 "Get help on macros, chat formatting and commands.": "获取关于宏、聊天格式化和命令的帮助。",
2782 "the current background": "当前背景",
2783 "background filename": "背景文件名",
2784 "Sets a background according to the provided filename. Partial names allowed.": "根据提供的文件名设置背景。允许部分名称。",
2785 "If no background is provided, this will return the currently selected background.": "如果没有提供背景,这将返回当前选择的背景。",
2786 "name must be a string": "名称必须是一个字符串",
2787 "preferCurrent cannot be a closure or array": "preferCurrent 无法作为闭包或数组",
2788 "quiet cannot be a closure or array": "quiet 无法作为闭包或数组",
2789 "the avatar key (unique identifier) of the character": "角色的头像键(唯一标识符)",
2790 "Supply one or more tags to filter down to the correct character for the provided name, if multiple characters have the same name.": "如果有多个同名角色,请提供一个或多个标签以筛选到正确的角色。",
2791 "Prefer current character or characters in a group, if multiple characters match": "如果匹配到多个角色,优先选择当前角色或群聊中的角色",
2792 "Do not show warning if multiple charactrers are found": "即使找到多个角色也不显示警告",
2793 "Character name - or unique character identifier (avatar key)": "角色名称 - 或唯一角色标识符(头像键)",
2794 "Searches for a character and returns its avatar key.": "搜索角色并返回其头像键。",
2795 "This can be used to choose the correct character for something like <code>/sendas</code> or other commands in need of a character name if you have multiple characters with the same name.": "如果您有多个同名角色,这可用于为 <code>/sendas</code> 或其他需要角色名称的命令选择正确的角色。",
2796 "Returns the avatar key for \"Chloe\".": "返回 \"Chloe\" 的头像键。",
2797 "Returns the avatar key for the character \"Chloe\" that is tagged with \"friend\".": "返回标记为 \"friend\" 的角色 \"Chloe\" 的头像键。",
2798 "This is useful if you for example have multiple characters named \"Chloe\", and the others are \"foe\", \"goddess\", or anything else, so you can actually select the character you are looking for.": "如果您有多个名为 \"Chloe\" 的角色,而其他的角色是 \"foe\"、\"goddess\" 或其他任何标签,这将非常有用,这样您就可以实际选择您正在寻找的角色。",
2799 "Optionally the text of the sent message, if specified in the \"return\" argument": "如果指定了 \"return\" 参数,则可选的发送消息文本",
2800 "Character avatar override (Can be either avatar key or just the character name to pull the avatar from)": "角色头像覆盖(可以是头像键,也可以只是角色名称以提取头像)",
2801 "Use compact layout": "使用紧凑布局",
2802 "position to insert the message (index-based, corresponding to message id). If not set, the message will be inserted at the end of the chat.\nNegative values (including -0) are accepted and will work similarly to how 'depth' usually works. For example, -1 will insert the message right before the last message in chat.": "插入消息的位置(基于索引,对应消息 ID)。如果未设置,则消息将插入到聊天的末尾。\n接受负值(包括 -0),其工作方式与 'depth' 通常的工作方式类似。例如,-1 将在聊天中的最后一条消息之前插入消息。",
2803 "The way how you want the return value to be provided": "你希望以何种方式提供返回值",
2804 "If true, does not alter quoted literal unnamed arguments": "如果为 true,则不修改带引号的字面量未命名参数",
2805 "Sends a message as a specific character. Uses the character avatar if it exists in the characters list.": "作为特定角色发送消息。如果字符列表中存在角色头像,则使用该头像。",
2806 "will send \"Hello, guys!\" from \"Chloe\".": "将从 \"Chloe\" 发送 \"Hello, guys!\"。",
2807 "will send a message as the character \"Chloe\", but utilizing the avatar from a character named \"BigBadBoss\".": "将作为角色 \"Chloe\" 发送消息,但使用名为 \"BigBadBoss\" 的角色的头像。",
2808 "If \"compact\" is set to true, the message is sent using a compact layout.": "如果 \"compact\" 设置为 true,则使用紧凑布局发送消息。",
2809 "compact layout": "紧凑布局",
2810 "Optional custom display name to use for this system narrator message.": "可选的自定义显示名称,用于此旁白消息。",
2811 "Sends a message as a system narrator.": "作为系统旁白发送消息。",
2812 "If <code>compact</code> is set to <code>true</code>, the message is sent using a compact layout.": "如果 <code>compact</code> 设置为 <code>true</code>,则使用紧凑布局发送消息。",
2813 "name": "名称",
2814 "Sets a name for future system narrator messages in this chat (display only). Default: System. Leave empty to reset.": "设置此聊天中未来旁白的名称(仅显示)。默认:系统。留空以重置。",
2815 "Whether to use a compact layout": "是否使用紧凑布局",
2816 "Adds a note/comment message not part of the chat.": "添加不属于聊天的备注/评论消息。",
2817 "Sets the message style to single document mode without names or avatars visible.": "设置消息样式为单文档样式,不显示名称或头像。",
2818 "Sets the message style to bubble chat mode.": "设置气泡聊天模式的消息样式。",
2819 "Sets the message style to flat chat mode.": "设置平面聊天模式的消息样式。",
2820 "Whether to await for the continued generation before proceeding": "是否要等待续写完成后再继续",
2821 "Continues the last message in the chat, with an optional additional prompt.": "继续聊天的最后一条消息,带有可选的附加提示词。",
2822 "If <code>await=true</code> named argument is passed, the command will await for the continued generation before proceeding.": "如果传递了 <code>await=true</code> 命名参数,命令将在继续之前等待继续生成。",
2823 "Continues the chat with no additional prompt and immediately proceeds to the next command.": "不带附加提示词继续聊天,并立即执行下一条命令。",
2824 "Continues the chat with the provided prompt and waits for the generation to finish.": "使用提供的提示词继续聊天,并等待生成完成。",
2825 "The character/group name": "角色/群聊名称",
2826 "Opens up a chat with the character or group by its name": "用它的名称打开与角色或群聊的聊天",
2827 "true/false - Whether the rename was successful": "true/false - 重命名是否成功",
2828 "Hide any blocking popups. (if false, the name is optional. If not supplied, a popup asking for it will appear)": "隐藏所有块弹窗(如果为 false,名称将会是可选的。如果未提供,将弹出一个窗口要求输入名称)",
2829 "Rename char in all previous chats": "在所有过往聊天中重命名角色",
2830 "new char name": "新的角色名称",
2831 "Renames the current character.": "重命名当前角色。",
2832 "Trim the output by the last sentence boundary": "按最后一个句子边界修剪输出",
2833 "Use a compact layout for the message": "为消息使用紧凑布局",
2834 "Position to insert the message (index-based, corresponding to message id). If not set, the message will be inserted at the end of the chat.\nNegative values (including -0) are accepted and will work similarly to how 'depth' usually works. For example, -1 will insert the message right before the last message in chat.": "插入消息的位置(基于索引,对应消息 ID)。如果未设置,则消息将插入到聊天的末尾。\n接受负值(包括 -0),其工作方式与 'depth' 通常的工作方式类似。例如,-1 将在聊天中的最后一条消息之前插入消息。",
2835 "Generates a system message using a specified prompt.": "使用指定提示词生成系统消息。",
2836 "Asks a specified character card a prompt. Character name must be provided in a named argument.": "向指定角色卡提出一个提示词。必须在命名参数中提供角色名称。",
2837 "Deletes all messages attributed to a specified name.": "删除所有归属于指定名称的消息。",
2838 "whether to use a compact layout": "是否使用紧凑布局",
2839 "display name": "显示名称",
2840 "Adds a user message to the chat log without triggering a generation.": "向聊天记录添加用户消息,而不触发生成。",
2841 "If <code>name</code> is set, it will be displayed as the message sender. Can be an empty for no name.": "如果设置了 <code>name</code>,它将显示为消息发送者。可以为空以表示没有名称。",
2842 "group member index (starts with 0) or name": "群聊成员索引(从 0 开始)或名称",
2843 "Triggers a message generation. If in group, can trigger a message for the specified group member index or name.": "触发消息生成。如果在群聊中,可以为指定索引或名字的群成员触发消息。",
2844 "If <code>await=true</code> named argument is passed, the command will await for the triggered generation before continuing.": "如果传递了 <code>await=true</code> 命名参数,命令将在继续之前等待触发的生成。",
2845 "only hide messages from a certain character or persona": "仅隐藏来自特定角色或用户设定的消息",
2846 "message index (starts with 0) or range, defaults to the last message index if not provided": "消息索引(从 0 开始)或范围,如果未提供则默认为最后一条消息的索引",
2847 "Hides a chat message from the prompt.": "从提示词中隐藏聊天消息。",
2848 "only unhide messages from a certain character or persona": "仅不隐藏来自特定角色或用户设定的消息",
2849 "Unhides a message from the prompt.": "从提示词中取消隐藏消息。",
2850 "Cannot run /member-get command outside of a group chat.": "无法在群聊外运行 /member-get 命令。",
2851 "'/member-get field=' argument required!": "'/member-get field=' 需要参数!",
2852 "No group member found using ${0} ${1}": "使用 ${0} ${1} 未找到群成员",
2853 "Whether to retrieve the name, index, id, or avatar.": "是否检索名称、索引、ID 或头像。",
2854 "Character name": "角色名称",
2855 "Group member index": "群成员索引",
2856 "Character avatar": "角色头像",
2857 "Character index": "角色索引",
2858 "member index (starts with 0), name, or avatar": "成员索引(从 0 开始)、名称或头像",
2859 "Retrieves a group member's name, index, id, or avatar.": "获取群成员的名称、索引、ID 或头像。",
2860 "member index (starts with 0) or name": "成员索引(从 0 开始)或名称",
2861 "Disables a group member from being drafted for replies.": "禁言群成员。",
2862 "Enables a group member to be drafted for replies.": "解除群成员禁言。",
2863 "Adds a new group member to the group chat.": "向群聊添加成员。",
2864 "Removes a group member from the group chat.": "从群聊中移除成员。",
2865 "Moves a group member up in the group chat list.": "将群成员在列表中上移。",
2866 "Moves a group member down in the group chat list.": "将群成员在列表中下移。",
2867 "Shows a group member character card without switching chats.": "在不切换聊天的情况下显示群成员角色卡。",
2868 "Shows the character card for the character named \"Gloria\".": "显示角色名为 \"Gloria\" 的角色卡。",
2869 "Returns the total number of group members in the group chat list.": "返回群人数。",
2870 "the new, currently selected swipe id": "新的,当前选中的备选回复 ID",
2871 "1-based swipe id": "备选回复编号从 1 开始",
2872 "Deletes a swipe from the last chat message. If swipe id is not provided, it deletes the current swipe.": "删除最后一条聊天消息的备选回复。如果未提供 ID,则删除当前备选回复。",
2873 "Deletes the current swipe.": "删除当前备选回复。",
2874 "Deletes the second swipe from the last chat message.": "删除最后一条聊天消息的第二个备选回复。",
2875 "the text": "文本",
2876 "title of the toast message": "弹窗消息的标题",
2877 "severity level of the toast message": "弹窗消息的级别",
2878 "time in milliseconds to display the toast message. Set this and 'extendedTimeout' to 0 to show indefinitely until dismissed.": "显示弹窗消息的时间(毫秒为单位)。将此值和 'extendedTimeout' 设置为 0 以无限期显示直到被关闭。",
2879 "time in milliseconds to display the toast message. Set this and 'timeout' to 0 to show indefinitely until dismissed.": "显示弹窗消息的时间(毫秒为单位)。将此值和 'timeout' 设置为 0 以无限期显示直到被关闭。",
2880 "prevent duplicate toasts with the same message from being displayed.": "防止显示具有相同消息的重复弹窗。",
2881 "wait for the toast to be dismissed before continuing.": "等待弹窗被关闭后再继续。",
2882 "additional CSS class to add to the toast message (e.g. for custom styling)": "添加到弹窗消息的额外 CSS 类(如自定义样式)",
2883 "custom CSS color of the toast message. Accepts all valid CSS color values (e.g. 'red', '#FF0000', 'rgb(255, 0, 0)').<br />>Can be more customizable with the 'cssClass' argument and custom classes.": "弹窗消息的自定义 CSS 颜色。接受所有有效的 CSS 颜色值(例如 'red', '#FF0000', 'rgb(255, 0, 0)')。<br />>可以通过 'cssClass' 参数和自定义类进行更多自定义。",
2884 "whether to escape HTML in the toast message.": "是否在弹窗消息中转义 HTML。",
2885 "a closure to call when the toast is clicked. This executed closure receives scope as provided in the script. Careful about possible side effects when manipulating variables and more.": "当弹窗被点击时调用的闭包。此执行闭包接收脚本中提供的作用域。小心操作变量时可能产生的副作用。",
2886 "generated text": "生成的文本",
2887 "lock user input during generation": "生成期间锁定用户输入",
2888 "in-prompt character name for instruct mode (or unique character identifier (avatar key), which will be used as name)": "格式指引模式的提示词内角色名称(或唯一的角色标识符(头像键),将被作为名称使用)",
2889 "API response length in tokens": "API 回复长度(Token 为单位)",
2890 "role of the output prompt": "输出提示词的身份",
2891 "Generates text using the provided prompt and passes it to the next command through the pipe, optionally locking user input while generating and allowing to configure the in-prompt name for instruct mode (default = \"System\").": "使用提供的提示词生成文本并通过管道将其传递给下一条命令,可选择在生成时锁定用户输入,并允许配置指令模式的提示词内名称(默认为 \"System\")。",
2892 "\"as\" argument controls the role of the output prompt: system (default) or char. If \"length\" argument is provided as a number in tokens, allows to temporarily override an API response length.": "\"as\" 参数控制输出提示词的身份:system(默认)或 char。如果 \"length\" 参数作为 Token 数提供,则允许临时覆盖 API 响应长度。",
2893 "use instruct mode": "使用格式指引模式",
2894 "one-time custom stop strings": "本次自定义停止字符串",
2895 "system prompt at the start": "开始时的系统提示词",
2896 "prefill prompt at the end": "结束时的预填充提示词",
2897 "trim {{user}} and {{char}} prefixes from the output": "从输出中去除 {{user}} 和 {{char}} 前缀",
2898 "Generates text using the provided prompt and passes it to the next command through the pipe, optionally locking user input while generating. Does not include chat history or character card.": "使用提供的提示词生成文本并通过管道将其传递给下一条命令,可选择在生成时锁定用户输入。不包括聊天历史记录或角色卡。",
2899 "Use instruct=off to skip instruct formatting, e.g. <pre><code>/genraw instruct=off Why is the sky blue?</code></pre>": "使用 instruct=off 跳过指令格式化,例如 <pre><code>/genraw instruct=off 为什么天空是蓝色的?</code></pre>",
2900 "Use stop=... with a JSON-serialized array to add one-time custom stop strings, e.g. <pre><code>/genraw stop=[\"\n\"] Say hi</code></pre>": "使用 stop=... 和 JSON 序列化数组添加一次性自定义停止字符串,例如 <pre><code>/genraw stop=[\"\n\"] 说嗨</code></pre>",
2901 "\"as\" argument controls the role of the output prompt: system (default) or char. \"system\" argument adds an (optional) system prompt at the start.": "\"as\" 参数控制输出提示词的身份:system(默认)或 char。\"system\" 参数在开头添加(可选)系统提示词。",
2902 "If \"length\" argument is provided as a number in tokens, allows to temporarily override an API response length.": "如果 \"length\" 参数作为 Token 数提供,则允许临时覆盖 API 响应长度。",
2903 "the new swipe id": "新的备选回复ID",
2904 "switch to the new swipe": "切换到新的备选回复",
2905 "Adds a swipe to the last chat message.": "向最后一条聊天消息添加备选回复。",
2906 "Use switch=true to switch to directly switch to the new swipe.": "使用 switch=true 直接切换到新回复。",
2907 "true/false, whether the generation was running and got stopped": "true/false,生成是否正在运行并被停止",
2908 "Stops the generation and any streaming if it is currently running.": "若当前正在运行,则停止所有生成和流式传输。",
2909 "Note: This command cannot be executed from the chat input, as sending any message or script from there is blocked during generation. But it can be executed via automations or QR scripts/buttons.": "注意:此命令无法从聊天输入执行,因为在生成期间会阻止从那里发送任何消息或脚本。但它可以通过自动化或 QR 脚本/按钮执行。",
2910 "Whether to suppress the toast message notifying about the /abort call.": "是否要隐藏 /abort 命令的弹窗消息。",
2911 "The reason for aborting command execution. Shown when quiet=false": "中止命令执行的原因。在 quiet=false 时显示",
2912 "Aborts the slash command batch execution.": "放弃批量执行的快捷命令。",
2913 "matching item": "匹配项",
2914 "list of items to match against": "用于进行比对的项目列表",
2915 "fuzzy match threshold (0.0 to 1.0)": "模糊匹配阈值(0.0 到 1.0)",
2916 "fuzzy match mode": "模糊匹配模式",
2917 "first match below the threshold": "低于阈值的第一个匹配项",
2918 "best match below the threshold": "低于阈值的最佳匹配项",
2919 "text to search": "要搜索的文本",
2920 "Performs a fuzzy match of each item in the <code>list</code> against the <code>text to search</code>. If any item matches, then its name is returned. If no item matches the text, no value is returned.": "对 <code>list</code> 中的每一项与 <code>text to search</code> 进行模糊匹配。如果有任何项目匹配,则返回其名称。如果没有项目匹配文本,则不返回任何值。",
2921 "The optional <code>threshold</code> (default is 0.4) allows control over the match strictness.": "可选的 <code>threshold</code>(默认值为 0.4)允许控制匹配的严格程度。",
2922 "A low value (min 0.0) means the match is very strict.": "较低的值(最小 0.0)意味着匹配非常严格。",
2923 "At 1.0 (max) the match is very loose and will match anything.": "在 1.0(最大值)时,匹配非常宽松,可以匹配任何内容。",
2924 "The optional <code>mode</code> argument allows to control the behavior when multiple items match the text.": "可选的 <code>mode</code> 参数允许控制多个项目匹配文本时的行为。",
2925 "<code>first</code> (default) returns the first match below the threshold.": "<code>first</code>(默认)返回低于阈值的第一个匹配项。",
2926 "<code>best</code> returns the best match below the threshold.": "<code>best</code> 返回低于阈值的最佳匹配项。",
2927 "The returned value passes to the next command through the pipe.": "返回值通过管道传递给下一条命令。",
2928 "Command /pass does not support multiple closures": "命令 /pass 不支持多个闭包",
2929 "the provided value": "提供的值",
2930 "text": "文本",
2931 "passes the text to the next command through the pipe.": "通过管道将文本传递给下一条命令。",
2932 "milliseconds": "毫秒",
2933 "Delays the next command in the pipe by the specified number of milliseconds.": "将管道中的下一条命令延迟指定的毫秒数。",
2934 "user input": "用户输入",
2935 "default value of the input field": "输入字段的默认值",
2936 "popup window will be shown larger in height, with more space for content (input field needs to be sized via 'rows' argument)": "弹窗窗口将显示得更高,为内容提供更多空间(输入字段需要通过 'rows' 参数进行调整大小)",
2937 "popup window will be shown wider, with a wider input field": "弹窗窗口和输入框将显示得更宽",
2938 "text for the ok button": "确定按钮的文本",
2939 "number of rows for the input field (lines being displayed)": "输入框显示的行数",
2940 "closure to execute when the ok button is clicked or the input is closed as successful (via Enter, etc)": "当点击确定按钮或输入关闭成功(如 Enter)时执行的闭包",
2941 "closure to execute when the cancel button is clicked or the input is closed as cancelled (via Escape, etc)": "当点击取消按钮或输入被取消(如 Esc)时执行的闭包",
2942 "text to display": "要显示的文本",
2943 "Shows a popup with the provided text and an input field.": "显示带有提供的文本和输入字段的弹出窗口。",
2944 "The <code>default</code> argument is the default value of the input field, and the text argument is the text to display.": "<code>default</code> 参数是输入字段的默认值,text 参数是要显示的文本。",
2945 "result of the executed closure of QR": "QR 执行闭包的结果",
2946 "named arguments": "命名参数",
2947 "scoped variable or qr label": "作用域变量或 QR 标签",
2948 "Runs a closure from a scoped variable, or a Quick Reply with the specified name from a currently active preset or from another preset.": "运行来自作用域变量的闭包,或来自当前活动预设或另一个预设的具有指定名称的快捷回复。",
2949 "Named arguments can be referenced in a QR with <code>{{arg::key}}</code>.": "命名参数可以在 QR 中用 <code>{{arg::key}}</code> 引用。",
2950 "show message author names": "显示消息作者名称",
2951 "include hidden messages": "包含隐藏消息",
2952 "filter messages by role": "按身份过滤消息",
2953 "message index (starts with 0) or range": "消息索引(从 0 开始)或范围",
2954 "the specified message or range of messages as a string": "指定消息或消息范围的字符串表示",
2955 "Returns the specified message or range of messages as a string.": "以字符串形式返回指定的消息或消息范围。",
2956 "Use the <code>hidden=off</code> argument to exclude hidden messages.": "使用 <code>hidden=off</code> 参数排除隐藏消息。",
2957 "Use the <code>role</code> argument to filter messages by role. Possible values are: system, assistant, user.": "使用 <code>role</code> 参数按身份过滤消息。可能的值为:system、assistant、user。",
2958 "Returns the 10th message.": "返回第 10 条消息。",
2959 "Returns messages 5 through 10 with author names.": "返回第 5 到第 10 条消息及作者姓名。",
2960 "Sets the user input to the specified text and passes it to the next command through the pipe.": "将用户输入设置为指定文本,并通过管道将其传递给下一条命令。",
2961 "popup text": "弹窗文本",
2962 "allows vertical scrolling of the content": "允许内容垂直滚动",
2963 "show large popup": "显示大弹窗",
2964 "show wide popup": "显示宽弹窗",
2965 "show wider popup": "显示更宽弹窗",
2966 "show transparent popup": "显示透明弹窗",
2967 "text for the OK button": "确定按钮的文本",
2968 "text for the Cancel button": "取消按钮的文本",
2969 "if enabled, returns the popup result (as an integer) instead of the popup text. Resolves to 1 for OK and 0 cancel button, empty string for exiting out.": "如果启用,返回弹窗结果(作为整数)而不是弹窗文本。确定按钮返回 1,取消按钮返回 0,退出时返回空字符串。",
2970 "Shows a blocking popup with the specified text and buttons.": "显示带有指定文本和按钮的阻塞弹出窗口。",
2971 "Returns the popup text.": "返回弹出文本。",
2972 "clicked button label (or array of labels if multiple is enabled)": "点击的按钮标签(如果启用多个按钮,则为标签数组)",
2973 "button labels": "按钮标签",
2974 "if enabled multiple buttons can be clicked/toggled, and all clicked buttons are returned as an array": "如果启用多个按钮,可以点击/切换,并将所有点击的按钮作为数组返回",
2975 "Returns the clicked button label into the pipe or empty string if canceled.": "将点击的按钮标签返回到管道中,如果取消则返回空字符串。",
2976 "trimmed text": "修剪后的文本",
2977 "number of tokens to keep": "保留的 Token 数量",
2978 "trim direction": "修剪方向",
2979 "Trims the start or end of text to the specified number of tokens.": "将文本的开头或结尾修剪为指定的 Token 数量。",
2980 "Trims the text to the start of the first full sentence.": "将文本修剪到第一个完整句子的开头。",
2981 "Trims the text to the end of the last full sentence.": "将文本修剪到最后一个完整句子的末尾。",
2982 "injection ID": "注入 ID",
2983 "injection position": "注入位置",
2984 "injection depth": "注入深度",
2985 "include injection content into World Info scans": "将注入内容包含在世界书扫描中",
2986 "role for in-chat injections": "聊天内注入的身份",
2987 "remove injection after generation": "在生成后移除注入",
2988 "if a filter is defined, an injection will only be performed if the closure returns true": "如果定义了过滤器,只有在闭包返回 true 时才会执行注入",
2989 "Injects a text into the LLM prompt for the current chat. Requires a unique injection ID (will be auto-generated if not provided). Positions: \"before\" main prompt, \"after\" main prompt, in-\"chat\", hidden with \"none\" (default: after). Depth: injection depth for the prompt (default: 4). Role: role for in-chat injections (default: system). Scan: include injection content into World Info scans (default: false). Hidden injects in \"none\" position are not inserted into the prompt but can be used for triggering WI entries. Returns the injection ID.": "为当前聊天的 LLM 提示词中注入文本。需要一个唯一的注入 ID(若未指定则自动生成)。位置:主提示词\"之前\",主提示词\"之后\",\"聊天\"内,\"隐藏\"(默认: after)。深度:提示词的注入深度(默认:4)。身份: 聊天内注入的身份(默认:system)。扫描:将注入内容也纳入世界书扫描(默认:false)。\"隐藏\" 则不会被注入到提示词,但是可以用于触发世界书条目。返回注入 ID。",
2990 "Lists all script injections for the current chat. Displays injects in a popup by default. Use the <code>return</code> argument to change the return type.": "列出当前聊天的所有脚本注入。默认在弹窗中显示注入内容。使用 <code>return</code> 参数来更改返回类型。",
2991 "Optionally the JSON object of script injections": "可选的脚本注入 JSON 对象",
2992 "!!! DEPRECATED - use \"return\" instead !!! output format": "!!! 已弃用 - 请改用 \"return\" !!! 输出格式",
2993 "Show injects in a popup.": "在弹窗中显示注入内容。",
2994 "Post a system message to the chat.": "向聊天发送系统消息。",
2995 "Just return the injects as a JSON object.": "仅返回注入内容作为 JSON 对象。",
2996 "injection ID or a variable name pointing to ID": "注入 ID 或指向 ID 的变量名",
2997 "Removes a script injection for the current chat. If no ID is provided, removes all script injections.": "移除当前聊天的脚本注入。如果没有提供 ID,则移除所有脚本注入。",
2998 "Unnamed argument cannot be a closure for command /tokens": "未命名参数不能作为命令 /tokens 的闭包",
2999 "number of tokens": "Token 数量",
3000 "Counts the number of tokens in the provided text.": "统计给定文本中的 Token 数量。",
3001 "current model": "当前模型",
3002 "suppress the toast message on model change": "在模型更改时隐藏提示弹窗",
3003 "model name": "模型名称",
3004 "Sets the model for the current API. Gets the current model name if no argument is provided.": "设置当前 API 的模型。若留空,则获取当前模型名称。",
3005 "true/false state of prompt(s)": "提示词的开关状态",
3006 "Prompt entry identifier(s) to retrieve": "要获取的提示词条目标识符",
3007 "Prompt entry name(s) to retrieve": "要获取的提示词条目名称",
3008 "Whether the return will be simple, a list, or a dict.": "返回类型为自动、列表还是字典。",
3009 "Gets the state of the specified prompt entries.": "获取指定提示词条目的状态。",
3010 "If <code>return</code> is <code>simple</code> (default) then the return will be a single value if only one value was retrieved; otherwise uses a dict (if the identifier parameter was used) or a list.": "如果 <code>return</code> 为 <code>simple</code>(默认),则如果只检索到一个值,则返回单个值;否则使用字典(如果使用了 identifier 参数)或列表。",
3011 "Prompt entry identifier(s) to target": "要操作的提示词条目标识符",
3012 "Prompt entry name(s) to target": "要操作的提示词条目名称",
3013 "Set entry/entries on or off": "开关条目",
3014 "Sets the specified prompt manager entry/entries on or off.": "开启或关闭指定的提示词管理器条目。",
3015 "The chosen icon name or false if cancelled.": "所选图标名称,如果取消则为 false。",
3016 "Opens a popup with all the available Font Awesome icons and returns the selected icon's name.": "打开包含所有可用 Font Awesome 图标的弹出窗口,并返回所选图标的名称。",
3017 "the current API url": "当前 API 地址",
3018 "API to set/get the URL for - if not provided, current API is used": "要设置/获取 URL 的 API - 如果未提供,则使用当前 API",
3019 "Whether to auto-connect to the API after setting the URL": "设置 URL 后是否自动连接 API",
3020 "suppress the toast message on API change": "在 API 更改时隐藏提示弹窗",
3021 "API url to connect to": "要连接的 API 地址",
3022 "Set the API URL / server URL / endpoint for the currently selected API, including the port. If no argument is provided, it will return the current API url.": "设置当前选择 API 的 API URL / 服务器 URL / 端点,包括端口。如果未提供参数,它将返回当前 API URL。",
3023 "If a manual API is provided to <b>set</b> the URL, make sure to set <code>connect=false</code>, as auto-connect only works for the currently selected API, or consider switching to it with <code>/api</code> first.": "如果提供手动 API 来<b>设置</b> URL,请确保设置 <code>connect=false</code>,因为自动连接仅适用于当前选择的 API,或者考虑先使用 <code>/api</code> 切换到它。",
3024 "This slash command works for most of the Text Completion sources, KoboldAI Classic, and also Custom OpenAI compatible, Z.AI, and Google Vertex AI for the Chat Completion sources. If unsure which APIs are supported, check the auto-completion of the optional <code>api</code> argument of this command.": "此快捷命令适用于大多数文本补全源、KoboldAI Classic,以及聊天补全源的 Custom OpenAI 兼容、Z.AI 和 Google Vertex AI。如果不确定支持哪些 API,请检查此命令的可选 <code>api</code> 参数的自动补全。",
3025 "current tokenizer": "当前分词器",
3026 "tokenizer name": "分词器名称",
3027 "Selects tokenizer by name. Gets the current tokenizer if no name is provided.": "按名称选择分词器。如果没有提供名称,则获取当前分词器。",
3028 "Available tokenizers:": "可用的分词器:",
3029 "uppercase string": "转换为大写",
3030 "text to affect": "受影响的文本",
3031 "Converts the provided string to uppercase.": "将提供的字符串转换为大写。",
3032 "lowercase string": "转换为小写",
3033 "Converts the provided string to lowercase.": "将提供的字符串转换为小写。",
3034 "substring": "子字符串",
3035 "start index": "起始索引",
3036 "end index": "结束索引",
3037 "Extracts text from the provided string.": "从提供的字符串中提取文本。",
3038 "If <code>start</code> is omitted, it's treated as 0.<br />": "如果省略 <code>start</code>,则视为 0。<br />",
3039 "If <code>start</code> < 0, the index is counted from the end of the string.<br />": "如果 <code>start</code> < 0,则从字符串末尾开始计数。<br />",
3040 "If <code>start</code> >= the string's length, an empty string is returned.<br />": "如果 <code>start</code> >= 字符串长度,则返回空字符串。<br />",
3041 "If <code>end</code> is omitted, or if <code>end</code> >= the string's length, extracts to the end of the string.<br />": "如果省略 <code>end</code>,或者 <code>end</code> >= 字符串长度,则提取到字符串末尾。<br />",
3042 "If <code>end</code> < 0, the index is counted from the end of the string.<br />": "如果 <code>end</code> < 0,则从字符串末尾开始计数。<br />",
3043 "If <code>end</code> <= <code>start</code> after normalizing negative values, an empty string is returned.": "如果归一化负值后 <code>end</code> <= <code>start</code>,则返回空字符串。",
3044 "Returns true if the current device is a mobile device, false otherwise. Equivalent to <code>{{isMobile}}</code> macro.": "如果当前设备是移动设备,则返回 true,否则返回 false。等同于 <code>{{isMobile}}</code> 宏。",
3045 "Renders a specified number of messages into the chat window. Displays all messages if no argument is provided.": "在聊天窗口中渲染指定数量的消息。如果未提供参数,则显示所有消息。",
3046 "scroll to the top after rendering": "渲染后滚动到顶部",
3047 "number of messages": "消息数量",
3048 "Reloads the current chat.": "重新加载当前聊天。",
3049 "Argument of 'pattern=' cannot be empty": "'pattern=' 的参数不能为空",
3050 "Invalid '/replace mode=' argument specified!": "指定的 '/replace mode=' 参数无效!",
3051 "replaced text": "替换后的文本",
3052 "Replaces occurrence(s) of a pattern": "替换匹配到的内容",
3053 "pattern to search with": "要匹配的规则",
3054 "replacement text for matches": "将匹配处替换为",
3055 "Replaces text within the provided string based on the pattern.": "根据模式替换提供的字符串中的文本。",
3056 "If <code>mode</code> is <code>literal</code> (or omitted), <code>pattern</code> is a literal search string (case-sensitive).<br />": "如果 <code>mode</code> 为 <code>literal</code>(或省略),则 <code>pattern</code> 为字面量搜索字符串(区分大小写)。<br />",
3057 "If <code>mode</code> is <code>regex</code>, <code>pattern</code> is parsed as an ECMAScript Regular Expression.<br />": "如果 <code>mode</code> 为 <code>regex</code>,则将 <code>pattern</code> 解析为 ECMAScript 正则表达式。<br />",
3058 "The <code>replacer</code> replaces based on the <code>pattern</code> in the input text.<br />": "<code>replacer</code> 基于输入文本中的 <code>pattern</code> 进行替换。<br />",
3059 "If <code>replacer</code> is omitted, the replacement(s) will be an empty string.<br />": "如果省略 <code>replacer</code>,则替换项将为空字符串。<br />",
3060 "The value of 'pattern' argument is not a valid regular expression.": "'pattern' 参数的值不是有效的正则表达式。",
3061 "pattern to find": "查找规则",
3062 "text to test": "要测试的文本",
3063 "Tests text for a regular expression match.": "测试文本是否匹配正则表达式。",
3064 "Returns <code>true</code> if the match is found, <code>false</code> otherwise.": "如果找到匹配项,则返回 <code>true</code>,否则返回 <code>false</code>。",
3065 "group array for each match": "每次匹配的分组数组",
3066 "text to match against": "要匹配的文本",
3067 "Retrieves regular expression matches in the given text": "检索给定文本中的正则表达式匹配项",
3068 "Returns an array of groups (with the first group being the full match). If the regex contains the global flag (i.e. <code>/g</code>), multiple nested arrays are returned for each match. If the regex is global, returns <code>[]</code> if no matches are found, otherwise it returns an empty string.": "返回组数组(第一个组为完全匹配)。如果正则表达式包含全局标志(即 <code>/g</code>),则为每个匹配项返回多个嵌套数组。如果正则表达式是全局的,如果未找到匹配项则返回 <code>[]</code>,否则返回空字符串。",
3069 "Invalid message index: ${0}. Please enter a number between 0 and ${1}.": "无效的消息索引:${0}。请输入 0 到 ${1} 之间的数字。",
3070 "Could not find element for message ${0}. It might not be rendered yet or the index is invalid.": "找不到消息 ${0} 的元素。可能尚未渲染或索引无效。",
3071 "The message index (0-based) to scroll to.": "要滚动到的消息索引(从 0 开始)。",
3072 "Scrolls the chat view to the specified message index. Index starts at 0.": "将聊天视图滚动到指定的消息索引。索引从 0 开始。",
3073 "Scrolls to the 11th message (id=10).": "滚动到第 11 条消息 (id=10)。",
3074 "clipboard text": "剪贴板文本",
3075 "Clipboard API not available in this context.": "剪贴板 API 在此上下文中不可用。",
3076 "Failed to read clipboard text. Have you granted the permission?": "读取剪贴板文本失败。您是否已授予权限?",
3077 "Retrieves the text from the OS clipboard. Only works in secure contexts (HTTPS or localhost). Browser may ask for permission.": "检索操作系统剪贴板中的文本。仅在安全上下文(HTTPS 或 localhost)中有效。浏览器可能会请求权限。",
3078 "text to copy to the clipboard": "要复制到剪贴板的文本",
3079 "Copies the provided text to the OS clipboard. Returns an empty string.": "将提供的文本复制到操作系统剪贴板。返回空字符串。",
3080 "Sets a \"Prompt Post-Processing\" type. Gets the current selection if no value is provided.": "设置 \"提示词后处理\" 类型。如果没有提供值,则获取当前选择。",
3081 "value": "值",
3082 "Invalid value \"${0}\". Valid values are: ${1}": "无效值 \"${0}\"。可用值有:${1}",
3083 "The new reroll seed value.": "重新生成的新种子值。",
3084 "Optional seed value to set. If not provided, increments current seed by 1.": "要设置的可选种子值。若未提供,则设置为当前种子 +1。",
3085 "Rerolls all <code>{{pick}}</code> macro choices in the current chat.": "重新随机选择当前聊天中的所有 <code>{{pick}}</code> 宏选项。",
3086 "The <code>{{pick}}</code> macro normally keeps stable choices per chat. This command changes the seed used for all picks, causing them to resolve to (possibly) different values.": "<code>{{pick}}</code> 宏通常在每个聊天中保持稳定的选择。此命令更改用于所有选择的种子,导致它们解析为(可能)不同的值。",
3087 "If a number is provided, sets the seed to that value. Otherwise, increments the current seed by 1.": "如果提供了数字,则将种子设置为该值。否则,将当前种子增加 1。",
3088 "Increments the seed by 1.": "将种子增加 1。",
3089 "Sets the seed to 5.": "将种子设置为 5。",
3090 "an empty string": "空字符串",
3091 "Plays the message received sound effect.": "播放收到消息的音效。",
3092 "Failed to parse the filter argument. Make sure it is a valid non-empty closure.": "解析过滤参数失败。请确保它是一个有效的非空闭包。",
3093 "Legacy argument 'format' with value '${0}' is deprecated. Please use 'return' instead. Routing to the correct return type...": "旧参数 'format'(值为 '${0}')已被弃用。请改用 'return'。正在路由到正确的返回类型...",
3094 "Deprecation warning": "弃用警告",
3095 "No script injections for the current chat": "当前聊天没有脚本注入",
3096 "unknown": "未知",
3097 "depth": "深度",
3098 "scan": "扫描",
3099 "role": "身份",
3100 "Script injections:": "脚本注入:",
3101 "OK": "确定",
3102 "Invalid role provided. Expected one of: system, assistant, user. Got: ${0}": "提供的身份无效。应为 system、assistant、user 之一。得到的是:${0}",
3103 "No name provided for /run command": "/run 命令未提供名称",
3104 "\"${0}\" is not callable.": "\"${0}\" 不可调用。",
3105 "Quick Reply extension is not loaded": "快捷回复扩展程序未加载",
3106 "Error running Quick Reply \"${0}\": ${1}": "运行快捷回复 \"${0}\" 时出错:${1}",
3107 "argument 'quiet' cannot be a closure for command /abort": "/abort 命令的 'quiet' 参数不能是闭包",
3108 "/abort command executed": "/abort 命令已执行",
3109 "argument 'onCancel' must be a closure for command /input": "/input 命令的 'onCancel' 参数必须是闭包",
3110 "argument 'onSuccess' must be a closure for command /input": "/input 命令的 'onSuccess' 参数必须是闭包",
3111 "Invalid severity provided for /echo command: ${0}": "向 /echo 命令提供的严重等级无效:${0}",
3112 "Invalid onClick provided for /echo command. This is not a closure": "向 /echo 命令提供的 onClick 无效。它不是一个闭包。",
3113 "No messages to add swipes to.": "没有可添加备选回复的消息。",
3114 "Can't add swipes to user messages.": "无法为用户消息添加备选回复。",
3115 "Can't add swipes to system messages.": "无法为系统消息添加备选回复。",
3116 "Cannot run /ask command in a group chat!": "无法在群聊中运行 /ask 命令!",
3117 "You must specify a name of the character to ask.": "您必须指定要提问的角色名称。",
3118 "Character not found.": "未找到角色。",
3119 "Asking ${0} something...": "正在询问 ${0} ...",
3120 "It is strongly recommended to reload the page.": "强烈建议重新加载页面。",
3121 "Something went wrong": "出错了",
3122 "Cannot run /member-disable command outside of a group chat.": "无法在群聊之外运行 /member-disable 命令。",
3123 "Cannot run /member-enable command outside of a group chat.": "无法在群聊之外运行 /member-enable 命令。",
3124 "Cannot run /member-up command outside of a group chat.": "无法在群聊之外运行 /member-up 命令。",
3125 "Cannot run /member-down command outside of a group chat.": "无法在群聊之外运行 /member-down 命令。",
3126 "Cannot run /member-peek command outside of a group chat.": "无法在群聊之外运行 /member-peek 命令。",
3127 "Cannot run /member-peek command while the group reply is generating.": "无法在群聊消息生成时运行 /member-peek 命令。",
3128 "Cannot run /member-count command outside of a group chat.": "无法在群聊之外运行 /member-count 命令。",
3129 "Cannot run /member-remove command outside of a group chat.": "无法在群聊之外运行 /member-remove 命令。",
3130 "Cannot run /memberadd command outside of a group chat.": "无法在群聊之外运行 /memberadd 命令。",
3131 "${0} is already a member of this group.": "${0} 已经是该群聊的成员。",
3132 "Cannot run /trigger command while the reply is being generated.": "在回复生成时无法运行 /trigger 命令。",
3133 "Deleted ${0} messages from ${1}": "从 ${1} 中删除了 ${0} 条消息",
3134 "Cannot run /continue command while the reply is being generated.": "在回复生成时无法运行 /continue 命令。",
3135 "You must provide a prompt for the system message": "您必须为系统消息提供提示词",
3136 "Generating...": "生成中...",
3137 "System narrator name set to ${0}": "系统旁白名称设置为 ${0}",
3138 "Argument \"${0}\" is undefined, but must be a string array": "参数 \"${0}\" 未定义,但必须是字符串数组",
3139 "Argument \"${0}\" must be an array": "参数 \"${0}\" 必须是一个数组",
3140 "Argument \"${0}\" must be an array of strings": "参数 \"${0}\" 必须是字符串数组",
3141 "Argument \"${0}\" is undefined, but must be an array of strings or closures": "参数 \"${0}\" 未定义,但必须是字符串或闭包的数组",
3142 "Argument \"${0}\" must be an array of strings or closures": "参数 \"${0}\" 必须是字符串或闭包的数组",
3143 "No message found at the specified index.": "指定索引处未找到消息。",
3144 "To avoid confusion, please use /sendas name=\"Character Name\"": "为避免混淆,请使用 /sendas name=\"角色名称\"",
3145 "Name defaulted to {{char}}": "名称默认为 {{char}}",
3146 "Character for avatar ${0} not found": "未找到头像 ${0} 的角色",
3147 "No background found with name \"${0}\"": "未找到名称为 \"${0}\" 的背景",
3148 "Setting a model for your API is not supported or not implemented yet.": "尚不支持或未实现为您的 API 设置模型。",
3149 "Model select control not found: ${0}[${1}]": "未找到模型选择控件:${0}[${1}]",
3150 "Model set to \"${0}\"": "模型已设置为 \"${0}\"",
3151 "No model options found. Check your API settings.": "未找到模型选项。请检查您的 API 设置。",
3152 "No model found with name \"${0}\"": "未找到名称为 \"${0}\" 的模型",
3153 "Custom OpenAI API is not the currently selected API, so we cannot do an auto-connect. Consider switching to it via /api beforehand.": "Custom OpenAI API 不是当前选择的 API,因此无法自动连接。请考虑先通过 /api 切换到它。",
3154 "Valid options are: ${0}": "有效选项为:${0}",
3155 "ZAI endpoint '${0}' is not a valid option.": "ZAI 端点 '${0}' 不是有效选项。",
3156 "Z.AI is not the currently selected API, so we cannot do an auto-connect. Consider switching to it via /api beforehand.": "Z.AI 不是当前选择的 API,因此无法自动连接。请考虑先通过 /api 切换到它。",
3157 "Generation requests may fail.": "生成请求可能会失败。",
3158 "Unknown VertexAI region '${0}'": "未知的 VertexAI 区域 '${0}'",
3159 "VertexAI is not the currently selected API, so we cannot do an auto-connect. Consider switching to it via /api beforehand.": "VertexAI 不是当前选择的 API,因此无法自动连接。请考虑先通过 /api 切换到它。",
3160 "Kobold Classic API is not the currently selected API, so we cannot do an auto-connect. Consider switching to it via /api beforehand.": "Kobold Classic API 不是当前选择的 API,因此无法自动连接。请考虑先通过 /api 切换到它。",
3161 "API '${0}' is not a valid text_gen API.": "API '${0}' 不是有效的 text_gen API。",
3162 "API type '${0}' does not support setting the server URL.": "API 类型 '${0}' 不支持设置服务器 URL。",
3163 "API '${0}' is not the currently selected API, so we cannot do an auto-connect. Consider switching to it via /api beforehand.": "API '${0}' 不是当前选择的 API,因此无法自动连接。请考虑先通过 /api 切换到它。",
3164 "API '${0}' does not have a server url input.": "API '${0}' 没用服务器地址输入选项。",
3165 "Tokenizer \"${0}\" not found": "分词器 \"${0}\" 不存在",
3166 "An unknown error occurred": "发生未知错误",
3167 "Line": "行",
3168 "Column": "列",
3169 "Click to see details": "点击查看详情",
3170 "Command execution aborted": "命令执行已中止",
3171 "Command name": "命令名称",
3172 "Does not alter quoted literal unnamed arguments. Pass raw=false argument to override.": "不会修改带引号的字面量未命名参数。传递 raw=false 参数以覆盖。",
3173 "Named argument": "命名参数",
3174 "Optional named argument": "可选命名参数",
3175 "${0} - Name": "${0} - 名称",
3176 "${0} - Accepted values": "${0} - 接受的值",
3177 "${0} - Accepted types": "${0} - 接受的类型",
3178 "Default value": "默认值",
3179 "Unnamed argument": "未命名参数",
3180 "Optional unnamed argument": "可选未命名参数",
3181 "Command does not return anything": "命令没有返回任何内容",
3182 "Return value": "返回值",
3183 "${0} Stats": "${0} 统计信息",
3184 "Chatting Since": "聊天开始时间",
3185 "First Interaction": "首次互动时间",
3186 "Chat Time": "聊天时长",
3187 "User Messages": "用户消息数",
3188 "Character Messages": "角色消息数",
3189 "User Words": "用户字数",
3190 "Character Words": "角色字数",
3191 "You deleted a character/chat and arrived back here for safety reasons! Pick another character!": "你删除了一个角色或聊天,出于安全原因返回了这里。选个新角色吧!",
3192 "Imported tags:": "已导入标签:",
3193 "Importing Tags": "正在导入标签",
3194 "Couldn't import tags:": "无法导入标签:",
3195 "No unused tags or references found.": "未找到未使用的标签或引用。",
3196 "Prune ${0} tags and ${1} references": "修剪 ${0} 个标签和 ${1} 个引用",
3197 "Are you sure you want to remove all unused tags and references to missing or deleted characters and groups?": "你确定要删除所有未使用的标签和对缺失或已删除角色和群聊的引用吗?",
3198 "Unused tags pruned successfully.": "已成功修剪未使用的标签。",
3199 "Hide on character card": "在角色卡上隐藏标签",
3200 "Show on character card": "在角色卡上显示标签",
1992 "Manager Users": "管理用户",3201 "Manager Users": "管理用户",
1993 "New User": "新用户",3202 "New User": "新用户",
1994 "Status:": "地位:",3203 "Status:": "地位:",
@@ -2017,10 +3226,9 @@
2017 "Import None": "不导入",3226 "Import None": "不导入",
2018 "Import All": "全部导入",3227 "Import All": "全部导入",
2019 "Import Existing": "导入现有",3228 "Import Existing": "导入现有",
2020 "Import": "导入",3229 "Chat Lorebook": "聊天世界书",
2021 "Chat Lorebook": "聊天知识书",3230 "Chat Lorebook for": "Ta 的聊天世界书",
2022 "Chat Lorebook for": "聊天知识书",3231 "chat_world_template_txt": "选定的世界书绑定到此聊天。生成 AI 回复时,\n它将与全局和角色世界书中的条目相结合。",
2023 "chat_world_template_txt": "选定的世界信息将绑定到此聊天。生成 AI 回复时,\n它将与全球和角色传说书中的条目相结合。",
2024 "chat_rename_1": "输入聊天的新名称:",3232 "chat_rename_1": "输入聊天的新名称:",
2025 "chat_rename_2": "注意!!与其他文件重名会导致错误!!",3233 "chat_rename_2": "注意!!与其他文件重名会导致错误!!",
2026 "chat_rename_3": "此举会将此聊天与标记为“检查点”的聊天解绑。",3234 "chat_rename_3": "此举会将此聊天与标记为“检查点”的聊天解绑。",
@@ -2042,7 +3250,7 @@
2042 "Once deleted, the files will be gone forever!": "一旦删除,文件将会永久消失!(真的很久!)",3250 "Once deleted, the files will be gone forever!": "一旦删除,文件将会永久消失!(真的很久!)",
2043 "Make sure to back up your data in advance.": "确保你已提前备份好数据。",3251 "Make sure to back up your data in advance.": "确保你已提前备份好数据。",
2044 "Scan": "扫描",3252 "Scan": "扫描",
2045 "No results yet. Tap 'Scan' to start scanning.": "暂无结果。点击 '扫描' 以启动扫描。",3253 "No results yet. Tap 'Scan' to start scanning.": "暂无结果。点击“扫描”以启动扫描。",
2046 "Functions in this category are for advanced users only. Don't click anything if you're not sure about the consequences.": "此类别中的功能仅供高级用户使用。如果您不确定后果,请不要点击任何内容。",3254 "Functions in this category are for advanced users only. Don't click anything if you're not sure about the consequences.": "此类别中的功能仅供高级用户使用。如果您不确定后果,请不要点击任何内容。",
2047 "THIS IS PERMANENT!": "此操作不可逆!",3255 "THIS IS PERMANENT!": "此操作不可逆!",
2048 "Also delete the chat files": "同时删除聊天文件",3256 "Also delete the chat files": "同时删除聊天文件",
@@ -2058,12 +3266,12 @@
2058 "Are you sure you want to duplicate this character?": "你确定要复制这个角色吗?",3266 "Are you sure you want to duplicate this character?": "你确定要复制这个角色吗?",
2059 "If you just want to start a new chat with the same character...": "如果你只是想要与此角色开启一个新的聊天,只需点击聊天左下方菜单中的“开始新聊天”按钮。",3267 "If you just want to start a new chat with the same character...": "如果你只是想要与此角色开启一个新的聊天,只需点击聊天左下方菜单中的“开始新聊天”按钮。",
2060 "There are no items to display.": "没有项目可供展示。",3268 "There are no items to display.": "没有项目可供展示。",
2061 "Do you want to export connection data with the preset?": "你想要把API连接配置和预设一起导出吗?",3269 "Do you want to export connection data with the preset?": "你想要把 API连接配置 和预设一起导出吗?",
2062 "This includes the selected source, models, and other preferences set in the API Connections panel.": "这包含你在API连接配置页面中设置的URL、模型和其他偏好设置。",3270 "This includes the selected source, models, and other preferences set in the API Connections panel.": "这包含你在 API 连接配置 页面中设置的 URL、模型和其他偏好设置。",
2063 "Your stored API keys are never exported.": "你存储的API密钥永远不会被导出。",3271 "Your stored API keys are never exported.": "你存储的 API 密钥永远不会被导出。",
2064 "Export connection data": "导出API连接配置",3272 "Export connection data": "导出 API 连接配置",
2065 "Do not export connection data": "不导出API连接配置",3273 "Do not export connection data": "不导出 API 连接配置",
2066 "Forbid Media Override explanation": "当前角色/群聊成员使用外部媒体的能力。",3274 "Forbid Media Override explanation": "当前角色/群成员使用外部媒体的能力。",
2067 "Forbid Media Override subtitle": "媒体:图像、视频、音频。外部:不在本地服务器上托管。",3275 "Forbid Media Override subtitle": "媒体:图像、视频、音频。外部:不在本地服务器上托管。",
2068 "forbid_media_global_state_forbidden": "(禁止)",3276 "forbid_media_global_state_forbidden": "(禁止)",
2069 "forbid_media_global_state_allowed": "(允许)",3277 "forbid_media_global_state_allowed": "(允许)",
@@ -2110,8 +3318,6 @@
2110 "button in the Creator's Notes block.": "按钮。",3318 "button in the Creator's Notes block.": "按钮。",
2111 "Class names will be automatically prefixed with 'custom-'.": "CSS类名将会自动添加 'custom-' 前缀。",3319 "Class names will be automatically prefixed with 'custom-'.": "CSS类名将会自动添加 'custom-' 前缀。",
2112 "Choose how to apply CSS style tags if they are defined in Creator's Notes of this character:": "选择如何应用这个角色的创作者注释中定义的CSS样式:",3320 "Choose how to apply CSS style tags if they are defined in Creator's Notes of this character:": "选择如何应用这个角色的创作者注释中定义的CSS样式:",
2113 "Just to Creator's Notes": "仅应用到创作者注释",
2114 "Apply to the entire app": "应用到整个页面",
2115 "help_1": "您好!请选择您想要详细了解的帮助主题:",3321 "help_1": "您好!请选择您想要详细了解的帮助主题:",
2116 "help_2": "斜线命令",3322 "help_2": "斜线命令",
2117 "help_or": "或者",3323 "help_or": "或者",
@@ -2121,7 +3327,7 @@
2121 "help_6": "还有其他问题吗?",3327 "help_6": "还有其他问题吗?",
2122 "help_7": "SillyTavern 官方文档网站",3328 "help_7": "SillyTavern 官方文档网站",
2123 "help_8": "有更多信息!",3329 "help_8": "有更多信息!",
2124 "Characters and groups hidden by filters or closed folders": "因筛选条件或关闭的文件夹隐藏的角色和群组",3330 "Characters and groups hidden by filters or closed folders": "因筛选条件或关闭的文件夹隐藏的角色和群聊",
2125 "help_hotkeys_0": "热键/按键绑定",3331 "help_hotkeys_0": "热键/按键绑定",
2126 "help_hotkeys_1": "上",3332 "help_hotkeys_1": "上",
2127 "help_hotkeys_2": "编辑聊天中的最后一条消息",3333 "help_hotkeys_2": "编辑聊天中的最后一条消息",
@@ -2137,7 +3343,7 @@
2137 "help_hotkeys_11": "Ctrl+Enter",3343 "help_hotkeys_11": "Ctrl+Enter",
2138 "help_hotkeys_12": "重新生成最后的 AI 响应",3344 "help_hotkeys_12": "重新生成最后的 AI 响应",
2139 "help_hotkeys_13": "Alt+Enter",3345 "help_hotkeys_13": "Alt+Enter",
2140 "help_hotkeys_14": "继续上一次AI响应",3346 "help_hotkeys_14": "继续上一次 AI 响应",
2141 "help_hotkeys_15": "Esc",3347 "help_hotkeys_15": "Esc",
2142 "help_hotkeys_16": "停止 AI 响应生成、关闭 UI 面板、取消消息编辑",3348 "help_hotkeys_16": "停止 AI 响应生成、关闭 UI 面板、取消消息编辑",
2143 "help_hotkeys_17": "Ctrl+Shift+上",3349 "help_hotkeys_17": "Ctrl+Shift+上",
@@ -2151,18 +3357,18 @@
2151 "help_hotkeys_25": "`行内代码`",3357 "help_hotkeys_25": "`行内代码`",
2152 "help_hotkeys_26": "~~删除线~~",3358 "help_hotkeys_26": "~~删除线~~",
2153 "Import Characters": "导入角色",3359 "Import Characters": "导入角色",
2154 "Enter the URL of the content to import": "输入要导入的内容的URL",3360 "Enter the URL of the content to import": "输入要导入的内容的 URL",
2155 "Supported sources:": "支持的来源:",3361 "Supported sources:": "支持的来源:",
2156 "char_import_1": "Chub 角色(直链或ID)",3362 "char_import_1": "Chub 角色(直链或 ID)",
2157 "char_import_example": "例子:",3363 "char_import_example": "例子:",
2158 "char_import_2": "Chub 知识书(直链或ID)",3364 "char_import_2": "Chub 知识书(直链或 ID)",
2159 "char_import_3": "JanitorAI 角色(直链或UUID)",3365 "char_import_3": "JanitorAI 角色(直链或 UUID)",
2160 "char_import_4": "Pygmalion.chat 角色(直链或UUID)",3366 "char_import_4": "Pygmalion.chat 角色(直链或 UUID)",
2161 "char_import_5": "AICharacterCards.com 角色(直链或ID)",3367 "char_import_5": "AICharacterCards.com 角色(直链或 ID)",
2162 "char_import_6": "被允许的PNG直链(请参阅",3368 "char_import_6": "被允许的PNG直链(请参阅",
2163 "char_import_7": ")",3369 "char_import_7": ")",
2164 "char_import_8": "RisuRealm 角色(直链)",3370 "char_import_8": "RisuRealm 角色(直链)",
2165 "char_import_10": "Perchance 角色(直链或UUID + .gz)",3371 "char_import_10": "Perchance 角色(直链或 UUID + .gz)",
2166 "Supports importing multiple characters.": "支持导入多个角色。",3372 "Supports importing multiple characters.": "支持导入多个角色。",
2167 "Write each URL or ID into a new line.": "将每个 URL 或 ID 写入新行。",3373 "Write each URL or ID into a new line.": "将每个 URL 或 ID 写入新行。",
2168 "Enter the Git URL of the extension to install": "输入扩展程序的 Git URL 以安装",3374 "Enter the Git URL of the extension to install": "输入扩展程序的 Git URL 以安装",
@@ -2177,19 +3383,19 @@
2177 "Tokenizer:": "分词器:",3383 "Tokenizer:": "分词器:",
2178 "Only the white numbers really matter. All numbers are estimates. Grey color items may not have been included in the context due to certain prompt format settings.": "只有白色的数字作数。所有数字均为估算。\n 灰色的数字可能因特定的提示词处理规则而被排除在外。",3384 "Only the white numbers really matter. All numbers are estimates. Grey color items may not have been included in the context due to certain prompt format settings.": "只有白色的数字作数。所有数字均为估算。\n 灰色的数字可能因特定的提示词处理规则而被排除在外。",
2179 "System Info:": "系统信息:",3385 "System Info:": "系统信息:",
2180 "Prompt Tokens:": "提示词Token:",3386 "Prompt Tokens:": "提示词 Token:",
2181 "World Info:": "世界书:",3387 "World Info:": "世界书:",
2182 "Chat History:": "聊天记录:",3388 "Chat History:": "聊天记录:",
2183 "Extensions:": "扩展程序:",3389 "Extensions:": "扩展程序:",
2184 "Bias:": "偏置:",3390 "Bias:": "偏置:",
2185 "Total Tokens in Prompt:": "提示词的总Token数量:",3391 "Total Tokens in Prompt:": "提示词总 Token 数量:",
2186 "Max Context": "最大上下文:",3392 "Max Context": "最大上下文:",
2187 "(Context Size - Response Length)": "(上下文长度 - 回复长度)",3393 "(Context Size - Response Length)": "(上下文长度 - 回复长度)",
2188 "Choose what to export": "选择您想要导出什么:",3394 "Choose what to export": "选择您想要导出什么:",
2189 "Choose what to import": "选择您想要导入什么:",3395 "Choose what to import": "选择您想要导入什么:",
2190 "If necessary, you can later restore this chat file from the /backups folder": "若需要,您可稍后在 /backups 文件夹中恢复此聊天文件。",3396 "If necessary, you can later restore this chat file from the /backups folder": "若需要,您可稍后在 /backups 文件夹中恢复此聊天文件。",
2191 "Also delete the current chat file": "同时删除当前聊天文件",3397 "Also delete the current chat file": "同时删除当前聊天文件",
2192 "Persona Lorebook for": "Ta 的角色世界书:",3398 "Persona Lorebook for": "Ta 的人设世界书:",
2193 "persona_world_template_txt": "将世界书绑定到此角色。生成 AI 回复时,\n 它将与全局、角色和聊天世界书中的条目结合使用。",3399 "persona_world_template_txt": "将世界书绑定到此角色。生成 AI 回复时,\n 它将与全局、角色和聊天世界书中的条目结合使用。",
2194 "Insert prompt": "插入提示词",3400 "Insert prompt": "插入提示词",
2195 "Import a prompt list": "导入提示词列表",3401 "Import a prompt list": "导入提示词列表",
@@ -2197,10 +3403,10 @@
2197 "Reset current character": "重置当前角色",3403 "Reset current character": "重置当前角色",
2198 "New prompt": "新提示词",3404 "New prompt": "新提示词",
2199 "Prompts": "提示词",3405 "Prompts": "提示词",
2200 "Total Tokens:": "总词符数:",3406 "Total Tokens:": "总 Token 数量:",
2201 "Name": "名称",3407 "Name": "名称",
2202 "prompt_manager_tokens": "词符",3408 "prompt_manager_tokens": "Token",
2203 "Are you sure you want to connect to the following proxy URL?": "你确定要连接到下面的代理URL吗?",3409 "Are you sure you want to connect to the following proxy URL?": "你确定要连接到下面的代理 URL 吗?",
2204 "Encountered an error while processing your request.": "处理请求时遇到了问题。",3410 "Encountered an error while processing your request.": "处理请求时遇到了问题。",
2205 "Check you have credits available on your": "检查您的 ",3411 "Check you have credits available on your": "检查您的 ",
2206 "OpenAI account quora_error": "OpenAI 账号余额是否充足",3412 "OpenAI account quora_error": "OpenAI 账号余额是否充足",
@@ -2227,7 +3433,6 @@
2227 "Type System Prompt here...": "在此输入系统提示词...",3433 "Type System Prompt here...": "在此输入系统提示词...",
2228 "API:": "API:",3434 "API:": "API:",
2229 "Key:": "密钥:",3435 "Key:": "密钥:",
2230 "Add Secret": "添加密钥",
2231 "No secrets saved.": "无保存的密钥。",3436 "No secrets saved.": "无保存的密钥。",
2232 "Copy ID": "复制 ID",3437 "Copy ID": "复制 ID",
2233 "Select": "选择",3438 "Select": "选择",
@@ -2239,7 +3444,6 @@
2239 "Downloader Options": "下载器选项",3444 "Downloader Options": "下载器选项",
2240 "Extra parameters for downloading/HuggingFace API": "下载/HuggingFace API 的额外参数。如果不确定,请将其留空。",3445 "Extra parameters for downloading/HuggingFace API": "下载/HuggingFace API 的额外参数。如果不确定,请将其留空。",
2241 "Revision": "修订",3446 "Revision": "修订",
2242 "Folder Name": "输出文件夹名称",
2243 "HF Token": "HF 令牌",3447 "HF Token": "HF 令牌",
2244 "Include Patterns": "包含模式",3448 "Include Patterns": "包含模式",
2245 "Glob patterns of files to include in the download.": "要包含在下载中的文件的全局模式。每个模式用换行符分隔。",3449 "Glob patterns of files to include in the download.": "要包含在下载中的文件的全局模式。每个模式用换行符分隔。",
@@ -2262,7 +3466,7 @@
2262 "To enable multi-account features, restart the SillyTavern server with": "要启用多帐户功能,请使用以下命令重新启动 SillyTavern 服务器",3466 "To enable multi-account features, restart the SillyTavern server with": "要启用多帐户功能,请使用以下命令重新启动 SillyTavern 服务器",
2263 "set to true in the config.yaml file.": "在 config.yaml 文件中设置为 true。",3467 "set to true in the config.yaml file.": "在 config.yaml 文件中设置为 true。",
2264 "Account Info": "帐户信息",3468 "Account Info": "帐户信息",
2265 "To change your user avatar, use the buttons below or select a default persona in the Persona Management menu.": "要更改您的用户头像,请使用下面的按钮或在角色管理菜单中选择一个默认角色。",3469 "To change your user avatar, use the buttons below or select a default persona in the Persona Management menu.": "要更改您的用户头像,请使用下面的按钮或在用户设定管理菜单中选择一个默认设定。",
2266 "Set your custom avatar.": "设置您的自定义头像。",3470 "Set your custom avatar.": "设置您的自定义头像。",
2267 "Remove your custom avatar.": "删除您的自定义头像。",3471 "Remove your custom avatar.": "删除您的自定义头像。",
2268 "Handle:": "账号:",3472 "Handle:": "账号:",
@@ -2297,7 +3501,7 @@
2297 "in the chat bar": "至聊天框",3501 "in the chat bar": "至聊天框",
2298 "SillyTavern Documentation Site": "访问 SillyTavern 帮助文档",3502 "SillyTavern Documentation Site": "访问 SillyTavern 帮助文档",
2299 "Still have questions?": "仍有疑问?",3503 "Still have questions?": "仍有疑问?",
2300 "Join the SillyTavern Discord": "加入 SillyTavern 的 Discord群组",3504 "Join the SillyTavern Discord": "加入 SillyTavern 的 Discord 群组",
2301 "Post a GitHub issue": "在 GitHub 发布问题",3505 "Post a GitHub issue": "在 GitHub 发布问题",
2302 "Contact the developers": "联系开发者",3506 "Contact the developers": "联系开发者",
2303 "Show recent chats": "显示最近的聊天",3507 "Show recent chats": "显示最近的聊天",
@@ -2308,9 +3512,136 @@
2308 "Discord": "Discord",3512 "Discord": "Discord",
2309 "Temporary Chat": "临时聊天",3513 "Temporary Chat": "临时聊天",
2310 "No recent chats": "无最近聊天",3514 "No recent chats": "无最近聊天",
3515 "Pinned chat": "已置顶聊天",
3516 "Pin chat": "置顶聊天",
2311 "Rename chat": "重命名聊天",3517 "Rename chat": "重命名聊天",
2312 "Delete chat": "删除聊天",3518 "Delete chat": "删除聊天",
2313 "Title/Memo": "标题(备忘)",3519 "Title/Memo": "标题(备忘)",
2314 "Strategy": "触发策略",3520 "Strategy": "触发策略",
2315 "Trigger %": "触发概率%"3521 "Trigger %": "触发概率 %",
3522 "Interleaved Thinking": "交错思维",
3523 "Since Last User Message": "自上一条用户消息起",
3524 "Active Tool Chain": "活动工具链",
3525 "openrouter_interleaved_thinking_hint": "发送前面助手轮次的推理并附在工具调用请求中,以维持交错思维上下文。",
3526 "Class": "类型",
3527 "Context Length": "上下文长度",
3528 "Added On": "添加于",
3529 "[Currently loaded]": "[当前已加载]",
3530 "Select quantizations. No selection = all quantizations.": "选择量化方式。未选择 = 所有量化方式。",
3531 "Search quantizations...": "搜索量化方式...",
3532 "Select providers. No selection = all providers.": "选择提供商。未选择 = 所有提供商。",
3533 "Search providers...": "搜索提供商...",
3534 "Enter a valid API URL": "输入一个有效的 API URL",
3535 "Banned tokens/strings are being sent in the request.": "请求中将包含被禁止的 Token 和字符串。",
3536 "Banned tokens/strings are NOT being sent in the request.": "请求中将不会包含被禁止的 Token 和字符串。",
3537 "No Ollama model selected.": "未选择 Ollama 模型。",
3538 "Next page": "下一页",
3539 "Previous page": "上一页",
3540 "First page": "第一页",
3541 "Last page": "最后一页",
3542 "/ page": "/ 页",
3543 "Multiple personas found for given conditions.": "找到多个用户设定。",
3544 "Multiple personas found for given conditions. Returning the first match.": "找到多个用户设定。将返回第一个匹配项。",
3545 "Multiple characters found for given conditions.": "找到多个角色。",
3546 "Multiple characters found for given conditions. Returning the first match.": "找到多个角色。将返回第一个匹配项。",
3547 "If you're connected to an API, try asking me something!": "如果您已连接到一个 API,试着问我点什么吧!",
3548 "**Hint:** Set any character as your welcome page assistant from their \"More...\" menu.": "**提示:**您可以通过角色设置中的 \"更多...\" 菜单将角色设为欢迎页面的助手。",
3549 "Show more recent chats": "显示更多最近聊天",
3550 "Show less recent chats": "显示更少最近聊天",
3551 "Failed to open recent chat. See console for details.": "未能打开最近聊天。详见控制台。",
3552 "Failed to open recent group chat. See console for details.": "未能打开最近群聊。详见控制台。",
3553 "Chat renamed.": "聊天已重命名。",
3554 "Failed to rename recent chat. See console for details.": "未能重命名最近聊天。详见控制台。",
3555 "Group chat renamed.": "群聊已重命名。",
3556 "Failed to rename recent group chat. See console for details.": "未能重命名最近群聊。详见控制台。",
3557 "Chat deleted.": "聊天已删除。",
3558 "Failed to delete recent chat. See console for details.": "未能删除最近聊天。详见控制台。",
3559 "Group chat deleted.": "群聊已删除。",
3560 "Failed to delete recent group chat. See console for details.": "未能删除最近群聊。详见控制台。",
3561 "Failed to create ${0}. See console for details.": "未能创建 ${0}。详见控制台。",
3562 "Failed to open permanent assistant chat. See console for details.": "未能打开永久助手聊天。详见控制台。",
3563 "Cannot create while generating.": "生成过程中无法创建。",
3564 "Automatically created character. Feel free to edit.": "自动创建的角色。可以自由编辑。",
3565 "Creation request did not succeed.": "创建请求未成功。",
3566 "Assistant not found. Try sending a chat message.": "未找到助手。请尝试发送聊天消息。",
3567 "${0} is a system assistant. Choose another character.": "${0} 是系统助手。请选择另一个角色。",
3568 "${0} is no longer your assistant.": "${0} 不再是你的助手。",
3569 "Set ${0} as your assistant.": "将 ${0} 设为你的助手。",
3570 "Valid World Info file name is required": "需要有效的世界书文件名",
3571 "World Info file has an invalid format": "世界书文件格式无效",
3572 "World Info file has no entries": "世界书文件没有条目",
3573 "Open a chat to get a name of the chat-bound lorebook": "打开一个聊天以获取聊天绑定的世界书名称",
3574 "Apply your current sorting to the \"Order\" field. The Order values will go down from the chosen number.": "将你当前的排序应用到 \"Order\" 字段。Order 值将从所选数字开始向下递减。",
3575 "More than 100 entries in this world. If you don't choose a number higher than that, the lower entries will default to 0.<br />(Usual default: 100)<br />Minimum: ${0}": "这个世界的条目超过 100 个。如果你没有选择一个比这更高的数字,较低的条目将默认为 0。<br />(通常默认值: 100)<br />最小值: ${0}",
3576 "Apply Current Sorting": "应用当前排序",
3577 "Apply": "应用",
3578 "Invalid number: ${0}": "无效数字:${0}",
3579 "A number lower than the entry count has been chosen. All entries below that will default to 0.": "选择了一个小于条目数量的数字。所有低于该数字的条目将默认为 0。",
3580 "Tie this entry to specific characters or characters with specific tags": "将此条目绑定到特定角色或具有特定标签的角色",
3581 "Select Target Lorebook": "选择目标世界书",
3582 "There are no other lorebooks to move to.": "没有可以移动到的其他世界书。",
3583 "Move/Copy '${0}' to:": "将 '${0}' 移动/复制到:",
3584 "Please select a target lorebook.": "请选择一个世界书。",
3585 "Delete the entry with UID: ${0}?": "删除 UID 为 ${0} 的条目?",
3586 "This action is irreversible!": "此操作不可撤销!",
3587 "New World": "新世界书",
3588 "Are you sure you want to import '${0}'?": "确定要导入 '${0}' 吗?",
3589 "It will overwrite the World/Lorebook with the same name.": "它将覆盖同名的世界书。",
3590 "The world '${0}' has been imported and linked to the character successfully.": "世界书 “${0}” 已成功导入并链接到角色。",
3591 "World/Lorebook imported": "世界书已导入",
3592 "Deactivated world: ${0}": "已停用世界书:${0}",
3593 "World was not active: ${0}": "世界书未激活:${0}",
3594 "Activated world: ${0}": "已激活世界书:${0}",
3595 "No world found named: ${0}": "未找到名为 ${0} 的世界书",
3596 "Deactivated all worlds": "已停用所有世界书",
3597 "The world with ${0} is invalid or corrupted.": "具有 ${0} 的世界书无效或已损坏。",
3598 "File is not valid: ${0}": "文件无效:${0}",
3599 "World Info \"${0}\" imported successfully!": "世界书 “${0}” 已成功导入!",
3600 "Failed to import World Info": "导入世界书失败",
3601 "Source lorebook '${0}' not found.": "源世界书 “${0}” 未找到。",
3602 "Target lorebook '${0}' not found.": "目标世界书 “${0}” 未找到。",
3603 "Failed to load data for source lorebook '${0}'.": "加载源世界书 “${0}” 数据失败。",
3604 "Failed to load data for target lorebook '${0}'.": "加载目标世界书 “${0}” 数据失败。",
3605 "Entry not found in source lorebook '${0}'.": "在源世界书 “${0}” 中未找到条目。",
3606 "Entry moved successfully from '${0}' to '${1}'.": "条目已成功从 “${0}” 移动到 “${1}”。",
3607 "Entry copied successfully to '${0}'.": "条目已成功复制到 “${0}”。",
3608 "An unexpected error occurred while moving the entry: ${0}": "移动条目时发生意外错误: ${0}",
3609 "Embedded lorebook will be removed from this character.": "嵌入的世界书将从该角色中移除。",
3610 "Create a new World Info": "创建新的世界书",
3611 "Enter a name for the new file:": "输入新文件的名称:",
3612 "No Worlds active. Click here to select.": "没有激活的世界书。点击这里选择。",
3613 "(if disabled, use": "(若禁用,请使用",
3614 "(label of the button, if no icon is chosen)": "(如果没有选择图标,则为按钮的标签)",
3615 "[ None ]": "[ 无 ]",
3616 "[Custom model]": "[自定义模型]",
3617 "Avoid sending sensitive information. Provider's outputs may include ads.": "请避免发送敏感信息。输出可能有提供商的广告。",
3618 "Clear all filters": "清除所有过滤器",
3619 "Enable 'Tags as Folder'\n\nAllows characters to be grouped in folders by their assigned tags.\nTags have to be explicitly chosen as folder to show up.\n\nClick here to start": "启用 '标签作为文件夹'\n\n允许按分配的标签将角色分组在文件夹中。\n标签必须显式选择为文件夹才能显示。\n\n点击这里开始",
3620 "Example: http://127.0.0.1:5000/api": "示例:http://127.0.0.1:5000/api",
3621 "Manage tags": "管理标签",
3622 "Provided free of charge by Pollinations.AI": "由 Pollinations.AI 免费提供",
3623 "sd_prompt_-1": "聊天消息模板",
3624 "sd_prompt_-2": "函数工具提示词描述",
3625 "sd_prompt_0": "角色 (第二人称,你)",
3626 "sd_prompt_1": "你的人设 (第一人称,我)",
3627 "sd_prompt_10": "肖像 (多模态模式)",
3628 "sd_prompt_11": "自由模式 (由 LLM 自动扩写)",
3629 "sd_prompt_2": "场景 (整个故事)",
3630 "sd_prompt_3": "最后原始消息",
3631 "sd_prompt_4": "最后消息",
3632 "sd_prompt_5": "角色的脸 (第二人称,你)",
3633 "sd_prompt_7": "背景",
3634 "sd_prompt_8": "角色 (多模态模式)",
3635 "sd_prompt_9": "你的人设 (多模态模式)",
3636 "Show only favorites": "仅显示收藏",
3637 "Show only groups": "仅显示群聊",
3638 "Show Tag List": "显示标签列表",
3639 "SillyTavern Logo": "SillyTavern 徽标",
3640 "Initializing…": "正在初始化…",
3641 "Renaming chat…": "正在重命名聊天…",
3642 "Delete Chat": "删除聊天",
3643 "Deleting chat…": "正在删除聊天…",
3644 "Loading chat…": "正在加载聊天…",
3645 "Bulk Delete": "批量删除",
3646 "Deleting ${0} character(s)…": "正在删除 ${0} 个角色…"
2316}3647}
public/locales/zh-tw.json+21 -5
@@ -334,6 +334,8 @@
334 "koboldcpp API key (optional)": "KoboldCpp API 金鑰(可選)",334 "koboldcpp API key (optional)": "KoboldCpp API 金鑰(可選)",
335 "Example: http://127.0.0.1:5001": "範例:http://127.0.0.1:5001",335 "Example: http://127.0.0.1:5001": "範例:http://127.0.0.1:5001",
336 "Authorize": "授權",336 "Authorize": "授權",
337 "OpenRouter API key already exists": "OpenRouter API 密鑰已存在",
338 "Do you really wish to create a new OpenRouter key? Your existing key will not be deleted.": "您確定要建立新的 OpenRouter API 金鑰嗎?您現有的金鑰不會被刪除。",
337 "Get your OpenRouter API token using OAuth flow. You will be redirected to openrouter.ai": "使用 OAuth 流程取得您的 OpenRouter API 符元。您將被重新導向到 openrouter.ai",339 "Get your OpenRouter API token using OAuth flow. You will be redirected to openrouter.ai": "使用 OAuth 流程取得您的 OpenRouter API 符元。您將被重新導向到 openrouter.ai",
338 "Bypass status check": "繞過狀態檢查",340 "Bypass status check": "繞過狀態檢查",
339 "Chat Completion Source": "聊天補全來源",341 "Chat Completion Source": "聊天補全來源",
@@ -1541,7 +1543,7 @@
1541 "When enabled, nodes that have swipes splitting off of them will appear subtly larger, in addition to having the double border.": "啟用後,具分支滑動的節點將顯示雙重邊框,還會略微放大。",1543 "When enabled, nodes that have swipes splitting off of them will appear subtly larger, in addition to having the double border.": "啟用後,具分支滑動的節點將顯示雙重邊框,還會略微放大。",
1542 "WI Entry Status:🔵 Constant🟢 Normal🔗 Vectorized": "世界資訊條目狀態:\\r🔵 恆定\\r🟢 正常\\r🔗 向量化",1544 "WI Entry Status:🔵 Constant🟢 Normal🔗 Vectorized": "世界資訊條目狀態:\\r🔵 恆定\\r🟢 正常\\r🔗 向量化",
1543 "Width of a node, in pixels at zoom level 1.0.": "縮放等級為 1.0 時,節點的像素寬度。",1545 "Width of a node, in pixels at zoom level 1.0.": "縮放等級為 1.0 時,節點的像素寬度。",
1544 "world_button_title": "角色背景設定\n「Shift+點選」可開啟「連結至世界資訊」彈窗",1546 "world_button_title": "角色背景設定\n「Shift/Alt+點選」或長按可開啟「連結至世界資訊」彈窗",
1545 "# of Beams": "# of Beams",1547 "# of Beams": "# of Beams",
1546 "Additional Parameters": "其他參數",1548 "Additional Parameters": "其他參數",
1547 "All": "全部",1549 "All": "全部",
@@ -2093,7 +2095,7 @@
2093 "Downloader Options": "下載選項",2095 "Downloader Options": "下載選項",
2094 "Extra parameters for downloading/HuggingFace API": "下載/使用 HuggingFace API 的額外參數。\r若不確定,請留空。",2096 "Extra parameters for downloading/HuggingFace API": "下載/使用 HuggingFace API 的額外參數。\r若不確定,請留空。",
2095 "Revision": "修訂",2097 "Revision": "修訂",
2096 "Folder Name": "輸出資料夾名稱",2098 "Output Folder Name": "輸出資料夾名稱",
2097 "HF Token": "HF 符元",2099 "HF Token": "HF 符元",
2098 "Include Patterns": "包含模式",2100 "Include Patterns": "包含模式",
2099 "Glob patterns of files to include in the download.": "要包含於下載中文件的全域模式。\r每行輸入一個模式。",2101 "Glob patterns of files to include in the download.": "要包含於下載中文件的全域模式。\r每行輸入一個模式。",
@@ -2365,8 +2367,8 @@
2365 "Contain": "自適應",2367 "Contain": "自適應",
2366 "Stretch": "拉伸",2368 "Stretch": "拉伸",
2367 "Center": "置中",2369 "Center": "置中",
2368 "Persona Lore Alt+Click to open the lorebook": "「Alt+點選」可開啟角色知識書",2370 "persona_lore_button_title": "人設知識書\n\n點選以載入\nShift/Alt+點選或長按開啟「連結人設知識書」彈窗",
2369 "Chat Lore Alt+Click to open the lorebook": "「Alt+點選」可開啟聊天知識書",2371 "chat_lorebook_button_title": "聊天知識書\n\n點選以載入\nShift/Alt+點選或長按開啟「連結聊天知識書」彈窗",
2370 "Function Tool": "功能工具",2372 "Function Tool": "功能工具",
2371 "Functions in this category are for advanced users only. Don't click anything if you're not sure about the consequences.": "此類功能僅供高階使用者使用。若不確定後果,請勿點選任何內容。",2373 "Functions in this category are for advanced users only. Don't click anything if you're not sure about the consequences.": "此類功能僅供高階使用者使用。若不確定後果,請勿點選任何內容。",
2372 "Are you sure you want to delete this user?": "確定要刪除該使用者嗎?",2374 "Are you sure you want to delete this user?": "確定要刪除該使用者嗎?",
@@ -2758,5 +2760,19 @@
2758 "[title]Create a new": "新增",2760 "[title]Create a new": "新增",
2759 "[title]Delete a ": "刪除 ",2761 "[title]Delete a ": "刪除 ",
2760 "[title]Rename a ": "重新命名 ",2762 "[title]Rename a ": "重新命名 ",
2761 "completions note prefix": "完成提示前綴"2763 "completions note prefix": "完成提示前綴",
2764 "Interleaved Thinking": "交錯思維",
2765 "Since Last User Message": "自上一則使用者訊息起",
2766 "Active Tool Chain": "使用中的工具鏈",
2767 "openrouter_interleaved_thinking_hint": "會在工具呼叫請求中附帶先前助理回合的推理,以維持交錯思維的上下文。",
2768 "SillyTavern Logo": "SillyTavern 標誌",
2769 "Initializing…": "正在初始化…",
2770 "Name not accepted, as it is the same as before (ignoring case and accents).": "名稱不被接受,因為它與之前相同(忽略大小寫和重音)。",
2771 "Rename Chat": "重新命名聊天",
2772 "Renaming chat…": "正在重新命名聊天…",
2773 "Delete Chat": "刪除聊天",
2774 "Deleting chat…": "正在刪除聊天…",
2775 "Loading chat…": "正在載入聊天…",
2776 "Bulk Delete": "批次刪除",
2777 "Deleting ${0} character(s)…": "正在刪除 ${0} 個角色…"
2762}2778}
public/script.js+426 -275
@@ -244,7 +244,7 @@ import {
244 isPersonaPanelOpen,244 isPersonaPanelOpen,
245} from './scripts/personas.js';245} from './scripts/personas.js';
246import { getBackgrounds, initBackgrounds, loadBackgroundSettings, background_settings } from './scripts/backgrounds.js';246import { getBackgrounds, initBackgrounds, loadBackgroundSettings, background_settings } from './scripts/backgrounds.js';
247import { hideLoader, showLoader } from './scripts/loader.js';247import { loader } from './scripts/action-loader.js';
248import { BulkEditOverlay } from './scripts/BulkEditOverlay.js';248import { BulkEditOverlay } from './scripts/BulkEditOverlay.js';
249import { initTextGenModels } from './scripts/textgen-models.js';249import { initTextGenModels } from './scripts/textgen-models.js';
250import { appendFileContent, hasPendingFileAttachment, populateFileAttachment, decodeStyleTags, encodeStyleTags, isExternalMediaAllowed, preserveNeutralChat, restoreNeutralChat, formatCreatorNotes, initChatUtilities, addDOMPurifyHooks } from './scripts/chats.js';250import { appendFileContent, hasPendingFileAttachment, populateFileAttachment, decodeStyleTags, encodeStyleTags, isExternalMediaAllowed, preserveNeutralChat, restoreNeutralChat, formatCreatorNotes, initChatUtilities, addDOMPurifyHooks } from './scripts/chats.js';
@@ -285,6 +285,8 @@ import { MacroEnvBuilder } from './scripts/macros/engine/MacroEnvBuilder.js';
285import { MacroEngine } from './scripts/macros/engine/MacroEngine.js';285import { MacroEngine } from './scripts/macros/engine/MacroEngine.js';
286import { addChatBackupsBrowser } from './scripts/chat-backups.js';286import { addChatBackupsBrowser } from './scripts/chat-backups.js';
287import { onboardingExperimentalMacroEngine } from './scripts/macros/engine/MacroDiagnostics.js';287import { onboardingExperimentalMacroEngine } from './scripts/macros/engine/MacroDiagnostics.js';
288import { compressRequest, setRequestCompressionConfig } from './scripts/request-compression.js';
289import { canJumpToSwipeForMessage, canOpenSwipePickerForMessage, initSwipePicker } from './scripts/swipe-picker.js';
288290
289// API OBJECT FOR EXTERNAL WIRING291// API OBJECT FOR EXTERNAL WIRING
290globalThis.SillyTavern = {292globalThis.SillyTavern = {
@@ -326,6 +328,8 @@ export {
326 setCharacterSettingsOverrides as setScenarioOverride,328 setCharacterSettingsOverrides as setScenarioOverride,
327 /** @deprecated Use appendMediaToMessage instead. */329 /** @deprecated Use appendMediaToMessage instead. */
328 appendMediaToMessage as appendImageToMessage,330 appendMediaToMessage as appendImageToMessage,
331 /** @deprecated Use getMaxPromptTokens instead. */
332 getMaxPromptTokens as getMaxContextSize,
329};333};
330334
331/**335/**
@@ -358,12 +362,36 @@ toastr.options = {
358 // so the toasts still show up inside there.362 // so the toasts still show up inside there.
359 fixToastrForDialogs();363 fixToastrForDialogs();
360 },364 },
361 onShown: function () {
362 // Set tooltip to the notification message
363 $(this).attr('title', t`Tap to close`);
364 },
365};365};
366366
367// Run once during startup
368toastr.subscribe(function (args) {
369 if (args.state !== 'visible') {
370 return;
371 }
372
373 const $container = toastr.getContainer(args.options, false);
374 if (!$container || !$container.length) {
375 return;
376 }
377
378 // toastr has already inserted the element at this point
379 const $toast = args.options.newestOnTop
380 ? $container.children().first()
381 : $container.children().last();
382
383 // Meaning of "clickable":
384 // Interactable unless tapToDismiss was explicitly false
385 const isInteractable = args.options.tapToDismiss !== false;
386 $toast.toggleClass('interactable', isInteractable);
387 if (isInteractable) {
388 $toast.attr('title', t`Tap to close`);
389 } else {
390 $toast.removeAttr('title');
391 $toast.addClass('toast-non-interactable');
392 }
393});
394
367export const characterGroupOverlay = new BulkEditOverlay();395export const characterGroupOverlay = new BulkEditOverlay();
368396
369// Markdown converter397// Markdown converter
@@ -512,8 +540,7 @@ export function reloadMarkdownProcessor() {
512export function getCurrentChatId() {540export function getCurrentChatId() {
513 if (selected_group) {541 if (selected_group) {
514 return groups.find(x => x.id == selected_group)?.chat_id;542 return groups.find(x => x.id == selected_group)?.chat_id;
515 }543 } else if (this_chid !== undefined) {
516 else if (this_chid !== undefined) {
517 return characters[this_chid]?.chat;544 return characters[this_chid]?.chat;
518 }545 }
519}546}
@@ -598,8 +625,7 @@ export let recentSwipes = 0;
598export let extension_prompts = {};625export let extension_prompts = {};
599626
600export let main_api;// = "kobold";627export let main_api;// = "kobold";
601/** @type {AbortController} */628let abortController = new AbortController();
602let abortController;
603629
604//css630//css
605var css_send_form_display = $('<div id=send_form></div>').css('display');631var css_send_form_display = $('<div id=send_form></div>').css('display');
@@ -673,7 +699,28 @@ async function firstLoadInit() {
673 throw new Error('Initialization failed');699 throw new Error('Initialization failed');
674 }700 }
675701
676 showLoader();702 const initLoaderOverlay = loader.createOverlay();
703 initLoaderOverlay.classList.add('splash-screen');
704
705 const splashLogo = document.createElement('img');
706 splashLogo.src = '/img/logo.png';
707 splashLogo.alt = 'SillyTavern';
708 splashLogo.className = 'splash-logo';
709 splashLogo.ariaLabel = t`SillyTavern Logo`;
710
711 const splashMessage = document.createElement('h2');
712 splashMessage.className = 'splash-message';
713 splashMessage.textContent = t`Initializing…`;
714 splashMessage.dataset.i18n = 'Initializing…';
715
716 initLoaderOverlay.prepend(splashLogo);
717 initLoaderOverlay.appendChild(splashMessage);
718
719 const initLoaderHandle = loader.show({
720 toastMode: loader.ToastMode.NONE,
721 overlayContent: initLoaderOverlay,
722 });
723
677 registerPromptManagerMigration();724 registerPromptManagerMigration();
678 initDomHandlers();725 initDomHandlers();
679 initStandaloneMode();726 initStandaloneMode();
@@ -699,7 +746,7 @@ async function firstLoadInit() {
699 ToolManager.initToolSlashCommands();746 ToolManager.initToolSlashCommands();
700 await initPresetManager();747 await initPresetManager();
701 await initSystemMessages();748 await initSystemMessages();
702 await getSettings();749 await getSettings(initLoaderHandle);
703 initKeyboard();750 initKeyboard();
704 initDynamicStyles();751 initDynamicStyles();
705 initTags();752 initTags();
@@ -730,10 +777,11 @@ async function firstLoadInit() {
730 initDataMaid();777 initDataMaid();
731 initItemizedPrompts();778 initItemizedPrompts();
732 initAccessibility();779 initAccessibility();
780 initSwipePicker();
733 addDebugFunctions();781 addDebugFunctions();
734 doDailyExtensionUpdatesCheck();782 doDailyExtensionUpdatesCheck();
735 await eventSource.emit(event_types.APP_INITIALIZED);783 await eventSource.emit(event_types.APP_INITIALIZED);
736 await hideLoader();784 await initLoaderHandle.hide();
737 await fixViewport();785 await fixViewport();
738 await eventSource.emit(event_types.APP_READY);786 await eventSource.emit(event_types.APP_READY);
739}787}
@@ -910,8 +958,7 @@ function getCharacterBlock(item, id) {
910 const description = item.data?.creator_notes || '';958 const description = item.data?.creator_notes || '';
911 if (description) {959 if (description) {
912 template.find('.ch_description').text(description);960 template.find('.ch_description').text(description);
913 }961 } else {
914 else {
915 template.find('.ch_description').hide();962 template.find('.ch_description').hide();
916 }963 }
917964
@@ -919,8 +966,7 @@ function getCharacterBlock(item, id) {
919 const auxFieldValue = (item.data && item.data[auxFieldName]) || '';966 const auxFieldValue = (item.data && item.data[auxFieldName]) || '';
920 if (auxFieldValue) {967 if (auxFieldValue) {
921 template.find('.character_version').text(auxFieldValue);968 template.find('.character_version').text(auxFieldValue);
922 }969 } else {
923 else {
924 template.find('.character_version').hide();970 template.find('.character_version').hide();
925 }971 }
926972
@@ -1364,16 +1410,14 @@ export async function replaceCurrentChat() {
1364 const chats = Object.values(await chatsResponse.json());1410 const chats = Object.values(await chatsResponse.json());
1365 chats.sort((a, b) => sortMoments(timestampToMoment(a.last_mes), timestampToMoment(b.last_mes)));1411 chats.sort((a, b) => sortMoments(timestampToMoment(a.last_mes), timestampToMoment(b.last_mes)));
13661412
1367 // pick existing chat
1368 if (chats.length && typeof chats[0] === 'object') {1413 if (chats.length && typeof chats[0] === 'object') {
1414 // pick existing chat
1369 characters[this_chid].chat = chats[0].file_name.replace('.jsonl', '');1415 characters[this_chid].chat = chats[0].file_name.replace('.jsonl', '');
1370 $('#selected_chat_pole').val(characters[this_chid].chat);1416 $('#selected_chat_pole').val(characters[this_chid].chat);
1371 saveCharacterDebounced();1417 saveCharacterDebounced();
1372 await getChat();1418 await getChat();
1373 }1419 } else {
13741420 // start new chat
1375 // start new chat
1376 else {
1377 characters[this_chid].chat = `${name2} - ${humanizedDateTime()}`;1421 characters[this_chid].chat = `${name2} - ${humanizedDateTime()}`;
1378 $('#selected_chat_pole').val(characters[this_chid].chat);1422 $('#selected_chat_pole').val(characters[this_chid].chat);
1379 saveCharacterDebounced();1423 saveCharacterDebounced();
@@ -1640,11 +1684,9 @@ export async function reloadCurrentChatUnsafe() {
16401684
1641 if (selected_group) {1685 if (selected_group) {
1642 await getGroupChat(selected_group, true);1686 await getGroupChat(selected_group, true);
1643 }1687 } else if (this_chid !== undefined) {
1644 else if (this_chid !== undefined) {
1645 await getChat();1688 await getChat();
1646 }1689 } else {
1647 else {
1648 resetChatState();1690 resetChatState();
1649 restoreNeutralChat();1691 restoreNeutralChat();
1650 await getCharacters();1692 await getCharacters();
@@ -1870,32 +1912,16 @@ export function messageFormatting(mes, ch_name, isSystem, isUser, messageId, san
1870/**1912/**
1871 * Inserts or replaces an SVG icon adjacent to the provided message's timestamp.1913 * Inserts or replaces an SVG icon adjacent to the provided message's timestamp.
1872 *1914 *
1873 * If the `extra.api` is "openai" and `extra.model` contains the substring "claude",
1874 * the function fetches the "claude.svg". Otherwise, it fetches the SVG named after
1875 * the value in `extra.api`.
1876 *
1877 * @param {JQuery<HTMLElement>} mes - The message element containing the timestamp where the icon should be inserted or replaced.1915 * @param {JQuery<HTMLElement>} mes - The message element containing the timestamp where the icon should be inserted or replaced.
1878 * @param {ChatMessageExtra} extra - Contains the API and model details.1916 * @param {ChatMessageExtra} extra - Contains the API and model details.
1879 */1917 */
1880function insertSVGIcon(mes, extra) {1918function insertSVGIcon(mes, extra) {
1881 // Determine the SVG filename1919 // Determine the SVG filename
1882 let modelName;1920 let modelName = extra?.api || '';
18831921
1884 // Claude on OpenRouter or Anthropic1922 // If there's no API information, we can't determine which SVG to use
1885 if (extra.api === 'openai' && extra.model?.toLowerCase().includes('claude')) {1923 if (!modelName) {
1886 modelName = 'claude';1924 return;
1887 }
1888 // OpenAI on OpenRouter
1889 else if (extra.api === 'openai' && extra.model?.toLowerCase().includes('openai')) {
1890 modelName = 'openai';
1891 }
1892 // OpenRouter website model or other models
1893 else if (extra.api === 'openai' && (extra.model === null || extra.model?.toLowerCase().includes('/'))) {
1894 modelName = 'openrouter';
1895 }
1896 // Everything else
1897 else {
1898 modelName = extra.api;
1899 }1925 }
19001926
1901 const insertOrReplaceSVG = (image, className, targetSelector, insertBefore) => {1927 const insertOrReplaceSVG = (image, className, targetSelector, insertBefore) => {
@@ -2516,7 +2542,6 @@ export function addOneMessage(mes, { type = undefined, insertAfter = null, scrol
2516 * @returns {JQuery<HTMLElement>} Rendered HTMLElement.2542 * @returns {JQuery<HTMLElement>} Rendered HTMLElement.
2517 */2543 */
2518export function updateMessageElement(mes, { messageId = chat.length - 1, messageElement = messageTemplate.clone(), adjustMediaScroll = SCROLL_BEHAVIOR.NONE } = {}) {2544export function updateMessageElement(mes, { messageId = chat.length - 1, messageElement = messageTemplate.clone(), adjustMediaScroll = SCROLL_BEHAVIOR.NONE } = {}) {
2519
2520 let avatarImg = getThumbnailUrl('persona', user_avatar);2545 let avatarImg = getThumbnailUrl('persona', user_avatar);
25212546
2522 //for non-user messages2547 //for non-user messages
@@ -2757,6 +2782,7 @@ export function substituteParamsLegacy(content, _name1, _name2, _original, _grou
2757 else if (/{{\s*[!?~#/]/.test(content)) feature = 'macro flags';2782 else if (/{{\s*[!?~#/]/.test(content)) feature = 'macro flags';
2758 else if (/{{\s*[.$]/.test(content)) feature = 'variable shorthands';2783 else if (/{{\s*[.$]/.test(content)) feature = 'variable shorthands';
2759 else if (/\{\{(?:(?!\}\}).)*\{\{(?=[\s\S]*?\}\}[\s\S]*?\}\})/.test(content)) feature = 'nested macro';2784 else if (/\{\{(?:(?!\}\}).)*\{\{(?=[\s\S]*?\}\}[\s\S]*?\}\})/.test(content)) feature = 'nested macro';
2785 else if (/{{(?:greeting|charFirstMessage)(?:::\d+)?}}/i.test(content)) feature = 'greeting macro';
27602786
2761 if (feature) void onboardingExperimentalMacroEngine(feature);2787 if (feature) void onboardingExperimentalMacroEngine(feature);
2762 }2788 }
@@ -2914,9 +2940,15 @@ export function substituteParams(content, options = {}) {
2914 * Gets stopping sequences for the prompt.2940 * Gets stopping sequences for the prompt.
2915 * @param {boolean} isImpersonate A request is made to impersonate a user2941 * @param {boolean} isImpersonate A request is made to impersonate a user
2916 * @param {boolean} isContinue A request is made to continue the message2942 * @param {boolean} isContinue A request is made to continue the message
2943 * @param {string} [api] Optional API name to get API-specific stopping sequences for
2917 * @returns {string[]} Array of stopping strings2944 * @returns {string[]} Array of stopping strings
2918 */2945 */
2919export function getStoppingStrings(isImpersonate, isContinue) {2946export function getStoppingStrings(isImpersonate, isContinue, api = main_api) {
2947 // Only custom stop strings apply to Chat Completion
2948 if (api === 'openai') {
2949 return getCustomStoppingStrings();
2950 }
2951
2920 const result = [];2952 const result = [];
29212953
2922 if (power_user.context.names_as_stop_strings) {2954 if (power_user.context.names_as_stop_strings) {
@@ -3252,11 +3284,13 @@ export function baseChatReplace(value, name1Override = null, name2Override = nul
3252 * @property {string} version Character version3284 * @property {string} version Character version
3253 * @property {string} charDepthPrompt Character depth note3285 * @property {string} charDepthPrompt Character depth note
3254 * @property {string} creatorNotes Character creator notes3286 * @property {string} creatorNotes Character creator notes
3287 * @property {string} firstMessage Character first message / greeting
3288 * @property {string[]} alternateGreetings Character alternate greetings
3255 */3289 */
32563290
3257/**3291/**
3258 * Helper to create an object with lazy, memoized getters from a map of field resolvers.3292 * Helper to create an object with lazy, memoized getters from a map of field resolvers.
3259 * @param {Record<string, () => string>} resolvers Map of field names to resolver functions3293 * @param {Record<string, () => string|string[]>} resolvers Map of field names to resolver functions
3260 * @returns {CharacterCardFields} Object with lazy getters3294 * @returns {CharacterCardFields} Object with lazy getters
3261 */3295 */
3262export function createLazyFields(resolvers) {3296export function createLazyFields(resolvers) {
@@ -3294,7 +3328,7 @@ export function getCharacterCardFieldsLazy({ chid = undefined } = {}) {
3294 const useGroupCards = selected_group && character;3328 const useGroupCards = selected_group && character;
3295 const groupCardsLazy = useGroupCards ? getGroupCharacterCardsLazy(selected_group, Number(currentChid)) : null;3329 const groupCardsLazy = useGroupCards ? getGroupCharacterCardsLazy(selected_group, Number(currentChid)) : null;
32963330
3297 /** @type {Record<string, () => string>} */3331 /** @type {Record<string, () => string|string[]>} */
3298 const resolvers = {3332 const resolvers = {
3299 persona: () => baseChatReplace(power_user.persona_description?.trim()),3333 persona: () => baseChatReplace(power_user.persona_description?.trim()),
3300 system: () => {3334 system: () => {
@@ -3338,6 +3372,17 @@ export function getCharacterCardFieldsLazy({ chid = undefined } = {}) {
3338 const exampleDialog = chat_metadata.mes_example || character.mes_example || '';3372 const exampleDialog = chat_metadata.mes_example || character.mes_example || '';
3339 return baseChatReplace(exampleDialog.trim());3373 return baseChatReplace(exampleDialog.trim());
3340 },3374 },
3375 firstMessage: () => {
3376 if (!character) return '';
3377 const firstMes = character.first_mes?.trim() || '';
3378 return baseChatReplace(firstMes);
3379 },
3380 alternateGreetings: () => {
3381 if (!character) return [];
3382 const altGreetings = character.data?.alternate_greetings;
3383 if (!Array.isArray(altGreetings)) return [];
3384 return altGreetings.map(greeting => baseChatReplace(greeting?.trim()));
3385 },
3341 };3386 };
33423387
3343 return createLazyFields(resolvers);3388 return createLazyFields(resolvers);
@@ -3364,6 +3409,8 @@ export function getCharacterCardFields({ chid = undefined } = {}) {
3364 version: lazy.version,3409 version: lazy.version,
3365 charDepthPrompt: lazy.charDepthPrompt,3410 charDepthPrompt: lazy.charDepthPrompt,
3366 creatorNotes: lazy.creatorNotes,3411 creatorNotes: lazy.creatorNotes,
3412 firstMessage: lazy.firstMessage,
3413 alternateGreetings: lazy.alternateGreetings,
3367 };3414 };
3368}3415}
33693416
@@ -3617,7 +3664,16 @@ class StreamingProcessor {
3617 }3664 }
3618 }3665 }
36193666
3620 async onFinishStreaming(messageId, text) {3667 /**
3668 * Finalizes an intermediary message after generation is complete, or a tool call is performed.
3669 * Performs essential message processing (code blocks, reasoning, swipes, attachments, events)
3670 * without the heavier finish operations (UI unlock - optional, auto-swipe, sound, save chat).
3671 * @param {number} messageId - The message ID to finalize.
3672 * @param {string} text - The message text.
3673 * @param {Object} options - Additional options for finalization.
3674 * @param {boolean} options.unlockUI - Whether to unlock the generation UI.
3675 */
3676 async finalizeIntermediaryMessage(messageId, text, { unlockUI = true }) {
3621 await this.onProgressStreaming(messageId, text, true);3677 await this.onProgressStreaming(messageId, text, true);
3622 const messageElement = chatElement.find(`.mes[mesid="${messageId}"]`);3678 const messageElement = chatElement.find(`.mes[mesid="${messageId}"]`);
3623 const message = chat[messageId];3679 const message = chat[messageId];
@@ -3656,7 +3712,9 @@ class StreamingProcessor {
3656 message.extra.reasoning_signature = this.reasoningSignature;3712 message.extra.reasoning_signature = this.reasoningSignature;
3657 }3713 }
36583714
3659 this.markUIGenStopped();3715 if (unlockUI) {
3716 this.markUIGenStopped();
3717 }
36603718
3661 if (this.type !== 'impersonate') {3719 if (this.type !== 'impersonate') {
3662 await eventSource.emit(event_types.MESSAGE_RECEIVED, this.messageId, this.type);3720 await eventSource.emit(event_types.MESSAGE_RECEIVED, this.messageId, this.type);
@@ -3666,6 +3724,10 @@ class StreamingProcessor {
3666 }3724 }
36673725
3668 updateSwipeCounter(messageId, { message, messageElement });3726 updateSwipeCounter(messageId, { message, messageElement });
3727 }
3728
3729 async onFinishStreaming(messageId, text) {
3730 await this.finalizeIntermediaryMessage(messageId, text, { unlockUI: true });
36693731
3670 const isAborted = this.abortController.signal.aborted;3732 const isAborted = this.abortController.signal.aborted;
3671 if (!isAborted && power_user.auto_swipe && generatedTextFiltered(text)) {3733 if (!isAborted && power_user.auto_swipe && generatedTextFiltered(text)) {
@@ -3709,9 +3771,9 @@ class StreamingProcessor {
3709 }3771 }
37103772
3711 /**3773 /**
3712 * @returns {Generator<{ text: string, swipes: string[], logprobs: import('./scripts/logprobs.js').TokenLogprobs, toolCalls: any[], state: any }, void, void>}3774 * @returns {AsyncGenerator<{ text: string, swipes: string[], logprobs: import('./scripts/logprobs.js').TokenLogprobs, toolCalls: any[], state: any }, void, void>}
3713 */3775 */
3714 *nullStreamingGeneration() {3776 async* nullStreamingGeneration() {
3715 throw new Error('Generation function for streaming is not hooked up');3777 throw new Error('Generation function for streaming is not hooked up');
3716 }3778 }
37173779
@@ -3726,7 +3788,7 @@ class StreamingProcessor {
3726 // when streaming, we cache the result of getStoppingStrings instead of calling it once per token.3788 // when streaming, we cache the result of getStoppingStrings instead of calling it once per token.
3727 const isImpersonate = this.type == 'impersonate';3789 const isImpersonate = this.type == 'impersonate';
3728 const isContinue = this.type == 'continue';3790 const isContinue = this.type == 'continue';
3729 this.stoppingStrings = getStoppingStrings(isImpersonate, isContinue);3791 this.stoppingStrings = getStoppingStrings(isImpersonate, isContinue, main_api);
37303792
3731 try {3793 try {
3732 const sw = new Stopwatch(1000 / power_user.streaming_fps);3794 const sw = new Stopwatch(1000 / power_user.streaming_fps);
@@ -3756,8 +3818,7 @@ class StreamingProcessor {
3756 }3818 }
3757 const seconds = (timestamps[timestamps.length - 1] - timestamps[0]) / 1000;3819 const seconds = (timestamps[timestamps.length - 1] - timestamps[0]) / 1000;
3758 console.warn(`Stream stats: ${timestamps.length} tokens, ${seconds.toFixed(2)} seconds, rate: ${Number(timestamps.length / seconds).toFixed(2)} TPS`);3820 console.warn(`Stream stats: ${timestamps.length} tokens, ${seconds.toFixed(2)} seconds, rate: ${Number(timestamps.length / seconds).toFixed(2)} TPS`);
3759 }3821 } catch (err) {
3760 catch (err) {
3761 // in the case of a self-inflicted abort, we have already cleaned up3822 // in the case of a self-inflicted abort, we have already cleaned up
3762 if (!this.isFinished) {3823 if (!this.isFinished) {
3763 console.error(err);3824 console.error(err);
@@ -3836,8 +3897,6 @@ export function createRawPrompt(prompt, api, instructOverride, quietToLoud, syst
3836}3897}
38373898
3838/**3899/**
3839 * Generates a message using the provided prompt.
3840 * If the prompt is an array of chat-style messages and not using chat completion, it will be converted to a text prompt.
3841 * @typedef {object} GenerateRawParams3900 * @typedef {object} GenerateRawParams
3842 * @prop {string | object[]} [prompt] Prompt to generate a message from. Can be a string or an array of chat-style messages, i.e. [{role: '', content: ''}, ...]3901 * @prop {string | object[]} [prompt] Prompt to generate a message from. Can be a string or an array of chat-style messages, i.e. [{role: '', content: ''}, ...]
3843 * @prop {string} [api] API to use. Main API is used if not specified.3902 * @prop {string} [api] API to use. Main API is used if not specified.
@@ -3848,15 +3907,15 @@ export function createRawPrompt(prompt, api, instructOverride, quietToLoud, syst
3848 * @prop {boolean} [trimNames] Whether to allow trimming "{{user}}:" and "{{char}}:" from the response.3907 * @prop {boolean} [trimNames] Whether to allow trimming "{{user}}:" and "{{char}}:" from the response.
3849 * @prop {string} [prefill] An optional prefill for the prompt.3908 * @prop {string} [prefill] An optional prefill for the prompt.
3850 * @prop {object} [jsonSchema] JSON schema to use for the structured generation. Usually requires a special instruction.3909 * @prop {object} [jsonSchema] JSON schema to use for the structured generation. Usually requires a special instruction.
3851 * @param {GenerateRawParams} params Parameters for generating a message
3852 * @returns {Promise<string>} Generated message
3853 */3910 */
3854export async function generateRaw({ prompt = '', api = null, instructOverride = false, quietToLoud = false, systemPrompt = '', responseLength = null, trimNames = true, prefill = '', jsonSchema = null } = {}) {
3855 if (arguments.length > 0 && typeof arguments[0] !== 'object') {
3856 console.trace('generateRaw called with positional arguments. Please use an object instead.');
3857 [prompt, api, instructOverride, quietToLoud, systemPrompt, responseLength, trimNames, prefill, jsonSchema] = arguments;
3858 }
38593911
3912/**
3913 * Generates a raw data object using the provided prompt.
3914 * This used to be part of `generateRaw`, but separating it out allows extensions to access other data such as reasoning message.
3915 * @param {GenerateRawParams} params Parameters for generating a message
3916 * @returns {Promise<object | string>} Raw API response data, or a JSON string extracted from the response when `jsonSchema` is provided.
3917 */
3918export async function generateRawData({ prompt = '', api = null, instructOverride = false, quietToLoud = false, systemPrompt = '', responseLength = null, prefill = '', jsonSchema = null } = {}) {
3860 if (!api) {3919 if (!api) {
3861 api = main_api;3920 api = main_api;
3862 }3921 }
@@ -3870,7 +3929,10 @@ export async function generateRaw({ prompt = '', api = null, instructOverride =
38703929
3871 // Allow extensions to stop generation before it happens3930 // Allow extensions to stop generation before it happens
3872 const eventAbortController = new AbortController();3931 const eventAbortController = new AbortController();
3873 const abortHook = () => eventAbortController.abort(new Error('Cancelled by extension'));3932 const abortHook = () => {
3933 abortController.abort(new Error('Cancelled by stop event'));
3934 eventAbortController.abort(new Error('Cancelled by extension'));
3935 };
3874 eventSource.on(event_types.GENERATION_STOPPED, abortHook);3936 eventSource.on(event_types.GENERATION_STOPPED, abortHook);
38753937
3876 try {3938 try {
@@ -3959,22 +4021,7 @@ export async function generateRaw({ prompt = '', api = null, instructOverride =
3959 return extractJsonFromData(data, { mainApi: api });4021 return extractJsonFromData(data, { mainApi: api });
3960 }4022 }
39614023
3962 // format result, exclude user prompt bias4024 return data;
3963 const message = cleanUpMessage({
3964 getMessage: extractMessageFromData(data),
3965 isImpersonate: false,
3966 isContinue: false,
3967 displayIncompleteSentences: true,
3968 includeUserPromptBias: false,
3969 trimNames: trimNames,
3970 trimWrongNames: trimNames,
3971 });
3972
3973 if (!message) {
3974 throw new Error('No message generated');
3975 }
3976
3977 return message;
3978 } finally {4025 } finally {
3979 eventSource.removeListener(event_types.GENERATION_STOPPED, abortHook);4026 eventSource.removeListener(event_types.GENERATION_STOPPED, abortHook);
3980 if (responseLengthCustomized && TempResponseLength.isCustomized()) {4027 if (responseLengthCustomized && TempResponseLength.isCustomized()) {
@@ -3984,6 +4031,43 @@ export async function generateRaw({ prompt = '', api = null, instructOverride =
3984 }4031 }
3985}4032}
39864033
4034/**
4035 * Generates a message using the provided prompt.
4036 * If the prompt is an array of chat-style messages and not using chat completion, it will be converted to a text prompt.
4037 * @param {GenerateRawParams} params Parameters for generating a message
4038 * @returns {Promise<string>} Generated output: a cleaned-up message string when `jsonSchema` is not provided, or an extracted JSON string conforming to `jsonSchema` when it is.
4039 */
4040export async function generateRaw({ prompt = '', api = null, instructOverride = false, quietToLoud = false, systemPrompt = '', responseLength = null, trimNames = true, prefill = '', jsonSchema = null } = {}) {
4041 if (arguments.length > 0 && typeof arguments[0] !== 'object') {
4042 console.trace('generateRaw called with positional arguments. Please use an object instead.');
4043 [prompt, api, instructOverride, quietToLoud, systemPrompt, responseLength, trimNames, prefill, jsonSchema] = arguments;
4044 }
4045
4046 const data = await generateRawData({ prompt, api, instructOverride, quietToLoud, systemPrompt, responseLength, prefill, jsonSchema });
4047
4048 // JSON string (matching the provided schema) will already be extracted.
4049 if (jsonSchema) {
4050 return data;
4051 }
4052
4053 // format result, exclude user prompt bias
4054 const message = cleanUpMessage({
4055 getMessage: extractMessageFromData(data, api),
4056 isImpersonate: false,
4057 isContinue: false,
4058 displayIncompleteSentences: true,
4059 includeUserPromptBias: false,
4060 trimNames: trimNames,
4061 trimWrongNames: trimNames,
4062 });
4063
4064 if (!message) {
4065 throw new Error('No message generated');
4066 }
4067
4068 return message;
4069}
4070
3987class TempResponseLength {4071class TempResponseLength {
3988 static #originalResponseLength = -1;4072 static #originalResponseLength = -1;
3989 static #lastApi = null;4073 static #lastApi = null;
@@ -4237,8 +4321,7 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
4237 textareaText = '';4321 textareaText = '';
4238 if (chat.length && lastMessage.is_user) {4322 if (chat.length && lastMessage.is_user) {
4239 //do nothing? why does this check exist?4323 //do nothing? why does this check exist?
4240 }4324 } else if (type !== 'quiet' && type !== 'swipe' && !isImpersonate && !dryRun && !depth && chat.length) {
4241 else if (type !== 'quiet' && type !== 'swipe' && !isImpersonate && !dryRun && !depth && chat.length) {
4242 deleteItemizedPromptForMessage(chat.length - 1);4325 deleteItemizedPromptForMessage(chat.length - 1);
4243 chat.length = chat.length - 1;4326 chat.length = chat.length - 1;
4244 await removeLastMessage();4327 await removeLastMessage();
@@ -4283,12 +4366,10 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
4283 // If user message contains no text other than bias - send as a system message4366 // If user message contains no text other than bias - send as a system message
4284 if (messageBias && !removeMacros(textareaText)) {4367 if (messageBias && !removeMacros(textareaText)) {
4285 sendSystemMessage(system_message_types.GENERIC, ' ', { bias: messageBias });4368 sendSystemMessage(system_message_types.GENERIC, ' ', { bias: messageBias });
4286 }4369 } else {
4287 else {
4288 await sendMessageAsUser(textareaText, messageBias);4370 await sendMessageAsUser(textareaText, messageBias);
4289 }4371 }
4290 }4372 } else if (textareaText == '' && !automatic_trigger && !dryRun && [undefined, 'normal'].includes(type) && main_api == 'openai' && oai_settings.send_if_empty.trim().length > 0 && !depth) {
4291 else if (textareaText == '' && !automatic_trigger && !dryRun && [undefined, 'normal'].includes(type) && main_api == 'openai' && oai_settings.send_if_empty.trim().length > 0 && !depth) {
4292 // Use send_if_empty if set and the user message is empty. Only when sending messages normally4373 // Use send_if_empty if set and the user message is empty. Only when sending messages normally
4293 await sendMessageAsUser(oai_settings.send_if_empty.trim(), messageBias);4374 await sendMessageAsUser(oai_settings.send_if_empty.trim(), messageBias);
4294 }4375 }
@@ -4387,7 +4468,7 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
4387 }4468 }
43884469
4389 // Determine token limit4470 // Determine token limit
4390 let this_max_context = getMaxContextSize();4471 let this_max_context = getMaxPromptTokens();
43914472
4392 if (!dryRun) {4473 if (!dryRun) {
4393 console.debug('Running extension interceptors');4474 console.debug('Running extension interceptors');
@@ -4407,8 +4488,7 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
4407 if (main_api == 'koboldhorde' && (horde_settings.auto_adjust_context_length || horde_settings.auto_adjust_response_length)) {4488 if (main_api == 'koboldhorde' && (horde_settings.auto_adjust_context_length || horde_settings.auto_adjust_response_length)) {
4408 try {4489 try {
4409 adjustedParams = await adjustHordeGenerationParams(max_context, amount_gen);4490 adjustedParams = await adjustHordeGenerationParams(max_context, amount_gen);
4410 }4491 } catch {
4411 catch {
4412 unblockGeneration(type);4492 unblockGeneration(type);
4413 return Promise.resolve();4493 return Promise.resolve();
4414 }4494 }
@@ -4586,8 +4666,7 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
4586 // When continuing generation of previous output, last user message precedes the message to continue4666 // When continuing generation of previous output, last user message precedes the message to continue
4587 if (isContinue) {4667 if (isContinue) {
4588 coreChat.splice(coreChat.length - 1, 0, { mes: jailbreak, is_user: true });4668 coreChat.splice(coreChat.length - 1, 0, { mes: jailbreak, is_user: true });
4589 }4669 } else {
4590 else {
4591 // This operation will result in the injectedIndices indexes being off by one4670 // This operation will result in the injectedIndices indexes being off by one
4592 coreChat.push({ mes: jailbreak, is_user: true });4671 coreChat.push({ mes: jailbreak, is_user: true });
4593 // Add +1 to the elements to correct for the new PHI/Jailbreak message.4672 // Add +1 to the elements to correct for the new PHI/Jailbreak message.
@@ -4862,7 +4941,6 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
48624941
4863 // Add quiet generation prompt at depth 04942 // Add quiet generation prompt at depth 0
4864 if (quiet_prompt && quiet_prompt.length) {4943 if (quiet_prompt && quiet_prompt.length) {
4865
4866 // here name1 is forced for all quiet prompts..why?4944 // here name1 is forced for all quiet prompts..why?
4867 const name = name1;4945 const name = name1;
4868 //checks if we are in instruct, if so, formats the chat as such, otherwise just adds the quiet prompt4946 //checks if we are in instruct, if so, formats the chat as such, otherwise just adds the quiet prompt
@@ -5209,8 +5287,7 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
52095287
5210 if (itemizedIndex !== -1) {5288 if (itemizedIndex !== -1) {
5211 itemizedPrompts[itemizedIndex] = additionalPromptStuff;5289 itemizedPrompts[itemizedIndex] = additionalPromptStuff;
5212 }5290 } else {
5213 else {
5214 itemizedPrompts.push(additionalPromptStuff);5291 itemizedPrompts.push(additionalPromptStuff);
5215 }5292 }
52165293
@@ -5246,7 +5323,12 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
5246 const hasToolCalls = ToolManager.hasToolCalls(streamingProcessor.toolCalls);5323 const hasToolCalls = ToolManager.hasToolCalls(streamingProcessor.toolCalls);
5247 const shouldDeleteMessage = type !== 'swipe' && ['', '...'].includes(lastMessage?.mes) && !lastMessage?.extra?.reasoning && ['', '...'].includes(streamingProcessor?.result);5324 const shouldDeleteMessage = type !== 'swipe' && ['', '...'].includes(lastMessage?.mes) && !lastMessage?.extra?.reasoning && ['', '...'].includes(streamingProcessor?.result);
5248 hasToolCalls && shouldDeleteMessage && await deleteLastMessage();5325 hasToolCalls && shouldDeleteMessage && await deleteLastMessage();
5249 const invocationResult = await ToolManager.invokeFunctionTools(streamingProcessor.toolCalls);5326 if (hasToolCalls && !shouldDeleteMessage) {
5327 await streamingProcessor.finalizeIntermediaryMessage(streamingProcessor.messageId, getMessage, { unlockUI: false });
5328 }
5329 const invocationResult = await ToolManager.invokeFunctionTools(streamingProcessor.toolCalls, {
5330 reasoningText: streamingProcessor.reasoningHandler.reasoning,
5331 });
5250 const shouldStopGeneration = (!invocationResult.invocations.length && shouldDeleteMessage) || invocationResult.stealthCalls.length;5332 const shouldStopGeneration = (!invocationResult.invocations.length && shouldDeleteMessage) || invocationResult.stealthCalls.length;
5251 if (hasToolCalls) {5333 if (hasToolCalls) {
5252 if (shouldStopGeneration) {5334 if (shouldStopGeneration) {
@@ -5352,17 +5434,14 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
5352 if (isImpersonate) {5434 if (isImpersonate) {
5353 $('#send_textarea').val(getMessage)[0].dispatchEvent(new Event('input', { bubbles: true }));5435 $('#send_textarea').val(getMessage)[0].dispatchEvent(new Event('input', { bubbles: true }));
5354 await eventSource.emit(event_types.IMPERSONATE_READY, getMessage);5436 await eventSource.emit(event_types.IMPERSONATE_READY, getMessage);
5355 }5437 } else if (type == 'quiet') {
5356 else if (type == 'quiet') {
5357 unblockGeneration(type);5438 unblockGeneration(type);
5358 return getMessage;5439 return getMessage;
5359 }5440 } else {
5360 else {
5361 // Without streaming we'll be having a full message on continuation. Treat it as a last chunk.5441 // Without streaming we'll be having a full message on continuation. Treat it as a last chunk.
5362 if (originalType !== 'continue') {5442 if (originalType !== 'continue') {
5363 ({ type, getMessage } = await saveReply({ type, getMessage, title, swipes, reasoning, imageUrls, reasoningSignature }));5443 ({ type, getMessage } = await saveReply({ type, getMessage, title, swipes, reasoning, imageUrls, reasoningSignature }));
5364 }5444 } else {
5365 else {
5366 ({ type, getMessage } = await saveReply({ type: 'appendFinal', getMessage, title, swipes, reasoning, imageUrls, reasoningSignature }));5445 ({ type, getMessage } = await saveReply({ type: 'appendFinal', getMessage, title, swipes, reasoning, imageUrls, reasoningSignature }));
5367 }5446 }
53685447
@@ -5374,7 +5453,7 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
5374 const hasToolCalls = ToolManager.hasToolCalls(data);5453 const hasToolCalls = ToolManager.hasToolCalls(data);
5375 const shouldDeleteMessage = type !== 'swipe' && ['', '...'].includes(getMessage) && !reasoning;5454 const shouldDeleteMessage = type !== 'swipe' && ['', '...'].includes(getMessage) && !reasoning;
5376 hasToolCalls && shouldDeleteMessage && await deleteLastMessage();5455 hasToolCalls && shouldDeleteMessage && await deleteLastMessage();
5377 const invocationResult = await ToolManager.invokeFunctionTools(data);5456 const invocationResult = await ToolManager.invokeFunctionTools(data, { reasoningText: reasoning });
5378 const shouldStopGeneration = (!invocationResult.invocations.length && shouldDeleteMessage) || invocationResult.stealthCalls.length;5457 const shouldStopGeneration = (!invocationResult.invocations.length && shouldDeleteMessage) || invocationResult.stealthCalls.length;
5379 if (hasToolCalls) {5458 if (hasToolCalls) {
5380 if (shouldStopGeneration) {5459 if (shouldStopGeneration) {
@@ -5399,7 +5478,6 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
5399 if (!isAborted && power_user.auto_swipe && generatedTextFiltered(getMessage)) {5478 if (!isAborted && power_user.auto_swipe && generatedTextFiltered(getMessage)) {
5400 is_send_press = false;5479 is_send_press = false;
5401 return await swipe(null, SWIPE_DIRECTION.RIGHT, { source: SWIPE_SOURCE.AUTO_SWIPE, repeated: true, forceMesId: chat.length - 1 });5480 return await swipe(null, SWIPE_DIRECTION.RIGHT, { source: SWIPE_SOURCE.AUTO_SWIPE, repeated: true, forceMesId: chat.length - 1 });
5402
5403 }5481 }
54045482
5405 console.debug('/api/chats/save called by /Generate');5483 console.debug('/api/chats/save called by /Generate');
@@ -5756,21 +5834,15 @@ export async function sendMessageAsUser(messageText, messageBias, insertAt = nul
5756}5834}
57575835
5758/**5836/**
5759 * Gets the maximum usable context size for the current API.5837 * Gets the maximum context token limit (the full context window size before subtracting response length).
5760 * @param {number|null} overrideResponseLength Optional override for the response length.5838 * @returns {number} The maximum context token limit for the current API.
5761 * @returns {number} Maximum usable context size.
5762 */5839 */
5763export function getMaxContextSize(overrideResponseLength = null) {5840export function getMaxContextTokens() {
5764 if (typeof overrideResponseLength !== 'number' || overrideResponseLength <= 0 || isNaN(overrideResponseLength)) {
5765 overrideResponseLength = null;
5766 }
5767
5768 let this_max_context = 1487;
5769 if (main_api == 'kobold' || main_api == 'koboldhorde' || main_api == 'textgenerationwebui') {5841 if (main_api == 'kobold' || main_api == 'koboldhorde' || main_api == 'textgenerationwebui') {
5770 this_max_context = (max_context - (overrideResponseLength || amount_gen));5842 return max_context;
5771 }5843 }
5772 if (main_api == 'novel') {5844 if (main_api == 'novel') {
5773 this_max_context = Number(max_context);5845 let this_max_context = Number(max_context);
5774 if (nai_settings.model_novel.includes('clio')) {5846 if (nai_settings.model_novel.includes('clio')) {
5775 this_max_context = Math.min(max_context, 8192);5847 this_max_context = Math.min(max_context, 8192);
5776 }5848 }
@@ -5790,13 +5862,39 @@ export function getMaxContextSize(overrideResponseLength = null) {
5790 // Added special tokens and whatnot5862 // Added special tokens and whatnot
5791 this_max_context -= 10;5863 this_max_context -= 10;
5792 }5864 }
5865 return this_max_context;
5866 }
5867 if (main_api == 'openai') {
5868 return oai_settings.openai_max_context;
5869 }
5870 return 1487;
5871}
57935872
5794 this_max_context = this_max_context - (overrideResponseLength || amount_gen);5873/**
5874 * Gets the maximum response token limit (the max generation/reply length).
5875 * @returns {number} The maximum response token limit for the current API.
5876 */
5877export function getMaxResponseTokens() {
5878 if (main_api == 'kobold' || main_api == 'koboldhorde' || main_api == 'textgenerationwebui' || main_api == 'novel') {
5879 return amount_gen;
5795 }5880 }
5796 if (main_api == 'openai') {5881 if (main_api == 'openai') {
5797 this_max_context = oai_settings.openai_max_context - (overrideResponseLength || oai_settings.openai_max_tokens);5882 return oai_settings.openai_max_tokens;
5798 }5883 }
5799 return this_max_context;5884 return 0;
5885}
5886
5887/**
5888 * Gets the maximum usable prompt size for the current API.
5889 * @param {number|null} overrideResponseLength Optional override for the response length.
5890 * @returns {number} Maximum usable prompt size.
5891 */
5892export function getMaxPromptTokens(overrideResponseLength = null) {
5893 if (typeof overrideResponseLength !== 'number' || overrideResponseLength <= 0 || isNaN(overrideResponseLength)) {
5894 overrideResponseLength = null;
5895 }
5896
5897 return getMaxContextTokens() - (overrideResponseLength || getMaxResponseTokens());
5800}5898}
58015899
5802function parseTokenCounts(counts, thisPromptBits) {5900function parseTokenCounts(counts, thisPromptBits) {
@@ -5832,41 +5930,65 @@ function addChatsPreamble(mesSendString) {
5832function addChatsSeparator(mesSendString) {5930function addChatsSeparator(mesSendString) {
5833 if (power_user.context.chat_start) {5931 if (power_user.context.chat_start) {
5834 return substituteParams(power_user.context.chat_start + '\n') + mesSendString;5932 return substituteParams(power_user.context.chat_start + '\n') + mesSendString;
5835 }5933 } else {
5836
5837 else {
5838 return mesSendString;5934 return mesSendString;
5839 }5935 }
5840}5936}
58415937
5842export async function duplicateCharacter() {5938/**
5843 if (this_chid === undefined || !characters[this_chid]) {5939 * Duplicates a character.
5844 toastr.warning(t`You must first select a character to duplicate!`);5940 * @param {object} [options={}] - Options
5845 return '';5941 * @param {string} [options.avatar] - Avatar key of the character to duplicate. Uses current character if not provided.
5942 * @param {boolean} [options.silent=false] - Whether to skip the confirmation popup
5943 * @returns {Promise<string>} The avatar key of the duplicated character, or empty string if cancelled/failed
5944 */
5945export async function duplicateCharacter({ avatar = null, silent = false } = {}) {
5946 // Determine the character to duplicate
5947 let targetAvatar;
5948 if (avatar) {
5949 const character = characters.find(c => c.avatar === avatar);
5950 if (!character) {
5951 toastr.warning(t`Character not found: ${avatar}`);
5952 return '';
5953 }
5954 targetAvatar = avatar;
5955 } else {
5956 if (this_chid === undefined || !characters[this_chid]) {
5957 toastr.warning(t`You must first select a character to duplicate!`);
5958 return '';
5959 }
5960 targetAvatar = characters[this_chid].avatar;
5846 }5961 }
58475962
5848 const confirmMessage = $(await renderTemplateAsync('duplicateConfirm'));5963 // Show confirmation unless silent
5849 const confirm = await callGenericPopup(confirmMessage, POPUP_TYPE.CONFIRM);5964 if (!silent) {
5965 const confirmMessage = $(await renderTemplateAsync('duplicateConfirm'));
5966 const confirm = await callGenericPopup(confirmMessage, POPUP_TYPE.CONFIRM);
58505967
5851 if (!confirm) {5968 if (!confirm) {
5852 console.log('User cancelled duplication');5969 console.log('User cancelled duplication');
5853 return '';5970 return '';
5971 }
5854 }5972 }
58555973
5856 const body = { avatar_url: characters[this_chid].avatar };5974 const body = { avatar_url: targetAvatar };
5857 const response = await fetch('/api/characters/duplicate', {5975 const response = await fetch('/api/characters/duplicate', {
5858 method: 'POST',5976 method: 'POST',
5859 headers: getRequestHeaders(),5977 headers: getRequestHeaders(),
5860 body: JSON.stringify(body),5978 body: JSON.stringify(body),
5861 });5979 });
5862 if (response.ok) {5980
5863 toastr.success(t`Character Duplicated`);5981 if (!response.ok) {
5864 const data = await response.json();5982 toastr.error(t`Failed to duplicate character`);
5865 await eventSource.emit(event_types.CHARACTER_DUPLICATED, { oldAvatar: body.avatar_url, newAvatar: data.path });5983 return '';
5866 await getCharacters();
5867 }5984 }
58685985
5869 return '';5986 toastr.success(t`Character Duplicated`);
5987 const data = await response.json();
5988 await eventSource.emit(event_types.CHARACTER_DUPLICATED, { oldAvatar: targetAvatar, newAvatar: data.path });
5989 await getCharacters();
5990
5991 return data.path;
5870}5992}
58715993
5872function setInContextMessages(msgInContextCount, type) {5994function setInContextMessages(msgInContextCount, type) {
@@ -6078,7 +6200,7 @@ export function extractMessageFromData(data, activeApi = null) {
6078 case 'novel':6200 case 'novel':
6079 return data.output;6201 return data.output;
6080 case 'openai':6202 case 'openai':
6081 return data?.content?.find(p => p.type === 'text')?.text ?? data?.choices?.[0]?.message?.content ?? data?.choices?.[0]?.text ?? data?.text ?? data?.message?.content?.[0]?.text ?? data?.message?.tool_plan ?? '';6203 return data?.content?.filter(p => p.type === 'text')?.map(p => p.text)?.join('\n\n') ?? data?.choices?.[0]?.message?.content ?? data?.choices?.[0]?.text ?? data?.text ?? data?.message?.content?.[0]?.text ?? data?.message?.tool_plan ?? '';
6082 default:6204 default:
6083 return '';6205 return '';
6084 }6206 }
@@ -6242,7 +6364,7 @@ export function cleanUpMessage({ getMessage, isImpersonate, isContinue, displayI
6242 // Allow for caching of stopping strings. getStoppingStrings is an expensive function, especially with macros6364 // Allow for caching of stopping strings. getStoppingStrings is an expensive function, especially with macros
6243 // enabled, so for streaming, we call it once and then pass it into each cleanUpMessage call.6365 // enabled, so for streaming, we call it once and then pass it into each cleanUpMessage call.
6244 if (!stoppingStrings) {6366 if (!stoppingStrings) {
6245 stoppingStrings = getStoppingStrings(isImpersonate, isContinue);6367 stoppingStrings = getStoppingStrings(isImpersonate, isContinue, main_api);
6246 }6368 }
62476369
6248 for (const stoppingString of stoppingStrings) {6370 for (const stoppingString of stoppingStrings) {
@@ -6517,7 +6639,6 @@ export async function saveReply({ type, getMessage, fromStreaming = false, title
6517 !fromStreaming && await eventSource.emit(event_types.MESSAGE_RECEIVED, chat_id, type);6639 !fromStreaming && await eventSource.emit(event_types.MESSAGE_RECEIVED, chat_id, type);
6518 addOneMessage(chat[chat_id], { type: 'swipe' });6640 addOneMessage(chat[chat_id], { type: 'swipe' });
6519 !fromStreaming && await eventSource.emit(event_types.CHARACTER_MESSAGE_RENDERED, chat_id, type);6641 !fromStreaming && await eventSource.emit(event_types.CHARACTER_MESSAGE_RENDERED, chat_id, type);
6520
6521 } else {6642 } else {
6522 console.debug('entering chat update routine for non-swipe post');6643 console.debug('entering chat update routine for non-swipe post');
6523 const newMessage = {};6644 const newMessage = {};
@@ -6728,20 +6849,24 @@ export function syncMesToSwipe(messageId = null) {
6728 * If the swipe data is invalid in some way, this function will exit out without doing anything.6849 * If the swipe data is invalid in some way, this function will exit out without doing anything.
6729 * @param {number?} [messageId=null] - The ID of the message to sync with the swipe data. If no ID is given, the last message is used.6850 * @param {number?} [messageId=null] - The ID of the message to sync with the swipe data. If no ID is given, the last message is used.
6730 * @param {number?} [swipeId=null] - The ID of the swipe to sync. If no ID is given, the current swipe ID in the message object is used.6851 * @param {number?} [swipeId=null] - The ID of the swipe to sync. If no ID is given, the current swipe ID in the message object is used.
6852 * @param {ChatMessage?} [targetMessage=null] - The message object to sync instead of resolving one from `chat`.
6731 * @returns {boolean} Whether the swipe data was successfully synced to the message6853 * @returns {boolean} Whether the swipe data was successfully synced to the message
6732 */6854 */
6733export function syncSwipeToMes(messageId = null, swipeId = null) {6855export function syncSwipeToMes(messageId = null, swipeId = null, targetMessage = null) {
6734 if (!chat.length) {6856 if (!targetMessage && !chat.length) {
6735 return false;6857 return false;
6736 }6858 }
67376859
6738 const targetMessageId = messageId ?? chat.length - 1;6860 if (!targetMessage) {
6739 if (targetMessageId >= chat.length || targetMessageId < 0) {6861 const targetMessageId = messageId ?? chat.length - 1;
6740 console.warn(`[syncSwipeToMes] Invalid message ID: ${messageId}`);6862 if (targetMessageId >= chat.length || targetMessageId < 0) {
6741 return false;6863 console.warn(`[syncSwipeToMes] Invalid message ID: ${messageId}`);
6864 return false;
6865 }
6866
6867 targetMessage = chat[targetMessageId];
6742 }6868 }
67436869
6744 const targetMessage = chat[targetMessageId];
6745 if (!targetMessage) {6870 if (!targetMessage) {
6746 return false;6871 return false;
6747 }6872 }
@@ -7059,16 +7184,13 @@ export async function renameCharacter(name = null, { silent = false, renameChats
7059 } else {7184 } else {
7060 toastr.success(t`Character renamed!`, t`Rename Character`);7185 toastr.success(t`Character renamed!`, t`Rename Character`);
7061 }7186 }
7062 }7187 } else {
7063 else {
7064 throw new Error('Newly renamed character was lost?');7188 throw new Error('Newly renamed character was lost?');
7065 }7189 }
7066 }7190 } else {
7067 else {
7068 throw new Error('Could not rename the character');7191 throw new Error('Could not rename the character');
7069 }7192 }
7070 }7193 } catch (error) {
7071 catch (error) {
7072 // Reloading to prevent data corruption7194 // Reloading to prevent data corruption
7073 if (!silent) await Popup.show.text(t`Rename Character`, t`Something went wrong. The page will be reloaded.`);7195 if (!silent) await Popup.show.text(t`Rename Character`, t`Something went wrong. The page will be reloaded.`);
7074 else toastr.error(t`Something went wrong. The page will be reloaded.`, t`Rename Character`);7196 else toastr.error(t`Something went wrong. The page will be reloaded.`, t`Rename Character`);
@@ -7113,7 +7235,7 @@ async function renamePastChats(oldAvatar, newAvatar, newName) {
71137235
7114 await eventSource.emit(event_types.CHARACTER_RENAMED_IN_PAST_CHAT, currentChat, oldAvatar, newAvatar);7236 await eventSource.emit(event_types.CHARACTER_RENAMED_IN_PAST_CHAT, currentChat, oldAvatar, newAvatar);
71157237
7116 const saveChatResponse = await fetch('/api/chats/save', {7238 const saveChatRequest = await compressRequest({
7117 method: 'POST',7239 method: 'POST',
7118 headers: getRequestHeaders(),7240 headers: getRequestHeaders(),
7119 body: JSON.stringify({7241 body: JSON.stringify({
@@ -7124,6 +7246,7 @@ async function renamePastChats(oldAvatar, newAvatar, newName) {
7124 }),7246 }),
7125 cache: 'no-cache',7247 cache: 'no-cache',
7126 });7248 });
7249 const saveChatResponse = await fetch('/api/chats/save', saveChatRequest);
71277250
7128 if (!saveChatResponse.ok) {7251 if (!saveChatResponse.ok) {
7129 throw new Error('Could not save chat');7252 throw new Error('Could not save chat');
@@ -7166,10 +7289,11 @@ export function saveChatDebounced() {
7166 * @param {object} [options.withMetadata] Additional metadata to save with the chat7289 * @param {object} [options.withMetadata] Additional metadata to save with the chat
7167 * @param {number} [options.mesId] The message ID to save the chat up to7290 * @param {number} [options.mesId] The message ID to save the chat up to
7168 * @param {boolean} [options.force] Force the saving despite the integrity check result7291 * @param {boolean} [options.force] Force the saving despite the integrity check result
7292 * @param {ChatMessage[]} [options.chatData] Chat snapshot to save instead of the current in-memory chat
7169 *7293 *
7170 * @returns {Promise<void>}7294 * @returns {Promise<void>}
7171 */7295 */
7172export async function saveChat({ chatName, withMetadata, mesId, force = false } = {}) {7296export async function saveChat({ chatName, withMetadata, mesId, force = false, chatData = undefined } = {}) {
7173 if (selected_group) {7297 if (selected_group) {
7174 toastr.error(t`Operation was aborted to prevent data corruption.`, t`saveChat called for a group chat`);7298 toastr.error(t`Operation was aborted to prevent data corruption.`, t`saveChat called for a group chat`);
7175 throw new Error('saveChat called for a group chat');7299 throw new Error('saveChat called for a group chat');
@@ -7195,9 +7319,11 @@ export async function saveChat({ chatName, withMetadata, mesId, force = false }
71957319
7196 characters[this_chid].date_last_chat = Date.now();7320 characters[this_chid].date_last_chat = Date.now();
71977321
7198 const trimmedChat = (mesId !== undefined && mesId >= 0 && mesId < chat.length)7322 const trimmedChat = Array.isArray(chatData)
7199 ? chat.slice(0, Number(mesId) + 1)7323 ? chatData
7200 : chat.slice();7324 : (mesId !== undefined && mesId >= 0 && mesId < chat.length)
7325 ? chat.slice(0, Number(mesId) + 1)
7326 : chat.slice();
72017327
7202 /** @type {ChatHeader} */7328 /** @type {ChatHeader} */
7203 const chatHeader = {7329 const chatHeader = {
@@ -7207,7 +7333,7 @@ export async function saveChat({ chatName, withMetadata, mesId, force = false }
7207 };7333 };
72087334
7209 try {7335 try {
7210 const result = await fetch('/api/chats/save', {7336 const saveChatRequest = await compressRequest({
7211 method: 'POST',7337 method: 'POST',
7212 cache: 'no-cache',7338 cache: 'no-cache',
7213 headers: getRequestHeaders(),7339 headers: getRequestHeaders(),
@@ -7219,6 +7345,7 @@ export async function saveChat({ chatName, withMetadata, mesId, force = false }
7219 force: force,7345 force: force,
7220 }),7346 }),
7221 });7347 });
7348 const result = await fetch('/api/chats/save', saveChatRequest);
72227349
7223 if (result.ok) {7350 if (result.ok) {
7224 return;7351 return;
@@ -7287,29 +7414,26 @@ async function read_avatar_load(input) {
7287 }7414 }
72887415
7289 await createOrEditCharacter();7416 await createOrEditCharacter();
7290 await delay(DEFAULT_SAVE_EDIT_TIMEOUT);
72917417
7292 const formData = new FormData(/** @type {HTMLFormElement} */($('#form_create').get(0)));7418 const formData = new FormData(/** @type {HTMLFormElement} */($('#form_create').get(0)));
7293 await fetch(getThumbnailUrl('avatar', formData.get('avatar_url').toString()), {7419 const avatarKey = formData.get('avatar_url').toString();
7294 method: 'GET',
7295 cache: 'reload',
7296 });
72977420
7298 const messages = $('.mes').toArray();7421 // Bust cache for the avatar thumbnail and character image
7299 for (const el of messages) {7422 const thumbnailUrl = getThumbnailUrl('avatar', avatarKey);
7300 const $el = $(el);7423 await fetch(thumbnailUrl, { method: 'GET', cache: 'reload' });
7301 const nameMatch = $el.attr('ch_name') == formData.get('ch_name');7424 await fetch(`/characters/${avatarKey}`, { method: 'GET', cache: 'reload' });
7302 if ($el.attr('is_system') == 'true' && !nameMatch) continue;
7303 if ($el.attr('is_user') == 'true') continue;
73047425
7305 if (nameMatch) {7426 // Refresh all visible avatar images that use this thumbnail URL
7306 const previewSrc = $('#avatar_load_preview').attr('src');7427 // This handles messages, character list, and any other place using the thumbnail
7307 const avatar = $el.find('.avatar img');7428 const avatarImages = document.querySelectorAll(`img[src^="${thumbnailUrl}"]`);
7308 avatar.attr('src', default_avatar);7429 for (const img of avatarImages) {
7309 await delay(1);7430 if (img instanceof HTMLImageElement) {
7310 avatar.attr('src', previewSrc);7431 const originalSrc = img.src;
7432 img.src = '';
7433 img.src = originalSrc;
7311 }7434 }
7312 }7435 }
7436 console.debug(`Refreshed ${avatarImages.length} avatar images for ${avatarKey}`);
73137437
7314 console.log('Avatar refreshed');7438 console.log('Avatar refreshed');
7315 }7439 }
@@ -7360,8 +7484,7 @@ export function buildAvatarList(block, entities, { templateId = 'inline_avatar_t
7360 avatarTemplate.append(grpTemplate.children());7484 avatarTemplate.append(grpTemplate.children());
7361 avatarTemplate.attr({ 'data-grid': id, 'data-chid': null });7485 avatarTemplate.attr({ 'data-grid': id, 'data-chid': null });
7362 avatarTemplate.attr('title', `[Group] ${entity.item.name}`);7486 avatarTemplate.attr('title', `[Group] ${entity.item.name}`);
7363 }7487 } else if (entity.type === 'persona') {
7364 else if (entity.type === 'persona') {
7365 avatarTemplate.attr({ 'data-pid': id, 'data-chid': null });7488 avatarTemplate.attr({ 'data-pid': id, 'data-chid': null });
7366 avatarTemplate.find('img').attr('src', getThumbnailUrl('persona', entity.item.avatar));7489 avatarTemplate.find('img').attr('src', getThumbnailUrl('persona', entity.item.avatar));
7367 avatarTemplate.attr('title', `[Persona] ${entity.item.name}\nFile: ${entity.item.avatar}`);7490 avatarTemplate.attr('title', `[Persona] ${entity.item.name}\nFile: ${entity.item.avatar}`);
@@ -7688,7 +7811,7 @@ function reloadLoop() {
76887811
7689//MARK: getSettings()7812//MARK: getSettings()
7690///////////////////////////////////////////7813///////////////////////////////////////////
7691export async function getSettings() {7814export async function getSettings(initLoaderHandle = null) {
7692 const response = await fetch('/api/settings/get', {7815 const response = await fetch('/api/settings/get', {
7693 method: 'POST',7816 method: 'POST',
7694 headers: getRequestHeaders(),7817 headers: getRequestHeaders(),
@@ -7712,6 +7835,7 @@ export async function getSettings() {
77127835
7713 accountStorage.init(settings?.accountStorage);7836 accountStorage.init(settings?.accountStorage);
7714 await setUserControls(data.enable_accounts);7837 await setUserControls(data.enable_accounts);
7838 setRequestCompressionConfig(data.request_compression);
77157839
7716 // Allow subscribers to mutate settings7840 // Allow subscribers to mutate settings
7717 await eventSource.emit(event_types.SETTINGS_LOADED_BEFORE, settings);7841 await eventSource.emit(event_types.SETTINGS_LOADED_BEFORE, settings);
@@ -7805,7 +7929,7 @@ export async function getSettings() {
7805 firstRun = !!settings.firstRun;7929 firstRun = !!settings.firstRun;
78067930
7807 if (firstRun) {7931 if (firstRun) {
7808 hideLoader();7932 await initLoaderHandle?.hide();
7809 await doOnboarding(user_avatar);7933 await doOnboarding(user_avatar);
7810 firstRun = false;7934 firstRun = false;
7811 }7935 }
@@ -7862,12 +7986,13 @@ export async function saveSettings(loopCounter = 0) {
7862 };7986 };
78637987
7864 try {7988 try {
7865 const result = await fetch('/api/settings/save', {7989 const saveSettingsRequest = await compressRequest({
7866 method: 'POST',7990 method: 'POST',
7867 headers: getRequestHeaders(),7991 headers: getRequestHeaders(),
7868 body: JSON.stringify(payload),7992 body: JSON.stringify(payload),
7869 cache: 'no-cache',7993 cache: 'no-cache',
7870 });7994 });
7995 const result = await fetch('/api/settings/save', saveSettingsRequest);
78717996
7872 if (!result.ok) {7997 if (!result.ok) {
7873 throw new Error(`Failed to save settings: ${result.statusText}`);7998 throw new Error(`Failed to save settings: ${result.statusText}`);
@@ -8103,8 +8228,7 @@ async function messageEditCancel(messageId = this_edit_mes_id) {
8103 await eventSource.emit(event_types.MESSAGE_UPDATED, messageId);8228 await eventSource.emit(event_types.MESSAGE_UPDATED, messageId);
8104 if (messageId == this_edit_mes_id) {8229 if (messageId == this_edit_mes_id) {
8105 this_edit_mes_id = undefined;8230 this_edit_mes_id = undefined;
8106 }8231 } else {
8107 else {
8108 console.warn(`The message editor was closed on message #${messageId} while #${this_edit_mes_id} is being edited.`);8232 console.warn(`The message editor was closed on message #${messageId} while #${this_edit_mes_id} is being edited.`);
8109 }8233 }
81108234
@@ -8138,8 +8262,7 @@ async function messageEditMove(sourceId, targetId) {
81388262
8139 if (sourceId <= targetId) {8263 if (sourceId <= targetId) {
8140 sourceMessageDiv.insertAfter(targetMessageDiv);8264 sourceMessageDiv.insertAfter(targetMessageDiv);
8141 }8265 } else {
8142 else {
8143 sourceMessageDiv.insertBefore(targetMessageDiv);8266 sourceMessageDiv.insertBefore(targetMessageDiv);
8144 }8267 }
81458268
@@ -8247,7 +8370,6 @@ export async function getChatsFromFiles(data, isGroupChat) {
8247 currentChat.shift();8370 currentChat.shift();
8248 }8371 }
8249 chat_dict[file_name] = currentChat;8372 chat_dict[file_name] = currentChat;
8250
8251 } catch (error) {8373 } catch (error) {
8252 console.error(error);8374 console.error(error);
8253 }8375 }
@@ -8906,7 +9028,22 @@ export async function updateSwipeCounter(mesId, { message = undefined, messageEl
89069028
8907 const swipeCounterText = formatSwipeCounter((message?.swipe_id + 1), message?.swipes?.length);9029 const swipeCounterText = formatSwipeCounter((message?.swipe_id + 1), message?.swipes?.length);
8908 const swipeCounter = messageElement.find('.swipes-counter');9030 const swipeCounter = messageElement.find('.swipes-counter');
8909 swipeCounter.text(swipeCounterText).prop('hidden', false);9031 const swipePickerButton = messageElement.find('.mes_swipe_picker');
9032 const canOpenSwipePicker = canOpenSwipePickerForMessage(mesId);
9033 const canJumpToSwipe = canJumpToSwipeForMessage(mesId);
9034
9035 swipeCounter
9036 .text(swipeCounterText)
9037 .prop('hidden', false)
9038 .toggleClass('swipe-picker-enabled', canOpenSwipePicker)
9039 .toggleClass(INTERACTABLE_CONTROL_CLASS, canOpenSwipePicker)
9040 .attr('role', canOpenSwipePicker ? 'button' : null)
9041 .attr('title', canJumpToSwipe ? t`Click to jump to a swipe` : canOpenSwipePicker ? t`Click to view swipe history` : null);
9042 swipePickerButton.toggle(canOpenSwipePicker);
9043
9044 if (!canOpenSwipePicker) {
9045 swipeCounter.removeAttr('tabindex');
9046 }
8910}9047}
89119048
8912/**9049/**
@@ -8956,11 +9093,13 @@ export function isMessageSwipeable(messageId, message = undefined) {
8956 //User messages are not swipeable.9093 //User messages are not swipeable.
8957 !message.is_user9094 !message.is_user
8958 )9095 )
8959 )9096 ) {
8960 //The message is swipeable.9097 // The message is swipeable.
8961 { return true; }9098 return true;
8962 //The message is not swipeable.9099 } else {
8963 else { return false; }9100 // The message is not swipeable.
9101 return false;
9102 }
8964}9103}
89659104
8966/**9105/**
@@ -9033,6 +9172,8 @@ export function refreshSwipeButtons(updateCounters = false, fade = true) {
9033 const isLastSwipe = (message?.swipes?.length ?? 1) - 1 <= (message?.swipe_id ?? 0);9172 const isLastSwipe = (message?.swipes?.length ?? 1) - 1 <= (message?.swipe_id ?? 0);
9034 const hasSwipes = (message?.swipes?.length > 1);9173 const hasSwipes = (message?.swipes?.length > 1);
9035 const overswipe = getOverswipeBehavior(messageId, message);9174 const overswipe = getOverswipeBehavior(messageId, message);
9175 const swipePickerButton = $(div).find('.mes_swipe_picker');
9176 const canOpenSwipePicker = canOpenSwipePickerForMessage(messageId);
90369177
9037 // Chevrons should always be shown on pristine greetings: https://github.com/SillyTavern/SillyTavern/pull/4712#issuecomment-35578933739178 // Chevrons should always be shown on pristine greetings: https://github.com/SillyTavern/SillyTavern/pull/4712#issuecomment-3557893373
9038 const pristineGreeting = overswipe == OVERSWIPE_BEHAVIOR.PRISTINE_GREETING;9179 const pristineGreeting = overswipe == OVERSWIPE_BEHAVIOR.PRISTINE_GREETING;
@@ -9046,12 +9187,14 @@ export function refreshSwipeButtons(updateCounters = false, fade = true) {
90469187
9047 //If there's only one swipe, the left arrow should not be shown.9188 //If there's only one swipe, the left arrow should not be shown.
9048 div.classList.toggle('swipes_visible', hasSwipes || pristineGreeting);9189 div.classList.toggle('swipes_visible', hasSwipes || pristineGreeting);
9190 swipePickerButton.toggle(canOpenSwipePicker);
90499191
9050 //updateSwipeCounter does not need to be awaited, It can run a bit later.9192 //updateSwipeCounter does not need to be awaited, It can run a bit later.
9051 if (updateCounters) updateSwipeCounter(messageId, { message, messageElement: $(div) });9193 if (updateCounters) updateSwipeCounter(messageId, { message, messageElement: $(div) });
9052 } else {9194 } else {
9053 //Hide all messages that are not swipeable.9195 //Hide all messages that are not swipeable.
9054 div.classList.remove('swipes_visible', 'last_swipe');9196 div.classList.remove('swipes_visible', 'last_swipe');
9197 $(div).find('.mes_swipe_picker').toggle(canOpenSwipePickerForMessage(messageId));
9055 }9198 }
9056 });9199 });
9057}9200}
@@ -9085,9 +9228,12 @@ export function hideSwipeButtons({ hideCounters = false } = {}) {
9085 * @returns {Promise<number>|undefined} - The ID of the new swipe after deletion.9228 * @returns {Promise<number>|undefined} - The ID of the new swipe after deletion.
9086 */9229 */
9087export async function deleteSwipe(swipeId = null, messageId = chat.length - 1) {9230export async function deleteSwipe(swipeId = null, messageId = chat.length - 1) {
9088 if (swipeId && (isNaN(swipeId) || swipeId < 0)) {9231 if (swipeId != null) {
9089 toastr.warning(t`Invalid swipe ID: ${swipeId + 1}`);9232 swipeId = Number(swipeId);
9090 return;9233 if (!Number.isInteger(swipeId) || swipeId < 0) {
9234 toastr.warning(t`Invalid swipe ID.`);
9235 return;
9236 }
9091 }9237 }
90929238
9093 const message = chat[messageId];9239 const message = chat[messageId];
@@ -9101,7 +9247,8 @@ export async function deleteSwipe(swipeId = null, messageId = chat.length - 1) {
9101 return;9247 return;
9102 }9248 }
91039249
9104 swipeId = swipeId ?? message.swipe_id;9250 swipeId = Number(swipeId ?? message.swipe_id);
9251 const currentSwipeId = clamp(Number(message.swipe_id ?? 0), 0, message.swipes.length - 1);
91059252
9106 if (swipeId < 0 || swipeId >= message.swipes.length) {9253 if (swipeId < 0 || swipeId >= message.swipes.length) {
9107 toastr.warning(t`Invalid swipe ID: ${swipeId + 1}`);9254 toastr.warning(t`Invalid swipe ID: ${swipeId + 1}`);
@@ -9114,17 +9261,35 @@ export async function deleteSwipe(swipeId = null, messageId = chat.length - 1) {
9114 message.swipe_info.splice(swipeId, 1);9261 message.swipe_info.splice(swipeId, 1);
9115 }9262 }
91169263
9117 // Select the next swipe, or the one before if it was the last one9264 let newSwipeId;
9118 const newSwipeId = Math.min(swipeId, message.swipes.length - 1);9265 if (swipeId < currentSwipeId) {
9266 newSwipeId = currentSwipeId - 1;
9267 } else if (swipeId > currentSwipeId) {
9268 newSwipeId = currentSwipeId;
9269 } else {
9270 // Select the next swipe, or the one before if it was the last one.
9271 newSwipeId = Math.min(swipeId, message.swipes.length - 1);
9272 }
91199273
9120 chat_metadata.tainted = true;9274 chat_metadata.tainted = true;
91219275
9122 messageId = Number(messageId);9276 messageId = Number(messageId);
9123 swipeId = Number(swipeId);9277 swipeId = Number(swipeId);
9278 message.swipe_id = newSwipeId;
9124 await eventSource.emit(event_types.MESSAGE_SWIPE_DELETED, { messageId, swipeId, newSwipeId });9279 await eventSource.emit(event_types.MESSAGE_SWIPE_DELETED, { messageId, swipeId, newSwipeId });
9125 let direction = (swipeId <= newSwipeId) ? SWIPE_DIRECTION.RIGHT : SWIPE_DIRECTION.LEFT;9280
9126 //Animate swipe and swap dispayed message.9281 if (swipeId === currentSwipeId) {
9127 await swipe(null, direction, { source: SWIPE_SOURCE.DELETE, repeated: false, forceMesId: messageId, forceSwipeId: newSwipeId });9282 const direction = (swipeId <= newSwipeId) ? SWIPE_DIRECTION.RIGHT : SWIPE_DIRECTION.LEFT;
9283 // Animate swipe and swap displayed message when the currently visible swipe was deleted.
9284 await swipe(null, direction, { source: SWIPE_SOURCE.DELETE, repeated: false, forceMesId: messageId, forceSwipeId: newSwipeId });
9285 } else {
9286 await updateSwipeCounter(messageId);
9287 if (messageId !== chat.length - 1) {
9288 await updateSwipeCounter(chat.length - 1);
9289 }
9290 refreshSwipeButtons();
9291 saveChatDebounced();
9292 }
91289293
9129 await saveChatConditional();9294 await saveChatConditional();
91309295
@@ -9150,8 +9315,7 @@ export async function saveChatConditional() {
91509315
9151 if (selected_group) {9316 if (selected_group) {
9152 await saveGroupChat(selected_group, true);9317 await saveGroupChat(selected_group, true);
9153 }9318 } else {
9154 else {
9155 await saveChat();9319 await saveChat();
9156 }9320 }
91579321
@@ -9257,8 +9421,7 @@ export function closeMessageEditor(what = 'all') {
9257export function setGenerationProgress(progress) {9421export function setGenerationProgress(progress) {
9258 if (!progress) {9422 if (!progress) {
9259 $('#send_textarea').css({ 'background': '', 'transition': '' });9423 $('#send_textarea').css({ 'background': '', 'transition': '' });
9260 }9424 } else {
9261 else {
9262 $('#send_textarea').css({9425 $('#send_textarea').css({
9263 'background': `linear-gradient(90deg, #008000d6 ${progress}%, transparent ${progress}%)`,9426 'background': `linear-gradient(90deg, #008000d6 ${progress}%, transparent ${progress}%)`,
9264 'transition': '0.25s ease-in-out',9427 'transition': '0.25s ease-in-out',
@@ -9588,7 +9751,6 @@ export async function createOrEditCharacter(e) {
9588 select_rm_info('char_create', avatarId, oldSelectedChar);9751 select_rm_info('char_create', avatarId, oldSelectedChar);
95899752
9590 crop_data = undefined;9753 crop_data = undefined;
9591
9592 } catch (error) {9754 } catch (error) {
9593 console.error('Error creating character', error);9755 console.error('Error creating character', error);
9594 toastr.error(t`Failed to create character`);9756 toastr.error(t`Failed to create character`);
@@ -9673,7 +9835,7 @@ function formatSwipeCounter(current, total) {
9673 * @param {SwipeEvent} event Event.9835 * @param {SwipeEvent} event Event.
9674 * @param {SWIPE_DIRECTION} direction The direction to swipe.9836 * @param {SWIPE_DIRECTION} direction The direction to swipe.
9675 * @param {object} params Additional parameters.9837 * @param {object} params Additional parameters.
9676 * @param {import('./scripts/constants.js').SWIPE_SOURCE} [params.source] The source of the swipe event. null, 'keyboard', 'auto_swipe', 'back' or 'delete'.9838 * @param {import('./scripts/constants.js').SWIPE_SOURCE} [params.source] The source of the swipe event.
9677 * @param {boolean} [params.repeated] Is the swipe event repeated.9839 * @param {boolean} [params.repeated] Is the swipe event repeated.
9678 * @param {ChatMessage} [params.message=chat[chat.length - 1]] The chat message to swipe.9840 * @param {ChatMessage} [params.message=chat[chat.length - 1]] The chat message to swipe.
9679 * @param {number} [params.forceMesId] The message id to swipe.9841 * @param {number} [params.forceMesId] The message id to swipe.
@@ -9699,7 +9861,7 @@ export async function swipe(event, direction, { source, repeated, message = chat
96999861
9700 const mesId = Number(forceMesId ?? event?.currentTarget?.closest('.mes')?.getAttribute('mesid') ?? messageIndex ?? chat.length - 1);9862 const mesId = Number(forceMesId ?? event?.currentTarget?.closest('.mes')?.getAttribute('mesid') ?? messageIndex ?? chat.length - 1);
97019863
9702 if (source === SWIPE_SOURCE.DELETE || source === SWIPE_SOURCE.BACK || source === SWIPE_SOURCE.AUTO_SWIPE) {9864 if ([SWIPE_SOURCE.DELETE, SWIPE_SOURCE.BACK, SWIPE_SOURCE.AUTO_SWIPE, SWIPE_SOURCE.SLASH_COMMAND, SWIPE_SOURCE.SWIPE_PICKER].includes(source)) {
9703 console.info(`The ${direction} swipe source on message #${mesId} is ${source}, Most checks have been bypassed. `);9865 console.info(`The ${direction} swipe source on message #${mesId} is ${source}, Most checks have been bypassed. `);
9704 } else {9866 } else {
9705 //Only show an error if swipes are not hidden and a message is generating.9867 //Only show an error if swipes are not hidden and a message is generating.
@@ -9773,8 +9935,7 @@ export async function swipe(event, direction, { source, repeated, message = chat
9773 document.body.dataset.swiping = 'true';9935 document.body.dataset.swiping = 'true';
9774 await generation;9936 await generation;
9775 }9937 }
9776 }9938 } catch (error) {
9777 catch (error) {
9778 console.warn(`Swipe failed, Swiping back. ${error}`);9939 console.warn(`Swipe failed, Swiping back. ${error}`);
9779 }9940 }
97809941
@@ -9810,8 +9971,7 @@ export async function swipe(event, direction, { source, repeated, message = chat
9810 //Update the chat.9971 //Update the chat.
9811 await loadFromSwipeId(mesId, chat[mesId].swipe_id);9972 await loadFromSwipeId(mesId, chat[mesId].swipe_id);
9812 await redisplayChat({ startIndex: mesId });9973 await redisplayChat({ startIndex: mesId });
9813 }9974 } else {
9814 else {
9815 await Popup.show.confirm(9975 await Popup.show.confirm(
9816 t`ERROR: <code>syncSwipeToMes</code> has failed to revert the failed ${direction} swipe on message #${mesId}.`,9976 t`ERROR: <code>syncSwipeToMes</code> has failed to revert the failed ${direction} swipe on message #${mesId}.`,
9817 t`<p>After you click OK, the chat will be reloaded to prevent data corruption.</p>`,9977 t`<p>After you click OK, the chat will be reloaded to prevent data corruption.</p>`,
@@ -9984,7 +10144,6 @@ export async function swipe(event, direction, { source, repeated, message = chat
9984 duration: 0, //used to be 100 //Disabled on Cohee's request. https://github.com/SillyTavern/SillyTavern/pull/4610/files#r240873174410144 duration: 0, //used to be 100 //Disabled on Cohee's request. https://github.com/SillyTavern/SillyTavern/pull/4610/files#r2408731744
9985 queue: false,10145 queue: false,
9986 progress: function (animation, progress, remainingMs) {10146 progress: function (animation, progress, remainingMs) {
9987
9988 if (is_animation_scroll) chatElement.scrollTop(getMessageBottomHeight(thisMesDiv));10147 if (is_animation_scroll) chatElement.scrollTop(getMessageBottomHeight(thisMesDiv));
9989 },10148 },
9990 complete: function () {10149 complete: function () {
@@ -10001,7 +10160,6 @@ export async function swipe(event, direction, { source, repeated, message = chat
10001 * @param {boolean} [skipSwipeOut=false]10160 * @param {boolean} [skipSwipeOut=false]
10002 */10161 */
10003 async function animateSwipe(run_generate = false, skipSwipeOut = false) {10162 async function animateSwipe(run_generate = false, skipSwipeOut = false) {
10004
10005 if (!skipSwipeOut) {10163 if (!skipSwipeOut) {
10006 //Swipe out.10164 //Swipe out.
10007 await animateSwipeTransition(mesId, { xEnd: `${swipeRange}px`, duration: swipeDuration });10165 await animateSwipeTransition(mesId, { xEnd: `${swipeRange}px`, duration: swipeDuration });
@@ -10069,7 +10227,6 @@ export async function swipe(event, direction, { source, repeated, message = chat
1006910227
10070 //If the swipe is not being deleted.10228 //If the swipe is not being deleted.
10071 if (source != SWIPE_SOURCE.DELETE && source != SWIPE_SOURCE.BACK) {10229 if (source != SWIPE_SOURCE.DELETE && source != SWIPE_SOURCE.BACK) {
10072
10073 // Make sure ad-hoc changes to extras are saved before swiping away10230 // Make sure ad-hoc changes to extras are saved before swiping away
10074 syncMesToSwipe(mesId);10231 syncMesToSwipe(mesId);
1007510232
@@ -10113,9 +10270,8 @@ export async function swipe(event, direction, { source, repeated, message = chat
10113 }10270 }
10114 await standardSwipe(newSwipeId);10271 await standardSwipe(newSwipeId);
10115 return;10272 return;
10116 }10273 } else if (direction === SWIPE_DIRECTION.RIGHT) {
10117 //If swiping right.10274 //If swiping right.
10118 else if (direction === SWIPE_DIRECTION.RIGHT) {
10119 // make new slot in array10275 // make new slot in array
10120 if (forceSwipeId == null) newSwipeId++;10276 if (forceSwipeId == null) newSwipeId++;
1012110277
@@ -10142,18 +10298,16 @@ export async function swipe(event, direction, { source, repeated, message = chat
10142 chat[mesId].swipe_id = originalSwipeId;10298 chat[mesId].swipe_id = originalSwipeId;
10143 await endSwipe();10299 await endSwipe();
10144 return;10300 return;
10145 }10301 } else if (overswipe == OVERSWIPE_BEHAVIOR.REGENERATE) {
10146 //Regenerate the message10302 //Regenerate the message
10147 else if (overswipe == OVERSWIPE_BEHAVIOR.REGENERATE) {
10148 clearMessageData(chat[mesId]);10303 clearMessageData(chat[mesId]);
10149 let run_generate = true;10304 let run_generate = true;
10150 //Generate.10305 //Generate.
10151 await animateSwipe(run_generate);10306 await animateSwipe(run_generate);
10152 await endSwipe();10307 await endSwipe();
10153 return;10308 return;
10154 }10309 } else if (overswipe == OVERSWIPE_BEHAVIOR.LOOP || overswipe == OVERSWIPE_BEHAVIOR.PRISTINE_GREETING) {
10155 // Loop to the first swipe.10310 // Loop to the first swipe.
10156 else if (overswipe == OVERSWIPE_BEHAVIOR.LOOP || overswipe == OVERSWIPE_BEHAVIOR.PRISTINE_GREETING) {
10157 newSwipeId = 0;10311 newSwipeId = 0;
10158 }10312 }
10159 }10313 }
@@ -10167,7 +10321,7 @@ export async function swipe(event, direction, { source, repeated, message = chat
10167 * Handles the swipe to the left event.10321 * Handles the swipe to the left event.
10168 * @param {SwipeEvent} [event] Event.10322 * @param {SwipeEvent} [event] Event.
10169 * @param {object} params Additional parameters.10323 * @param {object} params Additional parameters.
10170 * @param {import('./scripts/constants.js').SWIPE_SOURCE} [params.source] The source of the swipe event. null, 'keyboard', 'auto_swipe', 'back' or 'delete'.10324 * @param {import('./scripts/constants.js').SWIPE_SOURCE} [params.source] The source of the swipe event.
10171 * @param {boolean} [params.repeated] Is the swipe event repeated.10325 * @param {boolean} [params.repeated] Is the swipe event repeated.
10172 * @param {object} [params.message] The chat message to swipe.10326 * @param {object} [params.message] The chat message to swipe.
10173 */10327 */
@@ -10180,7 +10334,7 @@ export async function swipe_left(event, { source, repeated, message } = {}) {
10180 * Handles the swipe to the right event.10334 * Handles the swipe to the right event.
10181 * @param {SwipeEvent} [event] Event.10335 * @param {SwipeEvent} [event] Event.
10182 * @param {object} params Additional parameters.10336 * @param {object} params Additional parameters.
10183 * @param {import('./scripts/constants.js').SWIPE_SOURCE} [params.source] The source of the swipe event. null, 'keyboard', 'auto_swipe', 'back' or 'delete'.10337 * @param {import('./scripts/constants.js').SWIPE_SOURCE} [params.source] The source of the swipe event.
10184 * @param {boolean} [params.repeated] Is the swipe event repeated.10338 * @param {boolean} [params.repeated] Is the swipe event repeated.
10185 * @param {object} [params.message] The chat message to swipe.10339 * @param {object} [params.message] The chat message to swipe.
10186 */10340 */
@@ -10372,8 +10526,7 @@ export async function doNewChat({ deleteCurrentChat = false } = {}) {
10372 if (selected_group) {10526 if (selected_group) {
10373 await createNewGroupChat(selected_group);10527 await createNewGroupChat(selected_group);
10374 if (deleteCurrentChat) await deleteGroupChat(selected_group, chat_file_for_del, { jumpToNewChat: false }); // don't jump, new chat was already created and jumped to above10528 if (deleteCurrentChat) await deleteGroupChat(selected_group, chat_file_for_del, { jumpToNewChat: false }); // don't jump, new chat was already created and jumped to above
10375 }10529 } else {
10376 else {
10377 //RossAscends: added character name to new chat filenames and replaced Date.now() with humanizedDateTime;10530 //RossAscends: added character name to new chat filenames and replaced Date.now() with humanizedDateTime;
10378 chat_metadata = {};10531 chat_metadata = {};
10379 characters[this_chid].chat = `${name2} - ${humanizedDateTime()}`;10532 characters[this_chid].chat = `${name2} - ${humanizedDateTime()}`;
@@ -10382,7 +10535,6 @@ export async function doNewChat({ deleteCurrentChat = false } = {}) {
10382 await createOrEditCharacter(new CustomEvent('newChat'));10535 await createOrEditCharacter(new CustomEvent('newChat'));
10383 if (deleteCurrentChat) await delChat(chat_file_for_del + '.jsonl');10536 if (deleteCurrentChat) await delChat(chat_file_for_del + '.jsonl');
10384 }10537 }
10385
10386}10538}
1038710539
10388/**10540/**
@@ -10394,7 +10546,7 @@ export async function doNewChat({ deleteCurrentChat = false } = {}) {
10394 * @param {string} param.newFileName New name for the chat (no JSONL extension)10546 * @param {string} param.newFileName New name for the chat (no JSONL extension)
10395 * @param {boolean} [param.loader=true] Whether to show loader during the operation10547 * @param {boolean} [param.loader=true] Whether to show loader during the operation
10396 */10548 */
10397export async function renameGroupOrCharacterChat({ characterId, groupId, oldFileName, newFileName, loader }) {10549export async function renameGroupOrCharacterChat({ characterId, groupId, oldFileName, newFileName, loader: showLoader }) {
10398 const currentChatId = getCurrentChatId();10550 const currentChatId = getCurrentChatId();
10399 const body = {10551 const body = {
10400 is_group: !!groupId,10552 is_group: !!groupId,
@@ -10412,9 +10564,13 @@ export async function renameGroupOrCharacterChat({ characterId, groupId, oldFile
10412 return;10564 return;
10413 }10565 }
1041410566
10415 try {10567 const loaderHandle = showLoader ? loader.show({
10416 loader && showLoader();10568 title: t`Rename Chat`,
10569 message: t`Renaming chat…`,
10570 toastMode: loader.ToastMode.STATIC,
10571 }) : null;
1041710572
10573 try {
10418 const response = await fetch('/api/chats/rename', {10574 const response = await fetch('/api/chats/rename', {
10419 method: 'POST',10575 method: 'POST',
10420 body: JSON.stringify(body),10576 body: JSON.stringify(body),
@@ -10437,8 +10593,7 @@ export async function renameGroupOrCharacterChat({ characterId, groupId, oldFile
1043710593
10438 if (groupId) {10594 if (groupId) {
10439 await renameGroupChat(groupId, oldFileName, newFileName);10595 await renameGroupChat(groupId, oldFileName, newFileName);
10440 }10596 } else if (characterId !== undefined && String(characterId) === String(this_chid) && characters[characterId]?.chat === oldFileName) {
10441 else if (characterId !== undefined && String(characterId) === String(this_chid) && characters[characterId]?.chat === oldFileName) {
10442 characters[characterId].chat = newFileName;10597 characters[characterId].chat = newFileName;
10443 $('#selected_chat_pole').val(characters[characterId].chat);10598 $('#selected_chat_pole').val(characters[characterId].chat);
10444 await createOrEditCharacter();10599 await createOrEditCharacter();
@@ -10448,11 +10603,10 @@ export async function renameGroupOrCharacterChat({ characterId, groupId, oldFile
10448 await reloadCurrentChat();10603 await reloadCurrentChat();
10449 }10604 }
10450 } catch {10605 } catch {
10451 loader && hideLoader();
10452 await delay(500);10606 await delay(500);
10453 await callGenericPopup('An error has occurred. Chat was not renamed.', POPUP_TYPE.TEXT);10607 await callGenericPopup('An error has occurred. Chat was not renamed.', POPUP_TYPE.TEXT);
10454 } finally {10608 } finally {
10455 loader && hideLoader();10609 await loaderHandle?.hide();
10456 }10610 }
10457}10611}
1045810612
@@ -10556,7 +10710,7 @@ export async function handleDeleteCharacter(this_chid, delete_chats) {
10556 * @param {string|string[]} characterKey - The key (avatar) of the character to be deleted10710 * @param {string|string[]} characterKey - The key (avatar) of the character to be deleted
10557 * @param {Object} [options] - Optional parameters for the deletion10711 * @param {Object} [options] - Optional parameters for the deletion
10558 * @param {boolean} [options.deleteChats=true] - Whether to delete associated chats or not10712 * @param {boolean} [options.deleteChats=true] - Whether to delete associated chats or not
10559 * @return {Promise<void>} - A promise that resolves when the character is successfully deleted10713 * @return {Promise<boolean>} - A promise that resolves when the character is successfully deleted
10560 */10714 */
10561export async function deleteCharacter(characterKey, { deleteChats = true } = {}) {10715export async function deleteCharacter(characterKey, { deleteChats = true } = {}) {
10562 if (!Array.isArray(characterKey)) {10716 if (!Array.isArray(characterKey)) {
@@ -10570,15 +10724,17 @@ export async function deleteCharacter(characterKey, { deleteChats = true } = {})
10570 t`Deleting this character will close the chat and you will lose any unsaved messages. Do you want to proceed?`,10724 t`Deleting this character will close the chat and you will lose any unsaved messages. Do you want to proceed?`,
10571 );10725 );
10572 if (!confirmClose) {10726 if (!confirmClose) {
10573 return;10727 return false;
10574 }10728 }
10575 }10729 }
1057610730
10577 const closeChatResult = await closeCurrentChat();10731 const closeChatResult = await closeCurrentChat();
10578 if (!closeChatResult) {10732 if (!closeChatResult) {
10579 return;10733 return false;
10580 }10734 }
1058110735
10736 let deleted = false;
10737
10582 for (const key of characterKey) {10738 for (const key of characterKey) {
10583 const character = characters.find(x => x.avatar == key);10739 const character = characters.find(x => x.avatar == key);
10584 if (!character) {10740 if (!character) {
@@ -10617,9 +10773,11 @@ export async function deleteCharacter(characterKey, { deleteChats = true } = {})
10617 }10773 }
1061810774
10619 await eventSource.emit(event_types.CHARACTER_DELETED, { id: chid, character: character });10775 await eventSource.emit(event_types.CHARACTER_DELETED, { id: chid, character: character });
10776 deleted = true;
10620 }10777 }
1062110778
10622 await removeCharacterFromUI();10779 await removeCharacterFromUI();
10780 return deleted;
10623}10781}
1062410782
10625/**10783/**
@@ -11006,21 +11164,32 @@ jQuery(async function () {
11006 async function handleDeleteChat(chatFile, group, fromSlashCommand = false) {11164 async function handleDeleteChat(chatFile, group, fromSlashCommand = false) {
11007 // Close past chat popup.11165 // Close past chat popup.
11008 $('#select_chat_cross').trigger('click');11166 $('#select_chat_cross').trigger('click');
11009 showLoader();11167
11010 if (group) {11168 const loaderHandle = loader.show({
11011 await deleteGroupChat(group, chatFile);11169 title: t`Delete Chat`,
11012 } else {11170 message: t`Deleting chat…`,
11013 await delChat(`${chatFile}.jsonl`);11171 toastMode: loader.ToastMode.STATIC,
11172 });
11173
11174 try {
11175 if (group) {
11176 await deleteGroupChat(group, chatFile);
11177 } else {
11178 await delChat(`${chatFile}.jsonl`);
11179 }
11180 } catch (error) {
11181 loaderHandle.hide();
11182 throw error;
11014 }11183 }
1101511184
11016 if (fromSlashCommand) { // When called from `/delchat` command, don't re-open the history view.11185 if (fromSlashCommand) { // When called from `/delchat` command, don't re-open the history view.
11017 $('#options').hide(); // Hide option popup menu.11186 $('#options').hide(); // Hide option popup menu.
11018 hideLoader();11187 await loaderHandle.hide();
11019 } else { // Open the history view again after 2 seconds (delay to avoid edge cases for deleting last chat).11188 } else { // Open the history view again after 2 seconds (delay to avoid edge cases for deleting last chat).
11020 setTimeout(function () {11189 setTimeout(async function () {
11021 $('#option_select_chat').trigger('click');11190 $('#option_select_chat').trigger('click');
11022 $('#options').hide(); // Hide option popup menu.11191 $('#options').hide(); // Hide option popup menu.
11023 hideLoader();11192 await loaderHandle.hide();
11024 }, 2000);11193 }, 2000);
11025 }11194 }
11026 }11195 }
@@ -11090,8 +11259,7 @@ jQuery(async function () {
11090 if (popup_type == 'input') {11259 if (popup_type == 'input') {
11091 dialogueResolve($('#dialogue_popup_input').val());11260 dialogueResolve($('#dialogue_popup_input').val());
11092 $('#dialogue_popup_input').val('');11261 $('#dialogue_popup_input').val('');
11093 }11262 } else {
11094 else {
11095 dialogueResolve(true);11263 dialogueResolve(true);
11096 }11264 }
1109711265
@@ -11326,9 +11494,7 @@ jQuery(async function () {
11326 });11494 });
11327 }11495 }
11328 }11496 }
11329 }11497 } else if (id == 'option_start_new_chat') {
11330
11331 else if (id == 'option_start_new_chat') {
11332 if ((selected_group || this_chid !== undefined) && !is_send_press) {11498 if ((selected_group || this_chid !== undefined) && !is_send_press) {
11333 let deleteCurrentChat = false;11499 let deleteCurrentChat = false;
11334 const result = await Popup.show.confirm(t`Start new chat?`, await renderTemplateAsync('newChatConfirm'), {11500 const result = await Popup.show.confirm(t`Start new chat?`, await renderTemplateAsync('newChatConfirm'), {
@@ -11344,9 +11510,7 @@ jQuery(async function () {
11344 const alreadyInTempChat = this_chid === undefined && name2 === neutralCharacterName;11510 const alreadyInTempChat = this_chid === undefined && name2 === neutralCharacterName;
11345 await newAssistantChat({ temporary: alreadyInTempChat });11511 await newAssistantChat({ temporary: alreadyInTempChat });
11346 }11512 }
11347 }11513 } else if (id == 'option_regenerate') {
11348
11349 else if (id == 'option_regenerate') {
11350 //Attempting to regenerate a user message will instead generate a new message.11514 //Attempting to regenerate a user message will instead generate a new message.
11351 if (chat.length && chat.length - 1 === this_edit_mes_id && chat[this_edit_mes_id]?.is_user == false) {11515 if (chat.length && chat.length - 1 === this_edit_mes_id && chat[this_edit_mes_id]?.is_user == false) {
11352 toastr.warning(t`Finish the edit before starting a generation.`, t`You cannot regenerate the message you are editing.`);11516 toastr.warning(t`Finish the edit before starting a generation.`, t`You cannot regenerate the message you are editing.`);
@@ -11355,22 +11519,17 @@ jQuery(async function () {
11355 if (is_send_press == false) {11519 if (is_send_press == false) {
11356 if (selected_group) {11520 if (selected_group) {
11357 regenerateGroup();11521 regenerateGroup();
11358 }11522 } else {
11359 else {
11360 is_send_press = true;11523 is_send_press = true;
11361 Generate('regenerate', buildOrFillAdditionalArgs());11524 Generate('regenerate', buildOrFillAdditionalArgs());
11362 }11525 }
11363 }11526 }
11364 }11527 } else if (id == 'option_impersonate') {
11365
11366 else if (id == 'option_impersonate') {
11367 if (is_send_press == false || fromSlashCommand) {11528 if (is_send_press == false || fromSlashCommand) {
11368 is_send_press = true;11529 is_send_press = true;
11369 Generate('impersonate', buildOrFillAdditionalArgs());11530 Generate('impersonate', buildOrFillAdditionalArgs());
11370 }11531 }
11371 }11532 } else if (id == 'option_continue') {
11372
11373 else if (id == 'option_continue') {
11374 if (swipeState == SWIPE_STATE.EDITING) {11533 if (swipeState == SWIPE_STATE.EDITING) {
11375 toastr.warning(t`Confirm the edit to start a generation.`, t`You cannot send a message during a swipe-edit.`);11534 toastr.warning(t`Confirm the edit to start a generation.`, t`You cannot send a message during a swipe-edit.`);
11376 return;11535 return;
@@ -11384,17 +11543,11 @@ jQuery(async function () {
11384 is_send_press = true;11543 is_send_press = true;
11385 Generate('continue', buildOrFillAdditionalArgs());11544 Generate('continue', buildOrFillAdditionalArgs());
11386 }11545 }
11387 }11546 } else if (id == 'option_delete_mes') {
11388
11389 else if (id == 'option_delete_mes') {
11390 setTimeout(() => openMessageDelete(fromSlashCommand), animation_duration);11547 setTimeout(() => openMessageDelete(fromSlashCommand), animation_duration);
11391 }11548 } else if (id == 'option_close_chat') {
11392
11393 else if (id == 'option_close_chat') {
11394 await closeCurrentChat();11549 await closeCurrentChat();
11395 }11550 } else if (id === 'option_settings') {
11396
11397 else if (id === 'option_settings') {
11398 //var checkBox = document.getElementById("waifuMode");11551 //var checkBox = document.getElementById("waifuMode");
11399 var topBar = document.getElementById('top-bar');11552 var topBar = document.getElementById('top-bar');
11400 var topSettingsHolder = document.getElementById('top-settings-holder');11553 var topSettingsHolder = document.getElementById('top-settings-holder');
@@ -11407,9 +11560,7 @@ jQuery(async function () {
1140711560
11408 divchat.style.borderRadius = '';11561 divchat.style.borderRadius = '';
11409 divchat.style.backgroundColor = '';11562 divchat.style.backgroundColor = '';
11410
11411 } else {11563 } else {
11412
11413 divchat.style.borderRadius = '10px'; // Adjust the value to control the roundness of the corners11564 divchat.style.borderRadius = '10px'; // Adjust the value to control the roundness of the corners
11414 divchat.style.backgroundColor = ''; // Set the background color to your preference11565 divchat.style.backgroundColor = ''; // Set the background color to your preference
1141511566
public/scripts/BulkEditOverlay.js+10 -9
@@ -14,10 +14,11 @@ import {
14} from '../script.js';14} from '../script.js';
1515
16import { favsToHotswap } from './RossAscends-mods.js';16import { favsToHotswap } from './RossAscends-mods.js';
17import { hideLoader, showLoader } from './loader.js';17import { loader } from './action-loader.js';
18import { convertCharacterToPersona } from './personas.js';18import { convertCharacterToPersona } from './personas.js';
19import { callGenericPopup, POPUP_TYPE } from './popup.js';19import { callGenericPopup, POPUP_TYPE } from './popup.js';
20import { createTagInput, getTagKeyForEntity, getTagsList, printTagList, tag_map, compareTagsForSort, removeTagFromMap, importTags, tag_import_setting } from './tags.js';20import { createTagInput, getTagKeyForEntity, getTagsList, printTagList, tag_map, compareTagsForSort, removeTagFromMap, importTags, tag_import_setting } from './tags.js';
21import { t } from './i18n.js';
2122
22/**23/**
23 * Static object representing the actions of the24 * Static object representing the actions of the
@@ -101,7 +102,7 @@ class CharacterContextMenu {
101 * @param {number} characterId102 * @param {number} characterId
102 * @returns {Promise<void>}103 * @returns {Promise<void>}
103 */104 */
104 static persona = async (characterId) => void(await convertCharacterToPersona(characterId));105 static persona = async (characterId) => void (await convertCharacterToPersona(characterId));
105106
106 /**107 /**
107 * Delete one or more characters,108 * Delete one or more characters,
@@ -754,7 +755,7 @@ class BulkEditOverlay {
754755
755 handleContextMenuShow = (event) => {756 handleContextMenuShow = (event) => {
756 event.preventDefault();757 event.preventDefault();
757 const [x,y] = this.#getContextMenuPosition(event);758 const [x, y] = this.#getContextMenuPosition(event);
758 CharacterContextMenu.show(x, y);759 CharacterContextMenu.show(x, y);
759 this.#contextMenuOpen = true;760 this.#contextMenuOpen = true;
760 };761 };
@@ -846,15 +847,15 @@ class BulkEditOverlay {
846847
847 const deleteChats = checkbox.prop('checked') ?? false;848 const deleteChats = checkbox.prop('checked') ?? false;
848849
849 showLoader();850 const loaderHandle = loader.show({
850 const toast = toastr.info('We\'re deleting your characters, please wait...', 'Working on it');851 title: t`Bulk Delete`,
852 message: t`Deleting ${characterIds.length} character(s)…`,
853 toastMode: loader.ToastMode.STATIC,
854 });
851 const avatarList = characterIds.map(id => characters[id]?.avatar).filter(a => a);855 const avatarList = characterIds.map(id => characters[id]?.avatar).filter(a => a);
852 return CharacterContextMenu.delete(avatarList, deleteChats)856 return CharacterContextMenu.delete(avatarList, deleteChats)
853 .then(() => this.browseState())857 .then(() => this.browseState())
854 .finally(() => {858 .finally(() => loaderHandle.hide());
855 toastr.clear(toast);
856 hideLoader();
857 });
858 });859 });
859860
860 // At this moment the popup is already changed in the dom, but not yet closed/resolved. We build the avatar list here861 // At this moment the popup is already changed in the dom, but not yet closed/resolved. We build the avatar list here
public/scripts/PromptManager.js+1 -3
@@ -986,7 +986,6 @@ class PromptManager {
986 * @returns {void}986 * @returns {void}
987 */987 */
988 addPrompt(prompt, identifier) {988 addPrompt(prompt, identifier) {
989
990 if (typeof prompt !== 'object' || prompt === null) throw new Error('Object is not a prompt');989 if (typeof prompt !== 'object' || prompt === null) throw new Error('Object is not a prompt');
991990
992 const newPrompt = {991 const newPrompt = {
@@ -1301,7 +1300,7 @@ class PromptManager {
1301 const prompt = this.getPromptById(identifier);1300 const prompt = this.getPromptById(identifier);
1302 const textareaIdentifier = `${identifier}_prompt_quick_edit_textarea`;1301 const textareaIdentifier = `${identifier}_prompt_quick_edit_textarea`;
1303 const html = `<div class="range-block m-t-1">1302 const html = `<div class="range-block m-t-1">
1304 <div class="justifyLeft" data-i18n="${title}">${title}</div>1303 <div class="justifyLeft">${title}</div>
1305 <div class="wide100p">1304 <div class="wide100p">
1306 <textarea id="${textareaIdentifier}" class="text_pole textarea_compact" rows="6" placeholder="">${prompt.content}</textarea>1305 <textarea id="${textareaIdentifier}" class="text_pole textarea_compact" rows="6" placeholder="">${prompt.content}</textarea>
1307 </div>1306 </div>
@@ -1318,7 +1317,6 @@ class PromptManager {
1318 this.updatePromptByIdentifier(identifier, prompt);1317 this.updatePromptByIdentifier(identifier, prompt);
1319 debouncedSaveServiceSettings().then(() => this.render());1318 debouncedSaveServiceSettings().then(() => this.render());
1320 });1319 });
1321
1322 }1320 }
13231321
1324 /**1322 /**
public/scripts/RossAscends-mods.js+15 -19
@@ -101,7 +101,6 @@ observer.observe(document.documentElement, observerConfig);
101 * @returns {string} - A human-readable string that represents the time spent generating characters.101 * @returns {string} - A human-readable string that represents the time spent generating characters.
102 */102 */
103export function humanizeGenTime(total_gen_time) {103export function humanizeGenTime(total_gen_time) {
104
105 //convert time_spent to humanized format of "_ Hours, _ Minutes, _ Seconds" from milliseconds104 //convert time_spent to humanized format of "_ Hours, _ Minutes, _ Seconds" from milliseconds
106 let time_spent = total_gen_time || 0;105 let time_spent = total_gen_time || 0;
107 time_spent = Math.floor(time_spent / 1000);106 time_spent = Math.floor(time_spent / 1000);
@@ -380,8 +379,7 @@ function RA_autoconnect(PrevApi) {
380 || (textgen_settings.type === textgen_types.FEATHERLESS && secret_state[SECRET_KEYS.FEATHERLESS])379 || (textgen_settings.type === textgen_types.FEATHERLESS && secret_state[SECRET_KEYS.FEATHERLESS])
381 ) {380 ) {
382 $('#api_button_textgenerationwebui').trigger('click');381 $('#api_button_textgenerationwebui').trigger('click');
383 }382 } else if (isValidUrl(getTextGenServer())) {
384 else if (isValidUrl(getTextGenServer())) {
385 $('#api_button_textgenerationwebui').trigger('click');383 $('#api_button_textgenerationwebui').trigger('click');
386 }384 }
387 break;385 break;
@@ -1054,8 +1052,7 @@ export function initRossMods() {
1054 $('#send_textarea').trigger('focus');1052 $('#send_textarea').trigger('focus');
1055 reasoningMesDone.trigger('click');1053 reasoningMesDone.trigger('click');
1056 return;1054 return;
1057 }1055 } else if (is_send_press == false) {
1058 else if (is_send_press == false) {
1059 const skipConfirmKey = 'RegenerateWithCtrlEnter';1056 const skipConfirmKey = 'RegenerateWithCtrlEnter';
1060 const skipConfirm = accountStorage.getItem(skipConfirmKey) === 'true';1057 const skipConfirm = accountStorage.getItem(skipConfirmKey) === 'true';
1061 function doRegenerate() {1058 function doRegenerate() {
@@ -1228,32 +1225,33 @@ export function initRossMods() {
1228 return;1225 return;
1229 }1226 }
12301227
1231 if ($('#floatingPrompt').is(':visible')) {1228 if ($('#logprobsViewer').is(':visible')) {
1232 $('#ANClose').trigger('click');1229 $('#logprobsViewerClose').trigger('click');
1233 return;1230 return;
1234 }1231 }
12351232
1236 if ($('#WorldInfo').is(':visible')) {1233 if ($('#cfgConfig').is(':visible')) {
1237 $('#WIDrawerIcon').trigger('click');1234 $('#CFGClose').trigger('click');
1238 return;1235 return;
1239 }1236 }
12401237
1241 if ($('#cfgConfig').is(':visible')) {1238 if ($('#floatingPrompt').is(':visible')) {
1242 $('#CFGClose').trigger('click');1239 $('#ANClose').trigger('click');
1243 return;1240 return;
1244 }1241 }
12451242
1246 if ($('#logprobsViewer').is(':visible')) {1243 if ($('#WorldInfo').is(':visible')) {
1247 $('#logprobsViewerClose').trigger('click');1244 $('#WIDrawerIcon').trigger('click');
1248 return;1245 return;
1249 }1246 }
12501247
1251 $('#movingDivs > div').each(function () {1248 const movingDivs = $('#movingDivs > div').toArray().reverse();
1252 if ($(this).is(':visible')) {1249 for (const div of movingDivs) {
1253 $('#movingDivs > div .floating_panel_close').trigger('click');1250 if ($(div).is(':visible')) {
1251 $(div).find('.floating_panel_close, .dragClose').trigger('click');
1254 return;1252 return;
1255 }1253 }
1256 });1254 }
12571255
1258 if ($('#left-nav-panel').is(':visible') &&1256 if ($('#left-nav-panel').is(':visible') &&
1259 $(LPanelPin).prop('checked') === false) {1257 $(LPanelPin).prop('checked') === false) {
@@ -1274,8 +1272,6 @@ export function initRossMods() {
1274 }1272 }
12751273
12761274
1277
1278
1279 if (event.ctrlKey && /^[1-9]$/.test(event.key)) {1275 if (event.ctrlKey && /^[1-9]$/.test(event.key)) {
1280 // This will eventually be to trigger quick replies1276 // This will eventually be to trigger quick replies
1281 // event.preventDefault();1277 // event.preventDefault();
public/scripts/action-loader-slashcommands.js+355 -0
@@ -0,0 +1,355 @@
1import { ActionLoaderToastMode, getActiveLoaderHandles, getLoaderHandleById, hideActionLoader, showActionLoader } from './action-loader.js';
2import { t } from './i18n.js';
3import { SlashCommand } from './slash-commands/SlashCommand.js';
4import { SlashCommandNamedArgument, ARGUMENT_TYPE, SlashCommandArgument } from './slash-commands/SlashCommandArgument.js';
5import { SlashCommandClosure } from './slash-commands/SlashCommandClosure.js';
6import { commonEnumProviders, enumIcons } from './slash-commands/SlashCommandCommonEnumsProvider.js';
7import { SlashCommandEnumValue, enumTypes } from './slash-commands/SlashCommandEnumValue.js';
8import { SlashCommandParser } from './slash-commands/SlashCommandParser.js';
9import { isFalseBoolean } from './utils.js';
10
11/**
12 * Registers slash commands for the action loader module.
13 */
14export function registerActionLoaderSlashCommands() {
15 /**
16 * Helper to create a closure-based handler from a SlashCommandClosure argument.
17 * Allows all possible slash command arg types to be passed in, but only closure is accepted.
18 * @param {string | SlashCommandClosure | (string | SlashCommandClosure)[]} closure - The closure argument
19 * @param {Object} options - Configuration options
20 * @param {string} [options.argName='onStop'] - Name of the argument for error messages
21 * @param {boolean} [options.throwInvalid=true] - Whether to throw an error for invalid input
22 * @returns {(() => Promise<void>)|null} The handler function, or null if no closure
23 */
24 function createClosureHandler(closure, { argName = 'onStop', throwInvalid = true } = {}) {
25 if (!(closure instanceof SlashCommandClosure)) {
26 if (closure && throwInvalid) {
27 // Throw error on purpose. This is defined as a syntax error.
28 throw new Error(t`Invalid argument for ${argName} provided. This is not a closure.`);
29 }
30 return null;
31 }
32 return async () => {
33 try {
34 const localClosure = closure.getCopy();
35 localClosure.onProgress = () => { };
36 await localClosure.execute();
37 } catch (e) {
38 console.error('Error executing closure handler', e);
39 }
40 };
41 }
42
43 // Shared loader enum providers
44 const loaderEnumProviders = {
45 toastModeEnumProvider: () => [
46 new SlashCommandEnumValue(ActionLoaderToastMode.NONE, 'No toast displayed', enumTypes.enum, enumIcons.disabled),
47 new SlashCommandEnumValue(ActionLoaderToastMode.STATIC, 'Static toast without stop button', enumTypes.enum, enumIcons.spinner),
48 new SlashCommandEnumValue(ActionLoaderToastMode.STOPPABLE, 'Toast with stop button (default)', enumTypes.enum, enumIcons.stop),
49 ],
50 loaderHandleProvider: () => getActiveLoaderHandles().map(
51 handle => new SlashCommandEnumValue(handle.id, `Active loader: ${handle.id}`, enumTypes.enum, enumIcons.spinner),
52 ).concat(
53 new SlashCommandEnumValue('Temporary loader handle', 'Any loader handle saved in variables or similar', 'enum', '📄', () => true, () => ''),
54 ),
55 };
56
57 // /loader-wrap command - wraps a closure with loader display
58 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
59 name: 'loader-wrap',
60 returns: 'result of the closure execution',
61 helpString: `
62 <div>
63 Wraps a closure execution with an action loader overlay and optional toast notification.
64 By default, the loader blocks UI interaction until the closure completes.
65 Multiple loaders can be stacked - each gets its own toast, but the overlay stays single.
66 </div>
67 <div>
68 <strong>Toast modes:</strong>
69 <ul>
70 <li><code>stoppable</code> - Shows toast with a stop button (default)</li>
71 <li><code>static</code> - Shows toast without stop button</li>
72 <li><code>none</code> - No toast, only loader overlay</li>
73 </ul>
74 </div>
75 <div>
76 Set <code>blocking=false</code> to show only a toast without blocking the UI.
77 Useful for background operations like image captioning or generation.
78 </div>
79 <div>
80 The default stop behavior is calling <code>stopGeneration()</code>.
81 If the wrapped action is doing something different than generating, a custom stop closure can be provided.
82 </div>
83 <div>
84 <strong>Examples:</strong>
85 <ul>
86 <li><pre><code class="language-stscript">/loader-wrap message="Generating summary..." {: /gen Summary of the last message | /echo Done :}</code></pre></li>
87 <li><pre><code class="language-stscript">/loader-wrap blocking=false message="Captioning..." {: /caption :}</code></pre></li>
88 <li><pre><code class="language-stscript">/loader-wrap toast=stoppable onStop={: /echo "Stopped by user" :} {: /delay 10000 :}</code></pre></li>
89 </ul>
90 </div>
91 `,
92 namedArgumentList: [
93 SlashCommandNamedArgument.fromProps({
94 name: 'blocking',
95 description: 'Whether to show blocking overlay. Set to false for non-blocking toast-only loaders.',
96 typeList: [ARGUMENT_TYPE.BOOLEAN],
97 defaultValue: 'true',
98 enumList: commonEnumProviders.boolean()(),
99 }),
100 SlashCommandNamedArgument.fromProps({
101 name: 'toast',
102 description: 'Toast display mode: stoppable (with stop button), static (no stop button), or none',
103 typeList: [ARGUMENT_TYPE.STRING],
104 defaultValue: ActionLoaderToastMode.STOPPABLE,
105 enumList: loaderEnumProviders.toastModeEnumProvider(),
106 }),
107 SlashCommandNamedArgument.fromProps({
108 name: 'message',
109 description: 'Message to display in the toast notification',
110 typeList: [ARGUMENT_TYPE.STRING],
111 defaultValue: 'Generating...',
112 }),
113 SlashCommandNamedArgument.fromProps({
114 name: 'title',
115 description: 'Optional title for the toast notification',
116 typeList: [ARGUMENT_TYPE.STRING],
117 }),
118 SlashCommandNamedArgument.fromProps({
119 name: 'stopTooltip',
120 description: 'Tooltip text for the stop button (only used when toast=stoppable)',
121 typeList: [ARGUMENT_TYPE.STRING],
122 defaultValue: 'Stop',
123 }),
124 SlashCommandNamedArgument.fromProps({
125 name: 'onStop',
126 description: 'Closure to execute when the stop button is clicked. If not provided, uses default stop behavior.',
127 typeList: [ARGUMENT_TYPE.CLOSURE],
128 }),
129 ],
130 unnamedArgumentList: [
131 SlashCommandArgument.fromProps({
132 description: 'Closure to execute while the loader is displayed',
133 typeList: [ARGUMENT_TYPE.CLOSURE],
134 isRequired: true,
135 }),
136 ],
137 callback: async (args, value) => {
138 if (!(value instanceof SlashCommandClosure)) {
139 // Throw error on purpose. This is defined as a syntax error.
140 throw new Error(t`Invalid argument for unnamed argument provided. This is not a closure.`);
141 }
142
143 const blocking = !isFalseBoolean(String(args.blocking));
144 const toastMode = Object.values(ActionLoaderToastMode).includes(String(args.toast))
145 ? String(args.toast)
146 : ActionLoaderToastMode.STOPPABLE;
147 const message = String(args.message ?? t`Generating...`);
148 const title = args.title ? String(args.title) : '';
149 const stopTooltip = String(args.stopTooltip ?? t`Stop`);
150
151 const loader = showActionLoader({
152 blocking,
153 toastMode,
154 message,
155 title,
156 stopTooltip,
157 onStop: createClosureHandler(args.onStop),
158 });
159
160 try {
161 const closureCopy = value.getCopy();
162 const result = await closureCopy.execute();
163 return result.pipe;
164 } finally {
165 await loader.hide();
166 }
167 },
168 }));
169
170 // /loader-show command - manually show a loader, returns handle ID
171 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
172 name: 'loader-show',
173 returns: 'loader handle ID (use with /loader-hide)',
174 helpString: `
175 <div>
176 Manually shows an action loader. Returns a handle ID that can be used with <code>/loader-hide</code> to hide it.
177 Use this for fine-grained control when you need to show/hide the loader at specific points.
178 Multiple loaders can be stacked - each gets its own toast, but the overlay stays single.
179 </div>
180 <div>
181 <strong>Toast modes:</strong>
182 <ul>
183 <li><code>stoppable</code> - Shows toast with a stop button (default)</li>
184 <li><code>static</code> - Shows toast without stop button</li>
185 <li><code>none</code> - No toast, only loader overlay</li>
186 </ul>
187 </div>
188 <div>
189 Set <code>blocking=false</code> to show only a toast without blocking the UI.
190 Useful for background operations like image captioning or generation.
191 </div>
192 <div>
193 The default stop behavior is calling <code>stopGeneration()</code>.
194 If the wrapped action is doing something different than generating, a custom stop closure can be provided.
195 </div>
196 <div>
197 <strong>Example:</strong>
198 <pre>
199 <code class="language-stscript">
200/loader-show message="Loading..." |
201/setvar key=myLoader |
202/some-operation |
203/loader-hide handle={{getvar::myLoader}}
204 </code>
205 </pre>
206 </div>
207 `,
208 namedArgumentList: [
209 SlashCommandNamedArgument.fromProps({
210 name: 'blocking',
211 description: 'Whether to show blocking overlay. Set to false for non-blocking toast-only loaders.',
212 typeList: [ARGUMENT_TYPE.BOOLEAN],
213 defaultValue: 'true',
214 enumList: commonEnumProviders.boolean()(),
215 }),
216 SlashCommandNamedArgument.fromProps({
217 name: 'toast',
218 description: 'Toast display mode: stoppable (with stop button), static (no stop button), or none',
219 typeList: [ARGUMENT_TYPE.STRING],
220 defaultValue: ActionLoaderToastMode.STOPPABLE,
221 enumList: loaderEnumProviders.toastModeEnumProvider(),
222 }),
223 SlashCommandNamedArgument.fromProps({
224 name: 'message',
225 description: 'Message to display in the toast notification',
226 typeList: [ARGUMENT_TYPE.STRING],
227 defaultValue: 'Generating...',
228 }),
229 SlashCommandNamedArgument.fromProps({
230 name: 'title',
231 description: 'Optional title for the toast notification',
232 typeList: [ARGUMENT_TYPE.STRING],
233 }),
234 SlashCommandNamedArgument.fromProps({
235 name: 'stopTooltip',
236 description: 'Tooltip text for the stop button (only used when toast=stoppable)',
237 typeList: [ARGUMENT_TYPE.STRING],
238 defaultValue: 'Stop',
239 }),
240 SlashCommandNamedArgument.fromProps({
241 name: 'onStop',
242 description: 'Closure to execute when the stop button is clicked',
243 typeList: [ARGUMENT_TYPE.CLOSURE],
244 }),
245 SlashCommandNamedArgument.fromProps({
246 name: 'onHide',
247 description: 'Closure to execute when the loader is hidden (not stopped)',
248 typeList: [ARGUMENT_TYPE.CLOSURE],
249 }),
250 ],
251 unnamedArgumentList: [],
252 callback: async (args) => {
253 const blocking = !isFalseBoolean(String(args.blocking));
254 const toastMode = Object.values(ActionLoaderToastMode).includes(String(args.toast))
255 ? String(args.toast)
256 : ActionLoaderToastMode.STOPPABLE;
257 const message = String(args.message ?? t`Generating...`);
258 const title = args.title ? String(args.title) : '';
259 const stopTooltip = String(args.stopTooltip ?? t`Stop`);
260
261 const handle = showActionLoader({
262 blocking,
263 toastMode,
264 message,
265 title,
266 stopTooltip,
267 onStop: createClosureHandler(args.onStop),
268 onHide: createClosureHandler(args.onHide, { argName: 'onHide' }),
269 });
270
271 return handle.id;
272 },
273 }));
274
275 // /loader-hide command - manually hide a loader by handle ID
276 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
277 name: 'loader-hide',
278 returns: 'true if an active loader was hidden, otherwise false',
279 helpString: `
280 <div>
281 Hides an action loader that was shown with <code>/loader-show</code>.
282 If no handle is provided, hides <strong>all</strong> active loaders.
283 </div>
284 <div>
285 <strong>Example:</strong>
286 <pre><code class="language-stscript">/loader-hide handle={{getvar::myLoader}}</code></pre>
287 </div>
288 `,
289 namedArgumentList: [
290 SlashCommandNamedArgument.fromProps({
291 name: 'handle',
292 description: 'Loader handle ID returned by /loader-show. If not provided, hides all active loaders.',
293 typeList: [ARGUMENT_TYPE.STRING],
294 enumProvider: loaderEnumProviders.loaderHandleProvider,
295 }),
296 ],
297 callback: async (args) => {
298 const handleId = args.handle ? String(args.handle) : null;
299
300 if (handleId) {
301 const handle = getLoaderHandleById(handleId);
302 if (handle && handle.isActive) {
303 await handle.hide();
304 return 'true';
305 }
306 return 'false';
307 }
308
309 // No handle provided - hide all active loaders
310 const result = await hideActionLoader();
311 return result ? 'true' : 'false';
312 },
313 }));
314
315 // /loader-stop command - trigger the stop action on a loader
316 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
317 name: 'loader-stop',
318 returns: 'true if an active loader was stopped, otherwise false',
319 helpString: `
320 <div>
321 Triggers the stop action on a specific action loader, as if the user clicked the stop button.
322 Unlike <code>/loader-hide</code>, this command requires a handle - you must specify which loader to stop.
323 </div>
324 <div>
325 <strong>Example:</strong>
326 <pre><code class="language-stscript">/loader-stop handle={{getvar::myLoader}}</code></pre>
327 </div>
328 `,
329 namedArgumentList: [
330 SlashCommandNamedArgument.fromProps({
331 name: 'handle',
332 description: 'Loader handle ID returned by /loader-show.',
333 typeList: [ARGUMENT_TYPE.STRING],
334 isRequired: true,
335 enumProvider: loaderEnumProviders.loaderHandleProvider,
336 }),
337 ],
338 callback: async (args) => {
339 const handleId = args.handle ? String(args.handle) : null;
340
341 if (!handleId) {
342 toastr.warning(t`No handle provided. You must specify which loader to stop.`);
343 return 'false';
344 }
345
346 const handle = getLoaderHandleById(handleId);
347 if (handle && handle.isActive) {
348 await handle.stop();
349 return 'true';
350 }
351
352 return 'false';
353 },
354 }));
355}
public/scripts/action-loader.js+572 -0
@@ -0,0 +1,572 @@
1/**
2 * Unified action loader system - shows loader overlay with optional toast notifications.
3 * Designed to be flexible and reusable for various long-running operations.
4 *
5 * Features:
6 * - Stacking multiple loaders - overlay stays single, but toasts can stack
7 * - Blocking and non-blocking modes
8 * - Stoppable or static toasts
9 * - Class-based handle system for fine-grained control
10 *
11 * @module action-loader
12 */
13
14import { t } from './i18n.js';
15import { stopGeneration } from '../script.js';
16import { Popup, POPUP_RESULT, POPUP_TYPE } from './popup.js';
17
18/**
19 * Enum representing the toast display mode for the action loader.
20 * @readonly
21 * @enum {string}
22 */
23export const ActionLoaderToastMode = {
24 /** No toast is displayed */
25 NONE: 'none',
26 /** Toast is displayed without stop button (non-interactable) */
27 STATIC: 'static',
28 /** Toast is displayed with stop button (default) */
29 STOPPABLE: 'stoppable',
30};
31
32/**
33 * @typedef {object} ActionLoaderOptions
34 * @property {boolean} [blocking=true] - Whether to show the blocking overlay. Set to false for non-blocking toast-only loaders.
35 * @property {ActionLoaderToastMode} [toastMode='stoppable'] - Toast display mode
36 * @property {string} [message='Generating...'] - The message to display in the toast
37 * @property {string} [title] - Optional title for the toast notification
38 * @property {string} [stopTooltip='Stop'] - Tooltip text for the stop button
39 * @property {HTMLElement|string|null} [overlayContent=null] - Custom content for the overlay (replaces default spinner)
40 * @property {(() => void)|null} [onStop=null] - Custom stop handler. If null, calls `stopGeneration()`
41 * @property {(() => void)|null} [onHide=null] - Custom hide handler. Called when the loader is hidden (not stopped).
42 */
43
44/** Counter for generating unique loader IDs */
45let loaderIdCounter = 0;
46
47/** @type {Set<ActionLoaderHandle>} Set of all active loader handles */
48const activeHandles = new Set();
49
50/**
51 * Generates a unique loader ID.
52 * @returns {string} Unique loader ID
53 */
54function generateLoaderId() {
55 return `loader_${++loaderIdCounter}`;
56}
57
58/**
59 * Checks if there are any active blocking loaders.
60 * @returns {boolean} True if at least one blocking loader is active
61 */
62function hasBlockingLoaders() {
63 for (const handle of activeHandles) {
64 if (handle.isBlocking && handle.isActive) {
65 return true;
66 }
67 }
68 return false;
69}
70
71/**
72 * Class representing an action loader handle.
73 * Manages its own toast, stop handler, and lifecycle.
74 */
75export class ActionLoaderHandle {
76 /** @type {string} Unique identifier for this handle */
77 id;
78
79 /** @type {JQuery<HTMLElement>|null} The toast element for this loader */
80 #toast = null;
81
82 /** @type {(() => void)|null} Custom stop handler */
83 #onStop = null;
84
85 /** @type {(() => void)|null} Custom hide handler */
86 #onHide = null;
87
88 /** @type {boolean} Whether this loader blocks the UI with an overlay */
89 #blocking = true;
90
91 /** @type {boolean} Whether this handle has been disposed */
92 #disposed = false;
93
94 /**
95 * Creates a new ActionLoaderHandle.
96 * @param {object} options - Configuration options
97 * @param {boolean} [options.blocking=true] - Whether to show blocking overlay
98 * @param {ActionLoaderToastMode} [options.toastMode] - Toast display mode
99 * @param {string} [options.message='Generating...'] - Message to display in the toast
100 * @param {string} [options.title] - Title for the toast notification
101 * @param {string} [options.stopTooltip='Stop'] - Tooltip for the stop button
102 * @param {HTMLElement|string|null} [options.overlayContent] - Custom content for the overlay (replaces default spinner)
103 * @param {(() => void)|null} [options.onStop] - Custom stop handler
104 * @param {(() => void)|null} [options.onHide] - Custom hide handler
105 */
106 constructor({
107 blocking = true,
108 toastMode = ActionLoaderToastMode.STOPPABLE,
109 message = t`Generating...`,
110 title = '',
111 stopTooltip = t`Stop`,
112 overlayContent = null,
113 onStop = null,
114 onHide = null,
115 } = {}) {
116 this.id = generateLoaderId();
117 this.#blocking = blocking;
118 this.#onStop = onStop;
119 this.#onHide = onHide;
120
121 // Warn if non-blocking loader has no toast - it won't be visible to the user
122 if (!blocking && toastMode === ActionLoaderToastMode.NONE && !overlayContent) {
123 console.warn('[ActionLoader] Non-blocking loader created without a toast. This loader will not be visible to the user.');
124 }
125
126 // Show the blocking loader overlay if this is the first blocking handle
127 if (blocking && !hasBlockingLoaders() && !isOverlayDisplayed()) {
128 showOverlay(overlayContent);
129 }
130
131 // Register this handle
132 activeHandles.add(this);
133
134 // Create toast if needed
135 if (toastMode !== ActionLoaderToastMode.NONE) {
136 this.#createToast(message, title, toastMode, stopTooltip);
137 }
138 }
139
140 /**
141 * Creates the toast element for this loader.
142 * @param {string} message - Message to display
143 * @param {string} title - Title for the toast
144 * @param {ActionLoaderToastMode} toastMode - Toast mode
145 * @param {string} stopTooltip - Tooltip for stop button
146 */
147 #createToast(message, title, toastMode, stopTooltip) {
148 const toastContent = document.createElement('div');
149 toastContent.className = 'action-loader-toast';
150
151 const messageSpan = document.createElement('span');
152 messageSpan.className = 'action-loader-message';
153 messageSpan.textContent = message;
154 toastContent.appendChild(messageSpan);
155
156 // Add stop button if mode is STOPPABLE
157 if (toastMode === ActionLoaderToastMode.STOPPABLE) {
158 const stopButton = document.createElement('i');
159 stopButton.className = 'fa-solid fa-stop-circle action-loader-stop interactable';
160 stopButton.title = stopTooltip;
161 stopButton.addEventListener('click', (e) => {
162 e.preventDefault();
163 e.stopPropagation();
164 this.stop();
165 });
166 toastContent.appendChild(stopButton);
167 }
168
169 // Show toast with no timeout (sticky)
170 this.#toast = toastr.info($(toastContent), title, {
171 timeOut: 0,
172 extendedTimeOut: 0,
173 tapToDismiss: false,
174 escapeHtml: false,
175 });
176 }
177
178 /**
179 * Clears the toast element for this loader.
180 */
181 #clearToast() {
182 if (this.#toast) {
183 toastr.clear(this.#toast, { force: true }); // Need to force as the toast might have focus/hover
184 this.#toast = null;
185 }
186 }
187
188 /**
189 * Disposes this handle, removing it from active handles and hiding overlay if last.
190 */
191 async #dispose() {
192 if (this.#disposed) return;
193 this.#disposed = true;
194
195 this.#clearToast();
196 activeHandles.delete(this);
197
198 // Hide the overlay if this was the last blocking handle
199 if (this.#blocking && !hasBlockingLoaders()) {
200 await hideOverlay();
201 }
202 }
203
204 /**
205 * Whether this handle is still active (not disposed).
206 * @returns {boolean}
207 */
208 get isActive() {
209 return !this.#disposed;
210 }
211
212 /**
213 * Whether this loader blocks the UI with an overlay.
214 * @returns {boolean}
215 */
216 get isBlocking() {
217 return this.#blocking;
218 }
219
220 /**
221 * Triggers the stop action on this loader.
222 * Calls the custom onStop handler if provided, otherwise calls stopGeneration().
223 * Then hides this loader.
224 */
225 async stop() {
226 if (this.#disposed) return;
227
228 // Call custom stop handler or default
229 if (this.#onStop) {
230 try {
231 await this.#onStop();
232 } catch (e) {
233 console.error('Error executing onStop handler', e);
234 }
235 } else {
236 stopGeneration();
237 }
238
239 // Dispose without calling onHide (stop is different from hide)
240 await this.#dispose();
241 }
242
243 /**
244 * Hides this loader and clears its toast.
245 * Calls the custom onHide handler if provided.
246 */
247 async hide() {
248 if (this.#disposed) return;
249
250 // Call custom hide handler if provided
251 if (this.#onHide) {
252 try {
253 await this.#onHide();
254 } catch (e) {
255 console.error('Error executing onHide handler', e);
256 }
257 }
258
259 await this.#dispose();
260 }
261}
262
263/**
264 * Action loader utility API.
265 * Provides a convenient interface for showing and managing loading indicators.
266 *
267 * Read the functions documentation for more details.
268 *
269 * @example
270 * // Basic usage
271 * const handle = loader.show({ message: 'Loading...' });
272 * await someOperation();
273 * handle.hide();
274 *
275 * @example
276 * // Non-blocking background task
277 * const handle = loader.show({ blocking: false, message: 'Processing...' });
278 *
279 * @example
280 * // Hide all active loaders
281 * loader.hide();
282 */
283export const loader = {
284 /**
285 * Shows an action loader with optional toast notification.
286 * Returns a handle to control the loader.
287 * @type {typeof showActionLoader}
288 */
289 show: showActionLoader,
290
291 /**
292 * Hides a specific loader by handle, or all loaders if no handle provided.
293 * @type {typeof hideActionLoader}
294 */
295 hide: hideActionLoader,
296
297 /**
298 * Gets all currently active loader handles.
299 * @type {typeof getActiveLoaderHandles}
300 */
301 active: getActiveLoaderHandles,
302
303 /**
304 * Gets a loader handle by its ID.
305 * @type {typeof getLoaderHandleById}
306 */
307 get: getLoaderHandleById,
308
309 /**
310 * Checks if any blocking loader overlay is currently displayed.
311 * @returns {boolean} True if a blocking overlay is shown
312 */
313 isBlocking: isOverlayDisplayed,
314
315 /**
316 * Toast display mode constants.
317 * @type {typeof ActionLoaderToastMode}
318 */
319 ToastMode: ActionLoaderToastMode,
320
321 /**
322 * The ActionLoaderHandle class.
323 * @type {typeof ActionLoaderHandle}
324 */
325 Handle: ActionLoaderHandle,
326
327 /**
328 * Creates a fresh default loader overlay element.
329 * @type {typeof createDefaultLoaderOverlay}
330 */
331 createOverlay: createDefaultLoaderOverlay,
332};
333
334/**
335 * Shows an action loader with an optional stoppable toast notification.
336 * Multiple loaders can be stacked - the overlay stays single, but each gets its own toast.
337 * When the last loader is hidden, the overlay is removed.
338 *
339 * With default arguments, will function as a generation loader / wrapper.
340 *
341 * @param {ActionLoaderOptions} [options={}] - Configuration options
342 * @returns {ActionLoaderHandle} Handle to control the loader
343 *
344 * @example
345 * // Basic usage
346 * const loader = showActionLoader({ message: 'Generating title...' });
347 * try {
348 * const result = await generateRaw({ prompt });
349 * // process result
350 * } finally {
351 * await loader.hide();
352 * }
353 *
354 * @example
355 * // With custom stop and hide handlers
356 * const loader = showActionLoader({
357 * message: 'Downloading...',
358 * stopTooltip: 'Cancel download',
359 * onStop: () => myCustomCancelFunction(),
360 * onHide: () => console.log('Loader hidden'),
361 * });
362 *
363 * @example
364 * // Stacking multiple loaders
365 * const loader1 = showActionLoader({ message: 'Task 1...' });
366 * const loader2 = showActionLoader({ message: 'Task 2...' });
367 * await loader1.hide(); // Overlay stays, loader2 still active
368 * await loader2.hide(); // Now overlay hides
369 *
370 * @example
371 * // Non-blocking loader (toast only, no overlay)
372 * const loader = showActionLoader({
373 * message: 'Captioning image...',
374 * blocking: false,
375 * onStop: () => abortCaptioning(),
376 * });
377 */
378export function showActionLoader(options = {}) {
379 return new ActionLoaderHandle(options);
380}
381
382/**
383 * Hides a specific action loader by handle, or all active loaders if no handle provided.
384 * @param {ActionLoaderHandle|null} [handle=null] - Specific handle to hide, or undefined to hide all
385 * @returns {Promise<boolean>} Whether any loader was hidden
386 */
387export async function hideActionLoader(handle = null) {
388 if (handle instanceof ActionLoaderHandle) {
389 if (handle.isActive) {
390 await handle.hide();
391 return true;
392 }
393 return false;
394 }
395
396 // No handle provided - hide all active loaders
397 const handles = getActiveLoaderHandles();
398 for (const h of handles) {
399 await h.hide();
400 }
401 return handles.length > 0;
402}
403
404/**
405 * Gets all currently active loader handles.
406 * @returns {ActionLoaderHandle[]} Array of active handles
407 */
408export function getActiveLoaderHandles() {
409 return Array.from(activeHandles);
410}
411
412/**
413 * Gets a loader handle by its ID.
414 * @param {string} id - The handle ID
415 * @returns {ActionLoaderHandle|undefined} The handle, or undefined if not found
416 */
417export function getLoaderHandleById(id) {
418 for (const handle of activeHandles) {
419 if (handle.id === id) {
420 return handle;
421 }
422 }
423 return undefined;
424}
425
426// ============================================================================
427// Internal overlay management
428// ============================================================================
429
430/** @type {Popup|null} The current loader overlay popup */
431let loaderPopup = null;
432
433/** Whether the initial HTML preloader has been removed */
434let preloaderYoinked = false;
435
436/**
437 * Creates the default loader overlay element.
438 * Always returns a fresh element instance.
439 *
440 * @returns {HTMLDivElement} A new loader overlay element
441 */
442export function createDefaultLoaderOverlay() {
443 const loaderElement = document.createElement('div');
444 loaderElement.id = 'loader';
445
446 const spinnerElement = document.createElement('div');
447 spinnerElement.id = 'load-spinner';
448 spinnerElement.className = 'fa-solid fa-gear fa-spin fa-3x';
449
450 loaderElement.appendChild(spinnerElement);
451
452 return loaderElement;
453}
454
455/**
456 * Normalizes custom overlay content into a value supported by Popup.
457 * @param {string|HTMLElement|null} customContent - Custom overlay content
458 * @returns {string|HTMLElement} Content for Popup
459 */
460function getOverlayContent(customContent) {
461 if (typeof customContent === 'string') {
462 return customContent;
463 }
464
465 if (customContent instanceof HTMLElement) {
466 return customContent;
467 }
468
469 return createDefaultLoaderOverlay();
470}
471
472/**
473 * Checks if the loader overlay is currently displayed.
474 * @returns {boolean} True if overlay is shown
475 */
476function isOverlayDisplayed() {
477 return !!loaderPopup;
478}
479
480/**
481 * Shows the blocking loader overlay.
482 * Internal function - use showActionLoader() instead.
483 * @param {HTMLElement|string|null} [customContent] - Custom content for the overlay
484 */
485function showOverlay(customContent = null) {
486 // Two loaders don't make sense. Don't await, we can overlay the old loader while it closes
487 if (loaderPopup) loaderPopup.complete(POPUP_RESULT.CANCELLED);
488
489 const content = getOverlayContent(customContent);
490
491 loaderPopup = new Popup(content, POPUP_TYPE.DISPLAY, null, {
492 allowEscapeClose: false,
493 transparent: true,
494 animation: 'none',
495 wide: true,
496 large: true,
497 });
498
499 // No close button, loaders are not closable
500 loaderPopup.closeButton.style.display = 'none';
501
502 loaderPopup.show();
503}
504
505/**
506 * Hides the blocking loader overlay with animation.
507 * Internal function - use hideActionLoader() instead.
508 * @returns {Promise<void>}
509 */
510async function hideOverlay() {
511 if (!loaderPopup) {
512 return Promise.resolve();
513 }
514
515 return new Promise((resolve) => {
516 const loaderElement = $('#loader');
517 const spinner = $('#load-spinner');
518
519 if (!loaderElement.length) {
520 console.warn('Loader element not found, skipping animation');
521 cleanup();
522 return;
523 }
524
525 // Check if transitions are enabled on spinner (which has the transition property)
526 const transitionDuration = spinner.length && spinner[0] ? getComputedStyle(spinner[0]).transitionDuration : '0s';
527 const hasTransitions = parseFloat(transitionDuration) > 0;
528
529 if (hasTransitions) {
530 Promise.race([
531 new Promise((r) => setTimeout(r, 500)), // Fallback timeout
532 new Promise((r) => loaderElement.one('transitionend webkitTransitionEnd oTransitionEnd MSTransitionEnd', r)),
533 ]).finally(cleanup);
534 } else {
535 cleanup();
536 }
537
538 function cleanup() {
539 loaderElement.remove();
540 // Yoink preloader entirely; it only exists to cover up unstyled content while loading JS
541 // If it's present, we remove it once and then it's gone.
542 yoinkPreloader();
543
544 loaderPopup.complete(POPUP_RESULT.AFFIRMATIVE)
545 .catch((err) => console.error('Error completing loaderPopup:', err))
546 .finally(() => {
547 loaderPopup = null;
548 resolve();
549 });
550 }
551
552 // Apply the blur styles to the entire loader element
553 loaderElement.css({
554 'filter': 'blur(15px)',
555 'opacity': '0',
556 });
557 });
558}
559
560/**
561 * Removes the initial HTML preloader element.
562 * Called once after the first loader hide.
563 */
564function yoinkPreloader() {
565 if (preloaderYoinked) return;
566 document.getElementById('preloader')?.remove();
567 preloaderYoinked = true;
568}
569
570// ============================================================================
571// End internal overlay management
572// ============================================================================
public/scripts/authors-note.js+2 -4
@@ -231,11 +231,9 @@ function onExtensionFloatingCharaPromptInput() {
231 !existingCharaNote.useChara231 !existingCharaNote.useChara
232 ) {232 ) {
233 extension_settings.note.chara.splice(existingCharaNoteIndex, 1);233 extension_settings.note.chara.splice(existingCharaNoteIndex, 1);
234 }234 } else if (extension_settings.note.chara && existingCharaNote) {
235 else if (extension_settings.note.chara && existingCharaNote) {
236 Object.assign(existingCharaNote, tempCharaNote);235 Object.assign(existingCharaNote, tempCharaNote);
237 }236 } else if (avatarName && tempPrompt.length > 0) {
238 else if (avatarName && tempPrompt.length > 0) {
239 if (!extension_settings.note.chara) {237 if (!extension_settings.note.chara) {
240 extension_settings.note.chara = [];238 extension_settings.note.chara = [];
241 }239 }
public/scripts/autocomplete/AutoComplete.js+0 -5
@@ -80,8 +80,6 @@ export class AutoComplete {
80 }80 }
8181
8282
83
84
85 /**83 /**
86 * @param {HTMLTextAreaElement|HTMLInputElement} textarea The textarea to receive autocomplete.84 * @param {HTMLTextAreaElement|HTMLInputElement} textarea The textarea to receive autocomplete.
87 * @param {() => boolean} checkIfActivate Function should return true only if under the current conditions, autocomplete should display (e.g., for slash commands: autoComplete.text[0] == '/')85 * @param {() => boolean} checkIfActivate Function should return true only if under the current conditions, autocomplete should display (e.g., for slash commands: autoComplete.text[0] == '/')
@@ -414,7 +412,6 @@ export class AutoComplete {
414 });412 });
415413
416414
417
418 if (this.isForceHidden) {415 if (this.isForceHidden) {
419 // hidden with escape416 // hidden with escape
420 return this.hide();417 return this.hide();
@@ -465,7 +462,6 @@ export class AutoComplete {
465 }462 }
466463
467464
468
469 /**465 /**
470 * Create updated DOM.466 * Create updated DOM.
471 */467 */
@@ -514,7 +510,6 @@ export class AutoComplete {
514 }510 }
515511
516512
517
518 /**513 /**
519 * Update position of DOM.514 * Update position of DOM.
520 */515 */
public/scripts/autocomplete/AutoCompleteFuzzyScore.js+0 -3
@@ -1,6 +1,3 @@
1
2
3
4export class AutoCompleteFuzzyScore {1export class AutoCompleteFuzzyScore {
5 /**@type {number}*/ start;2 /**@type {number}*/ start;
6 /**@type {number}*/ longestConsecutive;3 /**@type {number}*/ longestConsecutive;
public/scripts/autocomplete/AutoCompleteNameResult.js+0 -1
@@ -2,7 +2,6 @@ import { AutoCompleteNameResultBase } from './AutoCompleteNameResultBase.js';
2import { AutoCompleteSecondaryNameResult } from './AutoCompleteSecondaryNameResult.js';2import { AutoCompleteSecondaryNameResult } from './AutoCompleteSecondaryNameResult.js';
33
44
5
6export class AutoCompleteNameResult extends AutoCompleteNameResultBase {5export class AutoCompleteNameResult extends AutoCompleteNameResultBase {
7 /**6 /**
8 *7 *
public/scripts/autocomplete/AutoCompleteNameResultBase.js+2 -3
@@ -1,14 +1,13 @@
1import { AutoCompleteOption } from './AutoCompleteOption.js';1import { AutoCompleteOption } from './AutoCompleteOption.js';
22
33
4
5export class AutoCompleteNameResultBase {4export class AutoCompleteNameResultBase {
6 /**@type {string} */ name;5 /**@type {string} */ name;
7 /**@type {number} */ start;6 /**@type {number} */ start;
8 /**@type {AutoCompleteOption[]} */ optionList = [];7 /**@type {AutoCompleteOption[]} */ optionList = [];
9 /**@type {boolean} */ canBeQuoted = false;8 /**@type {boolean} */ canBeQuoted = false;
10 /**@type {()=>string} */ makeNoMatchText = ()=>`No matches found for "${this.name}"`;9 /**@type {()=>string} */ makeNoMatchText = () => `No matches found for "${this.name}"`;
11 /**@type {()=>string} */ makeNoOptionsText = ()=>'No options';10 /**@type {()=>string} */ makeNoOptionsText = () => 'No options';
1211
1312
14 /**13 /**
public/scripts/autocomplete/AutoCompleteOption.js+1 -2
@@ -1,7 +1,6 @@
1import { AutoCompleteFuzzyScore } from './AutoCompleteFuzzyScore.js';1import { AutoCompleteFuzzyScore } from './AutoCompleteFuzzyScore.js';
22
33
4
5export class AutoCompleteOption {4export class AutoCompleteOption {
6 /** @type {string} */ name;5 /** @type {string} */ name;
7 /** @type {string} */ typeIcon;6 /** @type {string} */ typeIcon;
@@ -72,7 +71,7 @@ export class AutoCompleteOption {
72 name.classList.add('name');71 name.classList.add('name');
73 name.classList.add('monospace');72 name.classList.add('monospace');
74 name.textContent = noSlash ? '' : '/';73 name.textContent = noSlash ? '' : '/';
75 key.split('').forEach(char=>{74 key.split('').forEach(char => {
76 const span = document.createElement('span'); {75 const span = document.createElement('span'); {
77 span.textContent = char;76 span.textContent = char;
78 name.append(span);77 name.append(span);
public/scripts/backgrounds.js+799 -33
@@ -6,7 +6,7 @@ import { SlashCommandParser } from './slash-commands/SlashCommandParser.js';
6import { createThumbnail, flashHighlight, getBase64Async, stringFormat, debounce, setupScrollToTop, saveBase64AsFile, getFileExtension, sortIgnoreCaseAndAccents } from './utils.js';6import { createThumbnail, flashHighlight, getBase64Async, stringFormat, debounce, setupScrollToTop, saveBase64AsFile, getFileExtension, sortIgnoreCaseAndAccents } from './utils.js';
7import { debounce_timeout } from './constants.js';7import { debounce_timeout } from './constants.js';
8import { t } from './i18n.js';8import { t } from './i18n.js';
9import { Popup } from './popup.js';9import { callGenericPopup, Popup, POPUP_TYPE } from './popup.js';
10import { groups, selected_group } from './group-chats.js';10import { groups, selected_group } from './group-chats.js';
11import { humanizedDateTime } from './RossAscends-mods.js';11import { humanizedDateTime } from './RossAscends-mods.js';
12import { deleteMediaFromServer } from './chats.js';12import { deleteMediaFromServer } from './chats.js';
@@ -14,6 +14,17 @@ import { deleteMediaFromServer } from './chats.js';
14const BG_METADATA_KEY = 'custom_background';14const BG_METADATA_KEY = 'custom_background';
15const LIST_METADATA_KEY = 'chat_backgrounds';15const LIST_METADATA_KEY = 'chat_backgrounds';
1616
17/** @type {Array<{id: string, name: string, thumbnailFile: string}>} */
18let folderList = [];
19/** @type {Object.<string, string[]>} filename → folderIds */
20let imageFolderMap = {};
21/** @type {string|null} Currently active folder drill-in, or null for root */
22let activeFolderId = null;
23/** @type {Set<string>} Selected system backgrounds for group folder actions */
24const selectedSystemBackgroundFiles = new Set();
25/** @type {boolean} Whether click-to-select mode is active for system backgrounds */
26let isBackgroundSelectionMode = false;
27
17// A single transparent PNG pixel used as a placeholder for errored backgrounds28// A single transparent PNG pixel used as a placeholder for errored backgrounds
18const PNG_PIXEL = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=';29const PNG_PIXEL = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=';
19const PNG_PIXEL_BLOB = new Blob([Uint8Array.from(atob(PNG_PIXEL), c => c.charCodeAt(0))], { type: 'image/png' });30const PNG_PIXEL_BLOB = new Blob([Uint8Array.from(atob(PNG_PIXEL), c => c.charCodeAt(0))], { type: 'image/png' });
@@ -41,6 +52,8 @@ const THUMBNAIL_CONFIG = {
41 height: 90,52 height: 90,
42};53};
4354
55const ANIMATED_BACKGROUND_EXTENSIONS = ['mp4', 'webp', 'gif', 'apng'];
56
44/**57/**
45 * Cache for image metadata.58 * Cache for image metadata.
46 * @type {Map<string, import('../../src/endpoints/image-metadata.js').ImageMetadata>}59 * @type {Map<string, import('../../src/endpoints/image-metadata.js').ImageMetadata>}
@@ -88,7 +101,7 @@ let lazyLoadObserver = null;
88/**101/**
89 * Cache for the current list of system background filenames.102 * Cache for the current list of system background filenames.
90 * Used to re-sort backgrounds without refetching from the server.103 * Used to re-sort backgrounds without refetching from the server.
91 * @type {string[]}104 * @type {Array<{filename: string, isAnimated: boolean}>}
92 */105 */
93let cachedSystemBackgrounds = [];106let cachedSystemBackgrounds = [];
94107
@@ -136,12 +149,13 @@ function sortBackgrounds(backgrounds, isCustom = false) {
136149
137/**150/**
138 * Creates a single thumbnail DOM element. The CSS now handles all sizing.151 * Creates a single thumbnail DOM element. The CSS now handles all sizing.
139 * @param {object} imageData - Data for the image (filename, isCustom).152 * @param {object} imageData - Data for the image (filename, isCustom, isAnimated).
140 * @returns {HTMLElement} The created thumbnail element.153 * @returns {HTMLElement} The created thumbnail element.
141 */154 */
142function createThumbnailElement(imageData) {155function createThumbnailElement(imageData) {
143 const bg = imageData.filename;156 const bg = imageData.filename;
144 const isCustom = imageData.isCustom;157 const isCustom = imageData.isCustom;
158 const isAnimated = imageData.isAnimated ?? false;
145159
146 const thumbnail = $('#background_template .bg_example').clone();160 const thumbnail = $('#background_template .bg_example').clone();
147161
@@ -167,11 +181,12 @@ function createThumbnailElement(imageData) {
167181
168 const url = generateUrlParameter(bg, isCustom);182 const url = generateUrlParameter(bg, isCustom);
169 const title = isCustom ? bg.split('/').pop() : bg;183 const title = isCustom ? bg.split('/').pop() : bg;
170 const friendlyTitle = title.slice(0, title.lastIndexOf('.'));184 const friendlyTitle = String(title || '').slice(0, title.lastIndexOf('.'));
171185
172 thumbnail.attr('title', title);186 thumbnail.attr('title', title);
173 thumbnail.attr('bgfile', bg);187 thumbnail.attr('bgfile', bg);
174 thumbnail.attr('custom', String(isCustom));188 thumbnail.attr('custom', String(isCustom));
189 thumbnail.attr('animated', String(isAnimated));
175 thumbnail.data('url', url);190 thumbnail.data('url', url);
176 titleElement.text(friendlyTitle);191 titleElement.text(friendlyTitle);
177192
@@ -216,6 +231,7 @@ export function loadBackgroundSettings(settings) {
216 }231 }
217 background_settings.thumbnailColumns = columns;232 background_settings.thumbnailColumns = columns;
218 background_settings.sortOrder = backgroundSettings.sortOrder;233 background_settings.sortOrder = backgroundSettings.sortOrder;
234 background_settings.animation = backgroundSettings.animation;
219 applyThumbnailColumns(background_settings.thumbnailColumns);235 applyThumbnailColumns(background_settings.thumbnailColumns);
220236
221 setBackground(backgroundSettings.name, backgroundSettings.url);237 setBackground(backgroundSettings.name, backgroundSettings.url);
@@ -254,10 +270,36 @@ async function onChatChanged() {
254 highlightSelectedBackground();270 highlightSelectedBackground();
255}271}
256272
257function getBackgroundPath(fileUrl) {273/**
274 * Checks if a given URL corresponds to a custom background in the current chat's metadata.
275 * @param {string} fileUrl - The URL to check against the chat's custom backgrounds.
276 * @returns {boolean} True if the URL corresponds to a custom background, false otherwise.
277 */
278export function isCustomBackgroundUrl(fileUrl) {
279 const customBackgrounds = chat_metadata[LIST_METADATA_KEY] || [];
280 return customBackgrounds.some(bg => bg === fileUrl || generateUrlParameter(bg, true) === fileUrl);
281}
282
283/**
284 * Gets the client path for a background image, encoding the file name for safe URL usage.
285 * @param {string} fileUrl File name or URL of the background image
286 * @returns {string} Client path for the system backgroun
287 */
288export function getBackgroundPath(fileUrl) {
258 return `backgrounds/${encodeURIComponent(fileUrl)}`;289 return `backgrounds/${encodeURIComponent(fileUrl)}`;
259}290}
260291
292/**
293 * Gets the raw server-side relative path for a background image (no URL encoding).
294 * Used when communicating paths to the API (stored as plain strings in metadata).
295 * @param {string} file File name of the background image
296 * @returns {string} Raw relative path, e.g. "backgrounds/my file.jpg"
297 */
298function getBackgroundRelativePath(file) {
299 return `backgrounds/${file}`;
300}
301
302
261function highlightLockedBackground() {303function highlightLockedBackground() {
262 $('.bg_example.locked-background').removeClass('locked-background');304 $('.bg_example.locked-background').removeClass('locked-background');
263305
@@ -327,6 +369,11 @@ function removeBackgroundMetadata() {
327function onSelectBackgroundClick(e) {369function onSelectBackgroundClick(e) {
328 const bgFile = $(this).attr('bgfile');370 const bgFile = $(this).attr('bgfile');
329 const isCustom = $(this).attr('custom') === 'true';371 const isCustom = $(this).attr('custom') === 'true';
372 if (isBackgroundSelectionMode && !isCustom) {
373 toggleBackgroundGroupSelection(bgFile);
374 return;
375 }
376
330 const backgroundCssUrl = getUrlParameter(this);377 const backgroundCssUrl = getUrlParameter(this);
331 const bypassGlobalLock = !isCustom && e.shiftKey;378 const bypassGlobalLock = !isCustom && e.shiftKey;
332379
@@ -483,14 +530,13 @@ async function onDeleteBackgroundClick(e) {
483 const url = bgToDelete.data('url');530 const url = bgToDelete.data('url');
484 const isCustom = bgToDelete.attr('custom') === 'true';531 const isCustom = bgToDelete.attr('custom') === 'true';
485 const deleteFromServerId = 'delete_bg_from_server';532 const deleteFromServerId = 'delete_bg_from_server';
486 const customInputs = [533 /** @type {import('./popup.js').CustomPopupInput[]} */
487 {534 const customInputs = [{
488 type: 'checkbox',535 type: 'checkbox',
489 label: t`Also delete file from server`,536 label: t`Also delete file from server`,
490 id: deleteFromServerId,537 id: deleteFromServerId,
491 defaultState: true,538 defaultState: true,
492 },539 }];
493 ];
494 let deleteFromServer = false;540 let deleteFromServer = false;
495 const confirm = await Popup.show.confirm(t`Delete the background?`, null, {541 const confirm = await Popup.show.confirm(t`Delete the background?`, null, {
496 customInputs: isCustom ? customInputs : [],542 customInputs: isCustom ? customInputs : [],
@@ -507,7 +553,7 @@ async function onDeleteBackgroundClick(e) {
507 if (!isCustom) {553 if (!isCustom) {
508 await delBackground(bg);554 await delBackground(bg);
509 // Remove from cache to prevent reappearing on sort change555 // Remove from cache to prevent reappearing on sort change
510 const cacheIndex = cachedSystemBackgrounds.indexOf(bg);556 const cacheIndex = cachedSystemBackgrounds.findIndex(s => s.filename === bg);
511 if (cacheIndex !== -1) {557 if (cacheIndex !== -1) {
512 cachedSystemBackgrounds.splice(cacheIndex, 1);558 cachedSystemBackgrounds.splice(cacheIndex, 1);
513 }559 }
@@ -534,6 +580,25 @@ async function onDeleteBackgroundClick(e) {
534 }580 }
535 }581 }
536582
583 // Remove from local image list so it doesn't reappear on re-render
584 const deletedBg = bgToDelete.attr('bgfile');
585 if (deletedBg) {
586 const cachedIdx = cachedSystemBackgrounds.findIndex(img => img.filename === deletedBg);
587 if (cachedIdx !== -1) cachedSystemBackgrounds.splice(cachedIdx, 1);
588 selectedSystemBackgroundFiles.delete(deletedBg);
589
590 // Update folder map and clear folder thumbnail if it referenced this image
591 if (imageFolderMap[deletedBg]) {
592 delete imageFolderMap[deletedBg];
593 }
594 for (const folder of folderList) {
595 if (folder.thumbnailFile === deletedBg) {
596 folder.thumbnailFile = '';
597 }
598 }
599 renderFolderGrid();
600 }
601
537 bgToDelete.remove();602 bgToDelete.remove();
538603
539 if (url === chat_metadata[BG_METADATA_KEY]) {604 if (url === chat_metadata[BG_METADATA_KEY]) {
@@ -550,6 +615,7 @@ async function onDeleteBackgroundClick(e) {
550615
551 highlightLockedBackground();616 highlightLockedBackground();
552 highlightSelectedBackground();617 highlightSelectedBackground();
618 syncGroupSelectionUi();
553 }619 }
554}620}
555621
@@ -590,22 +656,28 @@ async function autoBackgroundCommand() {
590656
591/**657/**
592 * Renders the system backgrounds gallery.658 * Renders the system backgrounds gallery.
593 * @param {string[]} [backgrounds] - Optional filtered list of backgrounds.659 * @param {Array<{filename: string, isAnimated: boolean}>} [backgrounds] - Optional filtered list of backgrounds with metadata.
594 */660 */
595function renderSystemBackgrounds(backgrounds) {661function renderSystemBackgrounds(backgrounds) {
596 const sourceList = backgrounds || [];662 const sourceList = backgrounds || [];
597 const container = $('#bg_menu_content');663 const container = $('#bg_menu_content');
598 container.empty();664 container.empty();
599665
600 if (sourceList.length === 0) return;666 if (sourceList.length === 0) {
667 syncGroupSelectionUi();
668 return;
669 }
601670
602 const sortedList = sortBackgrounds(sourceList, false);671 const sortedList = sortBackgrounds(sourceList.map(bg => bg.filename), false);
603 sortedList.forEach(bg => {672 const metadataByFilename = new Map(sourceList.map(bg => [bg.filename, bg]));
604 const imageData = { filename: bg, isCustom: false };673 sortedList.forEach(filename => {
674 const bg = metadataByFilename.get(filename);
675 const imageData = { filename, isCustom: false, isAnimated: bg?.isAnimated ?? false };
605 const thumbnail = createThumbnailElement(imageData);676 const thumbnail = createThumbnailElement(imageData);
606 container.append(thumbnail);677 container.append(thumbnail);
607 });678 });
608679
680 syncGroupSelectionUi();
609 activateLazyLoader();681 activateLazyLoader();
610}682}
611683
@@ -623,7 +695,9 @@ function renderChatBackgrounds(backgrounds) {
623695
624 const sortedList = sortBackgrounds(sourceList, true);696 const sortedList = sortBackgrounds(sourceList, true);
625 sortedList.forEach(bg => {697 sortedList.forEach(bg => {
626 const imageData = { filename: bg, isCustom: true };698 // For custom backgrounds, infer isAnimated from extension since we don't have server metadata
699 const isAnimated = isAnimatedBackgroundExtension(bg);
700 const imageData = { filename: bg, isCustom: true, isAnimated };
627 const thumbnail = createThumbnailElement(imageData);701 const thumbnail = createThumbnailElement(imageData);
628 container.append(thumbnail);702 container.append(thumbnail);
629 });703 });
@@ -632,8 +706,6 @@ function renderChatBackgrounds(backgrounds) {
632}706}
633707
634export async function getBackgrounds() {708export async function getBackgrounds() {
635 const metadataPromise = preloadImageMetadata();
636
637 const response = await fetch('/api/backgrounds/all', {709 const response = await fetch('/api/backgrounds/all', {
638 method: 'POST',710 method: 'POST',
639 headers: getRequestHeaders(),711 headers: getRequestHeaders(),
@@ -642,12 +714,21 @@ export async function getBackgrounds() {
642 if (response.ok) {714 if (response.ok) {
643 const { images, config } = await response.json();715 const { images, config } = await response.json();
644 Object.assign(THUMBNAIL_CONFIG, config);716 Object.assign(THUMBNAIL_CONFIG, config);
645
646 cachedSystemBackgrounds = images;717 cachedSystemBackgrounds = images;
718 const existingFiles = new Set(images.map(x => x.filename));
719 for (const selectedFile of selectedSystemBackgroundFiles) {
720 if (!existingFiles.has(selectedFile)) {
721 selectedSystemBackgroundFiles.delete(selectedFile);
722 }
723 }
724
725 // Load folders first so getFilteredImages() works correctly in folder view
726 await loadFolders();
647727
648 await metadataPromise;728 await preloadImageMetadata();
649729
650 renderSystemBackgrounds(images);730 // Render only filtered images if inside a folder, otherwise all
731 renderSystemBackgrounds(getFilteredImages());
651 highlightSelectedBackground();732 highlightSelectedBackground();
652 }733 }
653}734}
@@ -677,6 +758,602 @@ async function preloadImageMetadata() {
677 }758 }
678}759}
679760
761/**
762 * Loads folder data from the server (separate from image loading).
763 */
764async function loadFolders() {
765 try {
766 const response = await fetch('/api/backgrounds/folders', {
767 method: 'POST',
768 headers: getRequestHeaders(),
769 body: JSON.stringify({}),
770 });
771 if (response.ok) {
772 const data = await response.json();
773 folderList = data.folders || [];
774 imageFolderMap = data.imageFolderMap || {};
775
776 // Auto-assign thumbnail for folders that don't have one, then persist
777 const allImages = cachedSystemBackgrounds.map(img => img.filename);
778 /** @type {{id: string, thumbnailFile: string}[]} */
779 const thumbnailUpdates = [];
780 for (const folder of folderList) {
781 if (!folder.thumbnailFile) {
782 const firstImage = allImages.find(img => {
783 const fids = imageFolderMap[img];
784 return fids && fids.includes(folder.id);
785 });
786 if (firstImage) {
787 folder.thumbnailFile = firstImage;
788 thumbnailUpdates.push({ id: folder.id, thumbnailFile: firstImage });
789 }
790 }
791 }
792 if (thumbnailUpdates.length > 0) {
793 await fetch('/api/image-metadata/folders/set-thumbnails', {
794 method: 'POST',
795 headers: getRequestHeaders(),
796 body: JSON.stringify({ updates: thumbnailUpdates }),
797 }).catch(err => console.debug('Auto-thumbnail save failed:', err));
798 }
799
800 renderFolderGrid();
801 }
802 } catch (error) {
803 console.error('Error loading folders:', error);
804 }
805}
806
807/**
808 * Renders the folder grid inside #bg_folder_grid.
809 */
810function renderFolderGrid() {
811 const container = $('#bg_folder_grid');
812 container.empty();
813
814 if (folderList.length === 0 && !activeFolderId) {
815 return;
816 }
817
818 for (const folder of folderList) {
819 const tile = createFolderTileElement(folder);
820 container.append(tile);
821 }
822}
823
824/**
825 * Creates a single folder tile DOM element.
826 * @param {{id: string, name: string, thumbnailFile: string}} folder
827 * @returns {HTMLElement}
828 */
829function createFolderTileElement(folder) {
830 const tile = $('#bg_folder_tile_template .bg_folder_tile').clone();
831 tile.attr('data-folder-id', folder.id);
832 tile.find('.bg_folder_tile_name').text(folder.name);
833
834 // Set cover image (async, update when resolved)
835 getFolderCoverUrl(folder).then(coverUrl => {
836 if (coverUrl) {
837 tile.find('.bg_folder_tile_cover').css('background-image', `url("${coverUrl}")`);
838 }
839 });
840
841 return tile.get(0);
842}
843
844/**
845 * Gets the cover image URL for a folder.
846 * Uses thumbnailFile if set, otherwise falls back to the first image in the folder.
847 * @param {{id: string, name: string, thumbnailFile: string}} folder
848 * @returns {Promise<string|null>}
849 */
850async function getFolderCoverUrl(folder) {
851 const file = folder.thumbnailFile || cachedSystemBackgrounds.find(img => {
852 const fids = imageFolderMap[img.filename];
853 return fids && fids.includes(folder.id);
854 })?.filename;
855 if (!file) return null;
856
857 if (isAnimatedBackgroundExtension(file) && !background_settings.animation) {
858 return getThumbnailFromStorage(file, false);
859 }
860 return getThumbnailUrl('bg', file);
861}
862
863/**
864 * Gets images filtered by the active folder.
865 * @returns {Array<{filename: string, isAnimated: boolean}>}
866 */
867function getFilteredImages() {
868 if (!activeFolderId) return cachedSystemBackgrounds;
869 return cachedSystemBackgrounds.filter(img => {
870 const fids = imageFolderMap[img.filename];
871 return fids && fids.includes(activeFolderId);
872 });
873}
874
875/**
876 * Drills into a folder — hides folder grid, shows breadcrumb, filters images.
877 * @param {string} folderId
878 */
879function onFolderDrillIn(folderId) {
880 const folder = folderList.find(f => f.id === folderId);
881 if (!folder) return;
882
883 clearBackgroundGroupSelection();
884 activeFolderId = folderId;
885 $('#Backgrounds').addClass('in-folder-view');
886
887 // Hide folder grid, show breadcrumb
888 $('#bg_folder_grid').hide();
889 $('#bg_folder_breadcrumb').show();
890 $('#bg_current_folder_name').text(folder.name);
891
892 // Render only this folder's images
893 renderSystemBackgrounds(getFilteredImages());
894 highlightSelectedBackground();
895}
896
897/**
898 * Returns to the root folder overview.
899 */
900function onBackToFolders() {
901 clearBackgroundGroupSelection();
902 activeFolderId = null;
903 $('#Backgrounds').removeClass('in-folder-view');
904
905 // Show folder grid, hide breadcrumb
906 $('#bg_folder_grid').show();
907 $('#bg_folder_breadcrumb').hide();
908 $('#bg_current_folder_name').text('');
909
910 // Show all images
911 renderSystemBackgrounds(getFilteredImages());
912 highlightSelectedBackground();
913}
914
915/**
916 * Refreshes click-to-select and group action UI state.
917 */
918function syncGroupSelectionUi() {
919 const selectedCount = selectedSystemBackgroundFiles.size;
920 const isGlobalTab = getActiveBackgroundTab() === BG_SOURCES.GLOBAL;
921 const showAddButton = isGlobalTab && isBackgroundSelectionMode && selectedCount > 0;
922 const showRemoveFromCurrentFolderButton = isGlobalTab && Boolean(activeFolderId) && isBackgroundSelectionMode && selectedCount > 0;
923
924 $('#Backgrounds').toggleClass('bg-selection-mode', isBackgroundSelectionMode);
925 $('#bg_selection_mode_button').toggleClass('active', isBackgroundSelectionMode);
926 $('#bg_group_select_count').text(selectedCount > 0 ? ` (${selectedCount})` : '').toggle(selectedCount > 0);
927
928 $('#bg_group_add_to_folder_button').toggle(showAddButton);
929 $('#bg_folder_remove_selected_button').toggle(showRemoveFromCurrentFolderButton);
930
931 $('#bg_menu_content .bg_example').each(function () {
932 const bgFile = String($(this).attr('bgfile') || '');
933 $(this).toggleClass('folder-group-selected', selectedSystemBackgroundFiles.has(bgFile));
934 });
935}
936
937/**
938 * Enables/disables click-to-select mode for system backgrounds.
939 * @param {boolean} enabled
940 */
941function setBackgroundSelectionMode(enabled) {
942 isBackgroundSelectionMode = enabled;
943 if (!enabled) {
944 selectedSystemBackgroundFiles.clear();
945 }
946 // Clear any open mobile menus
947 $('#bg_menu_content .bg_example.mobile-menu-open').removeClass('mobile-menu-open');
948 syncGroupSelectionUi();
949}
950
951/**
952 * Toggles selected state of a system background for group folder actions.
953 * @param {string} bgFile
954 */
955function toggleBackgroundGroupSelection(bgFile) {
956 if (!bgFile) return;
957 if (selectedSystemBackgroundFiles.has(bgFile)) {
958 selectedSystemBackgroundFiles.delete(bgFile);
959 } else {
960 selectedSystemBackgroundFiles.add(bgFile);
961 }
962 syncGroupSelectionUi();
963}
964
965/**
966 * Clears all selected system backgrounds for group folder actions.
967 */
968function clearBackgroundGroupSelection() {
969 selectedSystemBackgroundFiles.clear();
970 syncGroupSelectionUi();
971}
972
973/**
974 * Updates selection/folder action control visibility for the active tab.
975 */
976function updateGroupFolderControlsVisibility() {
977 const isGlobalTab = getActiveBackgroundTab() === BG_SOURCES.GLOBAL;
978 $('#bg_selection_mode_button').toggle(isGlobalTab);
979
980 if (!isGlobalTab && isBackgroundSelectionMode) {
981 setBackgroundSelectionMode(false);
982 return;
983 }
984 syncGroupSelectionUi();
985}
986
987/**
988 * Shows a folder selection popup and returns the selected folder id.
989 * @param {string} headingText
990 * @returns {Promise<string[]|null>} Array of selected folder IDs, or null if cancelled
991 */
992async function selectFoldersForGroupAction(headingText) {
993 if (folderList.length === 0) {
994 toastr.info(t`Create a folder first`);
995 return null;
996 }
997
998 const contentEl = document.createElement('div');
999 const heading = document.createElement('h3');
1000 heading.textContent = headingText;
1001 contentEl.appendChild(heading);
1002
1003 for (const folder of folderList) {
1004 const label = document.createElement('label');
1005 label.className = 'checkbox_label flexGap5';
1006 label.style.margin = '4px 0';
1007
1008 const checkbox = document.createElement('input');
1009 checkbox.type = 'checkbox';
1010 checkbox.dataset.folderId = folder.id;
1011
1012 const span = document.createElement('span');
1013 span.textContent = folder.name;
1014
1015 label.appendChild(checkbox);
1016 label.appendChild(span);
1017 contentEl.appendChild(label);
1018 }
1019
1020 const content = $(contentEl);
1021 const result = await callGenericPopup(content, POPUP_TYPE.CONFIRM, '', {
1022 okButton: t`Apply`,
1023 cancelButton: t`Cancel`,
1024 allowVerticalScrolling: true,
1025 leftAlign: true,
1026 });
1027 if (!result) return null;
1028
1029 const selectedIds = [];
1030 content.find('input[type="checkbox"]:checked').each(function () {
1031 selectedIds.push($(this).data('folderId'));
1032 });
1033 return selectedIds.length > 0 ? selectedIds : null;
1034}
1035
1036/**
1037 * Sends a folder assign/unassign request and updates local imageFolderMap state.
1038 * @param {string[]} bgFiles - Background filenames to update
1039 * @param {string} folderId - Target folder ID
1040 * @param {boolean} isRemove - Whether to remove (unassign) or add (assign)
1041 */
1042async function updateFolderAssignments(bgFiles, folderId, isRemove) {
1043 const paths = bgFiles.map(getBackgroundRelativePath);
1044 const endpoint = isRemove ? '/api/image-metadata/folders/unassign' : '/api/image-metadata/folders/assign';
1045
1046 const response = await fetch(endpoint, {
1047 method: 'POST',
1048 headers: getRequestHeaders(),
1049 body: JSON.stringify({ id: folderId, paths }),
1050 });
1051
1052 if (!response.ok) {
1053 throw new Error(`Folder ${isRemove ? 'unassign' : 'assign'} failed: ${response.status}`);
1054 }
1055
1056 for (const bgFile of bgFiles) {
1057 const currentFolderIds = imageFolderMap[bgFile] || [];
1058 if (isRemove) {
1059 const nextFolderIds = currentFolderIds.filter(id => id !== folderId);
1060 if (nextFolderIds.length > 0) {
1061 imageFolderMap[bgFile] = nextFolderIds;
1062 } else {
1063 delete imageFolderMap[bgFile];
1064 }
1065 } else if (!currentFolderIds.includes(folderId)) {
1066 imageFolderMap[bgFile] = [...currentFolderIds, folderId];
1067 }
1068 }
1069}
1070
1071/**
1072 * Adds selected system backgrounds to a chosen folder.
1073 */
1074async function onAddSelectedToFolder() {
1075 if (getActiveBackgroundTab() !== BG_SOURCES.GLOBAL) {
1076 toastr.warning(t`Folder actions are only available in the Global tab`);
1077 return;
1078 }
1079
1080 const bgFiles = Array.from(selectedSystemBackgroundFiles);
1081 if (bgFiles.length === 0) {
1082 toastr.info(t`Select one or more backgrounds first`);
1083 return;
1084 }
1085
1086 const folderIds = await selectFoldersForGroupAction(t`Add selected backgrounds to folders`);
1087 if (!folderIds) return;
1088
1089 try {
1090 let totalAdded = 0;
1091 for (const folderId of folderIds) {
1092 const actionableBgFiles = bgFiles.filter(bgFile => {
1093 const currentFolderIds = imageFolderMap[bgFile] || [];
1094 return !currentFolderIds.includes(folderId);
1095 });
1096 if (actionableBgFiles.length > 0) {
1097 await updateFolderAssignments(actionableBgFiles, folderId, false);
1098 totalAdded += actionableBgFiles.length;
1099 }
1100 }
1101
1102 renderFolderGrid();
1103
1104 if (activeFolderId) {
1105 renderSystemBackgrounds(getFilteredImages());
1106 highlightSelectedBackground();
1107 }
1108
1109 setBackgroundSelectionMode(false);
1110 if (totalAdded > 0) {
1111 toastr.success(t`Added backgrounds to ${folderIds.length} folder(s)`);
1112 } else {
1113 toastr.info(t`Selected backgrounds are already in the chosen folders`);
1114 }
1115 } catch (error) {
1116 console.error('Error adding selected backgrounds to folder:', error);
1117 toastr.error(t`Failed to update folder assignment`);
1118 }
1119}
1120
1121/**
1122 * Removes selected system backgrounds from the currently drilled-in folder.
1123 */
1124async function onRemoveSelectedFromCurrentFolder() {
1125 if (getActiveBackgroundTab() !== BG_SOURCES.GLOBAL) {
1126 toastr.warning(t`Folder actions are only available in the Global tab`);
1127 return;
1128 }
1129
1130 if (!activeFolderId) {
1131 toastr.info(t`Open a folder first`);
1132 return;
1133 }
1134
1135 const bgFiles = Array.from(selectedSystemBackgroundFiles);
1136 if (bgFiles.length === 0) {
1137 toastr.info(t`Select one or more backgrounds first`);
1138 return;
1139 }
1140
1141 try {
1142 await updateFolderAssignments(bgFiles, activeFolderId, true);
1143 renderFolderGrid();
1144 renderSystemBackgrounds(getFilteredImages());
1145 highlightSelectedBackground();
1146 setBackgroundSelectionMode(false);
1147 toastr.success(t`Removed ${bgFiles.length} background(s) from folder`);
1148 } catch (error) {
1149 console.error('Error removing selected backgrounds from current folder:', error);
1150 toastr.error(t`Failed to update folder assignment`);
1151 }
1152}
1153
1154/**
1155 * Creates a new folder via API.
1156 */
1157async function onCreateFolder() {
1158 const currentTab = getActiveBackgroundTab();
1159 if (currentTab !== BG_SOURCES.GLOBAL) {
1160 toastr.warning(t`Folders can only be created in the Global tab`);
1161 return;
1162 }
1163
1164 const name = await Popup.show.input(t`Enter folder name:`);
1165 if (!name || !name.trim()) return;
1166
1167 try {
1168 const response = await fetch('/api/image-metadata/folders/create', {
1169 method: 'POST',
1170 headers: getRequestHeaders(),
1171 body: JSON.stringify({ name: name.trim() }),
1172 });
1173 if (response.ok) {
1174 const folder = await response.json();
1175 folderList.push(folder);
1176 renderFolderGrid();
1177 toastr.success(t`Folder created: ${folder.name}`);
1178 }
1179 } catch (error) {
1180 console.error('Error creating folder:', error);
1181 toastr.error(t`Failed to create folder`);
1182 }
1183}
1184
1185/**
1186 * Renames a folder via API.
1187 * @param {string} folderId
1188 */
1189async function onRenameFolder(folderId) {
1190 const folder = folderList.find(f => f.id === folderId);
1191 if (!folder) return;
1192
1193 const newName = await Popup.show.input(t`Enter new folder name:`, null, folder.name);
1194 if (!newName || !newName.trim() || newName.trim() === folder.name) return;
1195
1196 try {
1197 const response = await fetch('/api/image-metadata/folders/update', {
1198 method: 'POST',
1199 headers: getRequestHeaders(),
1200 body: JSON.stringify({ id: folderId, name: newName.trim() }),
1201 });
1202 if (response.ok) {
1203 folder.name = newName.trim();
1204 renderFolderGrid();
1205 toastr.success(t`Folder renamed`);
1206 }
1207 } catch (error) {
1208 console.error('Error renaming folder:', error);
1209 toastr.error(t`Failed to rename folder`);
1210 }
1211}
1212
1213/**
1214 * Deletes a folder via API.
1215 * @param {string} folderId
1216 */
1217async function onDeleteFolder(folderId) {
1218 const folder = folderList.find(f => f.id === folderId);
1219 if (!folder) return;
1220
1221 const confirm = await Popup.show.confirm(t`Delete folder "${folder.name}"?`, t`Images will not be deleted, only the folder grouping.`);
1222 if (!confirm) return;
1223
1224 try {
1225 const response = await fetch('/api/image-metadata/folders/delete', {
1226 method: 'POST',
1227 headers: getRequestHeaders(),
1228 body: JSON.stringify({ id: folderId }),
1229 });
1230 if (response.ok) {
1231 folderList = folderList.filter(f => f.id !== folderId);
1232 // Clean imageFolderMap
1233 for (const fids of Object.values(imageFolderMap)) {
1234 const idx = fids.indexOf(folderId);
1235 if (idx !== -1) fids.splice(idx, 1);
1236 }
1237 // If we were inside this folder, go back
1238 if (activeFolderId === folderId) {
1239 onBackToFolders();
1240 }
1241 renderFolderGrid();
1242 toastr.success(t`Folder deleted`);
1243 }
1244 } catch (error) {
1245 console.error('Error deleting folder:', error);
1246 toastr.error(t`Failed to delete folder`);
1247 }
1248}
1249
1250/**
1251 * Shows a folder assignment popup for an image.
1252 * @param {string} bgFile - The background filename
1253 */
1254async function onAssignToFolder(bgFile) {
1255 if (folderList.length === 0) {
1256 toastr.info(t`Create a folder first`);
1257 return;
1258 }
1259
1260 const currentFolderIds = imageFolderMap[bgFile] || [];
1261
1262 // Build checkbox inputs for Popup using DOM construction (avoids HTML injection)
1263 const contentEl = document.createElement('div');
1264 const heading = document.createElement('h3');
1265 heading.textContent = t`Assign to folders`;
1266 contentEl.appendChild(heading);
1267
1268 for (const f of folderList) {
1269 const label = document.createElement('label');
1270 label.className = 'checkbox_label flexGap5';
1271 label.style.margin = '4px 0';
1272
1273 const checkbox = document.createElement('input');
1274 checkbox.type = 'checkbox';
1275 checkbox.dataset.folderId = f.id;
1276 checkbox.checked = currentFolderIds.includes(f.id);
1277
1278 const span = document.createElement('span');
1279 span.textContent = f.name;
1280
1281 label.appendChild(checkbox);
1282 label.appendChild(span);
1283 contentEl.appendChild(label);
1284 }
1285
1286 const content = $(contentEl);
1287
1288 const result = await callGenericPopup(content, POPUP_TYPE.CONFIRM, '', { okButton: t`Save`, cancelButton: t`Cancel` });
1289 if (!result) return;
1290
1291 // Determine which folders were toggled on/off
1292 const toAssign = [];
1293 const toUnassign = [];
1294 content.find('input[type="checkbox"]').each(function () {
1295 const fid = $(this).data('folder-id');
1296 const isChecked = $(this).prop('checked');
1297 const wasChecked = currentFolderIds.includes(fid);
1298 if (isChecked && !wasChecked) toAssign.push(fid);
1299 if (!isChecked && wasChecked) toUnassign.push(fid);
1300 });
1301
1302 try {
1303 for (const fid of toAssign) {
1304 await updateFolderAssignments([bgFile], fid, false);
1305 }
1306 for (const fid of toUnassign) {
1307 await updateFolderAssignments([bgFile], fid, true);
1308 }
1309
1310 renderFolderGrid();
1311
1312 // Re-render filtered image list if currently inside a folder view
1313 if (activeFolderId) {
1314 renderSystemBackgrounds(getFilteredImages());
1315 highlightSelectedBackground();
1316 }
1317
1318 toastr.success(t`Folder assignment updated`);
1319 } catch (error) {
1320 console.error('Error assigning to folder:', error);
1321 toastr.error(t`Failed to update folder assignment`);
1322 }
1323}
1324
1325/**
1326 * Sets an image as the folder cover.
1327 * @param {string} bgFile - The background filename
1328 */
1329async function onSetFolderCover(bgFile) {
1330 if (!activeFolderId) return;
1331
1332 try {
1333 const response = await fetch('/api/image-metadata/folders/update', {
1334 method: 'POST',
1335 headers: getRequestHeaders(),
1336 body: JSON.stringify({ id: activeFolderId, thumbnailFile: bgFile }),
1337 });
1338 if (response.ok) {
1339 const folder = folderList.find(f => f.id === activeFolderId);
1340 if (folder) {
1341 folder.thumbnailFile = bgFile;
1342 // Update the DOM tile cover image
1343 const coverUrl = await getFolderCoverUrl(folder);
1344 if (coverUrl) {
1345 $(`.bg_folder_tile[data-folder-id="${folder.id}"] .bg_folder_tile_cover`)
1346 .css('background-image', `url('${coverUrl}')`);
1347 }
1348 }
1349 toastr.success(t`Folder cover updated`);
1350 }
1351 } catch (error) {
1352 console.error('Error setting folder cover:', error);
1353 toastr.error(t`Failed to set folder cover`);
1354 }
1355}
1356
680function activateLazyLoader() {1357function activateLazyLoader() {
681 // Disconnect previous observer to prevent memory leaks1358 // Disconnect previous observer to prevent memory leaks
682 if (lazyLoadObserver) {1359 if (lazyLoadObserver) {
@@ -701,7 +1378,8 @@ function activateLazyLoader() {
701 if (parentThumbnail) {1378 if (parentThumbnail) {
702 const bg = parentThumbnail.getAttribute('bgfile');1379 const bg = parentThumbnail.getAttribute('bgfile');
703 const isCustom = parentThumbnail.getAttribute('custom') === 'true';1380 const isCustom = parentThumbnail.getAttribute('custom') === 'true';
704 resolveImageUrl(bg, isCustom)1381 const isAnimated = parentThumbnail.getAttribute('animated') === 'true';
1382 resolveImageUrl(bg, isCustom, isAnimated)
705 .then(url => { clipper.style.backgroundImage = url; })1383 .then(url => { clipper.style.backgroundImage = url; })
706 .catch(() => { clipper.style.backgroundImage = PLACEHOLDER_IMAGE; });1384 .catch(() => { clipper.style.backgroundImage = PLACEHOLDER_IMAGE; });
707 }1385 }
@@ -730,16 +1408,26 @@ function generateUrlParameter(bg, isCustom) {
730 return isCustom ? `url("${encodeURI(bg)}")` : `url("${getBackgroundPath(bg)}")`;1408 return isCustom ? `url("${encodeURI(bg)}")` : `url("${getBackgroundPath(bg)}")`;
731}1409}
7321410
1411function isAnimatedBackgroundExtension(fileName) {
1412 const fileExtension = fileName.split('.').pop().toLowerCase();
1413 return ANIMATED_BACKGROUND_EXTENSIONS.includes(fileExtension);
1414}
1415
733/**1416/**
734 * Resolves the image URL for the background.1417 * Resolves the image URL for the background.
735 * @param {string} bg Background file name1418 * @param {string} bg Background file name
736 * @param {boolean} isCustom Is a custom background1419 * @param {boolean} isCustom Is a custom background
1420 * @param {boolean|null} [isAnimated=null] Is the background animated (from metadata). If null, infers from extension.
737 * @returns {Promise<string>} CSS URL of the background1421 * @returns {Promise<string>} CSS URL of the background
738 */1422 */
739async function resolveImageUrl(bg, isCustom) {1423async function resolveImageUrl(bg, isCustom, isAnimated = null) {
740 const fileExtension = bg.split('.').pop().toLowerCase();1424 // If isAnimated is not provided (null), fall back to extension-based heuristic
741 const isAnimated = ['mp4', 'webp'].includes(fileExtension);1425 let animated = isAnimated;
742 const thumbnailUrl = isAnimated && !background_settings.animation1426 if (animated === null) {
1427 animated = isAnimatedBackgroundExtension(bg);
1428 }
1429
1430 const thumbnailUrl = animated && !background_settings.animation
743 ? await getThumbnailFromStorage(bg, isCustom)1431 ? await getThumbnailFromStorage(bg, isCustom)
744 : isCustom1432 : isCustom
745 ? bg1433 ? bg
@@ -971,6 +1659,21 @@ function onBackgroundFilterInput() {
971 const hasMatch = title.toLowerCase().includes(filterValue);1659 const hasMatch = title.toLowerCase().includes(filterValue);
972 $bg.toggle(hasMatch);1660 $bg.toggle(hasMatch);
973 });1661 });
1662
1663 // Show/hide folder tiles based on whether folder name matches the filter
1664 if (!activeFolderId) {
1665 $('#bg_folder_grid .bg_folder_tile').each(function () {
1666 const $tile = $(this);
1667 const folderId = $tile.attr('data-folder-id');
1668 if (!folderId || !filterValue) {
1669 $tile.show();
1670 return;
1671 }
1672 const folder = folderList.find(f => f.id === folderId);
1673 const folderName = folder ? folder.name.toLowerCase() : '';
1674 $tile.toggle(folderName.includes(filterValue));
1675 });
1676 }
974}1677}
9751678
976const debouncedOnBackgroundFilterInput = debounce(onBackgroundFilterInput, debounce_timeout.standard);1679const debouncedOnBackgroundFilterInput = debounce(onBackgroundFilterInput, debounce_timeout.standard);
@@ -980,13 +1683,52 @@ const debouncedOnBackgroundFilterInput = debounce(onBackgroundFilterInput, debou
980 * @returns {BG_SOURCES} Active background tab source1683 * @returns {BG_SOURCES} Active background tab source
981 */1684 */
982export function getActiveBackgroundTab() {1685export function getActiveBackgroundTab() {
983 return $('#bg_tabs').tabs('option', 'active');1686 const tabs = $('#bg_tabs');
1687 if (!tabs.length || !tabs.data('ui-tabs')) {
1688 return BG_SOURCES.GLOBAL;
1689 }
1690 return tabs.tabs('option', 'active');
984}1691}
9851692
986export function initBackgrounds() {1693export function initBackgrounds() {
987 eventSource.on(event_types.CHAT_CHANGED, onChatChanged);1694 eventSource.on(event_types.CHAT_CHANGED, onChatChanged);
988 eventSource.on(event_types.FORCE_SET_BACKGROUND, forceSetBackground);1695 eventSource.on(event_types.FORCE_SET_BACKGROUND, forceSetBackground);
9891696
1697 // Folder event handlers
1698 $(document)
1699 .on('click', '.bg_folder_tile:not(.bg_new_folder_tile)', function (e) {
1700 if ($(e.target).closest('.jg-button').length) return; // let button handler run
1701 const folderId = $(this).attr('data-folder-id');
1702 if (folderId) onFolderDrillIn(folderId);
1703 })
1704 .on('click', '#bg_add_folder_button', function () {
1705 onCreateFolder();
1706 })
1707 .on('click', '#bg_back_to_folders', function () {
1708 onBackToFolders();
1709 })
1710 .on('click', '.bg_folder_tile [data-action="rename-folder"]', function (e) {
1711 e.stopPropagation();
1712 const folderId = $(this).closest('.bg_folder_tile').attr('data-folder-id');
1713 if (folderId) onRenameFolder(folderId);
1714 })
1715 .on('click', '.bg_folder_tile [data-action="delete-folder"]', function (e) {
1716 e.stopPropagation();
1717 const folderId = $(this).closest('.bg_folder_tile').attr('data-folder-id');
1718 if (folderId) onDeleteFolder(folderId);
1719 })
1720 .on('click', '.bg_folder_tile .mobile-only-menu-toggle', function (e) {
1721 e.stopPropagation();
1722 const $context = $(this).closest('.bg_folder_tile');
1723 const wasOpen = $context.hasClass('mobile-menu-open');
1724 // Close all other open menus before opening a new one.
1725 $('.bg_folder_tile.mobile-menu-open').removeClass('mobile-menu-open');
1726 $('.bg_example.mobile-menu-open').removeClass('mobile-menu-open');
1727 if (!wasOpen) {
1728 $context.addClass('mobile-menu-open');
1729 }
1730 });
1731
990 $(document)1732 $(document)
991 .off('click', '.bg_example').on('click', '.bg_example', onSelectBackgroundClick)1733 .off('click', '.bg_example').on('click', '.bg_example', onSelectBackgroundClick)
992 .off('click', '.bg_example .mobile-only-menu-toggle').on('click', '.bg_example .mobile-only-menu-toggle', function (e) {1734 .off('click', '.bg_example .mobile-only-menu-toggle').on('click', '.bg_example .mobile-only-menu-toggle', function (e) {
@@ -995,6 +1737,7 @@ export function initBackgrounds() {
995 const wasOpen = $context.hasClass('mobile-menu-open');1737 const wasOpen = $context.hasClass('mobile-menu-open');
996 // Close all other open menus before opening a new one.1738 // Close all other open menus before opening a new one.
997 $('.bg_example.mobile-menu-open').removeClass('mobile-menu-open');1739 $('.bg_example.mobile-menu-open').removeClass('mobile-menu-open');
1740 $('.bg_folder_tile.mobile-menu-open').removeClass('mobile-menu-open');
998 if (!wasOpen) {1741 if (!wasOpen) {
999 $context.addClass('mobile-menu-open');1742 $context.addClass('mobile-menu-open');
1000 }1743 }
@@ -1006,6 +1749,9 @@ export function initBackgrounds() {
1006 })1749 })
1007 .off('click', '.jg-button').on('click', '.jg-button', function (e) {1750 .off('click', '.jg-button').on('click', '.jg-button', function (e) {
1008 e.stopPropagation();1751 e.stopPropagation();
1752 if (isBackgroundSelectionMode && $(this).closest('#bg_menu_content').length) {
1753 return;
1754 }
1009 const action = $(this).data('action');1755 const action = $(this).data('action');
10101756
1011 switch (action) {1757 switch (action) {
@@ -1024,6 +1770,20 @@ export function initBackgrounds() {
1024 case 'copy':1770 case 'copy':
1025 onCopyToSystemBackgroundClick.call(this, e.originalEvent);1771 onCopyToSystemBackgroundClick.call(this, e.originalEvent);
1026 break;1772 break;
1773 case 'folder': {
1774 const bgEl = $(this).closest('.bg_example');
1775 if (bgEl.attr('custom') === 'true') break; // Only system backgrounds
1776 const bgFile = bgEl.attr('bgfile');
1777 if (bgFile) onAssignToFolder(bgFile);
1778 break;
1779 }
1780 case 'set-cover': {
1781 const bgEl = $(this).closest('.bg_example');
1782 if (bgEl.attr('custom') === 'true') break; // Only system backgrounds
1783 const bgFile = bgEl.attr('bgfile');
1784 if (bgFile) onSetFolderCover(bgFile);
1785 break;
1786 }
1027 }1787 }
1028 });1788 });
10291789
@@ -1034,13 +1794,16 @@ export function initBackgrounds() {
1034 applyThumbnailColumns(background_settings.thumbnailColumns + 1);1794 applyThumbnailColumns(background_settings.thumbnailColumns + 1);
1035 });1795 });
1036 $('#auto_background').on('click', autoBackgroundCommand);1796 $('#auto_background').on('click', autoBackgroundCommand);
1797 $('#bg_selection_mode_button').on('click', () => setBackgroundSelectionMode(!isBackgroundSelectionMode));
1798 $('#bg_group_add_to_folder_button').on('click', onAddSelectedToFolder);
1799 $('#bg_folder_remove_selected_button').on('click', onRemoveSelectedFromCurrentFolder);
1037 $('#add_bg_button').on('change', (e) => onBackgroundUploadSelected(e.originalEvent));1800 $('#add_bg_button').on('change', (e) => onBackgroundUploadSelected(e.originalEvent));
1038 $('#bg-filter').on('input', () => debouncedOnBackgroundFilterInput());1801 $('#bg-filter').on('input', () => debouncedOnBackgroundFilterInput());
1039 $('#bg-sort').on('change', function () {1802 $('#bg-sort').on('change', function () {
1040 background_settings.sortOrder = String($(this).val());1803 background_settings.sortOrder = String($(this).val());
1041 saveSettingsDebounced();1804 saveSettingsDebounced();
1042 // Re-render both galleries with new sort order1805 // Re-render both galleries with new sort order (respecting active folder filter)
1043 renderSystemBackgrounds(cachedSystemBackgrounds);1806 renderSystemBackgrounds(getFilteredImages());
1044 renderChatBackgrounds();1807 renderChatBackgrounds();
1045 highlightSelectedBackground();1808 highlightSelectedBackground();
1046 highlightLockedBackground();1809 highlightLockedBackground();
@@ -1096,4 +1859,7 @@ export function initBackgrounds() {
1096 });1859 });
10971860
1098 $('#bg_tabs').tabs();1861 $('#bg_tabs').tabs();
1862 $('#bg_tabs').on('tabsactivate', () => updateGroupFolderControlsVisibility());
1863 updateGroupFolderControlsVisibility();
1864 syncGroupSelectionUi();
1099}1865}
public/scripts/bookmarks.js+58 -16
@@ -2,6 +2,7 @@ import {
2 characters,2 characters,
3 saveChat,3 saveChat,
4 system_message_types,4 system_message_types,
5 syncSwipeToMes,
5 this_chid,6 this_chid,
6 openCharacterChat,7 openCharacterChat,
7 chat_metadata,8 chat_metadata,
@@ -25,7 +26,7 @@ import {
25 saveGroupBookmarkChat,26 saveGroupBookmarkChat,
26 selected_group,27 selected_group,
27} from './group-chats.js';28} from './group-chats.js';
28import { hideLoader, showLoader } from './loader.js';29import { loader } from './action-loader.js';
29import { getLastMessageId } from './macros.js';30import { getLastMessageId } from './macros.js';
30import { Popup } from './popup.js';31import { Popup } from './popup.js';
31import { SlashCommand } from './slash-commands/SlashCommand.js';32import { SlashCommand } from './slash-commands/SlashCommand.js';
@@ -34,6 +35,7 @@ import { commonEnumProviders } from './slash-commands/SlashCommandCommonEnumsPro
34import { SlashCommandParser } from './slash-commands/SlashCommandParser.js';35import { SlashCommandParser } from './slash-commands/SlashCommandParser.js';
35import { createTagMapFromList } from './tags.js';36import { createTagMapFromList } from './tags.js';
36import { renderTemplateAsync } from './templates.js';37import { renderTemplateAsync } from './templates.js';
38import { compressRequest } from './request-compression.js';
37import { t } from './i18n.js';39import { t } from './i18n.js';
3840
39import {41import {
@@ -111,12 +113,10 @@ function getMainChatName() {
111 if (chat_metadata) {113 if (chat_metadata) {
112 if (chat_metadata.main_chat) {114 if (chat_metadata.main_chat) {
113 return chat_metadata.main_chat;115 return chat_metadata.main_chat;
114 }116 } else if (selected_group) {
115 // groups didn't support bookmarks before chat metadata was introduced117 // groups didn't support bookmarks before chat metadata was introduced
116 else if (selected_group) {
117 return null;118 return null;
118 }119 } else if (characters[this_chid].chat && characters[this_chid].chat.includes(bookmarkNameToken)) {
119 else if (characters[this_chid].chat && characters[this_chid].chat.includes(bookmarkNameToken)) {
120 const tokenIndex = characters[this_chid].chat.lastIndexOf(bookmarkNameToken);120 const tokenIndex = characters[this_chid].chat.lastIndexOf(bookmarkNameToken);
121 chat_metadata.main_chat = characters[this_chid].chat.substring(0, tokenIndex).trim();121 chat_metadata.main_chat = characters[this_chid].chat.substring(0, tokenIndex).trim();
122 return chat_metadata.main_chat;122 return chat_metadata.main_chat;
@@ -146,8 +146,7 @@ export function showBookmarksButtons() {
146 $('#option_back_to_main').hide();146 $('#option_back_to_main').hide();
147 $('#option_new_bookmark').show();147 $('#option_new_bookmark').show();
148 }148 }
149 }149 } catch {
150 catch {
151 $('#option_back_to_main').hide();150 $('#option_back_to_main').hide();
152 $('#option_new_bookmark').hide();151 $('#option_new_bookmark').hide();
153 $('#option_convert_to_group').hide();152 $('#option_convert_to_group').hide();
@@ -163,8 +162,28 @@ async function saveBookmarkMenu() {
163 return await createNewBookmark(chat.length - 1);162 return await createNewBookmark(chat.length - 1);
164}163}
165164
165/**
166 * Builds the branch chat snapshot, optionally selecting a specific swipe for the target message.
167 * @param {number} mesId
168 * @param {{swipeId?: number|null}} [options={}]
169 * @returns {ChatMessage[]|null}
170 */
171function getBranchChatSnapshot(mesId, { swipeId = null } = {}) {
172 const snapshot = structuredClone(chat.slice(0, Number(mesId) + 1));
173
174 if (swipeId === null) {
175 return snapshot;
176 }
177
178 if (!syncSwipeToMes(null, swipeId, snapshot[mesId])) {
179 return null;
180 }
181
182 return snapshot;
183}
184
166// Export is used by Timelines extension. Do not remove.185// Export is used by Timelines extension. Do not remove.
167export async function createBranch(mesId) {186export async function createBranch(mesId, { swipeId = null } = {}) {
168 if (!chat.length) {187 if (!chat.length) {
169 toastr.warning('The chat is empty.', 'Branch creation failed');188 toastr.warning('The chat is empty.', 'Branch creation failed');
170 return;189 return;
@@ -178,6 +197,12 @@ export async function createBranch(mesId) {
178 const lastMes = chat[mesId];197 const lastMes = chat[mesId];
179 const mainChatName = (getCurrentChatDetails()).sessionName;198 const mainChatName = (getCurrentChatDetails()).sessionName;
180 const newMetadata = { main_chat: mainChatName };199 const newMetadata = { main_chat: mainChatName };
200 const selectedSwipeId = swipeId === null ? null : Number(swipeId);
201
202 if (selectedSwipeId !== null && (!Number.isInteger(selectedSwipeId) || selectedSwipeId < 0 || selectedSwipeId >= (lastMes?.swipes?.length ?? 0))) {
203 toastr.warning('Invalid swipe ID.', 'Branch creation failed');
204 return;
205 }
181206
182 function buildBranchName(name, i) {207 function buildBranchName(name, i) {
183 // Strip off existing suffixes, then build new name208 // Strip off existing suffixes, then build new name
@@ -194,10 +219,16 @@ export async function createBranch(mesId) {
194 return;219 return;
195 }220 }
196221
222 const branchChatSnapshot = getBranchChatSnapshot(mesId, { swipeId: selectedSwipeId });
223 if (!branchChatSnapshot) {
224 toastr.warning('Could not prepare the selected swipe for branching.', 'Branch creation failed');
225 return;
226 }
227
197 if (selected_group) {228 if (selected_group) {
198 await saveGroupBookmarkChat(selected_group, name, newMetadata, mesId);229 await saveGroupBookmarkChat(selected_group, name, newMetadata, mesId, branchChatSnapshot);
199 } else {230 } else {
200 await saveChat({ chatName: name, withMetadata: newMetadata, mesId });231 await saveChat({ chatName: name, withMetadata: newMetadata, mesId, chatData: branchChatSnapshot });
201 }232 }
202 // append to branches list if it exists233 // append to branches list if it exists
203 // otherwise create it234 // otherwise create it
@@ -389,11 +420,12 @@ export async function convertSoloToGroupChat() {
389 }420 }
390421
391 // Save group chat422 // Save group chat
392 const createChatResponse = await fetch('/api/chats/group/save', {423 const createChatRequest = await compressRequest({
393 method: 'POST',424 method: 'POST',
394 headers: getRequestHeaders(),425 headers: getRequestHeaders(),
395 body: JSON.stringify({ id: chatName, chat: [chatHeader, ...groupChat] }),426 body: JSON.stringify({ id: chatName, chat: [chatHeader, ...groupChat] }),
396 });427 });
428 const createChatResponse = await fetch('/api/chats/group/save', createChatRequest);
397429
398 if (!createChatResponse.ok) {430 if (!createChatResponse.ok) {
399 console.error('Group chat creation unsuccessful');431 console.error('Group chat creation unsuccessful');
@@ -411,15 +443,20 @@ export async function convertSoloToGroupChat() {
411/**443/**
412 * Creates a new branch from the message with the given ID444 * Creates a new branch from the message with the given ID
413 * @param {number} mesId Message ID445 * @param {number} mesId Message ID
446 * @param {{swipeId?: number|null}} [options={}] Branch options
414 * @returns {Promise<string?>} Branch file name447 * @returns {Promise<string?>} Branch file name
415 */448 */
416export async function branchChat(mesId) {449export async function branchChat(mesId, { swipeId = null } = {}) {
417 if (this_chid === undefined && !selected_group) {450 if (this_chid === undefined && !selected_group) {
418 toastr.info('No character selected.', 'Create Branch');451 toastr.info('No character selected.', 'Create Branch');
419 return null;452 return null;
420 }453 }
421454
422 const fileName = await createBranch(mesId);455 const fileName = await createBranch(mesId, { swipeId });
456 if (!fileName) {
457 return null;
458 }
459
423 await saveItemizedPrompts(fileName);460 await saveItemizedPrompts(fileName);
424461
425 if (selected_group) {462 if (selected_group) {
@@ -662,15 +699,20 @@ export function initBookmarks() {
662 return;699 return;
663 }700 }
664701
702 const loaderHandle = loader.show({
703 title: t`Chat History`,
704 message: t`Loading chat…`,
705 toastMode: loader.ToastMode.STATIC,
706 });
707
665 try {708 try {
666 showLoader();
667 if (selected_group) {709 if (selected_group) {
668 await openGroupChat(selected_group, fileName);710 await openGroupChat(selected_group, fileName);
669 } else {711 } else {
670 await openCharacterChat(fileName);712 await openCharacterChat(fileName);
671 }713 }
672 } finally {714 } finally {
673 await hideLoader();715 await loaderHandle.hide();
674 }716 }
675717
676 $('#shadow_select_chat_popup').css('display', 'none');718 $('#shadow_select_chat_popup').css('display', 'none');
public/scripts/bulk-edit.js+1 -1
@@ -55,7 +55,7 @@ function onSelectAllButtonClick() {
5555
56 if (!atLeastOneSelected) {56 if (!atLeastOneSelected) {
57 // If none was selected, trigger click on all to deselect all of them57 // If none was selected, trigger click on all to deselect all of them
58 for(const character of characters) {58 for (const character of characters) {
59 const checked = $(character).find('.bulk_select_checkbox:checked') ?? false;59 const checked = $(character).find('.bulk_select_checkbox:checked') ?? false;
60 if (checked && character instanceof HTMLElement) {60 if (checked && character instanceof HTMLElement) {
61 characterGroupOverlay.toggleSingleCharacter(character);61 characterGroupOverlay.toggleSingleCharacter(character);
public/scripts/cfg-scale.js+0 -1
@@ -150,7 +150,6 @@ function onCfgMenuItemClick() {
150 setTimeout(function () {150 setTimeout(function () {
151 $('#cfgConfig').hide();151 $('#cfgConfig').hide();
152 }, animation_duration);152 }, animation_duration);
153
154 }153 }
155 //duplicate options menu close handler from script.js154 //duplicate options menu close handler from script.js
156 //because this listener takes priority155 //because this listener takes priority
public/scripts/chat-backups.js+1 -1
@@ -199,7 +199,7 @@ class BackupsBrowser {
199 const deleteButton = document.createElement('div');199 const deleteButton = document.createElement('div');
200 deleteButton.classList.add('right_menu_button', 'fa-solid', 'fa-trash');200 deleteButton.classList.add('right_menu_button', 'fa-solid', 'fa-trash');
201 deleteButton.title = t`Delete backup`;201 deleteButton.title = t`Delete backup`;
202 deleteButton.addEventListener('click',async () => {202 deleteButton.addEventListener('click', async () => {
203 const isDeleted = await this.deleteBackup(backup.file_name);203 const isDeleted = await this.deleteBackup(backup.file_name);
204 if (isDeleted) {204 if (isDeleted) {
205 listItem.remove();205 listItem.remove();
public/scripts/chats.js+3 -6
@@ -832,11 +832,9 @@ async function openExternalMediaOverridesDialog() {
832832
833 if (power_user.external_media_allowed_overrides.includes(entityId)) {833 if (power_user.external_media_allowed_overrides.includes(entityId)) {
834 template.find('#forbid_media_override_allowed').prop('checked', true);834 template.find('#forbid_media_override_allowed').prop('checked', true);
835 }835 } else if (power_user.external_media_forbidden_overrides.includes(entityId)) {
836 else if (power_user.external_media_forbidden_overrides.includes(entityId)) {
837 template.find('#forbid_media_override_forbidden').prop('checked', true);836 template.find('#forbid_media_override_forbidden').prop('checked', true);
838 }837 } else {
839 else {
840 template.find('#forbid_media_override_global').prop('checked', true);838 template.find('#forbid_media_override_global').prop('checked', true);
841 }839 }
842840
@@ -1678,8 +1676,7 @@ async function runScraper(scraperId, target, callback) {
16781676
1679 toastr.success(t`Scraped ${files.length} files from ${scraperId} to ${target}.`, t`Data Bank`);1677 toastr.success(t`Scraped ${files.length} files from ${scraperId} to ${target}.`, t`Data Bank`);
1680 callback();1678 callback();
1681 }1679 } catch (error) {
1682 catch (error) {
1683 console.error('Scraping failed', error);1680 console.error('Scraping failed', error);
1684 toastr.error(t`Check browser console for details.`, t`Scraping failed`);1681 toastr.error(t`Check browser console for details.`, t`Scraping failed`);
1685 }1682 }
public/scripts/constants.js+2 -0
@@ -175,6 +175,8 @@ export const SWIPE_SOURCE = {
175 KEYBOARD: 'keyboard',175 KEYBOARD: 'keyboard',
176 BACK: 'back',176 BACK: 'back',
177 AUTO_SWIPE: 'auto_swipe',177 AUTO_SWIPE: 'auto_swipe',
178 SLASH_COMMAND: 'slash_command',
179 SWIPE_PICKER: 'swipe_picker',
178};180};
179181
180/**182/**
public/scripts/custom-request.js+1 -1
@@ -591,7 +591,7 @@ export class ChatCompletionService {
591 }591 }
592592
593 // Ensure api-url is properly applied for all sources that accept it593 // Ensure api-url is properly applied for all sources that accept it
594 ['custom_url', 'vertexai_region', 'zai_endpoint'].forEach(field => {594 ['custom_url', 'vertexai_region', 'zai_endpoint', 'siliconflow_endpoint'].forEach(field => {
595 // The order is: connection profile => CC preset => CC settings595 // The order is: connection profile => CC preset => CC settings
596 overridePayload[field] = overridePayload[field] || settings[field] || oai_settings[field];596 overridePayload[field] = overridePayload[field] || settings[field] || oai_settings[field];
597 });597 });
public/scripts/data-maid.js+0 -1
@@ -251,7 +251,6 @@ class DataMaidDialog {
251 categoryElement.remove();251 categoryElement.remove();
252 this.displayEmptyPlaceholder();252 this.displayEmptyPlaceholder();
253 });253 });
254
255 });254 });
256 categoryElement.querySelectorAll('.dataMaidItemDelete').forEach(button => {255 categoryElement.querySelectorAll('.dataMaidItemDelete').forEach(button => {
257 button.addEventListener('click', async () => {256 button.addEventListener('click', async () => {
public/scripts/dynamic-styles.js+1 -2
@@ -73,8 +73,7 @@ function applyDynamicFocusStyles(styleSheet, { fromExtension = false } = {}) {
73 const isHover = selector.includes(':hover'), isFocus = selector.includes(':focus');73 const isHover = selector.includes(':hover'), isFocus = selector.includes(':focus');
74 if (isHover && isFocus) {74 if (isHover && isFocus) {
75 // We currently do nothing here. Rules containing both hover and focus are very specific and should never be automatically touched75 // We currently do nothing here. Rules containing both hover and focus are very specific and should never be automatically touched
76 }76 } else if (isHover) {
77 else if (isHover) {
78 const baseSelector = selector.replace(/:hover/g, PLACEHOLDER).trim();77 const baseSelector = selector.replace(/:hover/g, PLACEHOLDER).trim();
79 hoverRules.push({ baseSelector, rule, wrappers: [...wrappers] });78 hoverRules.push({ baseSelector, rule, wrappers: [...wrappers] });
80 } else if (isFocus) {79 } else if (isFocus) {
public/scripts/events.js+4 -0
@@ -96,6 +96,10 @@ export const event_types = {
96 WORLDINFO_ENTRIES_LOADED: 'worldinfo_entries_loaded',96 WORLDINFO_ENTRIES_LOADED: 'worldinfo_entries_loaded',
97 WORLDINFO_SCAN_DONE: 'worldinfo_scan_done',97 WORLDINFO_SCAN_DONE: 'worldinfo_scan_done',
98 MEDIA_ATTACHMENT_DELETED: 'media_attachment_deleted',98 MEDIA_ATTACHMENT_DELETED: 'media_attachment_deleted',
99 PERSONA_CHANGED: 'persona_changed',
100 TTS_JOB_STARTED: 'tts_job_started',
101 TTS_AUDIO_READY: 'tts_audio_ready',
102 TTS_JOB_COMPLETE: 'tts_job_complete',
99};103};
100104
101export const eventSource = new EventEmitter([event_types.APP_READY, event_types.APP_INITIALIZED]);105export const eventSource = new EventEmitter([event_types.APP_READY, event_types.APP_INITIALIZED]);
public/scripts/extensions.js+89 -10
@@ -1,7 +1,6 @@
1import { DOMPurify, Popper } from '../lib.js';1import { DOMPurify, Popper } from '../lib.js';
22
3import { eventSource, event_types, saveSettings, saveSettingsDebounced, getRequestHeaders, animation_duration, CLIENT_VERSION } from '../script.js';3import { eventSource, event_types, saveSettings, saveSettingsDebounced, getRequestHeaders, animation_duration, CLIENT_VERSION } from '../script.js';
4import { showLoader } from './loader.js';
5import { POPUP_RESULT, POPUP_TYPE, Popup, callGenericPopup } from './popup.js';4import { POPUP_RESULT, POPUP_TYPE, Popup, callGenericPopup } from './popup.js';
6import { renderTemplate, renderTemplateAsync } from './templates.js';5import { renderTemplate, renderTemplateAsync } from './templates.js';
7import { delay, equalsIgnoreCaseAndAccents, isSubsetOf, sanitizeSelector, setValueByPath, versionCompare } from './utils.js';6import { delay, equalsIgnoreCaseAndAccents, isSubsetOf, sanitizeSelector, setValueByPath, versionCompare } from './utils.js';
@@ -278,12 +277,10 @@ async function discoverExtensions() {
278 if (response.ok) {277 if (response.ok) {
279 const extensions = await response.json();278 const extensions = await response.json();
280 return extensions;279 return extensions;
281 }280 } else {
282 else {
283 return [];281 return [];
284 }282 }
285 }283 } catch (err) {
286 catch (err) {
287 console.error(err);284 console.error(err);
288 return [];285 return [];
289 }286 }
@@ -358,11 +355,82 @@ function onToggleAllExtensions(extensionsToToggle, toggleContainer) {
358}355}
359356
360/**357/**
358 * Calls a manifest hook for an extension.
359 * Hooks are optional function names exported from the extension's JS entry point module.
360 * The hook function can optionally return a Promise that will be awaited.
361 * @param {string} name Extension name
362 * @param {'install' | 'update' | 'delete' | 'enable' | 'disable' | 'activate'} hookName The hook to call
363 * @returns {Promise<void>}
364 */
365async function callExtensionHook(name, hookName) {
366 const manifest = manifests[name];
367
368 if (!manifest) {
369 console.debug(`callExtensionHook: Extension "${name}" has no manifest, skipping hook "${hookName}"`);
370 return;
371 }
372
373 if (!manifest.hooks || typeof manifest.hooks !== 'object') {
374 return;
375 }
376
377 if (!Object.hasOwn(manifest.hooks, hookName)) {
378 return;
379 }
380
381 const hookFunctionName = manifest.hooks[hookName];
382
383 if (typeof hookFunctionName !== 'string' || !hookFunctionName) {
384 console.warn(`callExtensionHook: Extension "${name}" hook "${hookName}" is not a valid string`);
385 return;
386 }
387
388 if (!manifest.js) {
389 console.warn(`callExtensionHook: Extension "${name}" has hook "${hookName}" but no JS entry point defined in manifest`);
390 return;
391 }
392
393 const url = `/scripts/extensions/${name}/${manifest.js}`;
394 console.debug(`callExtensionHook: Calling hook "${hookName}" (function "${hookFunctionName}") for extension "${name}"`);
395
396 try {
397 const module = await import(url);
398
399 if (typeof module[hookFunctionName] !== 'function') {
400 console.warn(`callExtensionHook: Extension "${name}" hook "${hookName}" references "${hookFunctionName}" which is not an exported function`);
401 return;
402 }
403
404 const hookCallResult = module[hookFunctionName]();
405
406 const HOOK_TIMEOUT = 5000;
407 const HOOK_RESULT = {
408 OK: 'ok',
409 TIMEOUT: 'timeout',
410 };
411
412 const result = await Promise.race([
413 (hookCallResult instanceof Promise ? hookCallResult : Promise.resolve(hookCallResult)).then(() => HOOK_RESULT.OK),
414 delay(HOOK_TIMEOUT).then(() => HOOK_RESULT.TIMEOUT),
415 ]);
416
417 if (result === HOOK_RESULT.TIMEOUT) {
418 console.warn(`callExtensionHook: Hook "${hookName}" for extension "${name}" timed out after ${HOOK_TIMEOUT}ms`);
419 } else {
420 console.debug(`callExtensionHook: Hook "${hookName}" completed for extension "${name}"`);
421 }
422 } catch (error) {
423 console.error(`callExtensionHook: Error calling hook "${hookName}" for extension "${name}":`, error);
424 }
425}
426
427/**
361 * Enables an extension by name.428 * Enables an extension by name.
362 * @param {string} name Extension name429 * @param {string} name Extension name
363 * @param {boolean} [reload=true] If true, reload the page after enabling the extension430 * @param {boolean} [reload=true] If true, reload the page after enabling the extension
364 */431 */
365export async function enableExtension(name, reload = true) {432export async function enableExtension(name, reload = true) {
433 await callExtensionHook(name, 'enable');
366 extension_settings.disabledExtensions = extension_settings.disabledExtensions.filter(x => x !== name);434 extension_settings.disabledExtensions = extension_settings.disabledExtensions.filter(x => x !== name);
367 stateChanged = true;435 stateChanged = true;
368 await saveSettings();436 await saveSettings();
@@ -379,6 +447,7 @@ export async function enableExtension(name, reload = true) {
379 * @param {boolean} [reload=true] If true, reload the page after disabling the extension447 * @param {boolean} [reload=true] If true, reload the page after disabling the extension
380 */448 */
381export async function disableExtension(name, reload = true) {449export async function disableExtension(name, reload = true) {
450 await callExtensionHook(name, 'disable');
382 extension_settings.disabledExtensions.push(name);451 extension_settings.disabledExtensions.push(name);
383 stateChanged = true;452 stateChanged = true;
384 await saveSettings();453 await saveSettings();
@@ -507,7 +576,10 @@ async function activateExtensions() {
507 Promise.all([addExtensionScript(name, manifest), addExtensionStyle(name, manifest)]),576 Promise.all([addExtensionScript(name, manifest), addExtensionStyle(name, manifest)]),
508 );577 );
509 await promise578 await promise
510 .then(() => activeExtensions.add(name))579 .then(() => {
580 activeExtensions.add(name);
581 return callExtensionHook(name, 'activate');
582 })
511 .catch(err => {583 .catch(err => {
512 console.log('Could not activate extension', name, err);584 console.log('Could not activate extension', name, err);
513 extensionLoadErrors.add(t`Extension "${displayName}" failed to load: ${err}`);585 extensionLoadErrors.add(t`Extension "${displayName}" failed to load: ${err}`);
@@ -627,8 +699,7 @@ async function connectToApi(baseUrl) {
627 }699 }
628700
629 updateStatus(getExtensionsResult.ok);701 updateStatus(getExtensionsResult.ok);
630 }702 } catch {
631 catch {
632 updateStatus(false);703 updateStatus(false);
633 }704 }
634}705}
@@ -1086,7 +1157,6 @@ async function showExtensionsDetails() {
1086 abortController.abort();1157 abortController.abort();
1087 }1158 }
1088 if (requiresReload) {1159 if (requiresReload) {
1089 showLoader();
1090 location.reload();1160 location.reload();
1091 }1161 }
1092}1162}
@@ -1150,6 +1220,8 @@ async function updateExtension(extensionName, quiet, timeout = null) {
1150 toastr.success('Extension is already up to date');1220 toastr.success('Extension is already up to date');
1151 }1221 }
1152 } else {1222 } else {
1223 const fullExtensionName = extensionName.startsWith('third-party') ? extensionName : `third-party${extensionName}`;
1224 await callExtensionHook(fullExtensionName, 'update');
1153 toastr.success(t`Extension ${extensionName} updated to ${data.shortCommitHash}`, t`Reload the page to apply updates`);1225 toastr.success(t`Extension ${extensionName} updated to ${data.shortCommitHash}`, t`Reload the page to apply updates`);
1154 }1226 }
1155 } catch (error) {1227 } catch (error) {
@@ -1283,6 +1355,8 @@ async function moveExtension(extensionName, source, destination) {
1283 * @param {string} extensionName Extension name to delete1355 * @param {string} extensionName Extension name to delete
1284 */1356 */
1285export async function deleteExtension(extensionName) {1357export async function deleteExtension(extensionName) {
1358 await callExtensionHook(extensionName, 'delete');
1359
1286 try {1360 try {
1287 await fetch('/api/extensions/delete', {1361 await fetch('/api/extensions/delete', {
1288 method: 'POST',1362 method: 'POST',
@@ -1390,7 +1464,7 @@ async function switchExtensionBranch(extensionName, isGlobal, branch) {
1390 return;1464 return;
1391 }1465 }
13921466
1393 toastr.success(t`Extension ${extensionName} switched to ${branch}`);1467 toastr.success(t`Extension ${extensionName} switched to ${branch}`, t`Reload the page to apply updates`);
1394 await loadExtensionSettings({}, false, false);1468 await loadExtensionSettings({}, false, false);
1395 void showExtensionsDetails();1469 void showExtensionsDetails();
1396 } catch (error) {1470 } catch (error) {
@@ -1431,6 +1505,11 @@ export async function installExtension(url, global, branch = '') {
1431 console.debug(`Extension "${response.display_name}" has been installed successfully at ${response.extensionPath}`);1505 console.debug(`Extension "${response.display_name}" has been installed successfully at ${response.extensionPath}`);
1432 await loadExtensionSettings({}, false, false);1506 await loadExtensionSettings({}, false, false);
1433 await eventSource.emit(event_types.EXTENSION_SETTINGS_LOADED, response);1507 await eventSource.emit(event_types.EXTENSION_SETTINGS_LOADED, response);
1508
1509 if (response.folderName) {
1510 const extensionName = `third-party/${response.folderName}`;
1511 await callExtensionHook(extensionName, 'install');
1512 }
1434}1513}
14351514
1436/**1515/**
public/scripts/extensions/assets/index.js+7 -14
@@ -82,8 +82,7 @@ function getAuthorFromUrl(url) {
82 result.name = pathSegments[0];82 result.name = pathSegments[0];
83 result.url = `${parsedUrl.protocol}//${parsedUrl.hostname}/${result.name}`;83 result.url = `${parsedUrl.protocol}//${parsedUrl.hostname}/${result.name}`;
84 }84 }
85 }85 } catch (error) {
86 catch (error) {
87 console.debug(DEBUG_PREFIX, 'Error parsing URL:', error);86 console.debug(DEBUG_PREFIX, 'Error parsing URL:', error);
88 }87 }
8988
@@ -94,8 +93,7 @@ async function downloadAssetsList(url) {
94 updateCurrentAssets().then(async function () {93 updateCurrentAssets().then(async function () {
95 fetch(url, { cache: 'no-cache' })94 fetch(url, { cache: 'no-cache' })
96 .then(response => response.json())95 .then(response => response.json())
97 .then(async function(json) {96 .then(async function (json) {
98
99 availableAssets = {};97 availableAssets = {};
100 $('#assets_menu').empty();98 $('#assets_menu').empty();
10199
@@ -200,8 +198,7 @@ async function downloadAssetsList(url) {
200 label.removeClass('fa-trash');198 label.removeClass('fa-trash');
201 label.removeClass('redOverlayGlow');199 label.removeClass('redOverlayGlow');
202 });200 });
203 }201 } else {
204 else {
205 console.debug(DEBUG_PREFIX, 'not installed, unchecked');202 console.debug(DEBUG_PREFIX, 'not installed, unchecked');
206 element.prop('checked', false);203 element.prop('checked', false);
207 element.on('click', assetInstall);204 element.on('click', assetInstall);
@@ -347,8 +344,7 @@ async function installAsset(url, assetType, filename) {
347 console.debug(DEBUG_PREFIX, 'Character downloaded.');344 console.debug(DEBUG_PREFIX, 'Character downloaded.');
348 }345 }
349 }346 }
350 }347 } catch (err) {
351 catch (err) {
352 console.log(err);348 console.log(err);
353 return [];349 return [];
354 }350 }
@@ -374,8 +370,7 @@ async function deleteAsset(assetType, filename) {
374 if (result.ok) {370 if (result.ok) {
375 console.debug(DEBUG_PREFIX, 'Deletion success.');371 console.debug(DEBUG_PREFIX, 'Deletion success.');
376 }372 }
377 }373 } catch (err) {
378 catch (err) {
379 console.log(err);374 console.log(err);
380 return [];375 return [];
381 }376 }
@@ -428,8 +423,7 @@ async function updateCurrentAssets() {
428 headers: getRequestHeaders({ omitContentType: true }),423 headers: getRequestHeaders({ omitContentType: true }),
429 });424 });
430 currentAssets = result.ok ? (await result.json()) : {};425 currentAssets = result.ok ? (await result.json()) : {};
431 }426 } catch (err) {
432 catch (err) {
433 console.log(err);427 console.log(err);
434 }428 }
435 console.debug(DEBUG_PREFIX, 'Current assets found:', currentAssets);429 console.debug(DEBUG_PREFIX, 'Current assets found:', currentAssets);
@@ -491,8 +485,7 @@ jQuery(async () => {
491 connectButton.addClass('fa-plug-circle-exclamation');485 connectButton.addClass('fa-plug-circle-exclamation');
492 connectButton.removeClass('redOverlayGlow');486 connectButton.removeClass('redOverlayGlow');
493 }487 }
494 }488 } else {
495 else {
496 console.debug(DEBUG_PREFIX, 'Connection refused by user');489 console.debug(DEBUG_PREFIX, 'Connection refused by user');
497 }490 }
498 });491 });
public/scripts/extensions/attachments/manage-button.html+1 -1
@@ -1,6 +1,6 @@
11
22
3<div id="manageAttachments" class="list-group-item flex-container flexGap5" title="View global, character, or data files.">3<div id="manageAttachments" class="list-group-item flex-container flexGap5" title="View global, character, or data files." data-i18n="[title]View global, character, or data files.">
4 <div class="fa-fw fa-solid fa-book-open-reader extensionsMenuExtensionButton"></div>4 <div class="fa-fw fa-solid fa-book-open-reader extensionsMenuExtensionButton"></div>
5 <span data-i18n="Open Data Bank">Open Data Bank</span>5 <span data-i18n="Open Data Bank">Open Data Bank</span>
6</div>6</div>
public/scripts/extensions/caption/index.js+4 -8
@@ -68,8 +68,7 @@ async function setImageIcon() {
68 const sendButton = $('#send_picture .extensionsMenuExtensionButton');68 const sendButton = $('#send_picture .extensionsMenuExtensionButton');
69 sendButton.addClass('fa-image');69 sendButton.addClass('fa-image');
70 sendButton.removeClass('fa-hourglass-half');70 sendButton.removeClass('fa-hourglass-half');
71 }71 } catch (error) {
72 catch (error) {
73 console.log(error);72 console.log(error);
74 }73 }
75}74}
@@ -82,8 +81,7 @@ async function setSpinnerIcon() {
82 const sendButton = $('#send_picture .extensionsMenuExtensionButton');81 const sendButton = $('#send_picture .extensionsMenuExtensionButton');
83 sendButton.removeClass('fa-image');82 sendButton.removeClass('fa-image');
84 sendButton.addClass('fa-hourglass-half');83 sendButton.addClass('fa-hourglass-half');
85 }84 } catch (error) {
86 catch (error) {
87 console.log(error);85 console.log(error);
88 }86 }
89}87}
@@ -376,14 +374,12 @@ async function getCaptionForFile(file, prompt, quiet) {
376 await sendCaptionedMessage(caption, imagePath, file.type);374 await sendCaptionedMessage(caption, imagePath, file.type);
377 }375 }
378 return caption;376 return caption;
379 }377 } catch (error) {
380 catch (error) {
381 const errorMessage = error.message || 'Unknown error';378 const errorMessage = error.message || 'Unknown error';
382 toastr.error(errorMessage, 'Failed to caption');379 toastr.error(errorMessage, 'Failed to caption');
383 console.error(error);380 console.error(error);
384 return '';381 return '';
385 }382 } finally {
386 finally {
387 setImageIcon();383 setImageIcon();
388 }384 }
389}385}
public/scripts/extensions/caption/settings.html+9 -0
@@ -53,6 +53,8 @@
53 <option data-type="cohere" value="c4ai-aya-vision-8b">c4ai-aya-vision-8b</option>53 <option data-type="cohere" value="c4ai-aya-vision-8b">c4ai-aya-vision-8b</option>
54 <option data-type="cohere" value="c4ai-aya-vision-32b">c4ai-aya-vision-32b</option>54 <option data-type="cohere" value="c4ai-aya-vision-32b">c4ai-aya-vision-32b</option>
55 <option data-type="cohere" value="command-a-vision-07-2025">command-a-vision-07-2025</option>55 <option data-type="cohere" value="command-a-vision-07-2025">command-a-vision-07-2025</option>
56 <option data-type="openai" value="gpt-5.4">gpt-5.4</option>
57 <option data-type="openai" value="gpt-5.3-chat-latest">gpt-5.3-chat-latest</option>
56 <option data-type="openai" value="gpt-5.2">gpt-5.2</option>58 <option data-type="openai" value="gpt-5.2">gpt-5.2</option>
57 <option data-type="openai" value="gpt-5.2-2025-12-11">gpt-5.2-2025-12-11</option>59 <option data-type="openai" value="gpt-5.2-2025-12-11">gpt-5.2-2025-12-11</option>
58 <option data-type="openai" value="gpt-5.2-chat-latest">gpt-5.2-chat-latest</option>60 <option data-type="openai" value="gpt-5.2-chat-latest">gpt-5.2-chat-latest</option>
@@ -89,6 +91,7 @@
89 <option data-type="anthropic" value="claude-opus-4-6">claude-opus-4-6</option>91 <option data-type="anthropic" value="claude-opus-4-6">claude-opus-4-6</option>
90 <option data-type="anthropic" value="claude-opus-4-5">claude-opus-4-5</option>92 <option data-type="anthropic" value="claude-opus-4-5">claude-opus-4-5</option>
91 <option data-type="anthropic" value="claude-opus-4-5-20251101">claude-opus-4-5-20251101</option>93 <option data-type="anthropic" value="claude-opus-4-5-20251101">claude-opus-4-5-20251101</option>
94 <option data-type="anthropic" value="claude-sonnet-4-6">claude-sonnet-4-6</option>
92 <option data-type="anthropic" value="claude-sonnet-4-5">claude-sonnet-4-5</option>95 <option data-type="anthropic" value="claude-sonnet-4-5">claude-sonnet-4-5</option>
93 <option data-type="anthropic" value="claude-sonnet-4-5-20250929">claude-sonnet-4-5-20250929</option>96 <option data-type="anthropic" value="claude-sonnet-4-5-20250929">claude-sonnet-4-5-20250929</option>
94 <option data-type="anthropic" value="claude-haiku-4-5">claude-haiku-4-5</option>97 <option data-type="anthropic" value="claude-haiku-4-5">claude-haiku-4-5</option>
@@ -108,6 +111,9 @@
108 <option data-type="anthropic" value="claude-3-5-haiku-20241022">claude-3-5-haiku-20241022</option>111 <option data-type="anthropic" value="claude-3-5-haiku-20241022">claude-3-5-haiku-20241022</option>
109 <option data-type="anthropic" value="claude-3-opus-20240229">claude-3-opus-20240229</option>112 <option data-type="anthropic" value="claude-3-opus-20240229">claude-3-opus-20240229</option>
110 <option data-type="anthropic" value="claude-3-haiku-20240307">claude-3-haiku-20240307</option>113 <option data-type="anthropic" value="claude-3-haiku-20240307">claude-3-haiku-20240307</option>
114 <option data-type="google" value="gemini-3.1-pro-preview">gemini-3.1-pro-preview</option>
115 <option data-type="google" value="gemini-3.1-flash-lite-preview">gemini-3.1-flash-lite-preview</option>
116 <option data-type="google" value="gemini-3.1-flash-image-preview">gemini-3.1-flash-image-preview</option>
111 <option data-type="google" value="gemini-3-pro-preview">gemini-3-pro-preview</option>117 <option data-type="google" value="gemini-3-pro-preview">gemini-3-pro-preview</option>
112 <option data-type="google" value="gemini-3-pro-image-preview">gemini-3-pro-image-preview</option>118 <option data-type="google" value="gemini-3-pro-image-preview">gemini-3-pro-image-preview</option>
113 <option data-type="google" value="gemini-3-flash-preview">gemini-3-flash-preview</option>119 <option data-type="google" value="gemini-3-flash-preview">gemini-3-flash-preview</option>
@@ -138,6 +144,9 @@
138 <option data-type="google" value="gemini-2.0-flash-lite-preview">gemini-2.0-flash-lite-preview</option>144 <option data-type="google" value="gemini-2.0-flash-lite-preview">gemini-2.0-flash-lite-preview</option>
139 <option data-type="google" value="learnlm-2.0-flash-experimental">learnlm-2.0-flash-experimental</option>145 <option data-type="google" value="learnlm-2.0-flash-experimental">learnlm-2.0-flash-experimental</option>
140 <option data-type="google" value="gemini-robotics-er-1.5-preview">gemini-robotics-er-1.5-preview</option>146 <option data-type="google" value="gemini-robotics-er-1.5-preview">gemini-robotics-er-1.5-preview</option>
147 <option data-type="vertexai" value="gemini-3.1-pro-preview">gemini-3.1-pro-preview</option>
148 <option data-type="vertexai" value="gemini-3.1-flash-lite-preview">gemini-3.1-flash-lite-preview</option>
149 <option data-type="vertexai" value="gemini-3.1-flash-image-preview">gemini-3.1-flash-image-preview</option>
141 <option data-type="vertexai" value="gemini-3-pro-preview">gemini-3-pro-preview</option>150 <option data-type="vertexai" value="gemini-3-pro-preview">gemini-3-pro-preview</option>
142 <option data-type="vertexai" value="gemini-3-pro-image-preview">gemini-3-pro-image-preview</option>151 <option data-type="vertexai" value="gemini-3-pro-image-preview">gemini-3-pro-image-preview</option>
143 <option data-type="vertexai" value="gemini-3-flash-preview">gemini-3-flash-preview</option>152 <option data-type="vertexai" value="gemini-3-flash-preview">gemini-3-flash-preview</option>
public/scripts/extensions/expressions/index.js+16 -29
@@ -363,8 +363,7 @@ export async function visualNovelUpdateLayers(container) {
363 if (power_user.reduced_motion) {363 if (power_user.reduced_motion) {
364 element.css('left', currentPosition + 'px');364 element.css('left', currentPosition + 'px');
365 requestAnimationFrame(() => resolve());365 requestAnimationFrame(() => resolve());
366 }366 } else {
367 else {
368 element.animate({ left: currentPosition + 'px' }, 500, () => {367 element.animate({ left: currentPosition + 'px' }, 500, () => {
369 resolve();368 resolve();
370 });369 });
@@ -525,8 +524,7 @@ async function moduleWorker({ newChat = false } = {}) {
525 }524 }
526525
527 return;526 return;
528 }527 } else {
529 else {
530 // force reload expressions list on connect to API528 // force reload expressions list on connect to API
531 if (offlineMode.is(':visible')) {529 if (offlineMode.is(':visible')) {
532 expressionsList = null;530 expressionsList = null;
@@ -599,11 +597,9 @@ async function moduleWorker({ newChat = false } = {}) {
599 }597 }
600598
601 await sendExpressionCall(spriteFolderName, expression, { force: force, vnMode: vnMode });599 await sendExpressionCall(spriteFolderName, expression, { force: force, vnMode: vnMode });
602 }600 } catch (error) {
603 catch (error) {
604 console.log(error);601 console.log(error);
605 }602 } finally {
606 finally {
607 inApiCall = false;603 inApiCall = false;
608 lastCharacter = context.groupId || context.characterId;604 lastCharacter = context.groupId || context.characterId;
609 lastMessage = currentLastMessage.mes;605 lastMessage = currentLastMessage.mes;
@@ -631,8 +627,7 @@ function getFolderNameByMessage(message) {
631627
632 if (context.groupId) {628 if (context.groupId) {
633 avatarPath = message.original_avatar || context.characters.find(x => message.force_avatar && message.force_avatar.includes(encodeURIComponent(x.avatar)))?.avatar;629 avatarPath = message.original_avatar || context.characters.find(x => message.force_avatar && message.force_avatar.includes(encodeURIComponent(x.avatar)))?.avatar;
634 }630 } else if (context.characterId !== undefined) {
635 else if (context.characterId !== undefined) {
636 avatarPath = getCharaFilename();631 avatarPath = getCharaFilename();
637 }632 }
638633
@@ -1303,8 +1298,7 @@ async function getSpritesList(name) {
1303 }1298 }
13041299
1305 return grouped;1300 return grouped;
1306 }1301 } catch (err) {
1307 catch (err) {
1308 console.log(err);1302 console.log(err);
1309 return [];1303 return [];
1310 }1304 }
@@ -1414,7 +1408,6 @@ export async function getExpressionsList({ filterAvailable = false } = {}) {
1414 });1408 });
14151409
1416 if (apiResult.ok) {1410 if (apiResult.ok) {
1417
1418 const data = await apiResult.json();1411 const data = await apiResult.json();
1419 expressionsList = data.labels;1412 expressionsList = data.labels;
1420 return expressionsList;1413 return expressionsList;
@@ -1477,9 +1470,8 @@ function chooseSpriteForExpression(spriteFolderName, expression, { prevExpressio
1477 const searched = sprite.files.find(x => x.fileName === overrideSpriteFile);1470 const searched = sprite.files.find(x => x.fileName === overrideSpriteFile);
1478 if (searched) spriteFile = searched;1471 if (searched) spriteFile = searched;
1479 else toastr.warning(t`Couldn't find sprite file ${overrideSpriteFile} for expression ${expression}.`, t`Sprite Not Found`);1472 else toastr.warning(t`Couldn't find sprite file ${overrideSpriteFile} for expression ${expression}.`, t`Sprite Not Found`);
1480 }1473 } else if (extension_settings.expressions.allowMultiple && sprite.files.length > 1) {
1481 // Else calculate next expression, if multiple are allowed1474 // Else calculate next expression, if multiple are allowed
1482 else if (extension_settings.expressions.allowMultiple && sprite.files.length > 1) {
1483 let possibleFiles = sprite.files;1475 let possibleFiles = sprite.files;
1484 if (extension_settings.expressions.rerollIfSame) {1476 if (extension_settings.expressions.rerollIfSame) {
1485 possibleFiles = possibleFiles.filter(x => !prevExpressionSrc || x.imageSrc !== prevExpressionSrc);1477 possibleFiles = possibleFiles.filter(x => !prevExpressionSrc || x.imageSrc !== prevExpressionSrc);
@@ -1488,7 +1480,6 @@ function chooseSpriteForExpression(spriteFolderName, expression, { prevExpressio
1488 }1480 }
14891481
1490 return spriteFile;1482 return spriteFile;
1491
1492}1483}
14931484
1494/**1485/**
@@ -1595,8 +1586,7 @@ async function setExpression(spriteFolderName, expression, { force = false, over
1595 }1586 }
15961587
1597 console.info('Expression set', { expression: spriteFile.expression, file: spriteFile.fileName });1588 console.info('Expression set', { expression: spriteFile.expression, file: spriteFile.fileName });
1598 }1589 } else {
1599 else {
1600 img.attr('data-sprite-folder-name', spriteFolderName);1590 img.attr('data-sprite-folder-name', spriteFolderName);
16011591
1602 img.off('error');1592 img.off('error');
@@ -1846,19 +1836,16 @@ async function onClickExpressionUpload(event) {
1846 const fileNameWithoutExtension = withoutExtension(file.name);1836 const fileNameWithoutExtension = withoutExtension(file.name);
1847 const validFileName = validateExpressionSpriteName(expression, fileNameWithoutExtension);1837 const validFileName = validateExpressionSpriteName(expression, fileNameWithoutExtension);
18481838
1849 // If there is no expression yet and it's a valid expression, we just take it
1850 if (!clickedFileName && validFileName) {1839 if (!clickedFileName && validFileName) {
1840 // If there is no expression yet and it's a valid expression, we just take it
1851 spriteName = fileNameWithoutExtension;1841 spriteName = fileNameWithoutExtension;
1852 }1842 } else if (clickedFileName === file.name) {
1853 // If the filename matches the one that was clicked, we just take it and replace it1843 // If the filename matches the one that was clicked, we just take it and replace it
1854 else if (clickedFileName === file.name) {
1855 spriteName = fileNameWithoutExtension;1844 spriteName = fileNameWithoutExtension;
1856 }1845 } else if (!matchesExisting && validFileName) {
1857 // If it's a valid filename and there's no existing file with the same name, we just take it1846 // If it's a valid filename and there's no existing file with the same name, we just take it
1858 else if (!matchesExisting && validFileName) {
1859 spriteName = fileNameWithoutExtension;1847 spriteName = fileNameWithoutExtension;
1860 }1848 } else {
1861 else {
1862 /** @type {import('../../popup.js').CustomPopupButton[]} */1849 /** @type {import('../../popup.js').CustomPopupButton[]} */
1863 const customButtons = [];1850 const customButtons = [];
1864 if (clickedFileName) {1851 if (clickedFileName) {
@@ -2333,7 +2320,7 @@ function migrateSettings() {
2333 name: 'expression-folder-override',2320 name: 'expression-folder-override',
2334 aliases: ['spriteoverride', 'costume'],2321 aliases: ['spriteoverride', 'costume'],
2335 callback: setSpriteFolderCommand,2322 callback: setSpriteFolderCommand,
2336 namedArgumentList:[2323 namedArgumentList: [
2337 SlashCommandNamedArgument.fromProps({2324 SlashCommandNamedArgument.fromProps({
2338 name: 'name',2325 name: 'name',
2339 description: 'Character name to set a subfolder for. If not provided, the character who last sent a message will be used.',2326 description: 'Character name to set a subfolder for. If not provided, the character who last sent a message will be used.',
public/scripts/extensions/gallery/index.js+21 -1
@@ -791,13 +791,32 @@ async function listGalleryCommand(args) {
791791
792 const items = await getGalleryItems(url);792 const items = await getGalleryItems(url);
793 return JSON.stringify(items.map(it => it.src));793 return JSON.stringify(items.map(it => it.src));
794
795 } catch (err) {794 } catch (err) {
796 console.error(err);795 console.error(err);
797 }796 }
798 return JSON.stringify([]);797 return JSON.stringify([]);
799}798}
800799
800function addGalleryWandButton() {
801 const showGalleryContainer = document.getElementById('gallery_wand_container') || document.getElementById('extensionsMenu');
802 if (!(showGalleryContainer instanceof HTMLElement)) {
803 return;
804 }
805 const showGalleryButton = document.createElement('div');
806 showGalleryButton.id = 'show_gallery_wand_button';
807 showGalleryButton.classList.add('list-group-item', 'flex-container', 'flexGap5');
808 const showGalleryIcon = document.createElement('div');
809 showGalleryIcon.classList.add('fa-solid', 'fa-sd-card', 'extensionsMenuExtensionButton');
810 const showGalleryText = document.createElement('span');
811 showGalleryText.textContent = translate('Show Gallery');
812 showGalleryButton.appendChild(showGalleryIcon);
813 showGalleryButton.appendChild(showGalleryText);
814 showGalleryButton.addEventListener('click', () => {
815 showCharGallery();
816 });
817 showGalleryContainer.appendChild(showGalleryButton);
818}
819
801// On extension load, ensure the settings are initialized820// On extension load, ensure the settings are initialized
802(function () {821(function () {
803 initSettings();822 initSettings();
@@ -830,4 +849,5 @@ async function listGalleryCommand(args) {
830 text: translate('Show Gallery'),849 text: translate('Show Gallery'),
831 }),850 }),
832 );851 );
852 addGalleryWandButton();
833})();853})();
public/scripts/extensions/memory/index.js+15 -8
@@ -13,7 +13,7 @@ import {
13 saveSettingsDebounced,13 saveSettingsDebounced,
14 substituteParamsExtended,14 substituteParamsExtended,
15 generateRaw,15 generateRaw,
16 getMaxContextSize,16 getMaxPromptTokens,
17 setExtensionPrompt,17 setExtensionPrompt,
18 streamingProcessor,18 streamingProcessor,
19 animation_easing,19 animation_easing,
@@ -71,7 +71,7 @@ async function getSourceContextSize() {
71 return 1024 - 64;71 return 1024 - 64;
72 }72 }
7373
74 return getMaxContextSize(overrideLength);74 return getMaxPromptTokens(overrideLength);
75}75}
7676
77const formatMemoryValue = function (value) {77const formatMemoryValue = function (value) {
@@ -881,11 +881,9 @@ async function summarizeChatExtras(context) {
881 }881 }
882882
883 setMemoryContext(summary, true);883 setMemoryContext(summary, true);
884 }884 } catch (error) {
885 catch (error) {
886 console.log(error);885 console.log(error);
887 }886 } finally {
888 finally {
889 inApiCall = false;887 inApiCall = false;
890 }888 }
891}889}
@@ -1109,16 +1107,25 @@ jQuery(async function () {
1109 returns: ARGUMENT_TYPE.STRING,1107 returns: ARGUMENT_TYPE.STRING,
1110 }));1108 }));
11111109
1110 const summaryMacroHandler = () => {
1111 // Checking content of the UI summary box first
1112 const uiSummary = $('#memory_contents').val().toString();
1113 if (uiSummary.trim().length > 0) {
1114 return uiSummary;
1115 }
1116 // Fallback to scanning the chat for the latest summary if the UI summary box is empty
1117 return getLatestMemoryFromChat(getContext().chat);
1118 };
1112 if (power_user.experimental_macro_engine) {1119 if (power_user.experimental_macro_engine) {
1113 macros.register('summary', {1120 macros.register('summary', {
1114 category: MacroCategory.CHAT,1121 category: MacroCategory.CHAT,
1115 description: 'Returns the latest memory/summary from the current chat.',1122 description: 'Returns the latest memory/summary from the current chat.',
1116 handler: () => getLatestMemoryFromChat(getContext().chat),1123 handler: () => summaryMacroHandler(),
1117 });1124 });
1118 } else {1125 } else {
1119 // TODO: Remove this when the experimental macro engine is replacing the old macro engine1126 // TODO: Remove this when the experimental macro engine is replacing the old macro engine
1120 MacrosParser.registerMacro('summary',1127 MacrosParser.registerMacro('summary',
1121 () => getLatestMemoryFromChat(getContext().chat),1128 () => summaryMacroHandler(),
1122 'Returns the latest memory/summary from the current chat.');1129 'Returns the latest memory/summary from the current chat.');
1123 }1130 }
1124});1131});
public/scripts/extensions/quick-reply/api/QuickReplyApi.js+9 -15
@@ -10,22 +10,18 @@ export class QuickReplyApi {
10 /** @type {SettingsUi} */ settingsUi;10 /** @type {SettingsUi} */ settingsUi;
1111
1212
13
14
15 constructor(/** @type {QuickReplySettings} */settings, /** @type {SettingsUi} */settingsUi) {13 constructor(/** @type {QuickReplySettings} */settings, /** @type {SettingsUi} */settingsUi) {
16 this.settings = settings;14 this.settings = settings;
17 this.settingsUi = settingsUi;15 this.settingsUi = settingsUi;
18 }16 }
1917
2018
21
22
23 /**19 /**
24 * @param {QuickReply} qr20 * @param {QuickReply} qr
25 * @returns {QuickReplySet}21 * @returns {QuickReplySet}
26 */22 */
27 getSetByQr(qr) {23 getSetByQr(qr) {
28 return QuickReplySet.list.find(it=>it.qrList.includes(qr));24 return QuickReplySet.list.find(it => it.qrList.includes(qr));
29 }25 }
3026
31 /**27 /**
@@ -48,13 +44,11 @@ export class QuickReplyApi {
48 getQrByLabel(setName, label) {44 getQrByLabel(setName, label) {
49 const set = this.getSetByName(setName);45 const set = this.getSetByName(setName);
50 if (!set) return;46 if (!set) return;
51 if (Number.isInteger(label)) return set.qrList.find(it=>it.id == label);47 if (Number.isInteger(label)) return set.qrList.find(it => it.id == label);
52 return set.qrList.find(it=>it.label == label);48 return set.qrList.find(it => it.label == label);
53 }49 }
5450
5551
56
57
58 /**52 /**
59 * Executes a quick reply by its index and returns the result.53 * Executes a quick reply by its index and returns the result.
60 *54 *
@@ -63,7 +57,7 @@ export class QuickReplyApi {
63 */57 */
64 async executeQuickReplyByIndex(idx) {58 async executeQuickReplyByIndex(idx) {
65 const qr = [...this.settings.config.setList, ...(this.settings.chatConfig?.setList ?? [])]59 const qr = [...this.settings.config.setList, ...(this.settings.chatConfig?.setList ?? [])]
66 .map(it=>it.set.qrList)60 .map(it => it.set.qrList)
67 .flat()[idx]61 .flat()[idx]
68 ;62 ;
69 if (qr) {63 if (qr) {
@@ -400,7 +394,7 @@ export class QuickReplyApi {
400 if (oldSet) {394 if (oldSet) {
401 QuickReplySet.list.splice(QuickReplySet.list.indexOf(oldSet), 1, set);395 QuickReplySet.list.splice(QuickReplySet.list.indexOf(oldSet), 1, set);
402 } else {396 } else {
403 const idx = QuickReplySet.list.findIndex(it=>it.name.localeCompare(name) == 1);397 const idx = QuickReplySet.list.findIndex(it => it.name.localeCompare(name) == 1);
404 if (idx > -1) {398 if (idx > -1) {
405 QuickReplySet.list.splice(idx, 0, set);399 QuickReplySet.list.splice(idx, 0, set);
406 } else {400 } else {
@@ -460,7 +454,7 @@ export class QuickReplyApi {
460 * @returns array with the names of all quick reply sets454 * @returns array with the names of all quick reply sets
461 */455 */
462 listSets() {456 listSets() {
463 return QuickReplySet.list.map(it=>it.name);457 return QuickReplySet.list.map(it => it.name);
464 }458 }
465 /**459 /**
466 * Gets a list of all globally active quick reply sets.460 * Gets a list of all globally active quick reply sets.
@@ -468,7 +462,7 @@ export class QuickReplyApi {
468 * @returns array with the names of all quick reply sets462 * @returns array with the names of all quick reply sets
469 */463 */
470 listGlobalSets() {464 listGlobalSets() {
471 return this.settings.config.setList.map(it=>it.set.name);465 return this.settings.config.setList.map(it => it.set.name);
472 }466 }
473 /**467 /**
474 * Gets a list of all quick reply sets activated by the current chat.468 * Gets a list of all quick reply sets activated by the current chat.
@@ -476,7 +470,7 @@ export class QuickReplyApi {
476 * @returns array with the names of all quick reply sets470 * @returns array with the names of all quick reply sets
477 */471 */
478 listChatSets() {472 listChatSets() {
479 return this.settings.chatConfig?.setList?.flatMap(it=>it.set.name) ?? [];473 return this.settings.chatConfig?.setList?.flatMap(it => it.set.name) ?? [];
480 }474 }
481475
482 /**476 /**
@@ -490,7 +484,7 @@ export class QuickReplyApi {
490 if (!set) {484 if (!set) {
491 throw new Error(`No quick reply set with name "${name}" found.`);485 throw new Error(`No quick reply set with name "${name}" found.`);
492 }486 }
493 return set.qrList.map(it=>it.label);487 return set.qrList.map(it => it.label);
494 }488 }
495489
496 /**490 /**
public/scripts/extensions/quick-reply/index.js+2 -6
@@ -14,8 +14,6 @@ import { selected_group } from '../../group-chats.js';
14export { debounceAsync };14export { debounceAsync };
1515
1616
17
18
19const _VERBOSE = true;17const _VERBOSE = true;
20export const debug = (...msg) => _VERBOSE ? console.debug('[QR2]', ...msg) : null;18export const debug = (...msg) => _VERBOSE ? console.debug('[QR2]', ...msg) : null;
21export const log = (...msg) => _VERBOSE ? console.log('[QR2]', ...msg) : null;19export const log = (...msg) => _VERBOSE ? console.log('[QR2]', ...msg) : null;
@@ -54,8 +52,6 @@ let autoExec;
54export let quickReplyApi;52export let quickReplyApi;
5553
5654
57
58
59const loadSets = async () => {55const loadSets = async () => {
60 const response = await fetch('/api/settings/get', {56 const response = await fetch('/api/settings/get', {
61 method: 'POST',57 method: 'POST',
@@ -72,7 +68,7 @@ const loadSets = async () => {
72 set.disableSend = set.quickActionEnabled ?? false;68 set.disableSend = set.quickActionEnabled ?? false;
73 set.placeBeforeInput = set.placeBeforeInputEnabled ?? false;69 set.placeBeforeInput = set.placeBeforeInputEnabled ?? false;
74 set.injectInput = set.AutoInputInject ?? false;70 set.injectInput = set.AutoInputInject ?? false;
75 set.qrList = set.quickReplySlots.map((slot,idx)=>{71 set.qrList = set.quickReplySlots.map((slot, idx) => {
76 const qr = {};72 const qr = {};
77 qr.id = idx + 1;73 qr.id = idx + 1;
78 qr.label = slot.label ?? '';74 qr.label = slot.label ?? '';
@@ -87,7 +83,7 @@ const loadSets = async () => {
87 qr.executeOnNewChat = slot.autoExecute_newChat ?? false;83 qr.executeOnNewChat = slot.autoExecute_newChat ?? false;
88 qr.executeBeforeGeneration = slot.autoExecute_beforeGeneration ?? false;84 qr.executeBeforeGeneration = slot.autoExecute_beforeGeneration ?? false;
89 qr.automationId = slot.automationId ?? '';85 qr.automationId = slot.automationId ?? '';
90 qr.contextList = (slot.contextMenu ?? []).map(it=>({86 qr.contextList = (slot.contextMenu ?? []).map(it => ({
91 set: it.preset,87 set: it.preset,
92 isChained: it.chain,88 isChained: it.chain,
93 }));89 }));
@@ -99,8 +95,8 @@ const loadSets = async () => {
99 }95 }
100 }96 }
101 // need to load QR lists after all sets are loaded to be able to resolve context menu entries97 // need to load QR lists after all sets are loaded to be able to resolve context menu entries
public/scripts/extensions/quick-reply/src/AutoExecuteHandler.js+0 -0
public/scripts/extensions/quick-reply/src/QuickReply.js+0 -0
public/scripts/extensions/quick-reply/src/QuickReplyConfig.js+0 -0
public/scripts/extensions/quick-reply/src/QuickReplyContextLink.js+0 -0
public/scripts/extensions/quick-reply/src/QuickReplySet.js+0 -0
public/scripts/extensions/quick-reply/src/QuickReplySetLink.js+0 -0
public/scripts/extensions/quick-reply/src/QuickReplySettings.js+0 -0
public/scripts/extensions/quick-reply/src/SlashCommandHandler.js+0 -0
public/scripts/extensions/quick-reply/src/ui/ButtonUi.js+0 -0
public/scripts/extensions/quick-reply/src/ui/SettingsUi.js+0 -0
public/scripts/extensions/quick-reply/src/ui/ctx/ContextMenu.js+0 -0
public/scripts/extensions/quick-reply/src/ui/ctx/MenuItem.js+0 -0
public/scripts/extensions/quick-reply/src/ui/ctx/SubMenu.js+0 -0
public/scripts/extensions/regex/index.js+0 -0
public/scripts/extensions/shared.js+0 -0
public/scripts/extensions/stable-diffusion/index.js+0 -0
public/scripts/extensions/stable-diffusion/settings.html+0 -0
public/scripts/extensions/token-counter/index.js+0 -0
public/scripts/extensions/tts/alltalk.js+0 -0
public/scripts/extensions/tts/chatterbox.js+0 -0
public/scripts/extensions/tts/coqui.js+0 -0
public/scripts/extensions/tts/cosyvoice.js+0 -0
public/scripts/extensions/tts/elevenlabs.js+0 -0
public/scripts/extensions/tts/google-native.js+0 -0
public/scripts/extensions/tts/gpt-sovits-v2.js+0 -0
public/scripts/extensions/tts/gsvi.js+0 -0
public/scripts/extensions/tts/index.js+0 -0
public/scripts/extensions/tts/kokoro-worker.js+0 -0
public/scripts/extensions/tts/minimax.js+0 -0
public/scripts/extensions/tts/openai.js+0 -0
public/scripts/extensions/tts/silerotts.js+0 -0
public/scripts/extensions/tts/system.js+0 -0
public/scripts/extensions/tts/vits.js+0 -0
public/scripts/extensions/tts/xtts.js+0 -0
public/scripts/extensions/vectors/index.js+0 -0
public/scripts/extensions/vectors/settings.html+0 -0
public/scripts/f-localStorage.js+0 -0
public/scripts/filters.js+0 -0
public/scripts/group-chats.js+0 -0
public/scripts/horde.js+0 -0
public/scripts/i18n.js+0 -0
public/scripts/input-md-formatting.js+0 -0
public/scripts/instruct-mode.js+0 -0
public/scripts/kai-settings.js+0 -0
public/scripts/keyboard.js+0 -0
public/scripts/loader.js+0 -0
public/scripts/logit-bias.js+0 -0
public/scripts/macros.js+0 -0
public/scripts/macros/definitions/chat-macros.js+0 -0
public/scripts/macros/definitions/core-macros.js+0 -0
public/scripts/macros/definitions/env-macros.js+0 -0
public/scripts/macros/engine/MacroCstWalker.js+0 -0
public/scripts/macros/engine/MacroEnv.types.js+0 -0
public/scripts/macros/engine/MacroEnvBuilder.js+0 -0
public/scripts/nai-settings.js+0 -0
public/scripts/openai.js+0 -0
public/scripts/personas.js+0 -0
public/scripts/popup.js+0 -0
public/scripts/power-user.js+0 -0
public/scripts/preset-manager.js+0 -0
public/scripts/reasoning.js+0 -0
public/scripts/request-compression.js+0 -0
public/scripts/samplerSelect.js+0 -0
public/scripts/secrets.js+0 -0
public/scripts/showdown-underscore.js+0 -0
public/scripts/slash-commands.js+0 -0
public/scripts/slash-commands/SlashCommand.js+0 -0
public/scripts/slash-commands/SlashCommandArgument.js+0 -0
public/scripts/slash-commands/SlashCommandAutoCompleteNameResult.js+0 -0
public/scripts/slash-commands/SlashCommandBrowser.js+0 -0
public/scripts/slash-commands/SlashCommandClosure.js+0 -0
public/scripts/slash-commands/SlashCommandCommandAutoCompleteOption.js+0 -0
public/scripts/slash-commands/SlashCommandCommonEnumsProvider.js+0 -0
public/scripts/slash-commands/SlashCommandDebugController.js+0 -0
public/scripts/slash-commands/SlashCommandEnumAutoCompleteOption.js+0 -0
public/scripts/slash-commands/SlashCommandExecutionError.js+0 -0
public/scripts/slash-commands/SlashCommandExecutor.js+0 -0
public/scripts/slash-commands/SlashCommandParser.js+0 -0
public/scripts/slash-commands/SlashCommandScope.js+0 -0
public/scripts/sse-stream.js+0 -0
public/scripts/st-context.js+0 -0
public/scripts/stats.js+0 -0
public/scripts/swipe-picker.js+0 -0
public/scripts/tags.js+0 -0
public/scripts/templates/itemizationText.html+0 -0
public/scripts/templates/tabbyDownloader.html+0 -0
public/scripts/templates/wandMenu.html+0 -0
public/scripts/templates/welcomePanel.html+0 -0
public/scripts/textgen-models.js+0 -0
public/scripts/textgen-settings.js+0 -0
public/scripts/tokenizers.js+0 -0
public/scripts/tool-calling.js+0 -0
public/scripts/user.js+0 -0
public/scripts/util/SimpleMutex.js+0 -0
public/scripts/util/ThemeGenerator.js+0 -0
public/scripts/utils.js+0 -0
public/scripts/variables.js+0 -0
public/scripts/welcome-screen.js+0 -0
public/scripts/world-info.js+0 -0
public/style.css+0 -0
src/command-line.js+0 -0
src/constants.js+0 -0
src/endpoints/assets.js+0 -0
src/endpoints/backends/chat-completions.js+0 -0
src/endpoints/backends/text-completions.js+0 -0
src/endpoints/backgrounds.js+0 -0
src/endpoints/backups.js+0 -0
src/endpoints/characters.js+0 -0
src/endpoints/chats.js+0 -0
src/endpoints/content-manager.js+0 -0
src/endpoints/extensions.js+0 -0
src/endpoints/groups.js+0 -0
src/endpoints/horde.js+0 -0
src/endpoints/image-metadata.js+0 -0
src/endpoints/minimax.js+0 -0
src/endpoints/novelai.js+0 -0
src/endpoints/openai.js+0 -0
src/endpoints/search.js+0 -0
src/endpoints/secrets.js+0 -0
src/endpoints/settings.js+0 -0
src/endpoints/sprites.js+0 -0
src/endpoints/stable-diffusion.js+0 -0
src/endpoints/thumbnails.js+0 -0
src/endpoints/tokenizers.js+0 -0
src/endpoints/users-private.js+0 -0
src/endpoints/vectors.js+0 -0
src/git/client.js+0 -0
src/middleware/webpack-serve.js+0 -0
src/prompt-converters.js+0 -0
src/server-main.js+0 -0
src/server-startup.js+0 -0
src/users.js+0 -0
src/util.js+0 -0
src/vectors/extras-vectors.js+0 -0
src/vectors/llamacpp-vectors.js+0 -0
src/vectors/ollama-vectors.js+0 -0
src/vectors/openai-vectors.js+0 -0
src/vectors/vllm-vectors.js+0 -0
tests/frontend/MacroEnvBuilder.e2e.js+0 -0
tests/package-lock.json+0 -0
tests/util-pure.test.js+0 -0
webpack.config.js+0 -0
Diff truncated