Merge branch 'staging' of github.com-qvink:SillyTavern/SillyTavern into get_chat_completion_presets_from_preset_manager

This commit is contained in:
qvink 2025-02-23 21:24:13 -07:00
commit 10a72b8c80
68 changed files with 2160 additions and 1287 deletions

View File

@ -4,7 +4,7 @@ FROM node:lts-alpine3.19
ARG APP_HOME=/home/node/app
# Install system dependencies
RUN apk add gcompat tini git
RUN apk add --no-cache gcompat tini git
# Create app directory
WORKDIR ${APP_HOME}

View File

@ -0,0 +1,13 @@
These are master copies of the default content files and are managed by SillyTavern.
Editing any of these files would not only have no effect, but will also cause merge conflicts during update pulls.
You should edit their respective copies instead, for example:
1. /default/config.yaml => /config.yaml
2. /default/public/css/user.css => /public/css/user.css
etc.
Any questions? You're always welcome at our official documentation website:
https://docs.sillytavern.app/

View File

@ -71,8 +71,6 @@ autheliaAuth: false
# the username and passwords for basic auth are the same as those
# for the individual accounts
perUserBasicAuth: false
# Minimum log level to display in the terminal (DEBUG = 0, INFO = 1, WARN = 2, ERROR = 3)
minLogLevel: 0
# User session timeout *in seconds* (defaults to 24 hours).
## Set to a positive number to expire session after a certain time of inactivity
@ -85,6 +83,18 @@ cookieSecret: ''
disableCsrfProtection: false
# Disable startup security checks - NOT RECOMMENDED
securityOverride: false
# -- LOGGING CONFIGURATION --
logging:
# Enable access logging to access.log file
# Records new connections with timestamp, IP address and user agent
enableAccessLog: true
# Minimum log level to display in the terminal (DEBUG = 0, INFO = 1, WARN = 2, ERROR = 3)
minLogLevel: 0
# -- RATE LIMITING CONFIGURATION --
rateLimiting:
# Use X-Real-IP header instead of socket IP for rate limiting
# Only enable this if you are using a properly configured reverse proxy (like Nginx/traefik/Caddy)
preferRealIpHeader: false
# -- ADVANCED CONFIGURATION --
# Open the browser automatically
autorun: true

View File

@ -1,4 +1,4 @@
import getWebpackServeMiddleware from '../src/middleware/webpack-serve.js';
const middleware = getWebpackServeMiddleware();
await middleware.runWebpackCompiler();
await middleware.runWebpackCompiler({ forceDist: true });

67
package-lock.json generated
View File

@ -28,7 +28,7 @@
"cors": "^2.8.5",
"csrf-sync": "^4.0.3",
"diff-match-patch": "^1.0.5",
"dompurify": "^3.1.7",
"dompurify": "^3.2.4",
"droll": "^0.2.1",
"express": "^4.21.0",
"form-data": "^4.0.0",
@ -43,6 +43,7 @@
"ip-matching": "^2.1.2",
"ip-regex": "^5.0.0",
"ipaddr.js": "^2.0.1",
"is-docker": "^3.0.0",
"jimp": "^0.22.10",
"localforage": "^1.10.0",
"lodash": "^4.17.21",
@ -87,7 +88,6 @@
"@types/cookie-session": "^2.0.49",
"@types/cors": "^2.8.17",
"@types/deno": "^2.0.0",
"@types/dompurify": "^3.0.5",
"@types/express": "^4.17.21",
"@types/jquery": "^3.5.29",
"@types/jquery-cropper": "^1.0.4",
@ -1181,16 +1181,6 @@
"dev": true,
"license": "MIT"
},
"node_modules/@types/dompurify": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/@types/dompurify/-/dompurify-3.0.5.tgz",
"integrity": "sha512-1Wg0g3BtQF7sSb27fJQAKck1HECM6zV1EB66j8JH9i3LCjYabJa0FSdiSgsD5K/RbrsR0SiraKacLB+T8ZVYAg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/trusted-types": "*"
}
},
"node_modules/@types/estree": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz",
@ -1478,8 +1468,8 @@
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
"dev": true,
"license": "MIT"
"license": "MIT",
"optional": true
},
"node_modules/@types/write-file-atomic": {
"version": "4.0.3",
@ -3236,10 +3226,13 @@
}
},
"node_modules/dompurify": {
"version": "3.1.7",
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.1.7.tgz",
"integrity": "sha512-VaTstWtsneJY8xzy7DekmYWEOZcmzIe3Qb3zPd4STve1OBTa+e+WmS1ITQec1fZYXI3HCsOZZiSMpG6oxoWMWQ==",
"license": "(MPL-2.0 OR Apache-2.0)"
"version": "3.2.4",
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.4.tgz",
"integrity": "sha512-ysFSFEDVduQpyhzAob/kkuJjf5zWkZD8/A9ywSp1byueyuCfHamrCBa14/Oc2iiB0e51B+NpxSl5gmzn+Ms/mg==",
"license": "(MPL-2.0 OR Apache-2.0)",
"optionalDependencies": {
"@types/trusted-types": "^2.0.7"
}
},
"node_modules/domutils": {
"version": "3.1.0",
@ -4657,15 +4650,15 @@
"license": "MIT"
},
"node_modules/is-docker": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz",
"integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==",
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz",
"integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==",
"license": "MIT",
"bin": {
"is-docker": "cli.js"
},
"engines": {
"node": ">=8"
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
@ -4742,6 +4735,21 @@
"node": ">=8"
}
},
"node_modules/is-wsl/node_modules/is-docker": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz",
"integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==",
"license": "MIT",
"bin": {
"is-docker": "cli.js"
},
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/isarray": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
@ -5526,6 +5534,21 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/open/node_modules/is-docker": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz",
"integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==",
"license": "MIT",
"bin": {
"is-docker": "cli.js"
},
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/openai": {
"version": "4.17.4",
"resolved": "https://registry.npmjs.org/openai/-/openai-4.17.4.tgz",

View File

@ -18,7 +18,7 @@
"cors": "^2.8.5",
"csrf-sync": "^4.0.3",
"diff-match-patch": "^1.0.5",
"dompurify": "^3.1.7",
"dompurify": "^3.2.4",
"droll": "^0.2.1",
"express": "^4.21.0",
"form-data": "^4.0.0",
@ -33,6 +33,7 @@
"ip-matching": "^2.1.2",
"ip-regex": "^5.0.0",
"ipaddr.js": "^2.0.1",
"is-docker": "^3.0.0",
"jimp": "^0.22.10",
"localforage": "^1.10.0",
"lodash": "^4.17.21",
@ -116,7 +117,6 @@
"@types/cookie-session": "^2.0.49",
"@types/cors": "^2.8.17",
"@types/deno": "^2.0.0",
"@types/dompurify": "^3.0.5",
"@types/express": "^4.17.21",
"@types/jquery": "^3.5.29",
"@types/jquery-cropper": "^1.0.4",

View File

@ -8,7 +8,7 @@ import path from 'node:path';
import process from 'node:process';
import { fileURLToPath } from 'node:url';
import { default as git } from 'simple-git';
import { default as git, CheckRepoActions } from 'simple-git';
import { color } from './src/util.js';
const __dirname = import.meta.dirname ?? path.dirname(fileURLToPath(import.meta.url));
@ -49,7 +49,7 @@ async function updatePlugins() {
const pluginPath = path.join(pluginsPath, directory);
const pluginRepo = git(pluginPath);
const isRepo = await pluginRepo.checkIsRepo();
const isRepo = await pluginRepo.checkIsRepo(CheckRepoActions.IS_REPO_ROOT);
if (!isRepo) {
console.log(`Directory ${color.yellow(directory)} is not a Git repository`);
continue;

View File

@ -104,6 +104,11 @@ const keyMigrationMap = [
newKey: 'extensions.models.textToSpeech',
migrate: (value) => value,
},
{
oldKey: 'minLogLevel',
newKey: 'logging.minLogLevel',
migrate: (value) => value,
},
];
/**

8
public/global.d.ts vendored
View File

@ -40,4 +40,12 @@ declare global {
searchInputCssClass?: string;
}
}
/**
* Translates a text to a target language using a translation provider.
* @param text Text to translate
* @param lang Target language
* @param provider Translation provider
*/
async function translate(text: string, lang: string, provider: string = null): Promise<string>;
}

View File

@ -2000,7 +2000,7 @@
</span>
</div>
</div>
<div class="range-block" data-source="deepseek,openrouter">
<div class="range-block" data-source="deepseek,openrouter,custom">
<label for="openai_show_thoughts" class="checkbox_label widthFreeExpand">
<input id="openai_show_thoughts" type="checkbox" />
<span>
@ -2015,7 +2015,7 @@
</div>
</div>
<div class="flex-container flexFlowColumn wide100p textAlignCenter marginTop10" data-source="openai,custom">
<div class="flex-container oneline-dropdown" title="Constrains effort on reasoning for reasoning models.&#10;Currently supported values are low, medium, and high.&#10;Reducing reasoning effort can result in faster responses and fewer tokens used on reasoning in a response.">
<div class="flex-container oneline-dropdown" title="Constrains effort on reasoning for reasoning models.&#10;Currently supported values are low, medium, and high.&#10;Reducing reasoning effort can result in faster responses and fewer tokens used on reasoning in a response." data-i18n="[title]Constrains effort on reasoning for reasoning models.">
<label for="openai_reasoning_effort" data-i18n="Reasoning Effort">
Reasoning Effort
</label>
@ -3186,21 +3186,33 @@
</div>
<h4 data-i18n="Groq Model">Groq Model</h4>
<select id="model_groq_select">
<optgroup label="Production Models">
<option value="gemma2-9b-it">gemma2-9b-it</option>
<option value="llama-3.3-70b-versatile">llama-3.3-70b-versatile</option>
<option value="llama-3.1-8b-instant">llama-3.1-8b-instant</option>
<option value="llama3-70b-8192">llama3-70b-8192</option>
<option value="llama3-8b-8192">llama3-8b-8192</option>
<option value="mixtral-8x7b-32768">mixtral-8x7b-32768</option>
<optgroup label="Alibaba Cloud">
<option value="qwen-2.5-32b">qwen-2.5-32b</option>
<option value="qwen-2.5-coder-32b">qwen-2.5-coder-32b</option>
</optgroup>
<optgroup label="Preview Models">
<optgroup label="DeepSeek / Alibaba Cloud">
<option value="deepseek-r1-distill-qwen-32b">deepseek-r1-distill-qwen-32b</option>
</optgroup>
<optgroup label="DeepSeek / Meta">
<option value="deepseek-r1-distill-llama-70b">deepseek-r1-distill-llama-70b</option>
<option value="llama-3.3-70b-specdec">llama-3.3-70b-specdec</option>
<option value="llama-3.2-1b-preview">llama-3.2-1b-preview</option>
<option value="llama-3.2-3b-preview">llama-3.2-3b-preview</option>
<option value="llama-3.2-11b-vision-preview">llama-3.2-11b-vision-preview</option>
<option value="llama-3.2-90b-vision-preview">llama-3.2-90b-vision-preview</option>
</optgroup>
<optgroup label="Google">
<option value="gemma2-9b-it">gemma2-9b-it</option>
</optgroup>
<optgroup label="Meta">
<option value="llama-3.1-8b-instant">llama-3.1-8b-instant </option>
<option value="llama-3.2-11b-vision-preview">llama-3.2-11b-vision-preview </option>
<option value="llama-3.2-1b-preview">llama-3.2-1b-preview </option>
<option value="llama-3.2-3b-preview">llama-3.2-3b-preview </option>
<option value="llama-3.2-90b-vision-preview">llama-3.2-90b-vision-preview </option>
<option value="llama-3.3-70b-specdec">llama-3.3-70b-specdec </option>
<option value="llama-3.3-70b-versatile">llama-3.3-70b-versatile </option>
<option value="llama-guard-3-8b">llama-guard-3-8b </option>
<option value="llama3-70b-8192">llama3-70b-8192 </option>
<option value="llama3-8b-8192">llama3-8b-8192 </option>
</optgroup>
<optgroup label="Mistral AI">
<option value="mixtral-8x7b-32768">mixtral-8x7b-32768</option>
</optgroup>
</select>
</div>
@ -3253,6 +3265,10 @@
<option value="sonar">sonar</option>
<option value="sonar-pro">sonar-pro</option>
<option value="sonar-reasoning">sonar-reasoning</option>
<option value="sonar-reasoning-pro">sonar-reasoning-pro</option>
</optgroup>
<optgroup label="Offline Models">
<option value="r1-1776">r1-1776</option>
</optgroup>
<optgroup label="Deprecated Models">
<!-- These are scheduled for deprecation after 2/22/2025 -->
@ -4001,7 +4017,7 @@
<div class="alignitemscenter flex-container flexFlowColumn flexGrow flexShrink gap0 flexBasis48p" title="Cap the number of entry activation recursions" data-i18n="[title]Cap the number of entry activation recursions">
<small>
<span data-i18n="Max Recursion Steps">Max Recursion Steps</span>
<div class="fa-solid fa-triangle-exclamation opacity50p" data-i18n="[title]0 = unlimited, 1 = scans once and doesn't recurse, 2 = scans once and recurses once, etc\n(disabled when min activations are used)" title="0 = unlimited, 1 = scans once and doesn't recurse, 2 = scans once and recurses once, etc&#10;(disabled when min activations are used)"></div>
<div class="fa-solid fa-triangle-exclamation opacity50p" data-i18n="[title]0 = unlimited, 1 = scans once and doesn't recurse, 2 = scans once and recurses once, etc" title="0 = unlimited, 1 = scans once and doesn't recurse, 2 = scans once and recurses once, etc&#10;(disabled when min activations are used)"></div>
</small>
<input class="neo-range-slider" type="range" id="world_info_max_recursion_steps" name="world_info_max_recursion_steps" min="0" max="10" step="1">
<input class="neo-range-input" type="number" min="0" max="10" step="1" data-for="world_info_max_recursion_steps" id="world_info_max_recursion_steps_counter">
@ -4413,7 +4429,7 @@
<input id="prefer_character_jailbreak" type="checkbox" />
<small data-i18n="Prefer Character Card Instructions">Prefer Char. Instructions</small>
</label>
<label class="checkbox_label" for="never_resize_avatars" title="Avoid cropping and resizing imported character images. When off, crop/resize to 512x768." data-i18n="[title]Avoid cropping and resizing imported character images. When off, crop/resize to 512x768">
<label class="checkbox_label" for="never_resize_avatars" title="Avoid cropping and resizing imported character images. When off, crop/resize to 512x768.&#10;This will disable the upload cropping popup for avatars." data-i18n="[title]never_resize_avatars_tooltip">
<input id="never_resize_avatars" type="checkbox" />
<small data-i18n="Never resize avatars">Never resize avatars</small>
</label>
@ -4666,7 +4682,7 @@
<small data-i18n="Enabled">Enabled</small>
</label>
<small data-i18n="Minimum generated message length">Minimum generated message length</small>
<input id="auto_swipe_minimum_length" name="auto_swipe_minimum_length" type="number" min="0" step="1" value="0" class="text_pole" title="If the generated message is shorter than this, trigger an auto-swipe." data-i18n="[title]If the generated message is shorter than this, trigger an auto-swipe">
<input id="auto_swipe_minimum_length" name="auto_swipe_minimum_length" type="number" min="0" step="1" value="0" class="text_pole" title="If the generated message is shorter than these many characters, trigger an auto-swipe." data-i18n="[title]If the generated message is shorter than these many characters, trigger an auto-swipe">
<small data-i18n="Blacklisted words">Blacklisted words</small>
<div class="auto_swipe">
<textarea id="auto_swipe_blacklist" name="auto_swipe_blacklist" data-i18n="[placeholder]words you dont want generated separated by comma ','" placeholder="words you don't want generated separated by comma ','" class="text_pole textarea_compact" value="" autocomplete="off" rows="3"></textarea>
@ -6890,8 +6906,8 @@
</div>
<div id="form_sheld">
<div id="dialogue_del_mes">
<div id="dialogue_del_mes_ok" class="menu_button">Delete</div>
<div id="dialogue_del_mes_cancel" class="menu_button">Cancel</div>
<div id="dialogue_del_mes_ok" data-i18n="Delete" class="menu_button">Delete</div>
<div id="dialogue_del_mes_cancel" data-i18n="Cancel" class="menu_button">Cancel</div>
</div>
<div id="send_form" class="no-connection">
<form id="file_form" class="wide100p displayNone">

View File

@ -24,10 +24,22 @@ if (typeof Array.prototype.indexOf === 'function') {
/* Polyfill EventEmitter. */
var EventEmitter = function () {
/**
* Creates an event emitter.
* @param {string[]} autoFireAfterEmit Auto-fire event names
*/
var EventEmitter = function (autoFireAfterEmit = []) {
this.events = {};
this.autoFireLastArgs = new Map();
this.autoFireAfterEmit = new Set(autoFireAfterEmit);
};
/**
* Adds a listener to an event.
* @param {string} event Event name
* @param {function} listener Event listener
* @returns
*/
EventEmitter.prototype.on = function (event, listener) {
// Unknown event used by external libraries?
if (event === undefined) {
@ -40,6 +52,10 @@ EventEmitter.prototype.on = function (event, listener) {
}
this.events[event].push(listener);
if (this.autoFireAfterEmit.has(event) && this.autoFireLastArgs.has(event)) {
listener.apply(this, this.autoFireLastArgs.get(event));
}
};
/**
@ -60,6 +76,10 @@ EventEmitter.prototype.makeLast = function (event, listener) {
}
events.push(listener);
if (this.autoFireAfterEmit.has(event) && this.autoFireLastArgs.has(event)) {
listener.apply(this, this.autoFireLastArgs.get(event));
}
}
/**
@ -80,8 +100,17 @@ EventEmitter.prototype.makeFirst = function (event, listener) {
}
events.unshift(listener);
if (this.autoFireAfterEmit.has(event) && this.autoFireLastArgs.has(event)) {
listener.apply(this, this.autoFireLastArgs.get(event));
}
}
/**
* Removes a listener from an event.
* @param {string} event Event name
* @param {function} listener Event listener
*/
EventEmitter.prototype.removeListener = function (event, listener) {
var idx;
@ -94,6 +123,10 @@ EventEmitter.prototype.removeListener = function (event, listener) {
}
};
/**
* Emits an event with optional arguments.
* @param {string} event Event name
*/
EventEmitter.prototype.emit = async function (event) {
let args = [].slice.call(arguments, 1);
if (localStorage.getItem('eventTracing') === 'true') {
@ -118,6 +151,10 @@ EventEmitter.prototype.emit = async function (event) {
}
}
}
if (this.autoFireAfterEmit.has(event)) {
this.autoFireLastArgs.set(event, args);
}
};
EventEmitter.prototype.emitAndWait = function (event) {
@ -144,10 +181,14 @@ EventEmitter.prototype.emitAndWait = function (event) {
}
}
}
if (this.autoFireAfterEmit.has(event)) {
this.autoFireLastArgs.set(event, args);
}
};
EventEmitter.prototype.once = function (event, listener) {
this.on(event, function g () {
this.on(event, function g() {
this.removeListener(event, g);
listener.apply(this, arguments);
});

View File

@ -633,7 +633,7 @@
"Prefer Character Card Prompt": "تفضيل التعليمات من بطاقة الشخصية",
"If checked and the character card contains a jailbreak override (Post History Instruction), use that instead": "إذا تم التحقق وكانت بطاقة الشخصية تحتوي على تجاوز للكسر (تعليمات تاريخ المشاركة)، استخدم ذلك بدلاً من ذلك",
"Prefer Character Card Jailbreak": "تفضيل كسر الحصار من بطاقة الشخصية",
"Avoid cropping and resizing imported character images. When off, crop/resize to 512x768": "تجنب اقتصاص صور الأحرف المستوردة وتغيير حجمها. عند إيقاف التشغيل، قم بالقص/تغيير الحجم إلى 512 × 768.",
"never_resize_avatars_tooltip": "تجنب اقتصاص صور الأحرف المستوردة وتغيير حجمها. عند إيقاف التشغيل، قم بالقص/تغيير الحجم إلى 512 × 768.",
"Never resize avatars": "لا تغيير حجم الصور الرمزية أبدًا",
"Show actual file names on the disk, in the characters list display only": "عرض الأسماء الفعلية للملفات على القرص، في عرض قائمة الشخصيات فقط",
"Show avatar filenames": "عرض أسماء ملفات الصور الرمزية",
@ -709,7 +709,7 @@
"Auto-swipe": "السحب التلقائي",
"Enable the auto-swipe function. Settings in this section only have an effect when auto-swipe is enabled": "تمكين وظيفة السحب التلقائي. الإعدادات في هذا القسم تؤثر فقط عند تمكين السحب التلقائي",
"Minimum generated message length": "الحد الأدنى لطول الرسالة المولدة",
"If the generated message is shorter than this, trigger an auto-swipe": "إذا كانت الرسالة المولدة أقصر من هذا، فتحريض السحب التلقائي",
"If the generated message is shorter than these many characters, trigger an auto-swipe": "إذا كانت الرسالة المولدة أقصر من هذا، فتحريض السحب التلقائي",
"Blacklisted words": "الكلمات الممنوعة",
"words you dont want generated separated by comma ','": "الكلمات التي لا تريد توليدها مفصولة بفاصلة ','",
"Blacklisted word count to swipe": "عدد الكلمات الممنوعة للسحب",

View File

@ -633,7 +633,7 @@
"Prefer Character Card Prompt": "Bevorzuge Charakterkarten-Prompt",
"If checked and the character card contains a jailbreak override (Post History Instruction), use that instead": "Wenn aktiviert und die Charakterkarte eine Jailbreak-Überschreibung enthält (Post-History-Instruction), verwende stattdessen diese",
"Prefer Character Card Jailbreak": "Bevorzuge Charakterkarten-Jailbreak",
"Avoid cropping and resizing imported character images. When off, crop/resize to 512x768": "Vermeiden Sie das Zuschneiden und Ändern der Größe importierter Zeichenbilder. Wenn deaktiviert, wird die Größe auf 512 x 768 zugeschnitten/angepasst.",
"never_resize_avatars_tooltip": "Vermeiden Sie das Zuschneiden und Ändern der Größe importierter Zeichenbilder. Wenn deaktiviert, wird die Größe auf 512 x 768 zugeschnitten/angepasst.",
"Never resize avatars": "Avatare niemals verkleinern",
"Show actual file names on the disk, in the characters list display only": "Zeige tatsächliche Dateinamen auf der Festplatte, nur in der Anzeige der Charakterliste",
"Show avatar filenames": "Avatar-Dateinamen anzeigen",
@ -709,7 +709,7 @@
"Auto-swipe": "Automatisches Wischen",
"Enable the auto-swipe function. Settings in this section only have an effect when auto-swipe is enabled": "Aktiviere die Auto-Wisch-Funktion. Einstellungen in diesem Abschnitt haben nur dann Auswirkungen, wenn das automatische Wischen aktiviert ist",
"Minimum generated message length": "Minimale generierte Nachrichtenlänge",
"If the generated message is shorter than this, trigger an auto-swipe": "Wenn die generierte Nachricht kürzer ist als diese, löse automatisches Wischen aus",
"If the generated message is shorter than these many characters, trigger an auto-swipe": "Wenn die generierte Nachricht kürzer ist als diese, löse automatisches Wischen aus",
"Blacklisted words": "Verbotene Wörter",
"words you dont want generated separated by comma ','": "Wörter, die du nicht generiert haben möchtest, durch Komma ',' getrennt",
"Blacklisted word count to swipe": "Anzahl der verbotenen Wörter, um zu wischen",

View File

@ -633,7 +633,7 @@
"Prefer Character Card Prompt": "Preferir Indicaciones en Tarjeta de Personaje",
"If checked and the character card contains a jailbreak override (Post History Instruction), use that instead": "Si está marcado y la tarjeta de personaje contiene una anulación de jailbreak (Instrucciones Post Historial), usar eso en su lugar",
"Prefer Character Card Jailbreak": "Preferir Jailbreak en Tarjeta de Personaje",
"Avoid cropping and resizing imported character images. When off, crop/resize to 512x768": "Evite recortar y cambiar el tamaño de las imágenes de personajes importados. Cuando esté desactivado, recorte/cambie el tamaño a 512x768.",
"never_resize_avatars_tooltip": "Evite recortar y cambiar el tamaño de las imágenes de personajes importados. Cuando esté desactivado, recorte/cambie el tamaño a 512x768.",
"Never resize avatars": "Nunca redimensionar avatares",
"Show actual file names on the disk, in the characters list display only": "Mostrar nombres de archivo reales en el disco, solo en la visualización de la lista de personajes",
"Show avatar filenames": "Mostrar nombres de archivo de avatares",
@ -709,7 +709,7 @@
"Auto-swipe": "Deslizamiento automático",
"Enable the auto-swipe function. Settings in this section only have an effect when auto-swipe is enabled": "Habilitar la función de deslizamiento automático. La configuración en esta sección solo tiene efecto cuando el deslizamiento automático está habilitado",
"Minimum generated message length": "Longitud mínima del mensaje generado",
"If the generated message is shorter than this, trigger an auto-swipe": "Si el mensaje generado es más corto que esto, activar un deslizamiento automático",
"If the generated message is shorter than these many characters, trigger an auto-swipe": "Si el mensaje generado es más corto que esto, activar un deslizamiento automático",
"Blacklisted words": "Palabras prohibidas",
"words you dont want generated separated by comma ','": "palabras que no desea generar separadas por coma ','",
"Blacklisted word count to swipe": "Número de palabras prohibidas para deslizar",

View File

@ -581,7 +581,7 @@
"Advanced Character Search": "Recherche de personnage avancée",
"If checked and the character card contains a prompt override (System Prompt), use that instead": "Si cochée et si la carte de personnage contient un prompt de remplacement (prompt système), l'utiliser à la place",
"Prefer Character Card Prompt": "Préférer le prompt du personnage",
"Avoid cropping and resizing imported character images. When off, crop/resize to 512x768": "Évitez de recadrer et de redimensionner les images de personnages importés. Lorsqu'il est désactivé, recadrez/redimensionnez à 512 x 768.",
"never_resize_avatars_tooltip": "Évitez de recadrer et de redimensionner les images de personnages importés. Lorsqu'il est désactivé, recadrez/redimensionnez à 512 x 768.",
"Never resize avatars": "Ne jamais redimensionner les avatars",
"Show actual file names on the disk, in the characters list display only": "Afficher les noms de fichier réels sur le disque, dans l'affichage de la liste de personnages uniquement",
"Show avatar filenames": "Afficher les noms de fichier des avatars",
@ -656,7 +656,7 @@
"Auto-swipe": "Balayage automatique",
"Enable the auto-swipe function. Settings in this section only have an effect when auto-swipe is enabled": "Activer la fonction de balayage automatique. Les paramètres de cette section n'ont d'effet que lorsque le balayage automatique est activé",
"Minimum generated message length": "Longueur minimale du message généré",
"If the generated message is shorter than this, trigger an auto-swipe": "Si le message généré est plus court que cela, déclenchez un balayage automatique",
"If the generated message is shorter than these many characters, trigger an auto-swipe": "Si le message généré est plus court que cela, déclenchez un balayage automatique",
"Blacklisted words": "Mots en liste noire",
"words you dont want generated separated by comma ','": "mots que vous ne voulez pas générer séparés par des virgules ','",
"Blacklisted word count to swipe": "Nombre de mots en liste noire pour balayer",
@ -1485,7 +1485,7 @@
"(disabled when max recursion steps are used)": "(désactivé lorsque le nombre maximum de pas de récursivité est utilisé)",
"Cap the number of entry activation recursions": "Plafonner le nombre de récursions d'activation d'entrée",
"Max Recursion Steps": "Nombre maximal d'étapes de récursivité",
"0 = unlimited, 1 = scans once and doesn't recurse, 2 = scans once and recurses once, etc\\n(disabled when min activations are used)": "0 = illimité, 1 = scanne une fois et ne récure pas, 2 = scanne une fois et récure une fois, etc.\n(désactivé lorsque des activations minimales sont utilisées)",
"0 = unlimited, 1 = scans once and doesn't recurse, 2 = scans once and recurses once, etc": "0 = illimité, 1 = scanne une fois et ne récure pas, 2 = scanne une fois et récure une fois, etc.\n(désactivé lorsque des activations minimales sont utilisées)",
"Include names with each message into the context for scanning": "Inclure les noms dans chaque message dans le contexte pour l'analyse.",
"Apply current sorting as Order": "Appliquer le tri actuel comme ordre",
"Display swipe numbers for all messages, not just the last.": "Afficher le nombre de balayage sur tous les messages, et pas seulement le dernier.",
@ -1602,7 +1602,6 @@
"Character Expressions": "Expressions de personnages",
"Translate text to English before classification": "Traduire le texte en anglais avant de le classer",
"Show default images (emojis) if sprite missing": "Afficher les images par défaut (emojis) si le sprite est manquant",
"Image Type - talkinghead (extras)": "Type d'image - talkinghead (extras)",
"Classifier API": "API de classification",
"Select the API for classifying expressions.": "Sélectionnez l'API pour classer les expressions.",
"Main API": "API principale",

View File

@ -633,7 +633,7 @@
"Prefer Character Card Prompt": "Kosstu kvenkortu fyrirspurn",
"If checked and the character card contains a jailbreak override (Post History Instruction), use that instead": "Ef merkt er og kortið inniheldur fangabrotsskil, notaðu það í staðinn",
"Prefer Character Card Jailbreak": "Kosstu kvenkortu fangabrot",
"Avoid cropping and resizing imported character images. When off, crop/resize to 512x768": "Forðastu að klippa og breyta stærð innfluttra stafamynda. Þegar slökkt er á því skaltu skera/breyta stærð í 512x768.",
"never_resize_avatars_tooltip": "Forðastu að klippa og breyta stærð innfluttra stafamynda. Þegar slökkt er á því skaltu skera/breyta stærð í 512x768.",
"Never resize avatars": "Aldrei breyta stærðinni á merkjum",
"Show actual file names on the disk, in the characters list display only": "Sýna raunveruleg nöfn skráa á diskinum, í lista yfir persónur sýna aðeins",
"Show avatar filenames": "Sýna nöfn merkja",
@ -709,7 +709,7 @@
"Auto-swipe": "Sjálfvirkur sveip",
"Enable the auto-swipe function. Settings in this section only have an effect when auto-swipe is enabled": "Virkjaðu sjálfvirka sveiflugerð. Stillingar í þessum hluta hafa aðeins áhrif þegar sjálfvirkur sveiflugerð er virk",
"Minimum generated message length": "Lágmarks lengd á mynduðum skilaboðum",
"If the generated message is shorter than this, trigger an auto-swipe": "Ef mynduðu skilaboðin eru styttri en þessi, kallaðu fram sjálfvirkar sveiflugerðar",
"If the generated message is shorter than these many characters, trigger an auto-swipe": "Ef mynduðu skilaboðin eru styttri en þessi, kallaðu fram sjálfvirkar sveiflugerðar",
"Blacklisted words": "Svört orð",
"words you dont want generated separated by comma ','": "orð sem þú vilt ekki að framleiða aðskilin með kommu ','",
"Blacklisted word count to swipe": "Fjöldi svörtra orða til að sveipa",

View File

@ -633,7 +633,7 @@
"Prefer Character Card Prompt": "Preferisci Prompt della Scheda Personaggio",
"If checked and the character card contains a jailbreak override (Post History Instruction), use that instead": "Se selezionato e la scheda del personaggio contiene una sovrascrittura jailbreak (Istruzione Storico Post), usalo invece",
"Prefer Character Card Jailbreak": "Preferisci Jailbreak della Scheda Personaggio",
"Avoid cropping and resizing imported character images. When off, crop/resize to 512x768": "Evita di ritagliare e ridimensionare le immagini dei personaggi importati. Quando è disattivato, ritaglia/ridimensiona a 512x768.",
"never_resize_avatars_tooltip": "Evita di ritagliare e ridimensionare le immagini dei personaggi importati. Quando è disattivato, ritaglia/ridimensiona a 512x768.",
"Never resize avatars": "Non ridimensionare mai gli avatar",
"Show actual file names on the disk, in the characters list display only": "Mostra i nomi file effettivi sul disco, solo nella visualizzazione dell'elenco dei personaggi",
"Show avatar filenames": "Mostra nomi file avatar",
@ -709,7 +709,7 @@
"Auto-swipe": "Auto-swipe",
"Enable the auto-swipe function. Settings in this section only have an effect when auto-swipe is enabled": "Abilita la funzione di auto-swipe. Le impostazioni in questa sezione hanno effetto solo quando l'auto-swipe è abilitato",
"Minimum generated message length": "Lunghezza minima del messaggio generato",
"If the generated message is shorter than this, trigger an auto-swipe": "Se il messaggio generato è più breve di questo, attiva un'automatica rimozione",
"If the generated message is shorter than these many characters, trigger an auto-swipe": "Se il messaggio generato è più breve di questo, attiva un'automatica rimozione",
"Blacklisted words": "Parole in blacklist",
"words you dont want generated separated by comma ','": "parole che non vuoi generate separate da virgola ','",
"Blacklisted word count to swipe": "Numero di parole in blacklist per attivare un'automatica rimozione",

View File

@ -633,7 +633,7 @@
"Prefer Character Card Prompt": "キャラクターカードのプロンプトを優先",
"If checked and the character card contains a jailbreak override (Post History Instruction), use that instead": "チェックされていてキャラクターカードにジェイルブレイクオーバーライド(投稿履歴指示)が含まれている場合、それを代わりに使用します",
"Prefer Character Card Jailbreak": "キャラクターカードのJailbreakを優先",
"Avoid cropping and resizing imported character images. When off, crop/resize to 512x768": "インポートした文字画像の切り取りやサイズ変更を避けます。オフにすると、512x768 に切り取り/サイズ変更されます。",
"never_resize_avatars_tooltip": "インポートした文字画像の切り取りやサイズ変更を避けます。オフにすると、512x768 に切り取り/サイズ変更されます。",
"Never resize avatars": "アバターを常にリサイズしない",
"Show actual file names on the disk, in the characters list display only": "ディスク上の実際のファイル名を表示します。キャラクターリストの表示にのみ",
"Show avatar filenames": "アバターのファイル名を表示",
@ -709,7 +709,7 @@
"Auto-swipe": "オートスワイプ",
"Enable the auto-swipe function. Settings in this section only have an effect when auto-swipe is enabled": "自動スワイプ機能を有効にします。このセクションの設定は、自動スワイプが有効になっている場合にのみ効果があります",
"Minimum generated message length": "生成されたメッセージの最小長",
"If the generated message is shorter than this, trigger an auto-swipe": "生成されたメッセージがこれよりも短い場合、自動スワイプをトリガーします",
"If the generated message is shorter than these many characters, trigger an auto-swipe": "生成されたメッセージがこれよりも短い場合、自動スワイプをトリガーします",
"Blacklisted words": "ブラックリストされた単語",
"words you dont want generated separated by comma ','": "コンマ ',' で区切られた生成したくない単語",
"Blacklisted word count to swipe": "スワイプするブラックリストされた単語の数",

View File

@ -646,7 +646,7 @@
"Prefer Character Card Prompt": "캐릭터 카드 프롬프트 선호",
"If checked and the character card contains a jailbreak override (Post History Instruction), use that instead": "선택되어 있고 캐릭터 카드에 (Post-History 지시)탈옥 재정의가 포함 된 경우, 그것을 대신 사용합니다.",
"Prefer Character Card Jailbreak": "캐릭터 카드 탈옥 선호",
"Avoid cropping and resizing imported character images. When off, crop/resize to 512x768": "가져온 캐릭터 이미지를 자르거나 크기를 조정하지 마세요. 꺼져 있으면 512x768로 자르거나 크기를 조정합니다.",
"never_resize_avatars_tooltip": "가져온 캐릭터 이미지를 자르거나 크기를 조정하지 마세요. 꺼져 있으면 512x768로 자르거나 크기를 조정합니다.",
"Never resize avatars": "아바타 크기 변경하지 않음",
"Show actual file names on the disk, in the characters list display only": "실제 파일 이름을 디스크에 표시하며 캐릭터 목록 디스플레이에만",
"Show avatar filenames": "아바타 파일 이름 표시",
@ -724,7 +724,7 @@
"Auto-swipe": "자동 스와이프",
"Enable the auto-swipe function. Settings in this section only have an effect when auto-swipe is enabled": "자동 스와이프 기능을 활성화합니다. 이 섹션의 설정은 자동 스와이프가 활성화되었을 때만 영향을 미칩니다",
"Minimum generated message length": "생성된 메시지 최소 길이",
"If the generated message is shorter than this, trigger an auto-swipe": "생성된 메시지가이보다 짧으면 자동 스와이프를 트리거합니다",
"If the generated message is shorter than these many characters, trigger an auto-swipe": "생성된 메시지가이보다 짧으면 자동 스와이프를 트리거합니다",
"Blacklisted words": "금지어",
"words you dont want generated separated by comma ','": "쉼표로 구분된 생성하지 않으려는 단어",
"Blacklisted word count to swipe": "스와이프할 금지어 개수",
@ -1467,7 +1467,6 @@
"menu within": "내의 메뉴",
"Translate text to English before classification": "분류 전에 텍스트를 영어로 번역합니다.",
"Show default images (emojis) if sprite missing": "해당하는 스프라이트가 없으면 기본 이미지 (이모지들)을 표시합니다.",
"Image Type - talkinghead (extras)": "이미지 유형 - 토킹 헤드 (부가 사항)",
"Classifier API": "분류를 위한 API",
"Select the API for classifying expressions.": "감정 이미지들을 분류할 API를 선택하세요.",
"Local": "로컬",

View File

@ -633,7 +633,7 @@
"Prefer Character Card Prompt": "Voorkeur karakterkaart prompt",
"If checked and the character card contains a jailbreak override (Post History Instruction), use that instead": "Als aangevinkt en de karakterkaart bevat een jailbreak-override (Post History Instruction), gebruik die in plaats daarvan",
"Prefer Character Card Jailbreak": "Voorkeur karakterkaart jailbreak",
"Avoid cropping and resizing imported character images. When off, crop/resize to 512x768": "Vermijd het bijsnijden en vergroten/verkleinen van geïmporteerde karakterafbeeldingen. Indien uitgeschakeld, bijsnijden/formaat wijzigen naar 512 x 768.",
"never_resize_avatars_tooltip": "Vermijd het bijsnijden en vergroten/verkleinen van geïmporteerde karakterafbeeldingen. Indien uitgeschakeld, bijsnijden/formaat wijzigen naar 512 x 768.",
"Never resize avatars": "Avatars nooit verkleinen",
"Show actual file names on the disk, in the characters list display only": "Toon de werkelijke bestandsnamen op de schijf, alleen in de weergave van de lijst met personages",
"Show avatar filenames": "Toon avatar bestandsnamen",
@ -709,7 +709,7 @@
"Auto-swipe": "Automatisch vegen",
"Enable the auto-swipe function. Settings in this section only have an effect when auto-swipe is enabled": "Schakel de automatische-vegen functie in. Instellingen in dit gedeelte hebben alleen effect wanneer automatisch vegen is ingeschakeld",
"Minimum generated message length": "Minimale gegenereerde berichtlengte",
"If the generated message is shorter than this, trigger an auto-swipe": "Als het gegenereerde bericht korter is dan dit, activeer dan een automatische veeg",
"If the generated message is shorter than these many characters, trigger an auto-swipe": "Als het gegenereerde bericht korter is dan dit, activeer dan een automatische veeg",
"Blacklisted words": "Verboden woorden",
"words you dont want generated separated by comma ','": "woorden die je niet gegenereerd wilt hebben gescheiden door komma ','",
"Blacklisted word count to swipe": "Aantal verboden woorden om te vegen",

View File

@ -633,7 +633,7 @@
"Prefer Character Card Prompt": "Preferir Prompt do Cartão de Personagem",
"If checked and the character card contains a jailbreak override (Post History Instruction), use that instead": "Se marcado e o cartão de personagem contiver uma substituição de jailbreak (Instrução de Histórico de Postagens), use isso em vez disso",
"Prefer Character Card Jailbreak": "Preferir Jailbreak do Cartão de Personagem",
"Avoid cropping and resizing imported character images. When off, crop/resize to 512x768": "Evite cortar e redimensionar imagens de personagens importados. Quando desativado, corte/redimensione para 512x768.",
"never_resize_avatars_tooltip": "Evite cortar e redimensionar imagens de personagens importados. Quando desativado, corte/redimensione para 512x768.",
"Never resize avatars": "Nunca redimensionar avatares",
"Show actual file names on the disk, in the characters list display only": "Mostrar nomes de arquivo reais no disco, apenas na exibição da lista de personagens",
"Show avatar filenames": "Mostrar nomes de arquivo de avatar",
@ -709,7 +709,7 @@
"Auto-swipe": "Auto-swipe",
"Enable the auto-swipe function. Settings in this section only have an effect when auto-swipe is enabled": "Ativar a função de auto-swipe. As configurações nesta seção só têm efeito quando o auto-swipe está ativado",
"Minimum generated message length": "Comprimento mínimo da mensagem gerada",
"If the generated message is shorter than this, trigger an auto-swipe": "Se a mensagem gerada for mais curta que isso, acione um auto-swipe",
"If the generated message is shorter than these many characters, trigger an auto-swipe": "Se a mensagem gerada for mais curta que isso, acione um auto-swipe",
"Blacklisted words": "Palavras proibidas",
"words you dont want generated separated by comma ','": "palavras que você não quer geradas separadas por vírgula ','",
"Blacklisted word count to swipe": "Contagem de palavras proibidas para swipe",

View File

@ -195,7 +195,7 @@
"Yes": "Да",
"No": "Нет",
"Context %": "Процент контекста",
"Budget Cap": "Бюджетный лимит",
"Budget Cap": "Лимит бюджета",
"(0 = disabled)": "(0 = отключено)",
"None": "Отсутствует",
"User Settings": "Настройки пользователя",
@ -426,7 +426,7 @@
"Requests logprobs from the API for the Token Probabilities feature": "Запросить логпробы из API для функции Token Probabilities.",
"Automatically reject and re-generate AI message based on configurable criteria": "Автоматическое отклонение и повторная генерация сообщений AI на основе настраиваемых критериев.",
"Enable the auto-swipe function. Settings in this section only have an effect when auto-swipe is enabled": "Включить авто-свайп. Настройки в этом разделе действуют только при включенном авто-свайпе.",
"If the generated message is shorter than this, trigger an auto-swipe": "Если сгенерированное сообщение короче этого значения, срабатывает авто-свайп.",
"If the generated message is shorter than these many characters, trigger an auto-swipe": "Если сгенерированное сообщение короче этого значения, срабатывает авто-свайп.",
"Reload and redraw the currently open chat": "Перезагрузить и перерисовать открытый в данный момент чат.",
"Auto-Expand Message Actions": "Развернуть действия",
"Persona Management": "Управление персоной",
@ -575,10 +575,10 @@
"Characters sorting order": "Порядок сортировки персонажей",
"Remove": "Убрать",
"Select a World Info file for": "Выбрать файл с миром для",
"Primary Lorebook": "Основного лорбука",
"A selected World Info will be bound to this character as its own Lorebook.": "Информация о мире будет привязана к персонажу как его собственный лорбук",
"When generating an AI reply, it will be combined with the entries from a global World Info selector.": "Когда ИИ генерирует ответ, он будет совмещён с записями из глобально выбранного мира",
"Exporting a character would also export the selected Lorebook file embedded in the JSON data.": "При экспорте персонажа вместе с ним также выгрузится выбранный лорбук в виде JSON",
"Primary Lorebook": "Основной лорбук",
"A selected World Info will be bound to this character as its own Lorebook.": "Информация о мире будет привязана к персонажу как его собственный лорбук.",
"When generating an AI reply, it will be combined with the entries from a global World Info selector.": "Когда ИИ генерирует ответ, он будет совмещён с записями из глобально выбранного мира.",
"Exporting a character would also export the selected Lorebook file embedded in the JSON data.": "При экспорте персонажа вместе с ним также выгрузится выбранный лорбук в виде JSON.",
"Additional Lorebooks": "Вспомогательные лорбуки",
"Associate one or more auxillary Lorebooks with this character.": "Привязать к этому персонажу один или больше вспомогательных лорбуков",
"NOTE: These choices are optional and won't be preserved on character export!": "ВНИМАНИЕ: эти выборы необязательные и не будут сохранены при экспорте персонажа!",
@ -593,7 +593,7 @@
"Prompt": "Промпт",
"Copy": "Скопировать",
"Confirm": "Подтвердить",
"Copy this message": "Скопировать сообщение",
"Copy this message": "Продублировать сообщение",
"Delete this message": "Удалить сообщение",
"Move message up": "Переместить сообщение вверх",
"Move message down": "Переместить сообщение вниз",
@ -612,7 +612,7 @@
"Ask AI to write your message for you": "Попросить ИИ написать сообщение за вас",
"Continue the last message": "Продолжить текущее сообщение",
"Bind user name to that avatar": "Закрепить имя за этим аватаром",
"Select this as default persona for the new chats.": "Выберать эту Персону в качестве персоны по умолчанию для новых чатов.",
"Select this as default persona for the new chats.": "Выбирать эту персону по умолчанию для всех новых чатов.",
"Change persona image": "Сменить аватар персоны",
"Delete persona": "Удалить персону",
"Reduced Motion": "Сокращение анимаций",
@ -640,7 +640,7 @@
"Token Probabilities": "Вероятности токенов",
"Close chat": "Закрыть чат",
"Manage chat files": "Все чаты",
"Import Extension From Git Repo": "Импортировать расширение из Git Repository",
"Import Extension From Git Repo": "Импортировать расширение из Git-репозитория.",
"Install extension": "Установить расширение",
"Manage extensions": "Управление расширениями",
"Tokens persona description": "Токенов",
@ -995,7 +995,7 @@
"Set your custom avatar.": "Установить аватарку",
"Remove your custom avatar.": "Сбросить аватарку",
"Make a Snapshot": "Сделать снимок",
"Avoid cropping and resizing imported character images. When off, crop/resize to 512x768": "Не менять размер картинок у импортируемых персонажей. При отключении все картинки будут приводиться к размеру 512х768",
"never_resize_avatars_tooltip": "Не менять размер картинок у импортируемых персонажей. При отключении все картинки будут приводиться к размеру 512х768",
"Char List Subheader": "Доп. заголовок в списке персонажей",
"# Messages to Load": "Сколько сообщений загружать",
"(0 = All)": "(0 = все)",
@ -1122,7 +1122,7 @@
"help_hotkeys_0": "Горячие клавиши",
"You can browse a list of bundled characters in the": "Комплектных персонажей можно найти в меню",
"Download Extensions & Assets": "Загрузить расширения и ресурсы",
"menu within": нутри этих кубиков",
"menu within": меню",
"Assets URL": "URL с описанием ресурсов",
"Custom (OpenAI-compatible)": "Кастомный (совместимый с OpenAI)",
"Custom Endpoint (Base URL)": "Кастомный эндпоинт (базовый URL)",
@ -1943,7 +1943,7 @@
"and connect to an": "и подключитесь к",
"You can add more": "Можете добавить больше",
"from other websites": "с других сайтов.",
"Go to the": "Загляните в",
"Go to the": "Заходите в",
"to install additional features.": ", чтобы установить разные дополнительные ресурсы.",
"or_welcome": "; также доступен",
"Claude API Key": "Ключ от API Claude",
@ -1958,7 +1958,7 @@
"Save": "Сохранить",
"Chat Lorebook": "Лорбук для чата",
"chat_world_template_txt": "Выбранный мир будет привязан к этому чату. Будет добавляться в промпт наряду с глобальным лорбуком и лором персонажа.",
"world_button_title": "Лор персонажа\n\nНажмите, чтобы загрузить\nShift + клик, чтобы открыть диалог привязки мира",
"world_button_title": "Лор персонажа\n\nНажмите, чтобы загрузить\nShift + ЛКМ, чтобы открыть диалог привязки мира",
"No auxillary Lorebooks set. Click here to select.": "Вспомогательный лорбук не выбран. Нажмите, чтобы выбрать.",
"ext_regex_user_input_desc": "Отправленные вами сообщения.",
"ext_regex_ai_input_desc": "Полученные от API ответы.",
@ -2144,5 +2144,65 @@
"Not connected to the API!": "Нет соединения с API!",
"ext_type_system": "Это комплектное расширение. Его нельзя удалить, а обновляется оно вместе со всей системой.",
"Update all": "Обновить все",
"Close": "Закрыть"
"Close": "Закрыть",
"Optional modules:": "Необязательные модули:",
"Sort: Display Name": "Сортировать: по названию",
"Sort: Loading Order": "Сортировать: в порядке загрузки",
"Click to toggle": "Нажмите, чтобы включить или выключить",
"Loading Asset List": "Загрузить список ресурсов",
"Don't ask again for this URL": "Запомнить выбор для этого адреса",
"Are you sure you want to connect to the following url?": "Вы точно хотите подключиться к этому адресу?",
"All": "Всё",
"Characters": "Персонажи",
"Ambient sounds": "Звуковой эмбиент",
"Blip sounds": "Звуки уведомлений",
"Background music": "Фоновая музыка",
"Search": "Поиск",
"extension_install_1": "Чтобы загружать расширения из этого списка, у вас должен быть установлен ",
"extension_install_2": ".",
"extension_install_3": "Нажмите на иконку ",
"extension_install_4": ", чтобы перейти в репозиторий расширения и получить более подробную информацию о нём.",
"Extension repo/guide:": "Репозиторий расширения:",
"Preview in browser": "Предпросмотр",
"Adds a function tool": "Частично или полностью работает через вызов функций",
"Tool": "Функции",
"Move extension": "Переместить расширение",
"ext_type_local": "Это локальное расширение, доступно только вам",
"ext_type_global": "Это глобальное расширение, доступно всем пользователям",
"Move": "Переместить",
"Enter the Git URL of the extension to install": "Введите Git-адрес расширения",
"Please be aware that using external extensions can have unintended side effects and may pose security risks. Always make sure you trust the source before importing an extension. We are not responsible for any damage caused by third-party extensions.": "помните, что используя расширения от сторонних авторов, вы можете подвергать систему опасности. Устанавливайте расширения только от проверенных разработчиков. Мы не несём ответственности за любой ущерб, причинённый сторонними расширениями.",
"Disclaimer:": "Внимание:",
"Example:": "Пример:",
"context_derived": "Считывать из метаданных модели (по возможности)",
"instruct_derived": "Считывать из метаданных модели (по возможности)",
"Confirm token parsing with": "Чтобы убедиться в правильности выделения токенов, используйте",
"Reasoning Effort": "Рассуждения",
"Constrains effort on reasoning for reasoning models.": "Регулирует объём внутренних рассуждений модели (reasoning), для моделей которые поддерживают эту возможность.\nНа данный момент поддерживаются три значения: Подробные, Обычные, Поверхностные.\nПри менее подробном рассуждении ответ получается быстрее, а также экономятся токены, уходящие на рассуждения.",
"openai_reasoning_effort_low": "Поверхностные",
"openai_reasoning_effort_medium": "Обычные",
"openai_reasoning_effort_high": "Подробные",
"Persona Lore Alt+Click to open the lorebook": "Лорбук данной персоны\nAlt + ЛКМ чтобы открыть лорбук",
"Persona Lorebook for": "Лорбук для персоны",
"persona_world_template_txt": "Выбранная Информация о мире будет привязана к этой персоне. Информация будет добавляться в каждом промпте вместе с глобальным лорбуком и лорбуками персонажа и чата.",
"Global list": "Глобальный список",
"Preset-specific list": "Список для данного пресета",
"Banned tokens/strings are being sent in the request.": "Запрещённые токены и строки отсылаются в запросе.",
"Banned tokens/strings are NOT being sent in the request.": "Запрещённые токены и строки НЕ отсылаются в запросе.",
"Add a reasoning block": "Добавить блок рассуждений",
"Create a copy of this message?": "Продублировать это сообщение?",
"Max Recursion Steps": "Макс. глубина рекурсии",
"0 = unlimited, 1 = scans once and doesn't recurse, 2 = scans once and recurses once, etc": "0 = неограничено, 1 = сканировать единожды, 2 = сканировать единожды и сделать один повторный проход, и т.д.\n(неактивно при указанном мин. числе активаций)",
"(disabled when max recursion steps are used)": "(неактивно при указанной макс. глубине рекурсии)",
"Enter a valid API URL": "Введите корректный адрес API",
"No Ollama model selected.": "Не выбрана модель Ollama",
"Background Fitting": "Способ подгонки фона под разрешение",
"Chat Lore Alt+Click to open the lorebook": "Лорбук данного чата\nAlt + ЛКМ чтобы открыть лорбук",
"Token Counter": "Подсчитать токены",
"Type / paste in the box below to see the number of tokens in the text.": "Введите или вставьте текст в окошко ниже, чтобы подсчитать количество токенов в нём.",
"Selected tokenizer:": "Выбранный токенайзер:",
"Input:": "Входные данные:",
"Tokenized text:": "Токенизированный текст:",
"Token IDs:": "Идентификаторы токенов:",
"Tokens:": "Токенов:"
}

View File

@ -633,7 +633,7 @@
"Prefer Character Card Prompt": "Перевага запиту персонажа",
"If checked and the character card contains a jailbreak override (Post History Instruction), use that instead": "Якщо відмічено і картка персонажа містить заміну джейлбрейку (Інструкцію), використовуйте її замість цього",
"Prefer Character Card Jailbreak": "Перевага джейлбрейку персонажа",
"Avoid cropping and resizing imported character images. When off, crop/resize to 512x768": "Уникайте обрізання та зміни розміру імпортованих зображень символів. Коли вимкнено, обрізати/змінити розмір до 512x768.",
"never_resize_avatars_tooltip": "Уникайте обрізання та зміни розміру імпортованих зображень символів. Коли вимкнено, обрізати/змінити розмір до 512x768.",
"Never resize avatars": "Ніколи не змінювати розмір аватарів",
"Show actual file names on the disk, in the characters list display only": "Показувати фактичні назви файлів на диску, тільки у відображенні списку персонажів",
"Show avatar filenames": "Показувати імена файлів аватарів",
@ -709,7 +709,7 @@
"Auto-swipe": "Автоматичний змах",
"Enable the auto-swipe function. Settings in this section only have an effect when auto-swipe is enabled": "Вмикає функцію автоматичного змаху. Налаштування в цьому розділі діють лише тоді, коли увімкнено автоматичний змах",
"Minimum generated message length": "Мінімальна довжина згенерованого повідомлення",
"If the generated message is shorter than this, trigger an auto-swipe": "Якщо згенероване повідомлення коротше за це, викликайте автоматичний змаху",
"If the generated message is shorter than these many characters, trigger an auto-swipe": "Якщо згенероване повідомлення коротше за це, викликайте автоматичний змаху",
"Blacklisted words": "Список заборонених слів",
"words you dont want generated separated by comma ','": "слова, які ви не хочете генерувати, розділені комою ','",
"Blacklisted word count to swipe": "Кількість заборонених слів для змаху",

View File

@ -633,7 +633,7 @@
"Prefer Character Card Prompt": "Ưu tiên Gợi ý từ Card",
"If checked and the character card contains a jailbreak override (Post History Instruction), use that instead": "Nếu được kiểm tra và thẻ nhân vật chứa một lệnh phá vỡ giam giữ (Hướng dẫn Lịch sử Bài viết), hãy sử dụng thay vào đó",
"Prefer Character Card Jailbreak": "Ưu tiên Jailbreak từ Card",
"Avoid cropping and resizing imported character images. When off, crop/resize to 512x768": "Tránh cắt xén và thay đổi kích thước hình ảnh ký tự đã nhập. Khi tắt, hãy cắt/thay đổi kích thước thành 512x768.",
"never_resize_avatars_tooltip": "Tránh cắt xén và thay đổi kích thước hình ảnh ký tự đã nhập. Khi tắt, hãy cắt/thay đổi kích thước thành 512x768.",
"Never resize avatars": "Không bao giờ thay đổi kích thước hình đại diện",
"Show actual file names on the disk, in the characters list display only": "Hiển thị tên tệp thực tế trên đĩa, chỉ trong danh sách nhân vật",
"Show avatar filenames": "Hiển thị tên tệp hình đại diện",
@ -709,7 +709,7 @@
"Auto-swipe": "Tự động vuốt",
"Enable the auto-swipe function. Settings in this section only have an effect when auto-swipe is enabled": "Bật chức năng tự động vuốt. Các cài đặt trong phần này chỉ có tác dụng khi tự động vuốt được bật",
"Minimum generated message length": "Độ dài tối thiểu của tin nhắn được tạo",
"If the generated message is shorter than this, trigger an auto-swipe": "Nếu tin nhắn được tạo ra ngắn hơn điều này, kích hoạt tự động vuốt",
"If the generated message is shorter than these many characters, trigger an auto-swipe": "Nếu tin nhắn được tạo ra ngắn hơn điều này, kích hoạt tự động vuốt",
"Blacklisted words": "Từ trong danh sách đen",
"words you dont want generated separated by comma ','": "các từ bạn không muốn được tạo ra được phân tách bằng dấu phẩy ','",
"Blacklisted word count to swipe": "Số từ trong danh sách đen để vuốt",

View File

@ -584,7 +584,7 @@
"(0 = unlimited, use budget)": "“0”为无限制使用预算",
"Cap the number of entry activation recursions": "限制条目激活递归的次数",
"Max Recursion Steps": "最大递归深度",
"0 = unlimited, 1 = scans once and doesn't recurse, 2 = scans once and recurses once, etc\\n(disabled when min activations are used)": "“0”为无限制“1”为扫描一次且不递归“2”为扫描一次且递归一次依此类推\n当使用最小激活次数时此功能被禁用",
"0 = unlimited, 1 = scans once and doesn't recurse, 2 = scans once and recurses once, etc": "“0”为无限制“1”为扫描一次且不递归“2”为扫描一次且递归一次依此类推\n当使用最小激活次数时此功能被禁用",
"Insertion Strategy": "插入策略",
"Sorted Evenly": "均匀排序",
"Character Lore First": "角色世界书优先",
@ -722,7 +722,7 @@
"Prefer Character Card Prompt": "角色卡提示词优先",
"If checked and the character card contains a Post-History Instructions override, use that instead": "开启后,如果角色卡包含后历史指令覆盖,则使用它。",
"Prefer Character Card Instructions": "首选角色卡说明",
"Avoid cropping and resizing imported character images. When off, crop/resize to 512x768": "避免裁剪和调整导入的角色图像的大小。关闭时,裁剪/调整大小为 512x768。",
"never_resize_avatars_tooltip": "避免裁剪和调整导入的角色图像的大小。关闭时,裁剪/调整大小为 512x768。",
"Never resize avatars": "永不调整头像大小",
"Show actual file names on the disk, in the characters list display only": "在角色列表显示中,显示磁盘上实际的文件名。",
"Show avatar filenames": "显示头像文件名",
@ -804,7 +804,7 @@
"Auto-swipe": "自动滑动",
"Enable the auto-swipe function. Settings in this section only have an effect when auto-swipe is enabled": "启用自动滑动功能。仅当启用自动滑动时,本节中的设置才会生效",
"Minimum generated message length": "生成的消息的最小长度",
"If the generated message is shorter than this, trigger an auto-swipe": "如果生成的消息短于此长度,则触发自动滑动",
"If the generated message is shorter than these many characters, trigger an auto-swipe": "如果生成的消息短于此长度,则触发自动滑动",
"Blacklisted words": "屏蔽词",
"words you dont want generated separated by comma ','": "不想生成的词语,用半角逗号“,”分隔",
"Blacklisted word count to swipe": "触发滑动的黑名单词语数量",
@ -1349,7 +1349,6 @@
"Character Expressions": "角色表情",
"Translate text to English before classification": "分类之前将文本翻译成英文",
"Show default images (emojis) if sprite missing": "如果表情包缺失,则显示默认图像(表情符号)",
"Image Type - talkinghead (extras)": "图像类型 - 说话头像(附加内容)",
"Classifier API": "分类器 API",
"Select the API for classifying expressions.": "选择用于对表达式进行分类的API。",
"Main API": "主要 API",

View File

@ -4,9 +4,9 @@
"Duplicate": "複製",
"Persona": "使用者角色",
"Delete": "刪除",
"AI Response Configuration": "設定 AI 回應",
"AI Response Configuration": "AI 回應設定",
"AI Configuration panel will stay open": "上鎖 = AI 設定面板將保持開啟",
"clickslidertips": "點選滑桿數字可手動輸入。",
"clickslidertips": "點擊滑桿旁的數字以手動輸入。",
"MAD LAB MODE ON": "瘋狂實驗室模式",
"Documentation on sampling parameters": "取樣參數的說明文件。",
"kobldpresets": "Kobold 預設設定檔",
@ -35,7 +35,7 @@
"Only enable this if your model supports context sizes greater than 8192 tokens": "僅在您的模型支援超過 8192 個符元的上下文長度時啟用此功能",
"Max prompt cost:": "最大提示詞費用:",
"Display the response bit by bit as it is generated.": "逐字顯示生成中的回應內容。",
"When this is off, responses will be displayed all at once when they are complete.": "關閉時,回應將在生成完成後一次顯示。",
"When this is off, responses will be displayed all at once when they are complete.": "關閉時,回應將在生成完成後一次全部顯示。",
"Temperature": "溫度",
"rep.pen": "重複懲罰",
"Rep. Pen. Range.": "重複懲罰範圍",
@ -51,7 +51,7 @@
"Aggressive": "積極",
"Very aggressive": "非常積極",
"Unlocked Context Size": "解鎖上下文長度",
"Unrestricted maximum value for the context slider": "不限制上下文滑桿最大值",
"Unrestricted maximum value for the context slider": "不限制上下文長度的最大值",
"Context Size (tokens)": "上下文長度(符元數)",
"Max Response Length (tokens)": "最大回應長度(符元數)",
"Multiple swipes per generation": "每次生成多次滑動",
@ -71,7 +71,7 @@
"Utility Prompts": "實用提示詞",
"Impersonation prompt": "AI 扮演提示詞",
"Restore default prompt": "還原預設提示詞",
"Prompt that is used for Impersonation function": "用於 AI 模仿功能的提示詞",
"Prompt that is used for Impersonation function": "用於「AI 扮演使用者」功能的提示詞",
"World Info Format Template": "世界資訊格式",
"Restore default format": "還原預設格式",
"Wraps activated World Info entries before inserting into the prompt.": "在插入提示詞前包裝已啟用的世界資訊條目。",
@ -79,7 +79,7 @@
"scenario_format_template_part_2": "來標示要插入內容的位置。",
"Scenario Format Template": "場景格式",
"Personality Format Template": "個性格式",
"Group Nudge Prompt Template": "群組推動提示詞範本",
"Group Nudge Prompt Template": "群組聊天格式微調",
"Sent at the end of the group chat history to force reply from a specific character.": "在群組聊天歷史結束時發送以強制特定角色回覆",
"New Chat": "新聊天",
"Restore new chat prompt": "還原新聊天的提示詞",
@ -89,17 +89,17 @@
"Set at the beginning of the chat history to indicate that a new group chat is about to start.": "設定在聊天歷史的開頭以表明即將開始新的群組聊天",
"New Example Chat": "新範例聊天",
"Set at the beginning of Dialogue examples to indicate that a new example chat is about to start.": "設定在對話範例的開頭以表明即將開始新的範例聊天",
"Continue nudge": "繼續輔助提示詞",
"Set at the end of the chat history when the continue button is pressed.": "按下繼續按鈕時設定在聊天歷史的末尾",
"Continue nudge": "繼續輔助微調",
"Set at the end of the chat history when the continue button is pressed.": "按下「繼續」按鈕時,插入於聊天歷史的末尾",
"Replace empty message": "取代空白訊息",
"Send this text instead of nothing when the text box is empty.": "當文字方塊為空時,發送此字串而不是空白。",
"Send this text instead of nothing when the text box is empty.": "當文本框為空時,發送此文本以取代空白。",
"Seed": "種子",
"Set to get deterministic results. Use -1 for random seed.": "設定以獲取確定性結果。使用 -1 作為隨機種子",
"Temperature controls the randomness in token selection": "溫度控制符元選擇的隨機性",
"Set to get deterministic results. Use -1 for random seed.": "設定數值以取得可重現的結果。使用 -1 作為隨機種子",
"Temperature controls the randomness in token selection": "溫度Temperature控制符元選擇的隨機性。\n- 低溫(<1.0):產生更可預測且具邏輯性的文本,優先選擇機率較高的符元。\n- 高溫(>1.0):提升創造性與輸出的多樣性,更常選擇機率較低的符元。\n將值設為 1.0 可使用原始機率。",
"Top_K_desc": "Top K 設定可以選擇的最高符元數量。\n例如Top K 為 20這意味著只保留排名前 20 的符元(無論它們的機率是多樣還是有限的)。\n設定為 0 以停用。",
"Top_P_desc": "Top P(又名核心取樣)會將所有頂級符元加總,直到達到目標百分比。\n例如如果前兩個符元都是 25%,而 Top P 設為 0.5,那麼只有前兩個符元會被考慮。\n設定為 1.0 以停用。",
"Typical P": "Typical P",
"Typical_P_desc": "Typical P 取樣根據符元偏離集合平均熵的程度進行優先排序。\n它會保留累積機率接近預設閾值(例如0.5)的符元,強調那些具有平均信息量的符元。\n設定為 1.0 以停用。",
"Typical_P_desc": "Typical P 取樣根據符元偏離集合平均熵的程度進行優先排序。\n它會保留累積機率接近預設閾值(例如0.5)的符元強調那些具有平均信息量的符元。\n設定為 1.0 以停用。",
"Min_P_desc": "Min P 設定基本最小機率。\n這個值會根據最高符元的機率進行調整。例如如果最高符元機率為 80%,而 Min P 設為 0.1 ,那麼只有機率高於 8% 的符元會被考慮。\n設定為 0 以停用。",
"Top_A_desc": "Top A 根據最高符元機率的平方設定符元選擇的門檻。\n例如如果 Top A 值為 0.2,而最高符元機率為 50%,那麼低於 5%(0.2 * 0.5^2) 的符元機率就會被排除。\n設定為 0 以停用。",
"Tail_Free_Sampling_desc": "無尾取樣(Tail-Free Sampling, TFS)會透過分析符元機率的變化率(使用導數)來尋找分佈中的低機率符元尾部。\n它會根據標準化的二階導數保留直到某個閾值(例如0.3)的符元。\n數值越接近 0 ,表示會棄去越多符元。設定為 1.0 以停用。",
@ -126,10 +126,10 @@
"Logit Bias": "Logit 偏差",
"Add": "新增",
"Helps to ban or reenforce the usage of certain words": "有助於禁止或強化某些符元的使用",
"CFG Scale": "CFG 比例",
"CFG Scale": "CFG 縮放比例",
"Negative Prompt": "負面提示詞",
"Add text here that would make the AI generate things you don't want in your outputs.": "在這裡新增文字,使 AI 生成您不希望在輸出中出現的內容。",
"Used if CFG Scale is unset globally, per chat or character": "如果CFG Scale未在全域、每個聊天或角色中設定則使用",
"Add text here that would make the AI generate things you don't want in your outputs.": "在此新增文字,以防止 AI 在輸出中生成您不希望出現的內容。",
"Used if CFG Scale is unset globally, per chat or character": "若 CFG 縮放比例未被全域設定,它將作用於所有聊天或角色",
"Mirostat Tau": "Tau",
"Mirostat LR": "Mirostat 學習率",
"Min Length": "最小長度",
@ -399,7 +399,7 @@
"Applies additional processing to the prompt before sending it to the API.": "這個選項會在將提示詞送往 API 之前,對它進行額外的處理。",
"Verifies your API connection by sending a short test message. Be aware that you'll be credited for it!": "透過發送簡短的測試訊息來驗證您的 API 連線。請注意,您將因此獲得榮譽!",
"Test Message": "測試訊息",
"Auto-connect to Last Server": "自動連線到上次伺服器",
"Auto-connect to Last Server": "自動連接至上次使用的伺服器",
"Missing key": "❌ 鑰匙遺失",
"Key saved": "✔️ 金鑰已儲存",
"View hidden API keys": "查看隱藏的 API 金鑰",
@ -634,7 +634,7 @@
"Prefer Character Card Prompt": "角色卡主要提示詞優先",
"If checked and the character card contains a jailbreak override (Post History Instruction), use that instead": "如果選中並且角色卡包含越獄覆寫(聊天歷史後指示),則使用該提示詞。",
"Prefer Character Card Jailbreak": "角色卡越獄優先",
"Avoid cropping and resizing imported character images. When off, crop/resize to 512x768": "避免裁剪和調整匯入的角色圖像大小。未勾選時將會裁剪/調整大小到 512x768。",
"never_resize_avatars_tooltip": "避免裁剪與調整導入的角色頭像大小。未啟用此選項時,圖片將被裁剪/調整為 512x768。此設定會關閉上傳頭像時的裁剪彈出視窗。",
"Never resize avatars": "永不調整頭像大小",
"Show actual file names on the disk, in the characters list display only": "僅在角色列表顯示實際檔案名稱。",
"Show avatar filenames": "顯示頭像檔案名",
@ -710,7 +710,7 @@
"Auto-swipe": "自動滑動",
"Enable the auto-swipe function. Settings in this section only have an effect when auto-swipe is enabled": "啟用自動滑動功能。此部分的設定僅在啟用自動滑動時有效。",
"Minimum generated message length": "生成訊息的最小長度",
"If the generated message is shorter than this, trigger an auto-swipe": "如果生成的訊息比這個短,將觸發自動滑動。",
"If the generated message is shorter than these many characters, trigger an auto-swipe": "如果生成的訊息比這個短,將觸發自動滑動。",
"Blacklisted words": "黑名單詞語",
"words you dont want generated separated by comma ','": "您不想生成的文字,使用逗號分隔",
"Blacklisted word count to swipe": "滑動的黑名單詞語數量",
@ -1008,7 +1008,7 @@
"prompt_manager_edit": "編輯",
"prompt_manager_name": "名稱",
"A name for this prompt.": "這個提示詞的名稱。",
"To whom this message will be attributed.": "此訊息將隸屬於誰。",
"To whom this message will be attributed.": "此訊息所屬的角色。",
"AI Assistant": "人工智慧助手",
"prompt_manager_position": "位置",
"Injection position. Next to other prompts (relative) or in-chat (absolute).": "注入位置。與其他提示詞相鄰(相對位置)或在聊天中(絕對位置)。",
@ -1458,7 +1458,6 @@
"Example: http://localhost:1234/v1": "例如http://localhost:1234/v1",
"popup-button-crop": "裁剪",
"(disabled when max recursion steps are used)": "(當最大遞歸步驟數使用時將停用)",
"0 = unlimited, 1 = scans once and doesn't recurse, 2 = scans once and recurses once, etc\n(disabled when min activations are used)": "0 = 無限制1 = 掃描一次且不遞歸2 = 掃描一次並遞歸一次,以此類推\n使用最小啟動設定時將停用",
"A greedy, brute-force algorithm used in LLM sampling to find the most likely sequence of words or tokens. It expands multiple candidate sequences at once, maintaining a fixed number (beam width) of top sequences at each step.": "一種用於 LLM 抽樣的貪婪演算法用於尋找最可能的單詞或標記序列。該方法會同時展開多個候選序列並在每一步中保持固定數量的頂級序列beam width。",
"A multiplicative factor to expand the overall area that the nodes take up.": "節點佔用該擴充功能區域的倍數。",
"Abort current image generation task": "終止目前的圖片生成任務",
@ -1653,7 +1652,6 @@
"HuggingFace Token": "HuggingFace 符元",
"Image Captioning": "圖片註解",
"Generate Caption": "產生圖片註解",
"Image Type - talkinghead (extras)": "圖片類型 - talkinghead額外選項",
"Injection Position": "插入位置",
"Injection position. Relative (to other prompts in prompt manager) or In-chat @ Depth.": "插入位置(與提示詞管理器中的其他提示相比)或聊天中的深度位置。",
"Injection Template": "插入範本",
@ -1708,7 +1706,7 @@
"Quick Impersonate button": "快速模擬按鈕",
"Recursion Level": "遞迴層級",
"Remove all image overrides": "移除所有圖片覆蓋",
"Restore default": "",
"Restore default": "恢復預設",
"Retain#": "保留#",
"Retrieve chunks": "檢索 Chunks",
"Sampler Order": "取樣順序",
@ -1741,7 +1739,7 @@
"Show group chat queue": "顯示群組聊天隊列",
"Size threshold (KB)": "大小閾值KB",
"Slash Command": "斜線命令",
"space_ slash command.": "斜線命令。",
"space_ slash command.": " 斜線命令。",
"Sprite Folder Override": "表情立繪資料夾覆蓋",
"Sprite set:": "立繪組:",
"Show Gallery": "查看圖庫",
@ -1762,7 +1760,7 @@
"Threshold": "閾值",
"to install 3rd party extensions.": "用於安裝第三方擴充功能。",
"Top": "頂部",
"Translate text to English before classification": "在分類前將文本翻譯為英文。",
"Translate text to English before classification": "分類前,將訊息翻譯為英文",
"Uncheck to hide the extensions messages in chat prompts.": "不勾選即可隱藏聊天提示詞中的擴充功能訊息。",
"Unchecked: only entries with ❌ status can be activated.": "未勾選時:僅允許啟用狀態為 ❌ 的條目。",
"Unified Sampling": "統一取樣Unified Sampling",
@ -1806,7 +1804,7 @@
"context_derived": "若可能,根據模型元數據推導。",
"instruct_derived": "若可能,根據模型元數據推導。",
"Inserted before the first User's message.": "插入於第一則使用者訊息之前。",
"0 = unlimited, 1 = scans once and doesn't recurse, 2 = scans once and recurses once, etc\\n(disabled when min activations are used)": "0 = 無限制1 = 掃描一次不遞歸2 = 掃描一次後遞歸一次 ⋯以此類推\n啟用最小啟動次數時無效",
"0 = unlimited, 1 = scans once and doesn't recurse, 2 = scans once and recurses once, etc": "0 = 無限制1 = 掃描一次不遞歸2 = 掃描一次後遞歸一次 ⋯以此類推\n啟用最小啟動次數時無效",
"Quick 'Impersonate' button": "快速「AI 扮演使用者」按鈕",
"Manual": "手動",
"Any contents here will replace the default Post-History Instructions used for this character. (v2 spec: post_history_instructions)": "此處填入的內容將取代該角色的默認聊天歷史後指示Post-History Instructions。\nv2 格式specpost_history_instructions",
@ -1891,7 +1889,7 @@
"Narrate user messages": "朗讀使用者訊息",
"Auto Generation": "自動生成",
"Requires auto generation to be enabled.": "需要啟用自動生成功能。",
"Narrate by paragraphs (when streaming)": "按段落朗讀(使用「串流」傳輸時)",
"Narrate by paragraphs (when streaming)": "按段落朗讀(使用「串流」時)",
"Only narrate quotes": "僅朗讀「引號」中的文字",
"Ignore text, even quotes, inside asterisk": "忽略 *(星號)內的文字(包括「引號」)",
"Narrate only the translated text": "僅朗讀翻譯後的文本",
@ -2452,5 +2450,179 @@
"Modules provided by your Extras API:": "由您的 Extras API 提供的模組:",
"Not connected to the API!": "未連線到 API",
"ext_type_system": "這是內建的擴充功能,無法刪除,且會跟隨系統更新。",
"Valid": "已驗證"
"Valid": "已驗證",
"Request Model Reasoning": "請求模型推理",
"Global list": "全域列表",
"Preset-specific list": "特定預設列表",
"Constrains effort on reasoning for reasoning models.": "限制推理模型的推理耗費。\n目前支援的值為低、中和高。\n降低推理耗費可加快回應速度並減少推理所使用的符元數量。",
"Reasoning Effort": "推理耗費",
"openai_reasoning_effort_low": "低",
"openai_reasoning_effort_medium": "中",
"openai_reasoning_effort_high": "高",
"Reasoning": "推理 Reasoning",
"reasoning_auto_parse": "自動解析主要內容中推理區塊,需定義且不為空的前綴與後綴欄位。",
"Auto-Parse": "自動解析",
"reasoning_auto_expand": "自動展開推理區塊。",
"Auto-Expand": "自動展開",
"reasoning_show_hidden": "顯示隱藏推理功能模型的推理時間",
"Show Hidden": "顯示隱藏內容",
"reasoning_add_to_prompts": "將現有推理區塊添加至提示詞中。若需新增推理區塊,請使用訊息編輯選單。",
"Add to Prompts": "添加至提示詞",
"reasoning_max_additions": "從最後一則訊息起算,每則提示詞中可添加的最大推理區塊數量。",
"Max": "最大值",
"Reasoning Formatting": "推理格式",
"reasoning_prefix": "插入於推理內容之前。",
"Prefix": "前綴",
"reasoning_suffix": "插入於推理內容之後。",
"Suffix": "後綴",
"reasoning_separator": "插入於推理內容與訊息內容之間。",
"Separator": "分隔符",
"Character details are hidden.": "角色詳情已隱藏。",
"Add a reasoning block": "新增推理區塊",
"Thought for some time": "思考了一段時間",
"Confirmedit": "確認",
"Remove reasoning": "移除推理",
"Cancel edit": "取消編輯",
"Copy reasoning": "複製推理",
"Edit reasoning": "編輯推理",
"extension_install_1": "若要從此頁面下載擴充功能,您需要安裝",
"extension_install_2": "已安裝。",
"extension_install_3": "點擊",
"extension_install_4": "圖示以訪問擴充功能的儲存庫,查看使用技巧。",
"Use the selected API from Chat Translation extension settings.": "使用擴充功能設定中「聊天翻譯」所選的翻譯提供者API。",
"A single expression can have multiple sprites. Whenever the expression is chosen, a random sprite for this expression will be selected.": "單個同名表情可以有多張角色立繪。每次使用該表情時,會隨機擇一顯示。",
"Allow multiple sprites per expression": "允許單一表情使用多張立繪",
"If the same expression is used again, re-roll the sprite. This only applies to expressions that have multiple available sprites assigned.": "若再次使用相同的表情,將重新隨機選擇。此功能僅適用於分配了多張立繪的表情。",
"Re-roll if same expression is used again": "重複使用同名表情時,隨機選用其他立繪",
"upload_expression_request": "請輸入角色立繪名稱(不含副檔名)。",
"upload_expression_naming_1": "角色立繪名稱必須符合所選表情的命名規則:{{expression}}",
"upload_expression_naming_2": "對於多個表情,名稱必須包含表情名稱和有效的後綴,允許的分隔符為「-」或「.」。",
"upload_expression_replace": "點擊「取代」以取代現有表情:",
"ext_regex_reasoning_desc": "推理區塊內容。當「僅格式化提示詞」已勾選時,這也會影響添加至提示詞的推理內容。",
"Token Counter": "符元計數器",
"Type / paste in the box below to see the number of tokens in the text.": "在下框中輸入或貼上文字以查看符元Token數量。",
"Selected tokenizer:": "選擇的分詞器:",
"Input:": "輸入:",
"Tokens:": "符元數:",
"Tokenized text:": "已符元化的文字:",
"Token IDs:": "符元 ID",
"Narrate by paragraphs (when not streaming)": "按段落朗讀(不使用「串流」時)",
" folder (typically in ": "資料夾(通常位於 ",
"Copy to Clipboard": "複製到剪貼簿",
"Reset to Defaults": "重設為預設值",
"Toggles Guinevere features.": "切換 Guinevere 功能。",
"Update customCSS": "更新 customCSS",
"Apply Theme": "套用主題",
"Enable Guinevere": "啟用 Guinevere",
"Note: Themes can be made/applied by going to the ": "注意:主題可通過前往以下位置進行創建/應用",
"Theme Name": "主題名稱",
"An unknown error occurred while counting tokens. Further information may be available in console.": "計算符元時發生未知錯誤。更多資訊可能可在主控台console中查看。",
"Qvink Memory": "Qvink Memory進階聊天記憶",
"Toggle whether memory is enabled for this chat specifically (overrides all settings).": "切換是否為此聊天啟用記憶功能(將覆蓋所有設定)。",
"Toggle Chat Memory": "切換聊天記憶",
"Preview current memory state (the exact text that will be injected into your context).": "預覽目前記憶狀態(包含將嵌入上下文的具體內容)。",
"Copy ALL memories to clipboard (all memories in the entire chat, not just those injected).": "將所有記憶複製到剪貼簿(包含整個聊天的所有記憶,而非僅限於注入的部分)。",
"Just refreshes which memories are included and re-renders the memories under each message, doesn't change summaries. This is done automatically all the time, the button is here just in case.": "不影響摘要,僅更新已包含的聊天記憶,並重新顯示在每則訊息下方。此過程通常會自動執行,按鈕只是備用選項。",
"Active Settings Profile ": "目前設定檔",
"Create, edit, and save configuration profiles for this extension.": "建立、編輯及儲存此擴充功能的設定檔。",
"The currently selected profile": "目前選取的設定檔",
"Save current profile": "儲存此設定檔",
"Rename current profile": "重新命名此設定檔",
"Create new profile": "建立新設定檔",
"Restore current profile": "還原此設定檔",
"Delete current profile": "刪除此設定檔",
"Set as default profile for current character": "設為目前角色的預設設定檔",
"Summarization": "摘要",
"Customize the prompt used to summarize a given message": "自訂用於摘要指定訊息的提示詞",
"Edit the summary prompt": "編輯摘要提示",
"Preview the filled-in summary prompt, using the last message as an example.": "以最後一則訊息為例,預覽填充完成的摘要提示",
"Mass re-summarization. Brings up dialog to choose subsets of messages to summarize or re-summarize.": "批量重新摘要:開啟對話框以選擇訊息子集進行摘要或重新摘要。",
"Stop all summarization immediately.": "立即停止所有摘要。",
"New messages will be automatically summarized if they will be included in short-term memory.": "如果新訊息將被納入短期記憶,將自動進行摘要。",
"Auto Summarize": "自動摘要",
"Auto-summarization will be triggered before a new message is sent instead of after.": "自動摘要將在發送新訊息之前觸發。",
"Auto Summarize Before Generation": "在生成內容前自動摘要",
"Show the progress bar when auto-summarizing more than 1 message.": "在自動摘要多於 1 則訊息時顯示進度條。",
"Auto Summarize Progress Bar": "自動摘要進度條",
"Number of messages to delay summarization (0 = summarize up to the most recent message, 1 = lag behind by one message, etc.)": "延遲摘要的訊息數量0 = 摘要至最新訊息1 = 延遲摘要 1 則訊息,以此類推)。",
"Auto Summarize Message Lag": "自動摘要訊息延遲",
"Wait until this many messages before auto-summarizing them all in sequence (1 = summarize every message immediately, 2 = summarize when you have two ready, etc). Still summarizes one at a time.": "在訊息數量達到此設定值後依序自動摘要1 = 即時摘要每則訊息2 = 等待 2 則訊息後再摘要,以此類推)。摘要將逐條執行。",
"Auto Summarize Batch Size": "自動摘要批次大小",
"The maximum number of messages back that auto-summarization will apply (-1 to disable).": "自動摘要可回溯的訊息最大數量(-1 表示禁用此功能)。",
"Auto Summarize Message Limit": "自動摘要訊息上限",
"Time in seconds to wait between summarizations. May be needed if you are using a external API with a rate limit.": "每次摘要的間隔時間(秒)。此設定適用於使用具有請求速率限制的外部 API。",
"Summarization Time Delay": "摘要時間延遲",
"The maximum token length a summary is allowed to be before cutting it off. Use the {{words}} macro in the summarization prompt to get this value.": "摘要在被截斷前允許的最大符元token長度。可在摘要提示中使用 {{words}} 巨集以取得此數值。",
"Summary Max Token Length": "摘要允許的最大符元長度",
"Editing a message will automatically trigger a re-summarization if it has already been summarized.": "編輯訊息時,若該訊息已被摘要,將自動觸發重新摘要。",
"Re-summarize on Edit": "編輯後重新摘要",
"Swiping a message will automatically trigger a re-summarization if it has already been summarized.": "滑動訊息後若已進行摘要,將自動觸發重新摘要。",
"Re-summarize on Swipe": "滑動後重新摘要",
"Block chat input while summarizing.": "在摘要進行時暫時禁用聊天訊息輸入。",
"Block Chat": "訊息輸入鎖定",
"Whether to use messages and/or summaries as context for summarization. You must use {{history}} in the summary prompt.": "決定是否在摘要中使用訊息及/或過往摘要作為背景資訊。需於摘要提示詞中,使用 {{history}}。",
"Message History": "訊息歷史",
"Messages": "僅訊息",
"Summaries": "僅摘要",
"Both": "訊息與摘要",
"Preview what the message history will look like": "預覽訊息歷史的顯示效果",
"How many previous messages to include in the summarization prompt as context.": "摘要提示中要包含多少先前訊息作為上下文。",
"Number of Previous Messages": "先前訊息數量",
"When including previous messages, also include user messages.": "包含先前訊息時,也包含使用者訊息。",
"Include Previous User Messages": "包含先前使用者訊息",
"The message to summarize will be inside the system instruct template itself. In unchecked (default), the message will instead be added separately after the prompt. Some models benefit from this, but it is not recommended.": "系統指令模板內將直接包含需要摘要的訊息。若未啟用此選項(預設設定),訊息會在提示後分開添加。儘管某些模型可能更適合此設定,但一般不建議使用。",
"Nest Message in Summary Prompt": "在摘要提示中內嵌訊息",
"WARNING: doesn't work great. Attempts to preserve context-shifting by including all the content that is sent in regular prompts (world info, description, personas, example messages, message history, etc). If your regular prompts are static, this can allow Context Shifting to work between summarizations, but it decreases the accuracy of summarization due to all the extra stuff in the prompt. It also can't be previewed as this injection is handled by ST, not the extension.": "警告:效果不佳。此功能嘗試透過在摘要時包含所有常規提示內容(如世界資訊、描述、角色設定、示範訊息、聊天歷史等)來保留上下文轉換。若您的常規提示為靜態內容,則可在摘要之間維持上下文轉換,但由於提示中包含大量額外資訊,將降低摘要的準確性。此外,此內容注入由 ST 處理,而非此擴充功能,因此無法進行預覽。",
"Include All Context Content": "包含所有上下文內容",
"Short-term Memory Injection": "短期記憶注入",
"Determines which messages are included in the short-term memory injection and where. If you change this and include messages that weren't summarized previously, you can either manually trigger a re-summarization or just wait until automatic summarization triggers.": "設定短期記憶注入中所包含的訊息及其插入位置。若更改此設定並包含先前未摘要的訊息,您可手動觸發重新摘要,或等待自動摘要啟動。",
"Edit the short-term memory prompt": "編輯短期記憶提示",
"Include User Messages": "包含使用者訊息",
"Include System Messages": "包含系統訊息",
"Include Thought Message": "包含思考訊息",
"Message Length Threshold": "訊息長度閾值",
"The minimum token length a message has to be in order to get summarized.": "可被摘要的訊息最小符元長度。",
"The max percent of the context that short-term memory can take up.": "短期記憶可佔用上下文的最大百分比。",
"Short-Term Context %": "短期記憶上下文%",
"Include short-term memory in the World Info Scan": "在世界資訊掃描中包含短期記憶",
"Do not inject": "不注入",
"Before main prompt": "主提示之前",
"After main prompt": "主提示之後",
"In chat at depth": "在對話中位於深度",
"Long-Term Memory Injection": "長期記憶注入",
"Determines where long-term messages are injected.": "決定長期訊息注入的位置。",
"Edit the long-term memory prompt": "編輯長期記憶提示",
"The max percent of the context that long-term memory can take up.": "長期記憶可佔用上下文的最大百分比。",
"Long-Term Context %": "長期記憶上下文%",
"Include long-term memory in the World Info Scan": "在世界資訊掃描中包含長期記憶",
"Misc.": "其他",
"Fill your console with debug messages": "將偵錯訊息填入主控台",
"Debug Mode": "偵錯模式",
"Display summarizations below each message": "在每則訊息下顯示摘要",
"Display Memories": "顯示記憶",
"Enable Memory in New Chats": "在新對話中啟用記憶",
"Limit Message History": "限制訊息歷史",
"Revert Settings": "還原設定",
"Auto-summarize user messages and include summaries in memory.": "自動摘要使用者訊息,並將該摘要納入記憶。",
"Auto-summarize system messages and include summaries in memory.": "自動摘要系統訊息,並將該摘要納入記憶。",
"Auto-summarize thought messages and include summaries in memory (from the Stepped Thinking extension).": "自動摘要思考訊息並將摘要納入記憶(來自 Stepped Thinking 擴充功能)。",
"Revert all settings to default (not the default profile, just the default that comes with the extension). Your other profiles won't be affected.": "將所有設定恢復為預設值(並非恢復至「預設設定檔」,而是擴充功能隨附的原始預設值)。其他設定檔將不受影響。",
"Limit the number of messages to send in regular prompts to this number (-1 for no limit). Message memories will still be sent.": "限制常規提示中傳送的訊息數量至此數值(-1 表示無限制)。訊息記憶仍將一併傳送。",
"Whether memory is enabled by default for new chats.": "是否在新對話中預設啟用記憶。",
"Summarize Chat": "摘要對話",
"Choose settings for the chat summarization. All message inclusion/exclusion settings from the main config profile are used, in addition to the following options.": "選擇聊天摘要的設定。摘要時將使用主要設定檔中的所有訊息包含/排除規則,並可額外設定以下選項。",
"Currently preparing to summarize:": "目前正在準備摘要:",
"Summarize messages with no existing summary": "摘要尚無摘要的訊息",
"Re-summarize messages with existing short-term memories": "重新摘要具有現有短期記憶的訊息",
"Re-summarize messages with existing long-term memories": "重新摘要具有現有長期記憶的訊息",
"Re-summarize messages with existing memories, but which are currently excluded from short-term and long-term memory": "重新摘要具有現有記憶,但目前被排除在短期和長期記憶之外的訊息",
"Re-summarize messages with existing memories that have been manually edited.": "重新摘要已手動編輯的訊息記憶",
"Type the folder name of the theme you want to apply.": "輸入您想套用的主題資料夾名稱。",
"Place your theme data in a folder.": "請將主題資料存於該資料夾內。",
"Unsure where to start? Type ": "不確定如何開始?輸入:",
" to apply the default Google Messages theme or click ": " 即可使用預設主題 Google Messages或點擊",
"here": "這裡",
" to learn how to create your own theme.": " 以學習如何創建個人化主題。",
"Guinevere (UI Theme Extension)": "Guinevere進階自定義 UI 主題)",
"and Guinaifen.": "和 Guinaifen桂乃芬呈獻。"
}

View File

@ -366,6 +366,10 @@ DOMPurify.addHook('uponSanitizeElement', (node, _, config) => {
return;
}
if (!(node instanceof Element)) {
return;
}
let mediaBlocked = false;
switch (node.tagName) {
@ -493,6 +497,7 @@ export const event_types = {
// TODO: Naming convention is inconsistent with other events
CHARACTER_DELETED: 'characterDeleted',
CHARACTER_DUPLICATED: 'character_duplicated',
CHARACTER_RENAMED: 'character_renamed',
/** @deprecated The event is aliased to STREAM_TOKEN_RECEIVED. */
SMOOTH_STREAM_TOKEN_RECEIVED: 'stream_token_received',
STREAM_TOKEN_RECEIVED: 'stream_token_received',
@ -507,7 +512,7 @@ export const event_types = {
TOOL_CALLS_RENDERED: 'tool_calls_rendered',
};
export const eventSource = new EventEmitter();
export const eventSource = new EventEmitter([event_types.APP_READY]);
eventSource.on(event_types.CHAT_CHANGED, processChatSlashCommands);
@ -1027,12 +1032,22 @@ export function setAnimationDuration(ms = null) {
document.documentElement.style.setProperty('--animation-duration', `${animation_duration}ms`);
}
/**
* Sets the currently active character
* @param {object|number|string} [entityOrKey] - An entity with id property (character, group, tag), or directly an id or tag key. If not provided, the active character is reset to `null`.
*/
export function setActiveCharacter(entityOrKey) {
active_character = getTagKeyForEntity(entityOrKey);
active_character = entityOrKey ? getTagKeyForEntity(entityOrKey) : null;
if (active_character) active_group = null;
}
/**
* Sets the currently active group.
* @param {object|number|string} [entityOrKey] - An entity with id property (character, group, tag), or directly an id or tag key. If not provided, the active group is reset to `null`.
*/
export function setActiveGroup(entityOrKey) {
active_group = getTagKeyForEntity(entityOrKey);
active_group = entityOrKey ? getTagKeyForEntity(entityOrKey) : null;
if (active_group) active_character = null;
}
/**
@ -3223,6 +3238,7 @@ class StreamingProcessor {
// Update reasoning
await this.reasoningHandler.process(messageId, mesChanged);
processedText = chat[messageId]['mes'];
// Token count update.
const tokenCountText = this.reasoningHandler.reasoning + processedText;
@ -3373,7 +3389,7 @@ class StreamingProcessor {
this.messageLogprobs.push(...(Array.isArray(logprobs) ? logprobs : [logprobs]));
}
// Get the updated reasoning string into the handler
this.reasoningHandler.updateReasoning(this.messageId, state?.reasoning ?? '');
this.reasoningHandler.updateReasoning(this.messageId, state?.reasoning);
await eventSource.emit(event_types.STREAM_TOKEN_RECEIVED, text);
await sw.tick(async () => await this.onProgressStreaming(this.messageId, this.continueMessage + text));
}
@ -6241,9 +6257,35 @@ export async function renameCharacter(name = null, { silent = false, renameChats
const data = await response.json();
const newAvatar = data.avatar;
// Replace tags list
const oldName = getCharaFilename(null, { manualAvatarKey: oldAvatar });
const newName = getCharaFilename(null, { manualAvatarKey: newAvatar });
// Replace other auxillery fields where was referenced by avatar key
// Tag List
renameTagKey(oldAvatar, newAvatar);
// Addtional lore books
const charLore = world_info.charLore?.find(x => x.name == oldName);
if (charLore) {
charLore.name = newName;
saveSettingsDebounced();
}
// Char-bound Author's Notes
const charNote = extension_settings.note.chara?.find(x => x.name == oldName);
if (charNote) {
charNote.name = newName;
saveSettingsDebounced();
}
// Update active character, if the current one was the currently active one
if (active_character === oldAvatar) {
active_character = newAvatar;
saveSettingsDebounced();
}
await eventSource.emit(event_types.CHARACTER_RENAMED, oldAvatar, newAvatar);
// Reload characters list
await getCharacters();
@ -10947,7 +10989,7 @@ jQuery(async function () {
});
$(document).on('click', '.mes_edit_copy', async function () {
const confirmation = await callGenericPopup('Create a copy of this message?', POPUP_TYPE.CONFIRM);
const confirmation = await callGenericPopup(t`Create a copy of this message?`, POPUP_TYPE.CONFIRM);
if (!confirmation) {
return;
}

View File

@ -280,17 +280,32 @@ async function RA_autoloadchat() {
// active character is the name, we should look it up in the character list and get the id
if (active_character !== null && active_character !== undefined) {
const active_character_id = characters.findIndex(x => getTagKeyForEntity(x) === active_character);
if (active_character_id !== null) {
if (active_character_id !== -1) {
await selectCharacterById(String(active_character_id));
// Do a little tomfoolery to spoof the tag selector
const selectedCharElement = $(`#rm_print_characters_block .character_select[chid="${active_character_id}"]`);
applyTagsOnCharacterSelect.call(selectedCharElement);
} else {
setActiveCharacter(null);
saveSettingsDebounced();
console.warn(`Currently active character with ID ${active_character} not found. Resetting to no active character.`);
}
}
if (active_group !== null && active_group !== undefined) {
await openGroupById(String(active_group));
if (active_character) {
console.warn('Active character and active group are both set. Only active character will be loaded. Resetting active group.');
setActiveGroup(null);
saveSettingsDebounced();
} else {
const result = await openGroupById(String(active_group));
if (!result) {
setActiveGroup(null);
saveSettingsDebounced();
console.warn(`Currently active group with ID ${active_group} not found. Resetting to no active group.`);
}
}
}
// if the character list hadn't been loaded yet, try again.

View File

@ -1487,7 +1487,7 @@ jQuery(function () {
...chat.filter(x => x?.extra?.type !== system_message_types.ASSISTANT_NOTE),
];
download(JSON.stringify(chatToSave, null, 4), `Assistant - ${humanizedDateTime()}.json`, 'application/json');
download(chatToSave.map((m) => JSON.stringify(m)).join('\n'), `Assistant - ${humanizedDateTime()}.jsonl`, 'application/json');
});
// Do not change. #attachFile is added by extension.

View File

@ -154,8 +154,18 @@ export const extension_settings = {
refine_mode: false,
},
expressions: {
/** @type {number} see `EXPRESSION_API` */
api: undefined,
/** @type {string[]} */
custom: [],
showDefault: false,
translate: false,
/** @type {string} */
fallback_expression: undefined,
/** @type {string} */
llmPrompt: undefined,
allowMultiple: true,
rerollIfSame: false,
},
connectionManager: {
selectedProfile: '',
@ -603,12 +613,12 @@ function generateExtensionHtml(name, manifest, isActive, isDisabled, isExternal,
}
let toggleElement = isActive || isDisabled ?
`<input type="checkbox" title="Click to toggle" data-name="${name}" class="${isActive ? 'toggle_disable' : 'toggle_enable'} ${checkboxClass}" ${isActive ? 'checked' : ''}>` :
'<input type="checkbox" title="' + t`Click to toggle` + `" data-name="${name}" class="${isActive ? 'toggle_disable' : 'toggle_enable'} ${checkboxClass}" ${isActive ? 'checked' : ''}>` :
`<input type="checkbox" title="Cannot enable extension" data-name="${name}" class="extension_missing ${checkboxClass}" disabled>`;
let deleteButton = isExternal ? `<button class="btn_delete menu_button" data-name="${externalId}" title="Delete"><i class="fa-fw fa-solid fa-trash-can"></i></button>` : '';
let deleteButton = isExternal ? `<button class="btn_delete menu_button" data-name="${externalId}" data-i18n="[title]Delete" title="Delete"><i class="fa-fw fa-solid fa-trash-can"></i></button>` : '';
let updateButton = isExternal ? `<button class="btn_update menu_button displayNone" data-name="${externalId}" title="Update available"><i class="fa-solid fa-download fa-fw"></i></button>` : '';
let moveButton = isExternal && isUserAdmin ? `<button class="btn_move menu_button" data-name="${externalId}" title="Move"><i class="fa-solid fa-folder-tree fa-fw"></i></button>` : '';
let moveButton = isExternal && isUserAdmin ? `<button class="btn_move menu_button" data-name="${externalId}" data-i18n="[title]Move" title="Move"><i class="fa-solid fa-folder-tree fa-fw"></i></button>` : '';
let modulesInfo = '';
if (isActive && Array.isArray(manifest.optional)) {
@ -616,7 +626,7 @@ function generateExtensionHtml(name, manifest, isActive, isDisabled, isExternal,
modules.forEach(x => optional.delete(x));
if (optional.size > 0) {
const optionalString = DOMPurify.sanitize([...optional].join(', '));
modulesInfo = `<div class="extension_modules">Optional modules: <span class="optional">${optionalString}</span></div>`;
modulesInfo = '<div class="extension_modules">' + t`Optional modules:` + ` <span class="optional">${optionalString}</span></div>`;
}
} else if (!isDisabled) { // Neither active nor disabled
const requirements = new Set(manifest.requires);

View File

@ -10,6 +10,7 @@ import { POPUP_TYPE, Popup, callGenericPopup } from '../../popup.js';
import { executeSlashCommands } from '../../slash-commands.js';
import { accountStorage } from '../../util/AccountStorage.js';
import { flashHighlight, getStringHash, isValidUrl } from '../../utils.js';
import { t } from '../../i18n.js';
export { MODULE_NAME };
const MODULE_NAME = 'assets';
@ -59,11 +60,11 @@ const KNOWN_TYPES = {
'blip': 'Blip sounds',
};
function downloadAssetsList(url) {
updateCurrentAssets().then(function () {
async function downloadAssetsList(url) {
updateCurrentAssets().then(async function () {
fetch(url, { cache: 'no-cache' })
.then(response => response.json())
.then(json => {
.then(async function(json) {
availableAssets = {};
$('#assets_menu').empty();
@ -84,10 +85,10 @@ function downloadAssetsList(url) {
$('#assets_type_select').empty();
$('#assets_search').val('');
$('#assets_type_select').append($('<option />', { value: '', text: 'All' }));
$('#assets_type_select').append($('<option />', { value: '', text: t`All` }));
for (const type of assetTypes) {
const option = $('<option />', { value: type, text: KNOWN_TYPES[type] || type });
const option = $('<option />', { value: type, text: t([KNOWN_TYPES[type] || type]) });
$('#assets_type_select').append(option);
}
@ -104,11 +105,7 @@ function downloadAssetsList(url) {
assetTypeMenu.append(`<h3>${KNOWN_TYPES[assetType] || assetType}</h3>`).hide();
if (assetType == 'extension') {
assetTypeMenu.append(`
<div class="assets-list-git">
To download extensions from this page, you need to have <a href="https://git-scm.com/downloads" target="_blank">Git</a> installed.<br>
Click the <i class="fa-solid fa-sm fa-arrow-up-right-from-square"></i> icon to visit the Extension's repo for tips on how to use it.
</div>`);
assetTypeMenu.append(await renderExtensionTemplateAsync('assets', 'installation'));
}
for (const i in availableAssets[assetType].sort((a, b) => a?.name && b?.name && a['name'].localeCompare(b['name']))) {
@ -184,7 +181,7 @@ function downloadAssetsList(url) {
const displayName = DOMPurify.sanitize(asset['name'] || asset['id']);
const description = DOMPurify.sanitize(asset['description'] || '');
const url = isValidUrl(asset['url']) ? asset['url'] : '';
const title = assetType === 'extension' ? `Extension repo/guide: ${url}` : 'Preview in browser';
const title = assetType === 'extension' ? t`Extension repo/guide:` + ` ${url}` : t`Preview in browser`;
const previewIcon = (assetType === 'extension' || assetType === 'character') ? 'fa-arrow-up-right-from-square' : 'fa-headphones-simple';
const toolTag = assetType === 'extension' && asset['tool'];
@ -195,9 +192,10 @@ function downloadAssetsList(url) {
<b>${displayName}</b>
<a class="asset_preview" href="${url}" target="_blank" title="${title}">
<i class="fa-solid fa-sm ${previewIcon}"></i>
</a>
${toolTag ? '<span class="tag" title="Adds a function tool"><i class="fa-solid fa-sm fa-wrench"></i> Tool</span>' : ''}
</span>
</a>` +
(toolTag ? '<span class="tag" title="' + t`Adds a function tool` + '"><i class="fa-solid fa-sm fa-wrench"></i> ' +
t`Tool` + '</span>' : '') +
`</span>
<small class="asset-description">
${description}
</small>
@ -435,7 +433,7 @@ jQuery(async () => {
const rememberKey = `Assets_SkipConfirm_${getStringHash(url)}`;
const skipConfirm = accountStorage.getItem(rememberKey) === 'true';
const confirmation = skipConfirm || await Popup.show.confirm('Loading Asset List', `<span>Are you sure you want to connect to the following url?</span><var>${url}</var>`, {
const confirmation = skipConfirm || await Popup.show.confirm(t`Loading Asset List`, '<span>' + t`Are you sure you want to connect to the following url?` + `</span><var>${url}</var>`, {
customInputs: [{ id: 'assets-remember', label: 'Don\'t ask again for this URL' }],
onClose: popup => {
if (popup.result) {

View File

@ -0,0 +1,4 @@
<div class="assets-list-git">
<span data-i18n="extension_install_1">To download extensions from this page, you need to have </span><a href="https://git-scm.com/downloads" target="_blank">Git</a><span data-i18n="extension_install_2"> installed.</span><br>
<span data-i18n="extension_install_3">Click the </span><i class="fa-solid fa-sm fa-arrow-up-right-from-square"></i><span data-i18n="extension_install_4"> icon to visit the Extension's repo for tips on how to use it.</span>
</div>

View File

@ -33,7 +33,7 @@ To install a single 3rd party extension, use the &quot;Install Extensions&quot;
<div id="assets_filters" class="flex-container">
<select id="assets_type_select" class="text_pole flex1">
</select>
<input id="assets_search" class="text_pole flex1" placeholder="Search" type="search">
<input id="assets_search" class="text_pole flex1" data-i18n="[placeholder]Search" placeholder="Search" type="search">
<div id="assets-characters-button" class="menu_button menu_button_icon">
<i class="fa-solid fa-image-portrait"></i>
<span data-i18n="Characters">Characters</span>

File diff suppressed because it is too large Load Diff

View File

@ -1,4 +1,5 @@
<div id="{{item}}" class="expression_list_item">
{{#each images}}
<div class="expression_list_item interactable" data-expression="{{../expression}}" data-expression-type="{{this.type}}" data-filename="{{this.fileName}}">
<div class="expression_list_buttons">
<div class="menu_button expression_list_upload" title="Upload image">
<i class="fa-solid fa-upload"></i>
@ -7,11 +8,14 @@
<i class="fa-solid fa-trash"></i>
</div>
</div>
<div class="expression_list_title {{textClass}}">
<span>{{item}}</span>
{{#if isCustom}}
<div class="expression_list_title">
<span>{{../expression}}</span>
{{#if ../isCustom}}
<small class="expression_list_custom">(custom)</small>
{{/if}}
</div>
<img class="expression_list_image" src="{{imageSrc}}" />
<div class="expression_list_image_container" title="{{this.title}}">
<img class="expression_list_image" src="{{this.imageSrc}}" alt="{{this.title}}" data-epression="{{../expression}}" />
</div>
</div>
{{/each}}

View File

@ -6,17 +6,17 @@
</div>
<div class="inline-drawer-content">
<label class="checkbox_label" for="expression_translate" title="Use the selected API from Chat Translation extension settings.">
<label class="checkbox_label" for="expression_translate" title="Use the selected API from Chat Translation extension settings." data-i18n="[title]Use the selected API from Chat Translation extension settings.">
<input id="expression_translate" type="checkbox">
<span data-i18n="Translate text to English before classification">Translate text to English before classification</span>
</label>
<label class="checkbox_label" for="expressions_show_default">
<input id="expressions_show_default" type="checkbox">
<span data-i18n="Show default images (emojis) if sprite missing">Show default images (emojis) if sprite missing</span>
<label class="checkbox_label" for="expressions_allow_multiple" title="A single expression can have multiple sprites. Whenever the expression is chosen, a random sprite for this expression will be selected." data-i18n="[title]A single expression can have multiple sprites. Whenever the expression is chosen, a random sprite for this expression will be selected.">
<input id="expressions_allow_multiple" type="checkbox">
<span data-i18n="Allow multiple sprites per expression">Allow multiple sprites per expression</span>
</label>
<label id="image_type_block" class="checkbox_label" for="image_type_toggle">
<input id="image_type_toggle" type="checkbox">
<span data-i18n="Image Type - talkinghead (extras)">Image Type - talkinghead (extras)</span>
<label class="checkbox_label" for="expressions_reroll_if_same" title="If the same expression is used again, re-roll the sprite. This only applies to expressions that have multiple available sprites assigned." data-i18n="[title]If the same expression is used again, re-roll the sprite. This only applies to expressions that have multiple available sprites assigned.">
<input id="expressions_reroll_if_same" type="checkbox">
<span data-i18n="Re-roll if same expression is used again">Re-roll if same sprite is used again</span>
</label>
<div class="expression_api_block m-b-1 m-t-1">
<label for="expression_api" data-i18n="Classifier API">Classifier API</label>
@ -75,8 +75,20 @@
<span data-i18n="Remove all image overrides">Remove all image overrides</span>
</div>
</div>
<p class="hint"><b data-i18n="Hint:">Hint:</b> <i><span data-i18n="Create new folder in the _space">Create new folder in the </span><b>/characters/</b> <span data-i18n="folder of your user data directory and name it as the name of the character.">folder of your user data directory and name it as the name of the character.</span>
<span data-i18n="Put images with expressions there. File names should follow the pattern:">Put images with expressions there. File names should follow the pattern: </span><tt data-i18n="expression_label_pattern">[expression_label].[image_format]</tt></i></p>
<p class="hint">
<b data-i18n="Hint:">Hint:</b>
<i>
<span data-i18n="Create new folder in the _space">Create new folder in the </span><b>/characters/</b> <span data-i18n="folder of your user data directory and name it as the name of the character.">folder of your user data directory and name it as the name of the character.</span>
<span data-i18n="Put images with expressions there. File names should follow the pattern:">Put images with expressions there. File names should follow the pattern: </span><tt data-i18n="expression_label_pattern">[expression_label].[image_format]</tt>
</i>
</p>
<p>
<i>
<span>In case of multiple files per expression, file names can contain a suffix, either separated by a dot or a
dash.
Examples: </span><tt>joy.png</tt>, <tt>joy-1.png</tt>, <tt>joy.expressive.png</tt>
</i>
</p>
<h3 id="image_list_header">
<strong data-i18n="Sprite set:">Sprite set:</strong>&nbsp;<span id="image_list_header_name"></span>
</h3>

View File

@ -111,6 +111,10 @@ img.expression.default {
justify-content: center;
}
.expression_list_image_container {
overflow: hidden;
}
.expression_list_title {
position: absolute;
bottom: 0;
@ -126,6 +130,9 @@ img.expression.default {
flex-direction: column;
line-height: 1;
}
.expression_list_custom {
font-size: 0.66rem;
}
.expression_list_buttons {
position: absolute;
@ -162,11 +169,24 @@ img.expression.default {
row-gap: 1rem;
}
#image_list .success {
#image_list .expression_list_item[data-expression-type="success"] .expression_list_title {
color: green;
}
#image_list .failure {
#image_list .expression_list_item[data-expression-type="additional"] .expression_list_title {
color: darkolivegreen;
}
#image_list .expression_list_item[data-expression-type="additional"] .expression_list_title::before {
content: '';
position: absolute;
top: -7px;
left: -9px;
font-size: 14px;
color: transparent;
text-shadow: 0 0 0 darkolivegreen;
}
#image_list .expression_list_item[data-expression-type="failure"] .expression_list_title {
color: red;
}
@ -189,3 +209,12 @@ img.expression.default {
flex-direction: row;
}
#expressions_container:has(#expressions_allow_multiple:not(:checked)) #image_list .expression_list_item[data-expression-type="additional"],
#expressions_container:has(#expressions_allow_multiple:not(:checked)) label[for="expressions_reroll_if_same"] {
opacity: 0.3;
transition: opacity var(--animation-duration) ease;
}
#expressions_container:has(#expressions_allow_multiple:not(:checked)) #image_list .expression_list_item[data-expression-type="additional"]:hover,
#expressions_container:has(#expressions_allow_multiple:not(:checked)) #image_list .expression_list_item[data-expression-type="additional"]:focus {
opacity: unset;
}

View File

@ -0,0 +1,12 @@
<div class="m-b-1" data-i18n="upload_expression_request">Please enter a name for the sprite (without extension).</div>
<div class="m-b-1" data-i18n="upload_expression_naming_1">
Sprite names must follow the naming schema for the selected expression: {{expression}}
</div>
<div data-i18n="upload_expression_naming_2">
For multiple expressions, the name must follow the expression name and a valid suffix. Allowed separators are '-' or dot '.'.
</div>
<span class="m-b-1" data-i18n="Examples:">Examples:</span> <tt>{{expression}}.png</tt>, <tt>{{expression}}-1.png</tt>, <tt>{{expression}}.expressive.png</tt>
{{#if clickedFileName}}
<div class="m-t-1" data-i18n="upload_expression_replace">Click 'Replace' to replace the existing expression:</div>
<tt>{{clickedFileName}}</tt>
{{/if}}

View File

@ -6,6 +6,8 @@ import { getFriendlyTokenizerName, getTextTokens, getTokenCountAsync, tokenizers
import { resetScrollHeight, debounce } from '../../utils.js';
import { debounce_timeout } from '../../constants.js';
import { POPUP_TYPE, callGenericPopup } from '../../popup.js';
import { renderExtensionTemplateAsync } from '../../extensions.js';
import { t } from '../../i18n.js';
function rgb2hex(rgb) {
rgb = rgb.match(/^rgba?[\s+]?\([\s+]?(\d+)[\s+]?,[\s+]?(\d+)[\s+]?,[\s+]?(\d+)[\s+]?/i);
@ -22,23 +24,7 @@ $('button').click(function () {
async function doTokenCounter() {
const { tokenizerName, tokenizerId } = getFriendlyTokenizerName(main_api);
const html = `
<div class="wide100p">
<h3>Token Counter</h3>
<div class="justifyLeft flex-container flexFlowColumn">
<h4>Type / paste in the box below to see the number of tokens in the text.</h4>
<p>Selected tokenizer: ${tokenizerName}</p>
<div>Input:</div>
<textarea id="token_counter_textarea" class="wide100p textarea_compact" rows="1"></textarea>
<div>Tokens: <span id="token_counter_result">0</span></div>
<hr>
<div>Tokenized text:</div>
<div id="tokenized_chunks_display" class="wide100p"></div>
<hr>
<div>Token IDs:</div>
<textarea id="token_counter_ids" class="wide100p textarea_compact" readonly rows="1"></textarea>
</div>
</div>`;
const html = await renderExtensionTemplateAsync('token-counter', 'window', {tokenizerName});
const dialog = $(html);
const countDebounced = debounce(async () => {
@ -131,9 +117,9 @@ async function doCount() {
jQuery(() => {
const buttonHtml = `
<div id="token_counter" class="list-group-item flex-container flexGap5">
<div class="fa-solid fa-1 extensionsMenuExtensionButton" /></div>
Token Counter
</div>`;
<div class="fa-solid fa-1 extensionsMenuExtensionButton" /></div>` +
t`Token Counter` +
'</div>';
$('#token_counter_wand_container').append(buttonHtml);
$('#token_counter').on('click', doTokenCounter);
SlashCommandParser.addCommandObject(SlashCommand.fromProps({

View File

@ -0,0 +1,16 @@
<div class="wide100p">
<h3 data-i18n="Token Counter">Token Counter</h3>
<div class="justifyLeft flex-container flexFlowColumn">
<h4 data-i18n="Type / paste in the box below to see the number of tokens in the text.">Type / paste in the box below to see the number of tokens in the text.</h4>
<p><span data-i18n="Selected tokenizer:">Selected tokenizer:</span> {{tokenizerName}}</p>
<div data-i18n="Input:">Input:</div>
<textarea id="token_counter_textarea" class="wide100p textarea_compact" rows="1"></textarea>
<div><span data-i18n="Tokens:">Tokens:</span> <span id="token_counter_result">0</span></div>
<hr>
<div data-i18n="Tokenized text:">Tokenized text:</div>
<div id="tokenized_chunks_display" class="wide100p"></div>
<hr>
<div data-i18n="Token IDs:">Token IDs:</div>
<textarea id="token_counter_ids" class="wide100p textarea_compact" readonly rows="1"></textarea>
</div>
</div>

View File

@ -605,7 +605,7 @@ const handleOutgoingMessage = createEventHandler(translateOutgoingMessage, () =>
const handleImpersonateReady = createEventHandler(translateImpersonate, () => shouldTranslate(incomingTypes));
const handleMessageEdit = createEventHandler(translateMessageEdit, () => true);
window['translate'] = translate;
globalThis.translate = translate;
jQuery(async () => {
const html = await renderExtensionTemplateAsync('translate', 'index');

View File

@ -27,14 +27,12 @@ import { SlashCommandEnumValue, enumTypes } from '../../slash-commands/SlashComm
import { enumIcons } from '../../slash-commands/SlashCommandCommonEnumsProvider.js';
import { POPUP_TYPE, callGenericPopup } from '../../popup.js';
import { GoogleTranslateTtsProvider } from './google-translate.js';
export { talkingAnimation };
const UPDATE_INTERVAL = 1000;
const wrapper = new ModuleWorkerWrapper(moduleWorker);
let voiceMapEntries = [];
let voiceMap = {}; // {charName:voiceid, charName2:voiceid2}
let talkingHeadState = false;
let lastChatId = null;
let lastMessage = null;
let lastMessageHash = null;
@ -166,27 +164,6 @@ async function moduleWorker() {
updateUiAudioPlayState();
}
function talkingAnimation(switchValue) {
if (!modules.includes('talkinghead')) {
console.debug('Talking Animation module not loaded');
return;
}
const apiUrl = getApiUrl();
const animationType = switchValue ? 'start' : 'stop';
if (switchValue !== talkingHeadState) {
try {
console.log(animationType + ' Talking Animation');
doExtrasFetch(`${apiUrl}/api/talkinghead/${animationType}_talking`);
talkingHeadState = switchValue;
} catch (error) {
// Handle the error here or simply ignore it to prevent logging
}
}
updateUiAudioPlayState();
}
function resetTtsPlayback() {
// Stop system TTS utterance
cancelTtsPlay();
@ -378,7 +355,6 @@ function onAudioControlClicked() {
// Not pausing, doing a full stop to anything TTS is doing. Better UX as pause is not as useful
if (!audioElement.paused || isTtsProcessing()) {
resetTtsPlayback();
talkingAnimation(false);
} else {
// Default play behavior if not processing or playing is to play the last message.
processAndQueueTtsMessage(context.chat[context.chat.length - 1]);
@ -405,7 +381,6 @@ function addAudioControl() {
function completeCurrentAudioJob() {
audioQueueProcessorReady = true;
currentAudioJob = null;
talkingAnimation(false); //stop lip animation
// updateUiPlayState();
wrapper.update();
}
@ -436,7 +411,6 @@ async function processAudioJobQueue() {
audioQueueProcessorReady = false;
currentAudioJob = audioJobQueue.shift();
playAudioData(currentAudioJob);
talkingAnimation(true);
} catch (error) {
toastr.error(error.toString());
console.error(error);

View File

@ -25,7 +25,7 @@ class OpenAICompatibleTtsProvider {
<label for="openai_compatible_tts_endpoint">Provider Endpoint:</label>
<div class="flex-container alignItemsCenter">
<div class="flex1">
<input id="openai_compatible_tts_endpoint" type="text" class="text_pole" maxlength="250" value="${this.defaultSettings.provider_endpoint}"/>
<input id="openai_compatible_tts_endpoint" type="text" class="text_pole" maxlength="500" value="${this.defaultSettings.provider_endpoint}"/>
</div>
<div id="openai_compatible_tts_key" class="menu_button menu_button_icon">
<i class="fa-solid fa-key"></i>
@ -33,9 +33,9 @@ class OpenAICompatibleTtsProvider {
</div>
</div>
<label for="openai_compatible_model">Model:</label>
<input id="openai_compatible_model" type="text" class="text_pole" maxlength="250" value="${this.defaultSettings.model}"/>
<input id="openai_compatible_model" type="text" class="text_pole" maxlength="500" value="${this.defaultSettings.model}"/>
<label for="openai_compatible_tts_voices">Available Voices (comma separated):</label>
<input id="openai_compatible_tts_voices" type="text" class="text_pole" maxlength="250" value="${this.defaultSettings.available_voices.join()}"/>
<input id="openai_compatible_tts_voices" type="text" class="text_pole" value="${this.defaultSettings.available_voices.join()}"/>
<label for="openai_compatible_tts_speed">Speed: <span id="openai_compatible_tts_speed_output"></span></label>
<input type="range" id="openai_compatible_tts_speed" value="1" min="0.25" max="4" step="0.05">`;
return html;

View File

@ -1,6 +1,5 @@
import { isMobile } from '../../RossAscends-mods.js';
import { getPreviewString } from './index.js';
import { talkingAnimation } from './index.js';
import { saveTtsProviderSettings } from './index.js';
export { SystemTtsProvider };
@ -70,7 +69,6 @@ var speechUtteranceChunker = function (utt, settings, callback) {
//placing the speak invocation inside a callback fixes ordering and onend issues.
setTimeout(function () {
speechSynthesis.speak(newUtt);
talkingAnimation(true);
}, 0);
};
@ -240,7 +238,6 @@ class SystemTtsProvider {
//some code to execute when done
resolve(silence);
console.log('System TTS done');
talkingAnimation(false);
});
});
}

View File

@ -561,9 +561,9 @@ async function retrieveFileChunks(queryText, collectionId) {
*/
async function vectorizeFile(fileText, fileName, collectionId, chunkSize, overlapPercent) {
try {
if (settings.translate_files && typeof window['translate'] === 'function') {
if (settings.translate_files && typeof globalThis.translate === 'function') {
console.log(`Vectors: Translating file ${fileName} to English...`);
const translatedText = await window['translate'](fileText, 'en');
const translatedText = await globalThis.translate(fileText, 'en');
fileText = translatedText;
}
@ -745,6 +745,44 @@ async function getQueryText(chat, initiator) {
return collapseNewlines(queryText).trim();
}
/**
* Gets common body parameters for vector requests.
* @returns {object}
*/
function getVectorsRequestBody() {
const body = {};
switch (settings.source) {
case 'extras':
body.extrasUrl = extension_settings.apiUrl;
body.extrasKey = extension_settings.apiKey;
break;
case 'togetherai':
body.model = extension_settings.vectors.togetherai_model;
break;
case 'openai':
body.model = extension_settings.vectors.openai_model;
break;
case 'cohere':
body.model = extension_settings.vectors.cohere_model;
break;
case 'ollama':
body.model = extension_settings.vectors.ollama_model;
body.apiUrl = textgenerationwebui_settings.server_urls[textgen_types.OLLAMA];
body.keep = !!extension_settings.vectors.ollama_keep;
break;
case 'llamacpp':
body.apiUrl = textgenerationwebui_settings.server_urls[textgen_types.LLAMACPP];
break;
case 'vllm':
body.apiUrl = textgenerationwebui_settings.server_urls[textgen_types.VLLM];
body.model = extension_settings.vectors.vllm_model;
break;
default:
break;
}
return body;
}
/**
* Gets the saved hashes for a collection
* @param {string} collectionId
@ -753,8 +791,9 @@ async function getQueryText(chat, initiator) {
async function getSavedHashes(collectionId) {
const response = await fetch('/api/vector/list', {
method: 'POST',
headers: getVectorHeaders(),
headers: getRequestHeaders(),
body: JSON.stringify({
...getVectorsRequestBody(),
collectionId: collectionId,
source: settings.source,
}),
@ -768,54 +807,6 @@ async function getSavedHashes(collectionId) {
return hashes;
}
function getVectorHeaders() {
const headers = getRequestHeaders();
switch (settings.source) {
case 'extras':
Object.assign(headers, {
'X-Extras-Url': extension_settings.apiUrl,
'X-Extras-Key': extension_settings.apiKey,
});
break;
case 'togetherai':
Object.assign(headers, {
'X-Togetherai-Model': extension_settings.vectors.togetherai_model,
});
break;
case 'openai':
Object.assign(headers, {
'X-OpenAI-Model': extension_settings.vectors.openai_model,
});
break;
case 'cohere':
Object.assign(headers, {
'X-Cohere-Model': extension_settings.vectors.cohere_model,
});
break;
case 'ollama':
Object.assign(headers, {
'X-Ollama-Model': extension_settings.vectors.ollama_model,
'X-Ollama-URL': textgenerationwebui_settings.server_urls[textgen_types.OLLAMA],
'X-Ollama-Keep': !!extension_settings.vectors.ollama_keep,
});
break;
case 'llamacpp':
Object.assign(headers, {
'X-LlamaCpp-URL': textgenerationwebui_settings.server_urls[textgen_types.LLAMACPP],
});
break;
case 'vllm':
Object.assign(headers, {
'X-Vllm-URL': textgenerationwebui_settings.server_urls[textgen_types.VLLM],
'X-Vllm-Model': extension_settings.vectors.vllm_model,
});
break;
default:
break;
}
return headers;
}
/**
* Inserts vector items into a collection
* @param {string} collectionId - The collection to insert into
@ -825,12 +816,11 @@ function getVectorHeaders() {
async function insertVectorItems(collectionId, items) {
throwIfSourceInvalid();
const headers = getVectorHeaders();
const response = await fetch('/api/vector/insert', {
method: 'POST',
headers: headers,
headers: getRequestHeaders(),
body: JSON.stringify({
...getVectorsRequestBody(),
collectionId: collectionId,
items: items,
source: settings.source,
@ -879,8 +869,9 @@ function throwIfSourceInvalid() {
async function deleteVectorItems(collectionId, hashes) {
const response = await fetch('/api/vector/delete', {
method: 'POST',
headers: getVectorHeaders(),
headers: getRequestHeaders(),
body: JSON.stringify({
...getVectorsRequestBody(),
collectionId: collectionId,
hashes: hashes,
source: settings.source,
@ -899,12 +890,11 @@ async function deleteVectorItems(collectionId, hashes) {
* @returns {Promise<{ hashes: number[], metadata: object[]}>} - Hashes of the results
*/
async function queryCollection(collectionId, searchText, topK) {
const headers = getVectorHeaders();
const response = await fetch('/api/vector/query', {
method: 'POST',
headers: headers,
headers: getRequestHeaders(),
body: JSON.stringify({
...getVectorsRequestBody(),
collectionId: collectionId,
searchText: searchText,
topK: topK,
@ -929,12 +919,11 @@ async function queryCollection(collectionId, searchText, topK) {
* @returns {Promise<Record<string, { hashes: number[], metadata: object[] }>>} - Results mapped to collection IDs
*/
async function queryMultipleCollections(collectionIds, searchText, topK, threshold) {
const headers = getVectorHeaders();
const response = await fetch('/api/vector/query-multi', {
method: 'POST',
headers: headers,
headers: getRequestHeaders(),
body: JSON.stringify({
...getVectorsRequestBody(),
collectionIds: collectionIds,
searchText: searchText,
topK: topK,
@ -965,8 +954,9 @@ async function purgeFileVectorIndex(fileUrl) {
const response = await fetch('/api/vector/purge', {
method: 'POST',
headers: getVectorHeaders(),
headers: getRequestHeaders(),
body: JSON.stringify({
...getVectorsRequestBody(),
collectionId: collectionId,
}),
});
@ -994,8 +984,9 @@ async function purgeVectorIndex(collectionId) {
const response = await fetch('/api/vector/purge', {
method: 'POST',
headers: getVectorHeaders(),
headers: getRequestHeaders(),
body: JSON.stringify({
...getVectorsRequestBody(),
collectionId: collectionId,
}),
});
@ -1019,7 +1010,10 @@ async function purgeAllVectorIndexes() {
try {
const response = await fetch('/api/vector/purge-all', {
method: 'POST',
headers: getVectorHeaders(),
headers: getRequestHeaders(),
body: JSON.stringify({
...getVectorsRequestBody(),
}),
});
if (!response.ok) {

View File

@ -1664,12 +1664,12 @@ function updateFavButtonState(state) {
export async function openGroupById(groupId) {
if (isChatSaving) {
toastr.info(t`Please wait until the chat is saved before switching characters.`, t`Your chat is still saving...`);
return;
return false;
}
if (!groups.find(x => x.id === groupId)) {
console.log('Group not found', groupId);
return;
return false;
}
if (!is_send_press && !is_group_generating) {
@ -1686,8 +1686,11 @@ export async function openGroupById(groupId) {
updateChatMetadata({}, true);
chat.length = 0;
await getGroupChat(groupId);
return true;
}
}
return false;
}
function openCharacterDefinition(characterSelect) {

View File

@ -2167,6 +2167,14 @@ function getStreamingReply(data, state) {
state.reasoning += (data.choices?.filter(x => x?.delta?.reasoning)?.[0]?.delta?.reasoning || '');
}
return data.choices?.[0]?.delta?.content ?? data.choices?.[0]?.message?.content ?? data.choices?.[0]?.text ?? '';
} else if (oai_settings.chat_completion_source === chat_completion_sources.CUSTOM) {
if (oai_settings.show_thoughts) {
state.reasoning +=
data.choices?.filter(x => x?.delta?.reasoning_content)?.[0]?.delta?.reasoning_content ??
data.choices?.filter(x => x?.delta?.reasoning)?.[0]?.delta?.reasoning ??
'';
}
return data.choices?.[0]?.delta?.content ?? data.choices?.[0]?.message?.content ?? data.choices?.[0]?.text ?? '';
} else {
return data.choices?.[0]?.delta?.content ?? data.choices?.[0]?.message?.content ?? data.choices?.[0]?.text ?? '';
}
@ -4107,6 +4115,40 @@ function getMaxContextWindowAI(value) {
}
}
/**
* Get the maximum context size for the Groq model
* @param {string} model Model identifier
* @param {boolean} isUnlocked Whether context limits are unlocked
* @returns {number} Maximum context size in tokens
*/
function getGroqMaxContext(model, isUnlocked) {
if (isUnlocked) {
return unlocked_max;
}
const contextMap = {
'gemma2-9b-it': max_8k,
'llama-3.3-70b-versatile': max_128k,
'llama-3.1-8b-instant': max_128k,
'llama3-70b-8192': max_8k,
'llama3-8b-8192': max_8k,
'llama-guard-3-8b': max_8k,
'mixtral-8x7b-32768': max_32k,
'deepseek-r1-distill-llama-70b': max_128k,
'llama-3.3-70b-specdec': max_8k,
'llama-3.2-1b-preview': max_128k,
'llama-3.2-3b-preview': max_128k,
'llama-3.2-11b-vision-preview': max_128k,
'llama-3.2-90b-vision-preview': max_128k,
'qwen-2.5-32b': max_128k,
'deepseek-r1-distill-qwen-32b': max_128k,
'deepseek-r1-distill-llama-70b-specdec': max_128k,
};
// Return context size if model found, otherwise default to 128k
return Object.entries(contextMap).find(([key]) => model.includes(key))?.[1] || max_128k;
}
async function onModelChange() {
biasCache = undefined;
let value = String($(this).val() || '');
@ -4387,7 +4429,7 @@ async function onModelChange() {
if (oai_settings.max_context_unlocked) {
$('#openai_max_context').attr('max', unlocked_max);
}
else if (['sonar', 'sonar-reasoning'].includes(oai_settings.perplexity_model)) {
else if (['sonar', 'sonar-reasoning', 'sonar-reasoning-pro', 'r1-1776'].includes(oai_settings.perplexity_model)) {
$('#openai_max_context').attr('max', 127000);
}
else if (['sonar-pro'].includes(oai_settings.perplexity_model)) {
@ -4408,33 +4450,8 @@ async function onModelChange() {
}
if (oai_settings.chat_completion_source == chat_completion_sources.GROQ) {
if (oai_settings.max_context_unlocked) {
$('#openai_max_context').attr('max', unlocked_max);
} else if (oai_settings.groq_model.includes('gemma2-9b-it')) {
$('#openai_max_context').attr('max', max_8k);
} else if (oai_settings.groq_model.includes('llama-3.3-70b-versatile')) {
$('#openai_max_context').attr('max', max_128k);
} else if (oai_settings.groq_model.includes('llama-3.1-8b-instant')) {
$('#openai_max_context').attr('max', max_128k);
} else if (oai_settings.groq_model.includes('llama3-70b-8192')) {
$('#openai_max_context').attr('max', max_8k);
} else if (oai_settings.groq_model.includes('llama3-8b-8192')) {
$('#openai_max_context').attr('max', max_8k);
} else if (oai_settings.groq_model.includes('mixtral-8x7b-32768')) {
$('#openai_max_context').attr('max', max_32k);
} else if (oai_settings.groq_model.includes('deepseek-r1-distill-llama-70b')) {
$('#openai_max_context').attr('max', max_128k);
} else if (oai_settings.groq_model.includes('llama-3.3-70b-specdec')) {
$('#openai_max_context').attr('max', max_8k);
} else if (oai_settings.groq_model.includes('llama-3.2-1b-preview')) {
$('#openai_max_context').attr('max', max_128k);
} else if (oai_settings.groq_model.includes('llama-3.2-3b-preview')) {
$('#openai_max_context').attr('max', max_128k);
} else if (oai_settings.groq_model.includes('llama-3.2-11b-vision-preview')) {
$('#openai_max_context').attr('max', max_128k);
} else if (oai_settings.groq_model.includes('llama-3.2-90b-vision-preview')) {
$('#openai_max_context').attr('max', max_128k);
}
const maxContext = getGroqMaxContext(oai_settings.groq_model, oai_settings.max_context_unlocked);
$('#openai_max_context').attr('max', maxContext);
oai_settings.openai_max_context = Math.min(Number($('#openai_max_context').attr('max')), oai_settings.openai_max_context);
$('#openai_max_context').val(oai_settings.openai_max_context).trigger('input');
oai_settings.temp_openai = Math.min(oai_max_temp, oai_settings.temp_openai);

View File

@ -1845,14 +1845,15 @@ async function loadContextSettings() {
/**
* Common function to perform fuzzy search with optional caching
* @template T
* @param {string} type - Type of search from fuzzySearchCategories
* @param {any[]} data - Data array to search in
* @param {Array<{name: string, weight: number, getFn?: (obj: any) => string}>} keys - Fuse.js keys configuration
* @param {T[]} data - Data array to search in
* @param {Array<{name: string, weight: number, getFn?: (obj: T) => string}>} keys - Fuse.js keys configuration
* @param {string} searchValue - The search term
* @param {Object.<string, { resultMap: Map<string, any> }>} [fuzzySearchCaches=null] - Optional fuzzy search caches
* @returns {import('fuse.js').FuseResult<any>[]} Results as items with their score
* @returns {import('fuse.js').FuseResult<T>[]} Results as items with their score
*/
function performFuzzySearch(type, data, keys, searchValue, fuzzySearchCaches = null) {
export function performFuzzySearch(type, data, keys, searchValue, fuzzySearchCaches = null) {
// Check cache if provided
if (fuzzySearchCaches) {
const cache = fuzzySearchCaches[type];

View File

@ -1,19 +1,32 @@
import {
moment,
} from '../lib.js';
import { chat, closeMessageEditor, event_types, eventSource, main_api, messageFormatting, saveChatConditional, saveSettingsDebounced, substituteParams, updateMessageBlock } from '../script.js';
import { chat, closeMessageEditor, event_types, eventSource, main_api, messageFormatting, saveChatConditional, saveChatDebounced, saveSettingsDebounced, substituteParams, updateMessageBlock } from '../script.js';
import { getRegexedString, regex_placement } from './extensions/regex/engine.js';
import { getCurrentLocale, t } from './i18n.js';
import { getCurrentLocale, t, translate } from './i18n.js';
import { MacrosParser } from './macros.js';
import { chat_completion_sources, getChatCompletionModel, oai_settings } from './openai.js';
import { Popup } from './popup.js';
import { power_user } from './power-user.js';
import { SlashCommand } from './slash-commands/SlashCommand.js';
import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from './slash-commands/SlashCommandArgument.js';
import { commonEnumProviders } from './slash-commands/SlashCommandCommonEnumsProvider.js';
import { commonEnumProviders, enumIcons } from './slash-commands/SlashCommandCommonEnumsProvider.js';
import { enumTypes, SlashCommandEnumValue } from './slash-commands/SlashCommandEnumValue.js';
import { SlashCommandParser } from './slash-commands/SlashCommandParser.js';
import { textgen_types, textgenerationwebui_settings } from './textgen-settings.js';
import { copyText, escapeRegex, isFalseBoolean, setDatasetProperty } from './utils.js';
import { copyText, escapeRegex, isFalseBoolean, setDatasetProperty, trimSpaces } from './utils.js';
/**
* Enum representing the type of the reasoning for a message (where it came from)
* @enum {string}
* @readonly
*/
export const ReasoningType = {
Model: 'model',
Parsed: 'parsed',
Manual: 'manual',
Edited: 'edited',
};
/**
* Gets a message from a jQuery element.
@ -63,6 +76,11 @@ export function extractReasoningFromData(data) {
return data?.choices?.[0]?.message?.reasoning ?? '';
case chat_completion_sources.MAKERSUITE:
return data?.responseContent?.parts?.filter(part => part.thought)?.map(part => part.text)?.join('\n\n') ?? '';
case chat_completion_sources.CUSTOM: {
return data?.choices?.[0]?.message?.reasoning_content
?? data?.choices?.[0]?.message?.reasoning
?? '';
}
}
break;
}
@ -94,7 +112,7 @@ export function isHiddenReasoningModel() {
{ name: 'gemini-2.0-pro-exp', func: FUNCS.startsWith },
];
const model = getChatCompletionModel();
const model = getChatCompletionModel() || '';
const isHidden = hiddenReasoningModels.some(({ name, func }) => func(model, name));
return isHidden;
@ -129,7 +147,12 @@ export const ReasoningState = {
* This class is used inside the {@link StreamingProcessor} to manage reasoning states and UI updates.
*/
export class ReasoningHandler {
/** @type {boolean} True if the model supports reasoning, but hides the reasoning output */
#isHiddenReasoningModel;
/** @type {boolean} True if the handler is currently handling a manual parse of reasoning blocks */
#isParsingReasoning = false;
/** @type {number?} When reasoning is being parsed manually, and the reasoning has ended, this will be the index at which the actual messages starts */
#parsingReasoningMesStartIndex = null;
/**
* @param {Date?} [timeStarted=null] - When the generation started
@ -137,6 +160,8 @@ export class ReasoningHandler {
constructor(timeStarted = null) {
/** @type {ReasoningState} The current state of the reasoning process */
this.state = ReasoningState.None;
/** @type {ReasoningType?} The type of the reasoning (where it came from) */
this.type = null;
/** @type {string} The reasoning output */
this.reasoning = '';
/** @type {Date} When the reasoning started */
@ -147,7 +172,6 @@ export class ReasoningHandler {
/** @type {Date} Initial starting time of the generation */
this.initialTime = timeStarted ?? new Date();
/** @type {boolean} True if the model supports reasoning, but hides the reasoning output */
this.#isHiddenReasoningModel = isHiddenReasoningModel();
// Cached DOM elements for reasoning
@ -194,6 +218,7 @@ export class ReasoningHandler {
this.state = ReasoningState.Hidden;
}
this.type = extra?.reasoning_type;
this.reasoning = extra?.reasoning ?? '';
if (this.state !== ReasoningState.None) {
@ -208,6 +233,7 @@ export class ReasoningHandler {
// Make sure reset correctly clears all relevant states
if (reset) {
this.state = this.#isHiddenReasoningModel ? ReasoningState.Thinking : ReasoningState.None;
this.type = null;
this.reasoning = '';
this.initialTime = new Date();
this.startTime = null;
@ -237,18 +263,19 @@ export class ReasoningHandler {
* Updates the reasoning text/string for a message.
*
* @param {number} messageId - The ID of the message to update
* @param {string?} [reasoning=null] - The reasoning text to update - If null, uses the current reasoning
* @param {string?} [reasoning=null] - The reasoning text to update - If null or empty, uses the current reasoning
* @param {Object} [options={}] - Optional arguments
* @param {boolean} [options.persist=false] - Whether to persist the reasoning to the message object
* @param {boolean} [options.allowReset=false] - Whether to allow empty reasoning provided to reset the reasoning, instead of just taking the existing one
* @returns {boolean} - Returns true if the reasoning was changed, otherwise false
*/
updateReasoning(messageId, reasoning = null, { persist = false } = {}) {
updateReasoning(messageId, reasoning = null, { persist = false, allowReset = false } = {}) {
if (messageId == -1 || !chat[messageId]) {
return false;
}
reasoning = reasoning ?? this.reasoning;
reasoning = power_user.trim_spaces ? reasoning.trim() : reasoning;
reasoning = allowReset ? reasoning ?? this.reasoning : reasoning || this.reasoning;
reasoning = trimSpaces(reasoning);
// Ensure the chat extra exists
if (!chat[messageId].extra) {
@ -259,10 +286,13 @@ export class ReasoningHandler {
const reasoningChanged = extra.reasoning !== reasoning;
this.reasoning = getRegexedString(reasoning ?? '', regex_placement.REASONING);
this.type = (this.#isParsingReasoning || this.#parsingReasoningMesStartIndex) ? ReasoningType.Parsed : ReasoningType.Model;
if (persist) {
// Build and save the reasoning data to message extras
extra.reasoning = this.reasoning;
extra.reasoning_duration = this.getDuration();
extra.reasoning_type = (this.#isParsingReasoning || this.#parsingReasoningMesStartIndex) ? ReasoningType.Parsed : ReasoningType.Model;
}
return reasoningChanged;
@ -279,7 +309,10 @@ export class ReasoningHandler {
* @returns {Promise<void>}
*/
async process(messageId, mesChanged) {
if (!this.reasoning && !this.#isHiddenReasoningModel) return;
mesChanged = this.#autoParseReasoningFromMessage(messageId, mesChanged);
if (!this.reasoning && !this.#isHiddenReasoningModel)
return;
// Ensure reasoning string is updated and regexes are applied correctly
const reasoningChanged = this.updateReasoning(messageId, null, { persist: true });
@ -294,6 +327,54 @@ export class ReasoningHandler {
}
}
#autoParseReasoningFromMessage(messageId, mesChanged) {
if (!power_user.reasoning.auto_parse)
return;
if (!power_user.reasoning.prefix || !power_user.reasoning.suffix)
return mesChanged;
/** @type {{ mes: string, [key: string]: any}} */
const message = chat[messageId];
if (!message) return mesChanged;
// If we are done with reasoning parse, we just split the message correctly so the reasoning doesn't show up inside of it.
if (this.#parsingReasoningMesStartIndex) {
message.mes = trimSpaces(message.mes.slice(this.#parsingReasoningMesStartIndex));
return mesChanged;
}
if (this.state === ReasoningState.None || this.#isHiddenReasoningModel) {
// If streamed message starts with the opening, cut it out and put all inside reasoning
if (message.mes.startsWith(power_user.reasoning.prefix) && message.mes.length > power_user.reasoning.prefix.length) {
this.#isParsingReasoning = true;
// Manually set starting state here, as we might already have received the ending suffix
this.state = ReasoningState.Thinking;
this.startTime = this.startTime ?? this.initialTime;
this.endTime = null;
}
}
if (!this.#isParsingReasoning)
return mesChanged;
// If we are in manual parsing mode, all currently streaming mes tokens will go the the reasoning block
const originalMes = message.mes;
this.reasoning = originalMes.slice(power_user.reasoning.prefix.length);
message.mes = '';
// If the reasoning contains the ending suffix, we cut that off and continue as message streaming
if (this.reasoning.includes(power_user.reasoning.suffix)) {
this.reasoning = this.reasoning.slice(0, this.reasoning.indexOf(power_user.reasoning.suffix));
this.#parsingReasoningMesStartIndex = originalMes.indexOf(power_user.reasoning.suffix) + power_user.reasoning.suffix.length;
message.mes = trimSpaces(originalMes.slice(this.#parsingReasoningMesStartIndex));
this.#isParsingReasoning = false;
}
// Only return the original mesChanged value if we haven't cut off the complete message
return message.mes.length ? mesChanged : false;
}
/**
* Completes the reasoning process for a message.
*
@ -336,9 +417,10 @@ export class ReasoningHandler {
// Update states to the relevant DOM elements
setDatasetProperty(this.messageDom, 'reasoningState', this.state !== ReasoningState.None ? this.state : null);
setDatasetProperty(this.messageReasoningDetailsDom, 'state', this.state);
setDatasetProperty(this.messageReasoningDetailsDom, 'type', this.type);
// Update the reasoning message
const reasoning = power_user.trim_spaces ? this.reasoning.trim() : this.reasoning;
const reasoning = trimSpaces(this.reasoning);
const displayReasoning = messageFormatting(reasoning, '', false, false, messageId, {}, true);
this.messageReasoningContentDom.innerHTML = displayReasoning;
@ -393,17 +475,14 @@ export class ReasoningHandler {
const element = this.messageReasoningHeaderDom;
const duration = this.getDuration();
let data = null;
let title = '';
if (duration) {
const seconds = moment.duration(duration).asSeconds();
const durationStr = moment.duration(duration).locale(getCurrentLocale()).humanize({ s: 50, ss: 3 });
const secondsStr = moment.duration(duration).asSeconds();
const span = document.createElement('span');
span.title = t`${secondsStr} seconds`;
span.textContent = durationStr;
element.textContent = t`Thought for `;
element.appendChild(span);
data = String(secondsStr);
element.textContent = t`Thought for ${durationStr}`;
data = String(seconds);
title = `${seconds} seconds`;
} else if ([ReasoningState.Done, ReasoningState.Hidden].includes(this.state)) {
element.textContent = t`Thought for some time`;
data = 'unknown';
@ -412,6 +491,12 @@ export class ReasoningHandler {
data = null;
}
if (this.type !== ReasoningType.Model) {
title += ` [${translate(this.type)}]`;
title = title.trim();
}
element.title = title;
setDatasetProperty(this.messageReasoningDetailsDom, 'duration', data);
setDatasetProperty(element, 'duration', data);
}
@ -573,11 +658,16 @@ function registerReasoningSlashCommands() {
callback: async (args, value) => {
const messageId = !isNaN(Number(args.at)) ? Number(args.at) : chat.length - 1;
const message = chat[messageId];
if (!message?.extra) {
if (!message) {
return '';
}
// Make sure the message has an extra object
if (!message.extra || typeof message.extra !== 'object') {
message.extra = {};
}
message.extra.reasoning = String(value ?? '');
message.extra.reasoning_type = ReasoningType.Manual;
await saveChatConditional();
closeMessageEditor('reasoning');
@ -598,7 +688,26 @@ function registerReasoningSlashCommands() {
typeList: [ARGUMENT_TYPE.BOOLEAN],
defaultValue: 'true',
isRequired: false,
enumProvider: commonEnumProviders.boolean('trueFalse'),
enumList: commonEnumProviders.boolean('trueFalse')(),
}),
SlashCommandNamedArgument.fromProps({
name: 'return',
description: 'Whether to return the parsed reasoning or the content without reasoning',
typeList: [ARGUMENT_TYPE.STRING],
defaultValue: 'reasoning',
isRequired: false,
enumList: [
new SlashCommandEnumValue('reasoning', null, enumTypes.enum, enumIcons.reasoning),
new SlashCommandEnumValue('content', null, enumTypes.enum, enumIcons.message),
],
}),
SlashCommandNamedArgument.fromProps({
name: 'strict',
description: 'Whether to require the reasoning block to be at the beginning of the string (excluding whitespaces).',
typeList: [ARGUMENT_TYPE.BOOLEAN],
defaultValue: 'true',
isRequired: false,
enumList: commonEnumProviders.boolean('trueFalse')(),
}),
],
unnamedArgumentList: [
@ -608,19 +717,27 @@ function registerReasoningSlashCommands() {
}),
],
callback: (args, value) => {
if (!value) {
if (!value || typeof value !== 'string') {
return '';
}
if (!power_user.reasoning.prefix || !power_user.reasoning.suffix) {
toastr.warning(t`Both prefix and suffix must be set in the Reasoning Formatting settings.`);
return String(value);
toastr.warning(t`Both prefix and suffix must be set in the Reasoning Formatting settings.`, t`Reasoning Parse`);
return value;
}
if (typeof args.return !== 'string' || !['reasoning', 'content'].includes(args.return)) {
toastr.warning(t`Invalid return type '${args.return}', defaulting to 'reasoning'.`, t`Reasoning Parse`);
}
const parsedReasoning = parseReasoningFromString(String(value));
const returnMessage = args.return === 'content';
const parsedReasoning = parseReasoningFromString(value, { strict: !isFalseBoolean(String(args.strict ?? '')) });
if (!parsedReasoning) {
return '';
return returnMessage ? value : '';
}
if (returnMessage) {
return parsedReasoning.content;
}
const applyRegex = !isFalseBoolean(String(args.regex ?? ''));
@ -638,6 +755,17 @@ function registerReasoningMacros() {
}
function setReasoningEventHandlers() {
/**
* Updates the reasoning block of a message from a value.
* @param {object} message Message object
* @param {string} value Reasoning value
*/
function updateReasoningFromValue(message, value) {
const reasoning = getRegexedString(value, regex_placement.REASONING, { isEdit: true });
message.extra.reasoning = reasoning;
message.extra.reasoning_type = message.extra.reasoning_type ? ReasoningType.Edited : ReasoningType.Manual;
}
$(document).on('click', '.mes_reasoning_details', function (e) {
if (!e.target.closest('.mes_reasoning_actions') && !e.target.closest('.mes_reasoning_header')) {
e.preventDefault();
@ -718,8 +846,7 @@ function setReasoningEventHandlers() {
}
const textarea = messageBlock.find('.reasoning_edit_textarea');
const reasoning = getRegexedString(String(textarea.val()), regex_placement.REASONING, { isEdit: true });
message.extra.reasoning = reasoning;
updateReasoningFromValue(message, String(textarea.val()));
await saveChatConditional();
updateMessageBlock(messageId, message);
textarea.remove();
@ -780,6 +907,8 @@ function setReasoningEventHandlers() {
return;
}
message.extra.reasoning = '';
delete message.extra.reasoning_type;
delete message.extra.reasoning_duration;
await saveChatConditional();
updateMessageBlock(messageId, message);
const textarea = messageBlock.find('.reasoning_edit_textarea');
@ -797,6 +926,20 @@ function setReasoningEventHandlers() {
await copyText(reasoning);
toastr.info(t`Copied!`, '', { timeOut: 2000 });
});
$(document).on('input', '.reasoning_edit_textarea', function () {
if (!power_user.auto_save_msg_edits) {
return;
}
const { message } = getMessageFromJquery(this);
if (!message?.extra) {
return;
}
updateReasoningFromValue(message, String($(this).val()));
saveChatDebounced();
});
}
/**
@ -819,16 +962,18 @@ export function removeReasoningFromString(str) {
* @property {string} reasoning Reasoning block
* @property {string} content Message content
* @param {string} str Content of the message
* @param {Object} options Optional arguments
* @param {boolean} [options.strict=true] Whether the reasoning block **has** to be at the beginning of the provided string (excluding whitespaces), or can be anywhere in it
* @returns {ParsedReasoning|null} Parsed reasoning block and message content
*/
function parseReasoningFromString(str) {
function parseReasoningFromString(str, { strict = true } = {}) {
// Both prefix and suffix must be defined
if (!power_user.reasoning.prefix || !power_user.reasoning.suffix) {
return null;
}
try {
const regex = new RegExp(`${escapeRegex(power_user.reasoning.prefix)}(.*?)${escapeRegex(power_user.reasoning.suffix)}`, 's');
const regex = new RegExp(`${(strict ? '^\\s*?' : '')}${escapeRegex(power_user.reasoning.prefix)}(.*?)${escapeRegex(power_user.reasoning.suffix)}`, 's');
let didReplace = false;
let reasoning = '';
@ -838,9 +983,9 @@ function parseReasoningFromString(str) {
return '';
});
if (didReplace && power_user.trim_spaces) {
reasoning = reasoning.trim();
content = content.trim();
if (didReplace) {
reasoning = trimSpaces(reasoning);
content = trimSpaces(content);
}
return { reasoning, content };
@ -851,7 +996,7 @@ function parseReasoningFromString(str) {
}
function registerReasoningAppEvents() {
eventSource.makeFirst(event_types.MESSAGE_RECEIVED, (/** @type {number} */ idx) => {
const eventHandler = (/** @type {number} */ idx) => {
if (!power_user.reasoning.auto_parse) {
return;
}
@ -869,6 +1014,11 @@ function registerReasoningAppEvents() {
return null;
}
if (message.extra?.reasoning) {
console.debug('[Reasoning] Message already has reasoning', idx);
return null;
}
const parsedReasoning = parseReasoningFromString(message.mes);
// No reasoning block found
@ -886,6 +1036,7 @@ function registerReasoningAppEvents() {
// If reasoning was found, add it to the message
if (parsedReasoning.reasoning) {
message.extra.reasoning = getRegexedString(parsedReasoning.reasoning, regex_placement.REASONING);
message.extra.reasoning_type = ReasoningType.Parsed;
}
// Update the message text if it was changed
@ -901,7 +1052,11 @@ function registerReasoningAppEvents() {
updateMessageBlock(idx, message);
}
}
});
};
for (const event of [event_types.MESSAGE_RECEIVED, event_types.MESSAGE_UPDATED]) {
eventSource.on(event, eventHandler);
}
}
export function initReasoning() {

View File

@ -34,6 +34,7 @@ export const enumIcons = {
preset: '⚙️',
file: '📄',
message: '💬',
reasoning: '💡',
voice: '🎤',
server: '🖥️',
popup: '🗔',

View File

@ -311,7 +311,7 @@ export function validateTextGenUrl() {
const formattedUrl = formatTextGenURL(url);
if (!formattedUrl) {
toastr.error('Enter a valid API URL', 'Text Completion API');
toastr.error(t`Enter a valid API URL`, 'Text Completion API');
return;
}
@ -1187,7 +1187,7 @@ export function getTextGenModel() {
return settings.aphrodite_model;
case OLLAMA:
if (!settings.ollama_model) {
toastr.error('No Ollama model selected.', 'Text Completion API');
toastr.error(t`No Ollama model selected.`, 'Text Completion API');
throw new Error('No Ollama model selected');
}
return settings.ollama_model;

View File

@ -679,7 +679,7 @@ export function getTokenizerModel() {
}
if (oai_settings.chat_completion_source === chat_completion_sources.PERPLEXITY) {
if (oai_settings.perplexity_model.includes('sonar-reasoning')) {
if (oai_settings.perplexity_model.includes('sonar-reasoning') || oai_settings.perplexity_model.includes('r1-1776')) {
return deepseekTokenizer;
}
if (oai_settings.perplexity_model.includes('llama-3') || oai_settings.perplexity_model.includes('llama3')) {
@ -694,6 +694,9 @@ export function getTokenizerModel() {
}
if (oai_settings.chat_completion_source === chat_completion_sources.GROQ) {
if (oai_settings.groq_model.includes('qwen')) {
return qwen2Tokenizer;
}
if (oai_settings.groq_model.includes('llama-3') || oai_settings.groq_model.includes('llama3')) {
return llama3Tokenizer;
}

View File

@ -9,6 +9,9 @@ import { ensureImageFormatSupported, getBase64Async, humanFileSize } from './uti
export let currentUser = null;
export let accountsEnabled = false;
// Extend the session every 30 minutes
const SESSION_EXTEND_INTERVAL = 30 * 60 * 1000;
/**
* Enable or disable user account controls in the UI.
* @param {boolean} isEnabled User account controls enabled
@ -894,6 +897,24 @@ async function slugify(text) {
}
}
/**
* Pings the server to extend the user session.
*/
async function extendUserSession() {
try {
const response = await fetch('/api/ping?extend=1', {
method: 'GET',
headers: getRequestHeaders(),
});
if (!response.ok) {
throw new Error('Ping did not succeed', { cause: response.status });
}
} catch (error) {
console.error('Failed to extend user session', error);
}
}
jQuery(() => {
$('#logout_button').on('click', () => {
logout();
@ -904,4 +925,9 @@ jQuery(() => {
$('#account_button').on('click', () => {
openUserProfile();
});
setInterval(async () => {
if (currentUser) {
await extendUserSession();
}
}, SESSION_EXTEND_INTERVAL);
});

View File

@ -8,7 +8,7 @@ import {
import { getContext } from './extensions.js';
import { characters, getRequestHeaders, this_chid } from '../script.js';
import { isMobile } from './RossAscends-mods.js';
import { collapseNewlines } from './power-user.js';
import { collapseNewlines, power_user } from './power-user.js';
import { debounce_timeout } from './constants.js';
import { Popup, POPUP_RESULT, POPUP_TYPE } from './popup.js';
import { SlashCommandClosure } from './slash-commands/SlashCommandClosure.js';
@ -676,6 +676,19 @@ export function sortByCssOrder(a, b) {
return _a - _b;
}
/**
* Trims leading and trailing whitespace from the input string based on a configuration setting.
* @param {string} input - The string to be trimmed
* @returns {string} The trimmed string if trimming is enabled; otherwise, returns the original string
*/
export function trimSpaces(input) {
if (!input || typeof input !== 'string') {
return input;
}
return power_user.trim_spaces ? input.trim() : input;
}
/**
* Trims a string to the end of a nearest sentence.
* @param {string} input The string to trim.
@ -994,13 +1007,18 @@ export function getImageSizeFromDataURL(dataUrl) {
});
}
export function getCharaFilename(chid) {
/**
* Gets the filename of the character avatar without extension
* @param {number?} [chid=null] - Character ID. If not provided, uses the current character ID
* @param {object} [options={}] - Options arguments
* @param {string?} [options.manualAvatarKey=null] - Manually take the following avatar key, instead of using the chid to determine the name
* @returns {string?} The filename of the character avatar without extension, or null if the character ID is invalid
*/
export function getCharaFilename(chid = null, { manualAvatarKey = null } = {}) {
const context = getContext();
const fileName = context.characters[chid ?? context.characterId]?.avatar;
const fileName = manualAvatarKey ?? context.characters[chid ?? context.characterId]?.avatar;
if (fileName) {
return fileName.replace(/\.[^/.]+$/, '');
}
return fileName?.replace(/\.[^/.]+$/, '') ?? null;
}
/**

View File

@ -55,6 +55,10 @@
--interactable-outline-color: var(--white100);
--interactable-outline-color-faint: var(--white20a);
--reasoning-body-color: var(--SmartThemeEmColor);
--reasoning-em-color: color-mix(in srgb, var(--SmartThemeEmColor) 67%, var(--SmartThemeBlurTintColor) 33%);
--reasoning-saturation: 0.5;
/*Default Theme, will be changed by ToolCool Color Picker*/
--SmartThemeBodyColor: rgb(220, 220, 210);
@ -348,13 +352,13 @@ input[type='checkbox']:focus-visible {
.mes_reasoning {
display: block;
border-left: 2px solid var(--SmartThemeEmColor);
border-left: 2px solid var(--reasoning-body-color);
border-radius: 2px;
padding: 5px;
padding-left: 14px;
margin-bottom: 0.5em;
overflow-y: auto;
color: var(--SmartThemeEmColor);
color: hsl(from var(--reasoning-body-color) h calc(s * var(--reasoning-saturation)) l);
}
.mes_reasoning_details {
@ -374,18 +378,6 @@ input[type='checkbox']:focus-visible {
margin-bottom: 0;
}
.mes_reasoning em,
.mes_reasoning i,
.mes_reasoning u,
.mes_reasoning q,
.mes_reasoning blockquote {
filter: saturate(0.5);
}
.mes_reasoning_details .mes_reasoning em {
color: color-mix(in srgb, var(--SmartThemeEmColor) 67%, var(--SmartThemeBlurTintColor) 33%);
}
.mes_reasoning_header_block {
flex-grow: 1;
}
@ -438,7 +430,7 @@ input[type='checkbox']:focus-visible {
}
/** If hidden reasoning should not be shown, we hide all blocks that don't have content */
#chat:not([data-show-hidden-reasoning="true"]) .mes:has(.mes_reasoning:empty) .mes_reasoning_details {
#chat:not([data-show-hidden-reasoning="true"]):not(:has(.reasoning_edit_textarea)) .mes:has(.mes_reasoning:empty) .mes_reasoning_details {
display: none;
}
@ -461,26 +453,36 @@ input[type='checkbox']:focus-visible {
}
.mes_text i,
.mes_text em,
.mes_text em {
color: var(--SmartThemeEmColor);
}
.mes_reasoning i,
.mes_reasoning em {
color: var(--SmartThemeEmColor);
color: hsl(from var(--reasoning-em-color) h calc(s * var(--reasoning-saturation)) l);
}
.mes_text q i,
.mes_text q em {
color: inherit;
}
.mes_text u,
.mes_reasoning u {
color: var(--SmartThemeUnderlineColor);
.mes_reasoning q i,
.mes_reasoning q em {
color: hsl(from var(--SmartThemeQuoteColor) h calc(s * var(--reasoning-saturation)) l);
}
.mes_text q,
.mes_reasoning q {
.mes_text u {
color: var(--SmartThemeUnderlineColor);
}
.mes_reasoning u {
color: hsl(from var(--SmartThemeUnderlineColor) h calc(s * var(--reasoning-saturation)) l);
}
.mes_text q {
color: var(--SmartThemeQuoteColor);
}
.mes_reasoning q {
color: hsl(from var(--SmartThemeQuoteColor) h calc(s * var(--reasoning-saturation)) l);
}
.mes_text font[color] em,
.mes_text font[color] i,

View File

@ -58,6 +58,7 @@ import {
import getWebpackServeMiddleware from './src/middleware/webpack-serve.js';
import basicAuthMiddleware from './src/middleware/basicAuth.js';
import whitelistMiddleware from './src/middleware/whitelist.js';
import accessLoggerMiddleware, { getAccessLogPath, migrateAccessLog } from './src/middleware/accessLogWriter.js';
import multerMonkeyPatch from './src/middleware/multerMonkeyPatch.js';
import initRequestProxy from './src/request-proxy.js';
import getCacheBusterMiddleware from './src/middleware/cacheBuster.js';
@ -243,7 +244,6 @@ const cliArguments = yargs(hideBin(process.argv))
describe: 'Request proxy URL (HTTP or SOCKS protocols)',
}).option('requestProxyBypass', {
type: 'array',
default: null,
describe: 'Request proxy bypass list (space separated list of hosts)',
}).parseSync();
@ -340,9 +340,17 @@ const CORS = cors({
app.use(CORS);
if (listen && basicAuthMode) app.use(basicAuthMiddleware);
if (listen && basicAuthMode) {
app.use(basicAuthMiddleware);
}
app.use(whitelistMiddleware(enableWhitelist, listen));
if (enableWhitelist) {
app.use(whitelistMiddleware());
}
if (listen) {
app.use(accessLoggerMiddleware());
}
if (enableCorsProxy) {
app.use(bodyParser.json({
@ -556,7 +564,13 @@ app.use('/api/users', usersPublicRouter);
// Everything below this line requires authentication
app.use(requireLoginMiddleware);
app.get('/api/ping', (_, response) => response.sendStatus(204));
app.get('/api/ping', (request, response) => {
if (request.query.extend && request.session) {
request.session.touch = Date.now();
}
response.sendStatus(204);
});
// File uploads
app.use(multer({ dest: uploadsPath, limits: { fieldSize: 10 * 1024 * 1024 } }).single('avatar'));
@ -754,6 +768,7 @@ const preSetupTasks = async function () {
await checkForNewContent(directories);
await ensureThumbnailCache();
cleanUploads();
migrateAccessLog();
await settingsInit();
await statsInit();
@ -856,7 +871,7 @@ const postSetupTasks = async function (v6Failed, v4Failed, useIPv6, useIPv4) {
if (listen) {
console.log();
console.log('To limit connections to internal localhost only ([::1] or 127.0.0.1), change the setting in config.yaml to "listen: false".');
console.log('Check the "access.log" file in the SillyTavern directory to inspect incoming connections.');
console.log('Check the "access.log" file in the data directory to inspect incoming connections:', color.green(getAccessLogPath()));
}
console.log('\n' + getSeparator(plainGoToLog.length) + '\n');
console.log(goToLog);

View File

@ -979,6 +979,7 @@ router.post('/generate', jsonParser, function (request, response) {
headers = { ...OPENROUTER_HEADERS };
bodyParams = {
'transforms': getOpenRouterTransforms(request),
'include_reasoning': Boolean(request.body.include_reasoning),
};
if (request.body.min_p !== undefined) {
@ -1004,10 +1005,6 @@ router.post('/generate', jsonParser, function (request, response) {
bodyParams['route'] = 'fallback';
}
if (request.body.include_reasoning) {
bodyParams['include_reasoning'] = true;
}
let cachingAtDepth = getConfigValue('claude.cachingAtDepth', -1);
if (Number.isInteger(cachingAtDepth) && cachingAtDepth >= 0 && request.body.model?.startsWith('anthropic/claude-3')) {
cachingAtDepthForOpenRouterClaude(request.body.messages, cachingAtDepth);

View File

@ -230,7 +230,7 @@ router.post('/version', jsonParser, async (request, response) => {
} catch (error) {
// it is not a git repo, or has no commits yet, or is a bare repo
// not possible to update it, most likely can't get the branch name either
return response.send({ currentBranchName: null, currentCommitHash, isUpToDate: true, remoteUrl: null });
return response.send({ currentBranchName: '', currentCommitHash: '', isUpToDate: true, remoteUrl: '' });
}
const currentBranch = await git.cwd(extensionPath).branch();

View File

@ -125,8 +125,14 @@ router.get('/get', jsonParser, function (request, response) {
.map((file) => {
const pathToSprite = path.join(spritesPath, file);
const mtime = fs.statSync(pathToSprite).mtime?.toISOString().replace(/[^0-9]/g, '').slice(0, 14);
const fileName = path.parse(pathToSprite).name.toLowerCase();
// Extract the label from the filename via regex, which can be suffixed with a sub-name, either connected with a dash or a dot.
// Examples: joy.png, joy-1.png, joy.expressive.png
const label = fileName.match(/^(.+?)(?:[-\\.].*?)?$/)?.[1] ?? fileName;
return {
label: path.parse(pathToSprite).name.toLowerCase(),
label: label,
path: `/characters/${name}/${file}` + (mtime ? `?t=${mtime}` : ''),
};
});
@ -141,8 +147,9 @@ router.get('/get', jsonParser, function (request, response) {
router.post('/delete', jsonParser, async (request, response) => {
const label = request.body.label;
const name = request.body.name;
const spriteName = request.body.spriteName || label;
if (!label || !name) {
if (!spriteName || !name) {
return response.sendStatus(400);
}
@ -158,7 +165,7 @@ router.post('/delete', jsonParser, async (request, response) => {
// Remove existing sprite with the same label
for (const file of files) {
if (path.parse(file).name === label) {
if (path.parse(file).name === spriteName) {
fs.rmSync(path.join(spritesPath, file));
}
}
@ -221,6 +228,7 @@ router.post('/upload', urlencodedParser, async (request, response) => {
const file = request.file;
const label = request.body.label;
const name = request.body.name;
const spriteName = request.body.spriteName || label;
if (!file || !label || !name) {
return response.sendStatus(400);
@ -243,12 +251,12 @@ router.post('/upload', urlencodedParser, async (request, response) => {
// Remove existing sprite with the same label
for (const file of files) {
if (path.parse(file).name === label) {
if (path.parse(file).name === spriteName) {
fs.rmSync(path.join(spritesPath, file));
}
}
const filename = label + path.parse(file.originalname).ext;
const filename = spriteName + path.parse(file.originalname).ext;
const spritePath = path.join(file.destination, file.filename);
const pathToFile = path.join(spritesPath, filename);
// Copy uploaded file to sprites folder

View File

@ -3,13 +3,16 @@ import crypto from 'node:crypto';
import storage from 'node-persist';
import express from 'express';
import { RateLimiterMemory, RateLimiterRes } from 'rate-limiter-flexible';
import { jsonParser, getIpFromRequest } from '../express-common.js';
import { jsonParser, getIpFromRequest, getRealIpFromHeader } from '../express-common.js';
import { color, Cache, getConfigValue } from '../util.js';
import { KEY_PREFIX, getUserAvatar, toKey, getPasswordHash, getPasswordSalt } from '../users.js';
const DISCREET_LOGIN = getConfigValue('enableDiscreetLogin', false);
const PREFER_REAL_IP_HEADER = getConfigValue('rateLimiting.preferRealIpHeader', false);
const MFA_CACHE = new Cache(5 * 60 * 1000);
const getIpAddress = (request) => PREFER_REAL_IP_HEADER ? getRealIpFromHeader(request) : getIpFromRequest(request);
export const router = express.Router();
const loginLimiter = new RateLimiterMemory({
points: 5,
@ -60,7 +63,7 @@ router.post('/login', jsonParser, async (request, response) => {
return response.status(400).json({ error: 'Missing required fields' });
}
const ip = getIpFromRequest(request);
const ip = getIpAddress(request);
await loginLimiter.consume(ip);
/** @type {import('../users.js').User} */
@ -92,7 +95,7 @@ router.post('/login', jsonParser, async (request, response) => {
return response.json({ handle: user.handle });
} catch (error) {
if (error instanceof RateLimiterRes) {
console.error('Login failed: Rate limited from', getIpFromRequest(request));
console.error('Login failed: Rate limited from', getIpAddress(request));
return response.status(429).send({ error: 'Too many attempts. Try again later or recover your password.' });
}
@ -108,7 +111,7 @@ router.post('/recover-step1', jsonParser, async (request, response) => {
return response.status(400).json({ error: 'Missing required fields' });
}
const ip = getIpFromRequest(request);
const ip = getIpAddress(request);
await recoverLimiter.consume(ip);
/** @type {import('../users.js').User} */
@ -132,7 +135,7 @@ router.post('/recover-step1', jsonParser, async (request, response) => {
return response.sendStatus(204);
} catch (error) {
if (error instanceof RateLimiterRes) {
console.error('Recover step 1 failed: Rate limited from', getIpFromRequest(request));
console.error('Recover step 1 failed: Rate limited from', getIpAddress(request));
return response.status(429).send({ error: 'Too many attempts. Try again later or contact your admin.' });
}
@ -150,7 +153,7 @@ router.post('/recover-step2', jsonParser, async (request, response) => {
/** @type {import('../users.js').User} */
const user = await storage.getItem(toKey(request.body.handle));
const ip = getIpFromRequest(request);
const ip = getIpAddress(request);
if (!user) {
console.error('Recover step 2 failed: User', request.body.handle, 'not found');
@ -186,7 +189,7 @@ router.post('/recover-step2', jsonParser, async (request, response) => {
return response.sendStatus(204);
} catch (error) {
if (error instanceof RateLimiterRes) {
console.error('Recover step 2 failed: Rate limited from', getIpFromRequest(request));
console.error('Recover step 2 failed: Rate limited from', getIpAddress(request));
return response.status(429).send({ error: 'Too many attempts. Try again later or contact your admin.' });
}

View File

@ -132,35 +132,35 @@ function getSourceSettings(source, request) {
switch (source) {
case 'togetherai':
return {
model: String(request.headers['x-togetherai-model']),
model: String(request.body.model),
};
case 'openai':
return {
model: String(request.headers['x-openai-model']),
model: String(request.body.model),
};
case 'cohere':
return {
model: String(request.headers['x-cohere-model']),
model: String(request.body.model),
};
case 'llamacpp':
return {
apiUrl: String(request.headers['x-llamacpp-url']),
apiUrl: String(request.body.apiUrl),
};
case 'vllm':
return {
apiUrl: String(request.headers['x-vllm-url']),
model: String(request.headers['x-vllm-model']),
apiUrl: String(request.body.apiUrl),
model: String(request.body.model),
};
case 'ollama':
return {
apiUrl: String(request.headers['x-ollama-url']),
model: String(request.headers['x-ollama-model']),
keep: Boolean(request.headers['x-ollama-keep']),
apiUrl: String(request.body.apiUrl),
model: String(request.body.model),
keep: Boolean(request.body.keep),
};
case 'extras':
return {
extrasUrl: String(request.headers['x-extras-url']),
extrasKey: String(request.headers['x-extras-key']),
extrasUrl: String(request.body.extrasUrl),
extrasKey: String(request.body.extrasKey),
};
case 'transformers':
return {

View File

@ -25,3 +25,17 @@ export function getIpFromRequest(req) {
}
return clientIp;
}
/**
* Gets the IP address of the client when behind reverse proxy using x-real-ip header, falls back to socket remote address.
* This function should be used when the application is running behind a reverse proxy (e.g., Nginx, traefik, Caddy...).
* @param {import('express').Request} req Request object
* @returns {string} IP address of the client
*/
export function getRealIpFromHeader(req) {
if (req.headers['x-real-ip']) {
return req.headers['x-real-ip'].toString();
}
return getIpFromRequest(req);
}

View File

@ -0,0 +1,59 @@
import path from 'node:path';
import fs from 'node:fs';
import { getRealIpFromHeader } from '../express-common.js';
import { color, getConfigValue } from '../util.js';
const enableAccessLog = getConfigValue('logging.enableAccessLog', true);
const knownIPs = new Set();
export const getAccessLogPath = () => path.join(globalThis.DATA_ROOT, 'access.log');
export function migrateAccessLog() {
try {
if (!fs.existsSync('access.log')) {
return;
}
const logPath = getAccessLogPath();
if (fs.existsSync(logPath)) {
return;
}
fs.renameSync('access.log', logPath);
console.log(color.yellow('Migrated access.log to new location:'), logPath);
} catch (e) {
console.error('Failed to migrate access log:', e);
console.info('Please move access.log to the data directory manually.');
}
}
/**
* Creates middleware for logging access and new connections
* @returns {import('express').RequestHandler}
*/
export default function accessLoggerMiddleware() {
return function (req, res, next) {
const clientIp = getRealIpFromHeader(req);
const userAgent = req.headers['user-agent'];
if (!knownIPs.has(clientIp)) {
// Log new connection
console.info(color.yellow(`New connection from ${clientIp}; User Agent: ${userAgent}\n`));
knownIPs.add(clientIp);
// Write to access log if enabled
if (enableAccessLog) {
const logPath = getAccessLogPath();
const timestamp = new Date().toISOString();
const log = `${timestamp} ${clientIp} ${userAgent}\n`;
fs.appendFile(logPath, log, (err) => {
if (err) {
console.error('Failed to write access log:', err);
}
});
}
}
next();
};
}

View File

@ -1,11 +1,8 @@
import path from 'node:path';
import webpack from 'webpack';
import { publicLibConfig } from '../../webpack.config.js';
import getPublicLibConfig from '../../webpack.config.js';
export default function getWebpackServeMiddleware() {
const outputPath = publicLibConfig.output?.path;
const outputFile = publicLibConfig.output?.filename;
/**
* A very spartan recreation of webpack-dev-middleware.
* @param {import('express').Request} req Request object.
@ -14,6 +11,10 @@ export default function getWebpackServeMiddleware() {
* @type {import('express').RequestHandler}
*/
function devMiddleware(req, res, next) {
const publicLibConfig = getPublicLibConfig();
const outputPath = publicLibConfig.output?.path;
const outputFile = publicLibConfig.output?.filename;
if (req.method === 'GET' && path.parse(req.path).base === outputFile) {
return res.sendFile(outputFile, { root: outputPath });
}
@ -23,9 +24,12 @@ export default function getWebpackServeMiddleware() {
/**
* Wait until Webpack is done compiling.
* @param {object} param Parameters.
* @param {boolean} [param.forceDist] Whether to force the use the /dist folder.
* @returns {Promise<void>}
*/
devMiddleware.runWebpackCompiler = () => {
devMiddleware.runWebpackCompiler = ({ forceDist = false } = {}) => {
const publicLibConfig = getPublicLibConfig(forceDist);
const compiler = webpack(publicLibConfig);
return new Promise((resolve) => {

View File

@ -10,7 +10,6 @@ import { color, getConfigValue, safeReadFileSync } from '../util.js';
const whitelistPath = path.join(process.cwd(), './whitelist.txt');
const enableForwardedWhitelist = getConfigValue('enableForwardedWhitelist', false);
let whitelist = getConfigValue('whitelist', []);
let knownIPs = new Set();
if (fs.existsSync(whitelistPath)) {
try {
@ -48,47 +47,39 @@ function getForwardedIp(req) {
/**
* Returns a middleware function that checks if the client IP is in the whitelist.
* @param {boolean} whitelistMode If whitelist mode is enabled via config or command line
* @param {boolean} listen If listen mode is enabled via config or command line
* @returns {import('express').RequestHandler} The middleware function
*/
export default function whitelistMiddleware(whitelistMode, listen) {
export default function whitelistMiddleware() {
const forbiddenWebpage = Handlebars.compile(
safeReadFileSync('./public/error/forbidden-by-whitelist.html') ?? '',
);
const noLogPaths = [
'/favicon.ico',
];
return function (req, res, next) {
const clientIp = getIpFromRequest(req);
const forwardedIp = getForwardedIp(req);
const userAgent = req.headers['user-agent'];
if (listen && !knownIPs.has(clientIp)) {
console.info(color.yellow(`New connection from ${clientIp}; User Agent: ${userAgent}\n`));
knownIPs.add(clientIp);
// Write access log
const timestamp = new Date().toISOString();
const log = `${timestamp} ${clientIp} ${userAgent}\n`;
fs.appendFile('access.log', log, (err) => {
if (err) {
console.error('Failed to write access log:', err);
}
});
}
//clientIp = req.connection.remoteAddress.split(':').pop();
if (whitelistMode === true && !whitelist.some(x => ipMatching.matches(clientIp, ipMatching.getMatch(x)))
|| forwardedIp && whitelistMode === true && !whitelist.some(x => ipMatching.matches(forwardedIp, ipMatching.getMatch(x)))
if (!whitelist.some(x => ipMatching.matches(clientIp, ipMatching.getMatch(x)))
|| forwardedIp && !whitelist.some(x => ipMatching.matches(forwardedIp, ipMatching.getMatch(x)))
) {
// Log the connection attempt with real IP address
const ipDetails = forwardedIp
? `${clientIp} (forwarded from ${forwardedIp})`
: clientIp;
console.warn(
color.red(
`Blocked connection from ${clientIp}; User Agent: ${userAgent}\n\tTo allow this connection, add its IP address to the whitelist or disable whitelist mode by editing config.yaml in the root directory of your SillyTavern installation.\n`,
),
);
if (!noLogPaths.includes(req.path)) {
console.warn(
color.red(
`Blocked connection from ${clientIp}; User Agent: ${userAgent}\n\tTo allow this connection, add its IP address to the whitelist or disable whitelist mode by editing config.yaml in the root directory of your SillyTavern installation.\n`,
),
);
}
return res.status(403).send(forbiddenWebpage({ ipDetails }));
}
next();

View File

@ -3,7 +3,7 @@ import path from 'node:path';
import url from 'node:url';
import express from 'express';
import { default as git } from 'simple-git';
import { default as git, CheckRepoActions } from 'simple-git';
import { sync as commandExistsSync } from 'command-exists';
import { getConfigValue, color } from './util.js';
@ -256,7 +256,7 @@ async function updatePlugins(pluginsPath) {
const pluginPath = path.join(pluginsPath, directory);
const pluginRepo = git(pluginPath);
const isRepo = await pluginRepo.checkIsRepo();
const isRepo = await pluginRepo.checkIsRepo(CheckRepoActions.IS_REPO_ROOT);
if (!isRepo) {
continue;
}

View File

@ -763,7 +763,7 @@ export function stringToBool(str) {
* Setup the minimum log level
*/
export function setupLogLevel() {
const logLevel = getConfigValue('minLogLevel', LOG_LEVELS.DEBUG);
const logLevel = getConfigValue('logging.minLogLevel', LOG_LEVELS.DEBUG);
globalThis.console.debug = logLevel <= LOG_LEVELS.DEBUG ? console.debug : () => {};
globalThis.console.info = logLevel <= LOG_LEVELS.INFO ? console.info : () => {};

View File

@ -1,35 +1,72 @@
import process from 'node:process';
import path from 'node:path';
import isDocker from 'is-docker';
/** @type {import('webpack').Configuration} */
export const publicLibConfig = {
mode: 'production',
entry: './public/lib.js',
cache: {
type: 'filesystem',
cacheDirectory: path.resolve(process.cwd(), 'dist/webpack'),
store: 'pack',
compression: 'gzip',
},
devtool: false,
watch: false,
module: {},
stats: {
preset: 'minimal',
assets: false,
modules: false,
colors: true,
timings: true,
},
experiments: {
outputModule: true,
},
performance: {
hints: false,
},
output: {
path: path.resolve(process.cwd(), 'dist'),
filename: 'lib.js',
libraryTarget: 'module',
},
};
/**
* Get the Webpack configuration for the public/lib.js file.
* 1. Docker has got cache and the output file pre-baked.
* 2. Non-Docker environments use the global DATA_ROOT variable to determine the cache and output directories.
* @param {boolean} forceDist Whether to force the use the /dist folder.
* @returns {import('webpack').Configuration}
* @throws {Error} If the DATA_ROOT variable is not set.
* */
export default function getPublicLibConfig(forceDist = false) {
function getCacheDirectory() {
if (forceDist || isDocker()) {
return path.resolve(process.cwd(), 'dist/webpack');
}
if (typeof globalThis.DATA_ROOT === 'string') {
return path.resolve(globalThis.DATA_ROOT, '_webpack', 'cache');
}
throw new Error('DATA_ROOT variable is not set.');
}
function getOutputDirectory() {
if (forceDist || isDocker()) {
return path.resolve(process.cwd(), 'dist');
}
if (typeof globalThis.DATA_ROOT === 'string') {
return path.resolve(globalThis.DATA_ROOT, '_webpack', 'output');
}
throw new Error('DATA_ROOT variable is not set.');
}
const cacheDirectory = getCacheDirectory();
const outputDirectory = getOutputDirectory();
return {
mode: 'production',
entry: './public/lib.js',
cache: {
type: 'filesystem',
cacheDirectory: cacheDirectory,
store: 'pack',
compression: 'gzip',
},
devtool: false,
watch: false,
module: {},
stats: {
preset: 'minimal',
assets: false,
modules: false,
colors: true,
timings: true,
},
experiments: {
outputModule: true,
},
performance: {
hints: false,
},
output: {
path: outputDirectory,
filename: 'lib.js',
libraryTarget: 'module',
},
};
}