diff --git a/.github/readme-zh_cn.md b/.github/readme-zh_cn.md index 415fce22..efb77c51 100644 --- a/.github/readme-zh_cn.md +++ b/.github/readme-zh_cn.md @@ -170,7 +170,7 @@ SillyTavern 会将 API 密钥保存在目录中的 `secrets.json` 文件内。 如果要想通过点击 API 输入框旁边的按钮来查看密钥,请按照以下设置: -1. 打开 `config.conf` 文件,将里面的 `allowKeysExposure` 设置为 `true`。 +1. 打开 `config.yaml` 文件,将里面的 `allowKeysExposure` 设置为 `true`。 2. 然后重启 SillyTavern 服务。 ## 远程访问 @@ -207,7 +207,7 @@ SillyTavern 会将 API 密钥保存在目录中的 `secrets.json` 文件内。 然后,文件中设置的 IP 就可以访问 SillyTavern 了。 -*注意:"config.conf" 文件内也有一个 "whitelist" 设置,你可以用同样的方法设置它,但如果 "whitelist.txt" 文件存在,这个设置将被忽略。 +*注意:"config.yaml" 文件内也有一个 "whitelist" 设置,你可以用同样的方法设置它,但如果 "whitelist.txt" 文件存在,这个设置将被忽略。 ### 2.获取 SillyTavern 服务的 IP 地址 @@ -233,19 +233,19 @@ SillyTavern 会将 API 密钥保存在目录中的 `secrets.json` 文件内。 ### 向所有 IP 开放您的 SillyTavern 服务 -我们不建议这样做,但您可以打开 `config.conf` 并将里面的 `whitelist` 设置改为 `false`。 +我们不建议这样做,但您可以打开 `config.yaml` 并将里面的 `whitelistMode` 设置改为 `false`。 你必须删除(或重命名)SillyTavern 文件夹中的 `whitelist.txt` 文件(如果有的话)。 这通常是不安全的做法,所以我们要求在这样做时必须设置用户名和密码。 -用户名和密码在`config.conf`文件中设置。 +用户名和密码在`config.yaml`文件中设置。 重启 SillyTavern 服务后,只要知道用户名和密码,任何设备都可以访问。 ### 还是无法访问? -* 为 `config.conf` 文件中的端口创建一条入站/出站防火墙规则。切勿将此误认为是路由器上的端口转发,否则,有人可能会发现你的聊天隐私,那就大错特错了。 +* 为 `config.yaml` 文件中的端口创建一条入站/出站防火墙规则。切勿将此误认为是路由器上的端口转发,否则,有人可能会发现你的聊天隐私,那就大错特错了。 * 在 "设置" > "网络和 Internet" > "以太网" 中启用 "专用网络" 配置。这对 Windows 11 非常重要,否则即使添加了上述防火墙规则也无法连接。 ### 性能问题? diff --git a/.github/readme.md b/.github/readme.md index 6bff95a5..5a196635 100644 --- a/.github/readme.md +++ b/.github/readme.md @@ -175,7 +175,7 @@ By default, they will not be exposed to a frontend after you enter them and relo In order to enable viewing your keys by clicking a button in the API block: -1. Set the value of `allowKeysExposure` to `true` in `config.conf` file. +1. Set the value of `allowKeysExposure` to `true` in `config.yaml` file. 2. Restart the SillyTavern server. ## Remote connections @@ -213,7 +213,7 @@ CIDR masks are also accepted (eg. 10.0.0.0/24). Now devices which have the IP specified in the file will be able to connect. -*Note: `config.conf` also has a `whitelist` array, which you can use in the same way, but this array will be ignored if `whitelist.txt` exists.* +*Note: `config.yaml` also has a `whitelist` array, which you can use in the same way, but this array will be ignored if `whitelist.txt` exists.* ### 2. Getting the IP for the ST host machine @@ -239,19 +239,19 @@ Use http:// NOT https:// ### Opening your ST to all IPs -We do not recommend doing this, but you can open `config.conf` and change `whitelist` to `false`. +We do not recommend doing this, but you can open `config.yaml` and change `whitelistMode` to `false`. You must remove (or rename) `whitelist.txt` in the SillyTavern base install folder if it exists. This is usually an insecure practice, so we require you to set a username and password when you do this. -The username and password are set in `config.conf`. +The username and password are set in `config.yaml`. After restarting your ST server, any device will be able to connect to it, regardless of their IP as long as they know the username and password. ### Still Unable To Connect? -* Create an inbound/outbound firewall rule for the port found in `config.conf`. Do NOT mistake this for port-forwarding on your router, otherwise, someone could find your chat logs and that's a big no-no. +* Create an inbound/outbound firewall rule for the port found in `config.yaml`. Do NOT mistake this for port-forwarding on your router, otherwise, someone could find your chat logs and that's a big no-no. * Enable the Private Network profile type in Settings > Network and Internet > Ethernet. This is VERY important for Windows 11, otherwise, you would be unable to connect even with the aforementioned firewall rules. ## Performance issues? diff --git a/.gitignore b/.gitignore index 507f9161..ae782859 100644 --- a/.gitignore +++ b/.gitignore @@ -20,6 +20,8 @@ public/stats.json /uploads/ *.jsonl /config.conf +/config.yaml +/config.conf.bak /docker/config .DS_Store public/settings.json diff --git a/Dockerfile b/Dockerfile index 3afd3b7a..0f4e6c0b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -31,7 +31,8 @@ RUN \ echo "*** Create symbolic links to config directory ***" && \ for R in $RESOURCES; do ln -s "../config/$R" "public/$R"; done || true && \ \ - ln -s "./config/config.conf" "config.conf" || true && \ + rm -f "config.yaml" "public/settings.json" "public/css/bg_load.css" || true && \ + ln -s "./config/config.yaml" "config.yaml" || true && \ ln -s "../config/settings.json" "public/settings.json" || true && \ ln -s "../../config/bg_load.css" "public/css/bg_load.css" || true && \ mkdir "config" || true diff --git a/Remote-Link.cmd b/Remote-Link.cmd index f44f232f..2747abdd 100644 --- a/Remote-Link.cmd +++ b/Remote-Link.cmd @@ -4,7 +4,7 @@ echo WARNING: Cloudflare Tunnel! echo ======================================================================================================================== echo This script downloads and runs the latest cloudflared.exe from Cloudflare to set up an HTTPS tunnel to your SillyTavern! echo Using the randomly generated temporary tunnel URL, anyone can access your SillyTavern over the Internet while the tunnel -echo is active. Keep the URL safe and secure your SillyTavern installation by setting a username and password in config.conf! +echo is active. Keep the URL safe and secure your SillyTavern installation by setting a username and password in config.yaml! echo. echo See https://docs.sillytavern.app/usage/remoteconnections/ for more details about how to secure your SillyTavern install. echo. diff --git a/default/config.conf b/default/config.conf deleted file mode 100644 index 257db767..00000000 --- a/default/config.conf +++ /dev/null @@ -1,56 +0,0 @@ -const port = 8000; -const whitelist = ['127.0.0.1']; //Example for add several IP in whitelist: ['127.0.0.1', '192.168.0.10'] -const whitelistMode = true; //Disabling enabling the ip whitelist mode. true/false -const basicAuthMode = false; //Toggle basic authentication for endpoints. -const basicAuthUser = {username: "user", password: "password"}; //Login credentials when basicAuthMode is true. -const disableThumbnails = false; //Disables the generation of thumbnails, opting to use the raw images instead -const autorun = true; //Autorun in the browser. true/false -const enableExtensions = true; //Enables support for TavernAI-extras project -const listen = true; // If true, Can be access from other device or PC. otherwise can be access only from hosting machine. -const allowKeysExposure = false; // If true, private API keys could be fetched to the frontend. -const skipContentCheck = false; // If true, no new default content will be delivered to you. -const thumbnailsQuality = 95; // Quality of thumbnails. 0-100 -const disableChatBackup = false; // Disables the backup of chat logs to the /backups folder - -// If true, Allows insecure settings for listen, whitelist, and authentication. -// Change this setting only on "trusted networks". Do not change this value unless you are aware of the issues that can arise from changing this setting and configuring a insecure setting. -const securityOverride = false; - -// Additional settings for extra modules / extensions -const extras = { - // Disables auto-download of models from the HuggingFace Hub. - // You will need to manually download the models and put them into the /cache folder. - disableAutoDownload: false, - // Text classification model for sentiment analysis. HuggingFace ID of a model in ONNX format. - classificationModel: 'Cohee/distilbert-base-uncased-go-emotions-onnx', - // Image captioning model. HuggingFace ID of a model in ONNX format. - captioningModel: 'Xenova/vit-gpt2-image-captioning', - // Feature extraction model. HuggingFace ID of a model in ONNX format. - embeddingModel: 'Xenova/all-mpnet-base-v2', - // GPT-2 text generation model. HuggingFace ID of a model in ONNX format. - promptExpansionModel: 'Cohee/fooocus_expansion-onnx', -}; - -// Request overrides for additional headers -// Format is an array of objects: -// { hosts: [ "" ], headers: {
: "" } } -const requestOverrides = []; - -module.exports = { - port, - whitelist, - whitelistMode, - basicAuthMode, - basicAuthUser, - autorun, - enableExtensions, - listen, - disableThumbnails, - allowKeysExposure, - securityOverride, - skipContentCheck, - requestOverrides, - thumbnailsQuality, - extras, - disableChatBackup, -}; diff --git a/default/config.yaml b/default/config.yaml new file mode 100644 index 00000000..c129b39a --- /dev/null +++ b/default/config.yaml @@ -0,0 +1,53 @@ +# -- NETWORK CONFIGURATION -- +# Listen for incoming connections +listen: true +# Server port +port: 8000 +# Toggle whitelist mode +whitelistMode: true +# Whitelist of allowed IP addresses +whitelist: + - 127.0.0.1 +# Toggle basic authentication for endpoints +basicAuthMode: false +# Basic authentication credentials +basicAuthUser: + username: user + password: password +# Enables CORS proxy middleware +enableCorsProxy: false +# Disable security checks - NOT RECOMMENDED +securityOverride: false +# -- ADVANCED CONFIGURATION -- +# Open the browser automatically +autorun: true +# Disable thumbnail generation +disableThumbnails: false +# Thumbnail quality (0-100) +thumbnailsQuality: 95 +# Allow secret keys exposure via API +allowKeysExposure: false +# Skip new default content checks +skipContentCheck: false +# Disable automatic chats backup +disableChatBackup: false +# API request overrides (for KoboldAI and Text Completion APIs) +## Format is an array of objects: +## - hosts: +## - example.com +## headers: +## Content-Type: application/json +requestOverrides: [] +# -- PLUGIN CONFIGURATION -- +# Enable UI extensions +enableExtensions: true +# Extension settings +extras: + # Disables automatic model download from HuggingFace + disableAutoDownload: false + # Extra models for plugins. Expects model IDs from HuggingFace model hub in ONNX format + classificationModel: Cohee/distilbert-base-uncased-go-emotions-onnx + captioningModel: Xenova/vit-gpt2-image-captioning + embeddingModel: Xenova/all-mpnet-base-v2 + promptExpansionModel: Cohee/fooocus_expansion-onnx + diff --git a/default/content/Default_Comfy_Workflow.json b/default/content/Default_Comfy_Workflow.json new file mode 100644 index 00000000..10bb0cc3 --- /dev/null +++ b/default/content/Default_Comfy_Workflow.json @@ -0,0 +1,86 @@ +{ + "3": { + "class_type": "KSampler", + "inputs": { + "cfg": "%scale%", + "denoise": 1, + "latent_image": [ + "5", + 0 + ], + "model": [ + "4", + 0 + ], + "negative": [ + "7", + 0 + ], + "positive": [ + "6", + 0 + ], + "sampler_name": "%sampler%", + "scheduler": "%scheduler%", + "seed": "%seed%", + "steps": "%steps%" + } + }, + "4": { + "class_type": "CheckpointLoaderSimple", + "inputs": { + "ckpt_name": "%model%" + } + }, + "5": { + "class_type": "EmptyLatentImage", + "inputs": { + "batch_size": 1, + "height": "%height%", + "width": "%width%" + } + }, + "6": { + "class_type": "CLIPTextEncode", + "inputs": { + "clip": [ + "4", + 1 + ], + "text": "%prompt%" + } + }, + "7": { + "class_type": "CLIPTextEncode", + "inputs": { + "clip": [ + "4", + 1 + ], + "text": "%negative_prompt%" + } + }, + "8": { + "class_type": "VAEDecode", + "inputs": { + "samples": [ + "3", + 0 + ], + "vae": [ + "4", + 2 + ] + } + }, + "9": { + "class_type": "SaveImage", + "inputs": { + "filename_prefix": "SillyTavern", + "images": [ + "8", + 0 + ] + } + } +} diff --git a/default/content/index.json b/default/content/index.json index a6df7118..8d8fbb14 100644 --- a/default/content/index.json +++ b/default/content/index.json @@ -22,5 +22,9 @@ { "filename": "user-default.png", "type": "avatar" + }, + { + "filename": "Default_Comfy_Workflow.json", + "type": "workflow" } ] diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 8edc89f9..13387f54 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -4,7 +4,7 @@ services: build: .. container_name: sillytavern hostname: sillytavern - image: sillytavern/sillytavern:latest + image: ghcr.io/sillytavern/sillytavern:latest ports: - "8000:8000" volumes: diff --git a/docker/docker-entrypoint.sh b/docker/docker-entrypoint.sh index f3dd2aae..8f0cdf86 100644 --- a/docker/docker-entrypoint.sh +++ b/docker/docker-entrypoint.sh @@ -9,9 +9,9 @@ for R in $RESOURCES; do fi done -if [ ! -e "config/config.conf" ]; then - echo "Resource not found, copying from defaults: config.conf" - cp -r "default/config.conf" "config/config.conf" +if [ ! -e "config/config.yaml" ]; then + echo "Resource not found, copying from defaults: config.yaml" + cp -r "default/config.yaml" "config/config.yaml" fi if [ ! -e "config/settings.json" ]; then @@ -24,15 +24,18 @@ if [ ! -e "config/bg_load.css" ]; then cp -r "default/bg_load.css" "config/bg_load.css" fi -CONFIG_FILE="config.conf" +CONFIG_FILE="config.yaml" -if grep -q "listen = false" $CONFIG_FILE; then - echo -e "\033[1;31mThe listen parameter is set to false. If you can't connect to the server, edit the \"docker/config/config.conf\" file and restart the container.\033[0m" +echo "Starting with the following config:" +cat $CONFIG_FILE + +if grep -q "listen: false" $CONFIG_FILE; then + echo -e "\033[1;31mThe listen parameter is set to false. If you can't connect to the server, edit the \"docker/config/config.yaml\" file and restart the container.\033[0m" sleep 5 fi -if grep -q "whitelistMode = true" $CONFIG_FILE; then - echo -e "\033[1;31mThe whitelistMode parameter is set to true. If you can't connect to the server, edit the \"docker/config/config.conf\" file and restart the container.\033[0m" +if grep -q "whitelistMode: true" $CONFIG_FILE; then + echo -e "\033[1;31mThe whitelistMode parameter is set to true. If you can't connect to the server, edit the \"docker/config/config.yaml\" file and restart the container.\033[0m" sleep 5 fi diff --git a/package-lock.json b/package-lock.json index 666982fb..3ca8ea04 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "sillytavern", - "version": "1.10.9", + "version": "1.10.10", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "sillytavern", - "version": "1.10.9", + "version": "1.10.10", "hasInstallScript": true, "license": "AGPL-3.0", "dependencies": { @@ -21,6 +21,7 @@ "csrf-csrf": "^2.2.3", "device-detector-js": "^3.0.3", "express": "^4.18.2", + "form-data": "^4.0.0", "google-translate-api-browser": "^3.0.1", "gpt3-tokenizer": "^1.1.5", "ip-matching": "^2.1.2", @@ -42,6 +43,7 @@ "vectra": "^0.2.2", "write-file-atomic": "^5.0.1", "ws": "^8.13.0", + "yaml": "^2.3.4", "yargs": "^17.7.1", "yauzl": "^2.10.0" }, @@ -4387,6 +4389,14 @@ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", "dev": true }, + "node_modules/yaml": { + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.3.4.tgz", + "integrity": "sha512-8aAvwVUSHpfEqTQ4w/KMlf3HcRdt50E5ODIQJBw1fQ5RL34xabzxtUlzTXVqc4rkZsPbvrXKWnABCD7kWSmocA==", + "engines": { + "node": ">= 14" + } + }, "node_modules/yargs": { "version": "17.7.2", "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", diff --git a/package.json b/package.json index ab117a4a..2289b052 100644 --- a/package.json +++ b/package.json @@ -3,6 +3,7 @@ "@agnai/sentencepiece-js": "^1.1.1", "@agnai/web-tokenizers": "^0.1.3", "@dqbd/tiktoken": "^1.0.2", + "bing-translate-api": "^2.9.1", "command-exists": "^1.2.9", "compression": "^1", "cookie-parser": "^1.4.6", @@ -10,8 +11,8 @@ "csrf-csrf": "^2.2.3", "device-detector-js": "^3.0.3", "express": "^4.18.2", + "form-data": "^4.0.0", "google-translate-api-browser": "^3.0.1", - "bing-translate-api": "^2.9.1", "gpt3-tokenizer": "^1.1.5", "ip-matching": "^2.1.2", "ipaddr.js": "^2.0.1", @@ -32,6 +33,7 @@ "vectra": "^0.2.2", "write-file-atomic": "^5.0.1", "ws": "^8.13.0", + "yaml": "^2.3.4", "yargs": "^17.7.1", "yauzl": "^2.10.0" }, @@ -50,7 +52,7 @@ "type": "git", "url": "https://github.com/SillyTavern/SillyTavern.git" }, - "version": "1.10.9", + "version": "1.10.10", "scripts": { "start": "node server.js", "start-multi": "node server.js --disableCsrf", diff --git a/post-install.js b/post-install.js index 489bf840..541ce35f 100644 --- a/post-install.js +++ b/post-install.js @@ -4,6 +4,102 @@ const fs = require('fs'); const path = require('path'); const crypto = require('crypto'); +const yaml = require('yaml'); +const _ = require('lodash'); + +/** + * Colorizes console output. + */ +const color = { + byNum: (mess, fgNum) => { + mess = mess || ''; + fgNum = fgNum === undefined ? 31 : fgNum; + return '\u001b[' + fgNum + 'm' + mess + '\u001b[39m'; + }, + black: (mess) => color.byNum(mess, 30), + red: (mess) => color.byNum(mess, 31), + green: (mess) => color.byNum(mess, 32), + yellow: (mess) => color.byNum(mess, 33), + blue: (mess) => color.byNum(mess, 34), + magenta: (mess) => color.byNum(mess, 35), + cyan: (mess) => color.byNum(mess, 36), + white: (mess) => color.byNum(mess, 37) +}; + +/** + * Gets all keys from an object recursively. + * @param {object} obj Object to get all keys from + * @param {string} prefix Prefix to prepend to all keys + * @returns {string[]} Array of all keys in the object + */ +function getAllKeys(obj, prefix = '') { + if (typeof obj !== 'object' || Array.isArray(obj)) { + return []; + } + + return _.flatMap(Object.keys(obj), key => { + const newPrefix = prefix ? `${prefix}.${key}` : key; + if (typeof obj[key] === 'object' && !Array.isArray(obj[key])) { + return getAllKeys(obj[key], newPrefix); + } else { + return [newPrefix]; + } + }); +} + +/** + * Converts the old config.conf file to the new config.yaml format. + */ +function convertConfig() { + if (fs.existsSync('./config.conf')) { + if (fs.existsSync('./config.yaml')) { + console.log(color.yellow('Both config.conf and config.yaml exist. Please delete config.conf manually.')); + return; + } + + try { + console.log(color.blue('Converting config.conf to config.yaml. Your old config.conf will be renamed to config.conf.bak')); + const config = require(path.join(process.cwd(), './config.conf')); + fs.renameSync('./config.conf', './config.conf.bak'); + fs.writeFileSync('./config.yaml', yaml.stringify(config)); + console.log(color.green('Conversion successful. Please check your config.yaml and fix it if necessary.')); + } catch (error) { + console.error(color.red('FATAL: Config conversion failed. Please check your config.conf file and try again.')); + return; + } + } +} + +/** + * Compares the current config.yaml with the default config.yaml and adds any missing values. + */ +function addMissingConfigValues() { + try { + const defaultConfig = yaml.parse(fs.readFileSync(path.join(process.cwd(), './default/config.yaml'), 'utf8')); + let config = yaml.parse(fs.readFileSync(path.join(process.cwd(), './config.yaml'), 'utf8')); + + // Get all keys from the original config + const originalKeys = getAllKeys(config); + + // Use lodash's defaultsDeep function to recursively apply default properties + config = _.defaultsDeep(config, defaultConfig); + + // Get all keys from the updated config + const updatedKeys = getAllKeys(config); + + // Find the keys that were added + const addedKeys = _.difference(updatedKeys, originalKeys); + + if (addedKeys.length === 0) { + return; + } + + console.log('Adding missing config values to config.yaml:', addedKeys); + fs.writeFileSync('./config.yaml', yaml.stringify(config)); + } catch (error) { + console.error(color.red('FATAL: Could not add missing config values to config.yaml'), error); + } +} /** * Creates the default config files if they don't exist yet. @@ -12,7 +108,7 @@ function createDefaultFiles() { const files = { settings: './public/settings.json', bg_load: './public/css/bg_load.css', - config: './config.conf', + config: './config.yaml', user: './public/css/user.css', }; @@ -21,10 +117,10 @@ function createDefaultFiles() { if (!fs.existsSync(file)) { const defaultFilePath = path.join('./default', path.parse(file).base); fs.copyFileSync(defaultFilePath, file); - console.log(`Created default file: ${file}`); + console.log(color.green(`Created default file: ${file}`)); } } catch (error) { - console.error(`FATAL: Could not write default file: ${file}`, error); + console.error(color.red(`FATAL: Could not write default file: ${file}`), error); } } } @@ -73,10 +169,14 @@ function copyWasmFiles() { } try { + // 0. Convert config.conf to config.yaml + convertConfig(); // 1. Create default config files createDefaultFiles(); // 2. Copy transformers WASM binaries from node_modules copyWasmFiles(); + // 3. Add missing config values + addMissingConfigValues(); } catch (error) { console.error(error); } diff --git a/public/css/file-form.css b/public/css/file-form.css new file mode 100644 index 00000000..67801ba3 --- /dev/null +++ b/public/css/file-form.css @@ -0,0 +1,57 @@ +.file_attached { + display: flex; + min-width: 150px; + max-width: calc(var(--sheldWidth) * 0.9); + flex-direction: row; + gap: 10px; + align-items: center; + margin: 0.25em auto; + padding: 0 0.75em; + border: 2px solid var(--SmartThemeBorderColor); + border-radius: 15px; + background-color: var(--white20a); +} + +.mes_file_container { + cursor: default; + display: flex; + gap: 15px; + align-items: center; + width: fit-content; + max-width: 100%; + background-color: var(--white20a); + border: 2px solid var(--SmartThemeBorderColor); + padding: 0.5em 1em; + border-radius: 15px; +} + +.mes_file_container .right_menu_button { + padding-right: 0; +} + +.mes_file_container .mes_file_size, +.file_attached .file_size { + font-size: 0.9em; + color: var(--SmartThemeQuoteColor); +} + +.file_attached .file_name, +.mes_file_container .mes_file_name { + flex: 1; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +#file_form { + display: flex; + width: 100%; +} + +.file_modal { + width: 100%; + height: 100%; + overflow-y: auto; + display: flex; + text-align: left; +} diff --git a/public/css/mobile-styles.css b/public/css/mobile-styles.css index 80df97ff..5b885aba 100644 --- a/public/css/mobile-styles.css +++ b/public/css/mobile-styles.css @@ -369,6 +369,18 @@ top: unset; bottom: unset; } + + + #leftSendForm, + #rightSendForm { + width: 1.15em; + flex-wrap: wrap; + height: unset; + } + + #extensionsMenuButton { + order: 1; + } } /*iOS specific*/ @@ -445,4 +457,4 @@ #horde_model { height: unset; } -} +} \ No newline at end of file diff --git a/public/css/st-tailwind.css b/public/css/st-tailwind.css index d86d53f4..015cf6f5 100644 --- a/public/css/st-tailwind.css +++ b/public/css/st-tailwind.css @@ -297,6 +297,10 @@ align-content: flex-start; } +.alignContentCenter { + align-content: center; +} + .overflowHidden { overflow: hidden; } @@ -526,4 +530,4 @@ textarea:disabled { height: 30px; text-align: center; padding: 5px; -} +} \ No newline at end of file diff --git a/public/css/toggle-dependent.css b/public/css/toggle-dependent.css index 692ebd23..cefca474 100644 --- a/public/css/toggle-dependent.css +++ b/public/css/toggle-dependent.css @@ -17,6 +17,13 @@ body.no-modelIcons .icon-svg { display: none !important; } +body.square-avatars .avatar, +body.square-avatars .avatar img, +body.square-avatars .hotswapAvatar, +body.square-avatars .hotswapAvatar img { + border-radius: 2px !important; +} + /*char list grid mode*/ body.charListGrid #rm_print_characters_block { @@ -359,10 +366,10 @@ body.expandMessageActions .mes .mes_buttons .extraMesButtonsHint { display: none !important; } -#openai_image_inlining:not(:checked) ~ #image_inlining_hint { +#openai_image_inlining:not(:checked)~#image_inlining_hint { display: none; } -#openai_image_inlining:checked ~ #image_inlining_hint { +#openai_image_inlining:checked~#image_inlining_hint { display: block; } diff --git a/public/i18n.json b/public/i18n.json index ee0b0870..51b06d85 100644 --- a/public/i18n.json +++ b/public/i18n.json @@ -38,36 +38,36 @@ "Temperature": "温度", "Frequency Penalty": "频率惩罚", "Presence Penalty": "存在惩罚", - "Top-p": "Top-p", + "Top-p": "Top P", "Display bot response text chunks as they are generated": "显示机器人生成的响应文本块", - "Top A": "Top-a", + "Top A": "Top A", "Typical Sampling": "典型采样", "Tail Free Sampling": "无尾采样", - "Rep. Pen. Slope": "重复惩罚斜率", + "Rep. Pen. Slope": "重复惩罚梯度", "Single-line mode": "单行模式", - "Top K": "Top-k", - "Top P": "Top-p", - "Typical P": "典型P", - "Do Sample": "采样", - "Add BOS Token": "添加BOS Token", - "Add the bos_token to the beginning of prompts. Disabling this can make the replies more creative.": "在提示的开头添加bos_token。禁用此功能可以让回复更加创造性.", - "Ban EOS Token": "禁止EOS Token", - "Ban the eos_token. This forces the model to never end the generation prematurely": "禁止eos_token。这会迫使模型不会过早结束生成", - "Skip Special Tokens": "跳过特殊Tokens", - "Beam search": "光束搜索", - "Number of Beams": "光束数目", + "Top K": "Top-K", + "Top P": "Top-P", + "Typical P": "典型 P", + "Do Sample": "样本测试", + "Add BOS Token": "添加 BOS Token", + "Add the bos_token to the beginning of prompts. Disabling this can make the replies more creative.": "在提示的开头添加 bos_token,禁用此功能可以让回复更加创造性。", + "Ban EOS Token": "禁止 EOS Token", + "Ban the eos_token. This forces the model to never end the generation prematurely": "禁止 EOS Token,这会迫使模型不会过早结束生成。", + "Skip Special Tokens": "跳过特殊 Tokens", + "Beam search": "Beam 搜索", + "Number of Beams": "Beams 的数量", "Length Penalty": "长度惩罚", "Early Stopping": "提前终止", "Contrastive search": "对比搜索", "Penalty Alpha": "惩罚系数", - "Seed": "种子", + "Seed": "随机种子", "Inserts jailbreak as a last system message.": "插入越狱作为最后一个系统消息", "This tells the AI to ignore its usual content restrictions.": "这告诉人工智能忽略其通常的内容限制", "NSFW Encouraged": "NSFW鼓励", - "Tell the AI that NSFW is allowed.": "告诉人工智能,NSFW是允许的。", - "NSFW Prioritized": "NSFW优先", + "Tell the AI that NSFW is allowed.": "告诉人工智能,NSFW 是允许的。", + "NSFW Prioritized": "NSFW 优先", "NSFW prompt text goes first in the prompt to emphasize its effect.": "NSFW 提示文本排在提示的顶部,以强调其效果", - "Streaming": "流式回复", + "Streaming": "流式生成", "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.": "关闭此选项后,响应将在全部完成后立即显示。", "Generate only one line per request (KoboldAI only, ignored by KoboldCpp).": "每个请求仅生成一行(仅限 KoboldAI,被 KoboldCpp 忽略)。", @@ -109,7 +109,7 @@ "For privacy reasons": "出于隐私原因,您的 API 密钥将在您重新加载页面后隐藏", "Model": "模型", "Hold Control / Command key to select multiple models.": "按住控制/命令键选择多个模型。", - "Horde models not loaded": "未加载Horde模型。", + "Horde models not loaded": "未加载 Horde 模型。", "Not connected": "未连接", "Novel API key": "NovelAI API 密钥", "Follow": "跟随", @@ -126,11 +126,11 @@ "OpenAI Model": "OpenAI模型", "View API Usage Metrics": "查看 API 使用情况", "Bot": "Bot", - "Connect to the API": "连接到API", + "Connect to the API": "连接到 API", "Auto-connect to Last Server": "自动连接到最后设置的 API 服务器", "View hidden API keys": "查看隐藏的 API 密钥", - "Advanced Formatting": "高级格式", - "AutoFormat Overrides": "自动套用格式替代", + "Advanced Formatting": "高级格式化", + "AutoFormat Overrides": "覆盖自动格式化", "Disable description formatting": "禁用描述格式", "Disable personality formatting": "禁用人设格式", "Disable scenario formatting": "禁用场景格式", @@ -166,7 +166,6 @@ "Style then Character": "样式然后字符", "Character Anchor": "字符锚点", "Style Anchor": "样式锚点", - "World Info": "", "Scan Depth": "扫描深度", "depth": "深度", "Token Budget": "Token 预算", @@ -400,8 +399,8 @@ "Samplers Order": "采样器顺序", "Samplers will be applied in a top-down order. Use with caution.": "采样器将按从上到下的顺序应用。谨慎使用。", "Repetition Penalty": "重复惩罚", - "Epsilon Cutoff": "Epsilon切断", - "Eta Cutoff": "Eta切断", + "Epsilon Cutoff": "Epsilon 切断", + "Eta Cutoff": "Eta 切断", "Rep. Pen. Range.": "重复惩罚范围", "Rep. Pen. Freq.": "重复频率惩罚", "Rep. Pen. Presence": "重复存在惩罚", @@ -483,7 +482,7 @@ "removes blur and uses alternative background color for divs": "去除模糊并为div使用替代的背景颜色", "If checked and the character card contains a prompt override (System Prompt), use that instead.": "如果选中并且角色卡包含提示覆盖(系统提示),请改用该选项。", "If checked and the character card contains a jailbreak override (Post History Instruction), use that instead.": "如果选中并且角色卡包含越狱覆盖(发布历史指令),请改用该选项。", - "AI Response Formatting": "AI回复格式", + "AI Response Formatting": "AI 回复格式", "Change Background Image": "更改背景图片", "Extensions": "扩展", "Click to set a new User Name": "点击设置新用户名", @@ -493,7 +492,7 @@ "Character Management": "角色管理", "Locked = Character Management panel will stay open": "锁定=角色管理面板将保持打开状态", "Select/Create Characters": "选择/创建角色", - "Token counts may be inaccurate and provided just for reference.": "Token计数可能不准确,仅供参考。", + "Token counts may be inaccurate and provided just for reference.": "Token 计数可能不准确,仅供参考。", "Click to select a new avatar for this character": "点击选择此角色的新头像", "Add to Favorites": "添加到收藏夹", "Advanced Definition": "高级定义", @@ -525,7 +524,7 @@ "Associate one or more auxillary Lorebooks with this character.": "将一个或多个辅助的 Lorebook 与这个角色关联。", "NOTE: These choices are optional and won't be preserved on character export!": "注意:这些选择是可选的,不会在导出角色时保留!", "Rename chat file": "重命名聊天文件", - "Export JSONL chat file": "导出JSONL聊天文件", + "Export JSONL chat file": "导出 JSONL 聊天文件", "Download chat as plain text document": "将聊天内容下载为纯文本文档", "Delete chat file": "删除聊天文件", "Delete tag": "删除标签", @@ -553,7 +552,7 @@ "Add": "添加", "Abort request": "取消请求", "Send a message": "发送消息", - "Ask AI to write your message for you": "让AI代替你写消息", + "Ask AI to write your message for you": "让 AI 代替你写消息", "Continue the last message": "继续上一条消息", "Bind user name to that avatar": "将用户名绑定到该头像", "Select this as default persona for the new chats.": "将此选择为新聊天的默认角色。", diff --git a/public/index.html b/public/index.html index 4da67bb0..ad3e2417 100644 --- a/public/index.html +++ b/public/index.html @@ -8,6 +8,7 @@ + @@ -182,7 +183,7 @@
-

Text Gen WebUI presets

+

Text Completion presets

@@ -411,7 +412,7 @@ Max Response Length (tokens)
- +
@@ -1142,17 +1143,17 @@
-
+
Repetition Penalty
-
+
Repetition Penalty Range
-
+
Encoder Penalty @@ -1167,7 +1168,7 @@
-
+
No Repeat Ngram Size @@ -1178,22 +1179,22 @@
+ + + - + + +
-
+
Seed
@@ -1306,23 +1307,23 @@

Banned Tokens -
+

-
+

CFG

-
+
Scale
-
+
Negative Prompt @@ -1345,6 +1346,49 @@
+
+
+
+ Samplers Order +
+
+ Samplers will be applied in a top-down order. + Use with caution. +
+
+
+ Top K + 0 +
+
+ Top A + 1 +
+
+ Top P & Min P + 2 +
+
+ Tail Free Sampling + 3 +
+
+ Typical P + 4 +
+
+ Temperature + 5 +
+
+ Repetition Penalty + 6 +
+
+ +
@@ -1383,6 +1427,17 @@
+
+ +
+
+ +

Tabby API key

+
+ + +
+
+ For privacy reasons, your API key will be hidden after you reload the page. +
+
+

API URL

+ Example: http://127.0.0.1:5000 + +
+
+
+ +
+

API URL

+ Example: http://127.0.0.1:5001 + +
+
- +
-
@@ -1791,6 +1870,7 @@ + @@ -1849,7 +1929,7 @@
@@ -2271,6 +2351,7 @@ +
@@ -2618,6 +2699,7 @@ Avatars:
@@ -2721,6 +2803,19 @@
+
+
+ Chat Truncation (0 = unlimited) +
+
+
+ +
+
+ +
+
+
@@ -3093,8 +3188,33 @@
-

Persona Management

- How do I use this? +
+
+

Persona Management

+ + + +
+
+ + + + + +
+

Name

@@ -3134,13 +3254,6 @@

Your Persona - -

+
@@ -3399,6 +3512,10 @@ Auto Mode +
@@ -3674,6 +3791,7 @@ Chat History
+
@@ -4100,7 +4218,7 @@
-
+
@@ -4280,6 +4398,15 @@
CHAR is typing
+
+
+
+
+
+
+
+
+
@@ -4567,17 +4694,33 @@
- -
- -
-
- +
+ +
+ + + + File Name + File Size + +
+ +
+
+
+
+ +
+
+ +
+
+
-
-
- +
diff --git a/public/manifest.json b/public/manifest.json new file mode 100644 index 00000000..28df3de4 --- /dev/null +++ b/public/manifest.json @@ -0,0 +1,30 @@ +{ + "name": "SillyTavern", + "short_name": "SillyTavern", + "start_url": "/", + "display": "standalone", + "theme_color": "#202124", + "background_color": "#202124", + "icons": [ + { + "src": "img/apple-icon-57x57.png", + "sizes": "57x57", + "type": "image/png" + }, + { + "src": "img/apple-icon-72x72.png", + "sizes": "72x72", + "type": "image/png" + }, + { + "src": "img/apple-icon-114x114.png", + "sizes": "114x114", + "type": "image/png" + }, + { + "src": "img/apple-icon-144x144.png", + "sizes": "144x144", + "type": "image/png" + } + ] +} diff --git a/public/script.js b/public/script.js index 1e1f825a..9f598585 100644 --- a/public/script.js +++ b/public/script.js @@ -19,10 +19,12 @@ import { getTextGenUrlSourceId, isMancer, isAphrodite, + isTabby, textgen_types, textgenerationwebui_banned_in_macros, isOoba, MANCER_SERVER, + isKoboldCpp, } from "./scripts/textgen-settings.js"; import { @@ -80,6 +82,7 @@ import { registerDebugFunction, ui_mode, switchSimpleMode, + flushEphemeralStoppingStrings, } from "./scripts/power-user.js"; import { @@ -144,6 +147,7 @@ import { resetScrollHeight, onlyUnique, getBase64Async, + humanFileSize, } from "./scripts/utils.js"; import { ModuleWorkerWrapper, doDailyExtensionUpdatesCheck, extension_settings, getContext, loadExtensionSettings, processExtensionHelpers, registerExtensionHelper, renderExtensionTemplate, runGenerationInterceptors, saveMetadataDebounced } from "./scripts/extensions.js"; @@ -182,6 +186,7 @@ import { getInstructStoppingSequences, autoSelectInstructPreset, formatInstructModeSystemPrompt, + replaceInstructMacros, } from "./scripts/instruct-mode.js"; import { applyLocale } from "./scripts/i18n.js"; import { getFriendlyTokenizerName, getTokenCount, getTokenizerModel, initTokenizers, saveTokenCache } from "./scripts/tokenizers.js"; @@ -190,6 +195,8 @@ import { getBackgrounds, initBackgrounds } from "./scripts/backgrounds.js"; import { hideLoader, showLoader } from "./scripts/loader.js"; import { CharacterContextMenu, BulkEditOverlay } from "./scripts/BulkEditOverlay.js"; import { loadMancerModels } from "./scripts/mancer-settings.js"; +import { hasPendingFileAttachment, populateFileAttachment } from "./scripts/chats.js"; +import { replaceVariableMacros } from "./scripts/variables.js"; //exporting functions and vars for mods export { @@ -279,6 +286,7 @@ window["SillyTavern"] = {}; // Event source init export const event_types = { + APP_READY: 'app_ready', EXTRAS_CONNECTED: 'extras_connected', MESSAGE_SWIPED: 'message_swiped', MESSAGE_SENT: 'message_sent', @@ -643,7 +651,7 @@ let create_save = { }; //animation right menu -let animation_duration = 125; +export let animation_duration = 125; let animation_easing = "ease-in-out"; let popup_type = ""; let chat_file_for_del = ""; @@ -654,7 +662,7 @@ let api_server_textgenerationwebui = ""; let is_send_press = false; //Send generation -let this_del_mes = 0; +let this_del_mes = -1; //message editing and chat scroll position persistence var this_edit_mes_text = ""; @@ -738,6 +746,7 @@ async function firstLoadInit() { initCfg(); doDailyExtensionUpdatesCheck(); hideLoader(); + await eventSource.emit(event_types.APP_READY); } function cancelStatusCheck() { @@ -882,6 +891,8 @@ async function getStatus() { use_mancer: main_api == "textgenerationwebui" ? isMancer() : false, use_aphrodite: main_api == "textgenerationwebui" ? isAphrodite() : false, use_ooba: main_api == "textgenerationwebui" ? isOoba() : false, + use_tabby: main_api == "textgenerationwebui" ? isTabby() : false, + use_koboldcpp: main_api == "textgenerationwebui" ? isKoboldCpp() : false, legacy_api: main_api == "textgenerationwebui" ? textgenerationwebui_settings.legacy_api && !isMancer() : false, }), signal: abortStatusCheck.signal, @@ -1291,11 +1302,9 @@ async function replaceCurrentChat() { } } -const TRUNCATION_THRESHOLD = 100; - export function showMoreMessages() { let messageId = Number($('#chat').children('.mes').first().attr('mesid')); - let count = TRUNCATION_THRESHOLD; + let count = power_user.chat_truncation || Number.MAX_SAFE_INTEGER; console.debug('Inserting messages before', messageId, 'count', count, 'chat length', chat.length); const prevHeight = $('#chat').prop('scrollHeight'); @@ -1316,9 +1325,10 @@ export function showMoreMessages() { async function printMessages() { let startIndex = 0; + let count = power_user.chat_truncation || Number.MAX_SAFE_INTEGER; - if (chat.length > TRUNCATION_THRESHOLD) { - count_view_mes = chat.length - TRUNCATION_THRESHOLD; + if (chat.length > count) { + count_view_mes = chat.length - count; startIndex = count_view_mes; $('#chat').append('
Show more messages
'); } @@ -1327,11 +1337,36 @@ async function printMessages() { const item = chat[i]; addOneMessage(item, { scroll: i === chat.length - 1 }); } + + // Scroll to bottom when all images are loaded + const images = document.querySelectorAll('#chat .mes img'); + let imagesLoaded = 0; + + for (let i = 0; i < images.length; i++) { + const image = images[i]; + if (image instanceof HTMLImageElement) { + if (image.complete) { + incrementAndCheck(); + } else { + image.addEventListener('load', incrementAndCheck); + } + } + } + + function incrementAndCheck() { + imagesLoaded++; + if (imagesLoaded === images.length) { + scrollChatToBottom(); + } + } } async function clearChat() { count_view_mes = 0; extension_prompts = {}; + if (is_delete_mode) { + $("#dialogue_del_mes_cancel").trigger('click'); + } $("#chat").children().remove(); if ($('.zoomed_avatar[forChar]').length) { console.debug('saw avatars to remove') @@ -1364,6 +1399,9 @@ export async function reloadCurrentChat() { await printMessages(); await eventSource.emit(event_types.CHAT_CHANGED, getCurrentChatId()); } + + hideSwipeButtons(); + showSwipeButtons(); } function messageFormatting(mes, ch_name, isSystem, isUser) { @@ -1567,20 +1605,49 @@ export function updateMessageBlock(messageId, message) { const text = message?.extra?.display_text ?? message.mes; messageElement.find('.mes_text').html(messageFormatting(text, message.name, message.is_system, message.is_user)); addCopyToCodeBlocks(messageElement) - appendImageToMessage(message, messageElement); + appendMediaToMessage(message, messageElement); } -export function appendImageToMessage(mes, messageElement) { +export function appendMediaToMessage(mes, messageElement) { + // Add image to message if (mes.extra?.image) { + const chatHeight = $('#chat').prop('scrollHeight'); const image = messageElement.find('.mes_img'); const text = messageElement.find('.mes_text'); const isInline = !!mes.extra?.inline_image; + image.on('load', function () { + const scrollPosition = $('#chat').scrollTop(); + const newChatHeight = $('#chat').prop('scrollHeight'); + const diff = newChatHeight - chatHeight; + $('#chat').scrollTop(scrollPosition + diff); + }); image.attr('src', mes.extra?.image); image.attr('title', mes.extra?.title || mes.title || ''); messageElement.find(".mes_img_container").addClass("img_extra"); image.toggleClass("img_inline", isInline); text.toggleClass('displayNone', !isInline); } + + // Add file to message + if (mes.extra?.file) { + messageElement.find(".mes_file_container").remove(); + const messageId = messageElement.attr('mesid'); + const template = $('#message_file_template .mes_file_container').clone(); + template.find('.mes_file_name').text(mes.extra.file.name); + template.find('.mes_file_size').text(humanFileSize(mes.extra.file.size)); + template.find('.mes_file_download').attr('mesid', messageId); + template.find('.mes_file_delete').attr('mesid', messageId); + messageElement.find(".mes_block").append(template); + } else { + messageElement.find(".mes_file_container").remove(); + } +} + +/** + * @deprecated Use appendMediaToMessage instead. + */ +export function appendImageToMessage(mes, messageElement) { + appendMediaToMessage(mes, messageElement); } export function addCopyToCodeBlocks(messageElement) { @@ -1772,7 +1839,7 @@ function addOneMessage(mes, { type = "normal", insertAfter = null, scroll = true const swipeMessage = $("#chat").find(`[mesid="${count_view_mes - 1}"]`); swipeMessage.find('.mes_text').html(''); swipeMessage.find('.mes_text').append(messageText); - appendImageToMessage(mes, swipeMessage); + appendMediaToMessage(mes, swipeMessage); swipeMessage.attr('title', title); swipeMessage.find('.timestamp').text(timestamp).attr('title', `${params.extra.api} - ${params.extra.model}`); if (power_user.timestamp_model_icon && params.extra?.api) { @@ -1789,12 +1856,12 @@ function addOneMessage(mes, { type = "normal", insertAfter = null, scroll = true } } else if (typeof forceId == 'number') { $("#chat").find(`[mesid="${forceId}"]`).find('.mes_text').append(messageText); - appendImageToMessage(mes, newMessage); + appendMediaToMessage(mes, newMessage); hideSwipeButtons(); showSwipeButtons(); } else { $("#chat").find(`[mesid="${count_view_mes}"]`).find('.mes_text').append(messageText); - appendImageToMessage(mes, newMessage); + appendMediaToMessage(mes, newMessage); hideSwipeButtons(); count_view_mes++; } @@ -1812,10 +1879,35 @@ function addOneMessage(mes, { type = "normal", insertAfter = null, scroll = true } } -function getUserAvatar(avatarImg) { +/** + * Returns the URL of the avatar for the given user avatar Id. + * @param {string} avatarImg User avatar Id + * @returns {string} User avatar URL + */ +export function getUserAvatar(avatarImg) { return `User Avatars/${avatarImg}`; } +/** + * Returns the URL of the avatar for the given character Id. + * @param {number} characterId Character Id + * @returns {string} Avatar URL + */ +export function getCharacterAvatar(characterId) { + const character = characters[characterId]; + const avatarImg = character?.avatar; + + if (!avatarImg || avatarImg === 'none') { + return default_avatar; + } + + return formatCharacterAvatar(avatarImg); +} + +export function formatCharacterAvatar(characterAvatar) { + return `characters/${characterAvatar}`; +} + /** * Formats the title for the generation timer. * @param {Date} gen_started Date when generation was started @@ -1844,6 +1936,10 @@ function formatGenerationTimer(gen_started, gen_finished, tokenCount) { tokenCount > 0 ? `Token rate: ${Number(tokenCount / seconds).toFixed(1)} t/s` : '', ].join('\n'); + if (isNaN(seconds)) { + return { timerValue: '', timerTitle }; + } + return { timerValue, timerTitle }; } @@ -1878,6 +1974,20 @@ function getLastMessageId() { return ''; } +/** + * Returns the last message in the chat. + * @returns {string} The last message in the chat. + */ +function getLastMessage() { + const index = chat?.length - 1; + + if (!isNaN(index) && index >= 0) { + return chat[index].mes; + } + + return ''; +} + /** * Substitutes {{macro}} parameters in a string. * @param {string} content - The string to substitute parameters in. @@ -1902,11 +2012,17 @@ function substituteParams(content, _name1, _name2, _original, _group, _replaceCh if (typeof _original === 'string') { content = content.replace(/{{original}}/i, _original); } - + content = diceRollReplace(content); + content = randomReplace(content); + content = replaceInstructMacros(content); + content = replaceVariableMacros(content); + content = content.replace(/{{newline}}/gi, "\n"); content = content.replace(/{{input}}/gi, String($('#send_textarea').val())); if (_replaceCharacterCard) { const fields = getCharacterCardFields(); + content = content.replace(/{{charPrompt}}/gi, fields.system || ''); + content = content.replace(/{{charJailbreak}}/gi, fields.jailbreak || ''); content = content.replace(/{{description}}/gi, fields.description || ''); content = content.replace(/{{personality}}/gi, fields.personality || ''); content = content.replace(/{{scenario}}/gi, fields.scenario || ''); @@ -1918,6 +2034,7 @@ function substituteParams(content, _name1, _name2, _original, _group, _replaceCh content = content.replace(/{{char}}/gi, _name2); content = content.replace(/{{charIfNotGroup}}/gi, _group); content = content.replace(/{{group}}/gi, _group); + content = content.replace(/{{lastMessage}}/gi, getLastMessage()); content = content.replace(/{{lastMessageId}}/gi, getLastMessageId()); content = content.replace(//gi, _name1); @@ -1943,8 +2060,6 @@ function substituteParams(content, _name1, _name2, _original, _group, _replaceCh const utcTime = moment().utc().utcOffset(utcOffset).format('LT'); return utcTime; }); - content = randomReplace(content); - content = diceRollReplace(content); content = bannedWordsReplace(content); return content; } @@ -2009,21 +2124,34 @@ function getTimeSinceLastMessage() { } function randomReplace(input, emptyListPlaceholder = '') { - const randomPattern = /{{random[ : ]([^}]+)}}/gi; + const randomPatternNew = /{{random\s?::\s?([^}]+)}}/gi; + const randomPatternOld = /{{random\s?:\s?([^}]+)}}/gi; - return input.replace(randomPattern, (match, listString) => { - const list = listString.split(',').map(item => item.trim()).filter(item => item.length > 0); - - if (list.length === 0) { - return emptyListPlaceholder; - } - - var rng = new Math.seedrandom('added entropy.', { entropy: true }); - const randomIndex = Math.floor(rng() * list.length); - - //const randomIndex = Math.floor(Math.random() * list.length); - return list[randomIndex]; - }); + if (randomPatternNew.test(input)) { + return input.replace(randomPatternNew, (match, listString) => { + //split on double colons instead of commas to allow for commas inside random items + const list = listString.split('::').filter(item => item.length > 0); + if (list.length === 0) { + return emptyListPlaceholder; + } + var rng = new Math.seedrandom('added entropy.', { entropy: true }); + const randomIndex = Math.floor(rng() * list.length); + //trim() at the end to allow for empty random values + return list[randomIndex].trim(); + }); + } else if (randomPatternOld.test(input)) { + return input.replace(randomPatternOld, (match, listString) => { + const list = listString.split(',').map(item => item.trim()).filter(item => item.length > 0); + if (list.length === 0) { + return emptyListPlaceholder; + } + var rng = new Math.seedrandom('added entropy.', { entropy: true }); + const randomIndex = Math.floor(rng() * list.length); + return list[randomIndex]; + }); + } else { + return input + } } function diceRollReplace(input, invalidRollPlaceholder = '') { @@ -2048,13 +2176,23 @@ function diceRollReplace(input, invalidRollPlaceholder = '') { }); } -function getStoppingStrings(isImpersonate) { +/** + * Gets stopping sequences for the prompt. + * @param {boolean} isImpersonate A request is made to impersonate a user + * @param {boolean} isContinue A request is made to continue the message + * @returns {string[]} Array of stopping strings + */ +function getStoppingStrings(isImpersonate, isContinue) { const charString = `\n${name2}:`; const userString = `\n${name1}:`; const result = isImpersonate ? [charString] : [userString]; result.push(userString); + if (isContinue && Array.isArray(chat) && chat[chat.length - 1]?.is_user) { + result.push(charString); + } + // Add other group members as the stopping strings if (selected_group) { const group = groups.find(x => x.id === selected_group); @@ -2087,16 +2225,17 @@ function getStoppingStrings(isImpersonate) { * @param {string} quiet_prompt Instruction prompt for the AI * @param {boolean} quietToLoud Whether the message should be sent in a foreground (loud) or background (quiet) mode * @param {boolean} skipWIAN whether to skip addition of World Info and Author's Note into the prompt + * @param {string} quietImage Image to use for the quiet prompt * @returns */ -export async function generateQuietPrompt(quiet_prompt, quietToLoud, skipWIAN) { +export async function generateQuietPrompt(quiet_prompt, quietToLoud, skipWIAN, quietImage = null) { console.log('got into genQuietPrompt') const skipWIANvalue = skipWIAN return await new Promise( async function promptPromise(resolve, reject) { if (quietToLoud === true) { try { - await Generate('quiet', { resolve, reject, quiet_prompt, quietToLoud: true, skipWIAN: skipWIAN, force_name2: true, }); + await Generate('quiet', { resolve, reject, quiet_prompt, quietToLoud: true, skipWIAN: skipWIAN, force_name2: true, quietImage: quietImage }); } catch { reject(); @@ -2105,7 +2244,7 @@ export async function generateQuietPrompt(quiet_prompt, quietToLoud, skipWIAN) { else { try { console.log('going to generate non-QuietToLoud') - await Generate('quiet', { resolve, reject, quiet_prompt, quietToLoud: false, skipWIAN: skipWIAN, force_name2: true, }); + await Generate('quiet', { resolve, reject, quiet_prompt, quietToLoud: false, skipWIAN: skipWIAN, force_name2: true, quietImage: quietImage }); } catch { reject(); @@ -2114,20 +2253,30 @@ export async function generateQuietPrompt(quiet_prompt, quietToLoud, skipWIAN) { }); } -async function processCommands(message, type) { - if (type == "regenerate" || type == "swipe" || type == 'quiet') { +async function processCommands(message, type, dryRun) { + if (dryRun || type == "regenerate" || type == "swipe" || type == 'quiet') { return null; } + const previousText = String($("#send_textarea").val()); const result = await executeSlashCommands(message); - $("#send_textarea").val(result.newText).trigger('input'); + + if (!result || typeof result !== 'object') { + return null; + } + + const currentText = String($("#send_textarea").val()); + + if (previousText === currentText) { + $("#send_textarea").val(result.newText).trigger('input'); + } // interrupt generation if the input was nothing but a command - if (message.length > 0 && result.newText.length === 0) { + if (message.length > 0 && result?.newText.length === 0) { return true; } - return result.interrupt; + return result?.interrupt; } function sendSystemMessage(type, text, extra = {}) { @@ -2579,14 +2728,16 @@ class StreamingProcessor { * Generates a message using the provided prompt. * @param {string} prompt Prompt to generate a message from * @param {string} api API to use. Main API is used if not specified. + * @param {boolean} instructOverride true to override instruct mode, false to use the default value + * @returns {Promise} Generated message */ -export async function generateRaw(prompt, api) { +export async function generateRaw(prompt, api, instructOverride) { if (!api) { api = main_api; } const abortController = new AbortController(); - const isInstruct = power_user.instruct.enabled && main_api !== 'openai' && main_api !== 'novel'; + const isInstruct = power_user.instruct.enabled && main_api !== 'openai' && main_api !== 'novel' && !instructOverride; prompt = substituteParams(prompt); prompt = api == 'novel' ? adjustNovelInstructionPrompt(prompt) : prompt; @@ -2608,10 +2759,10 @@ export async function generateRaw(prompt, api) { break; case 'novel': const novelSettings = novelai_settings[novelai_setting_names[nai_settings.preset_settings_novel]]; - generateData = getNovelGenerationData(prompt, novelSettings, amount_gen, false, null); + generateData = getNovelGenerationData(prompt, novelSettings, amount_gen, false, false, null); break; case 'textgenerationwebui': - generateData = getTextGenGenerationData(prompt, amount_gen, false, null); + generateData = getTextGenGenerationData(prompt, amount_gen, false, false, null); break; case 'openai': generateData = [{ role: 'user', content: prompt.trim() }]; @@ -2654,7 +2805,7 @@ export async function generateRaw(prompt, api) { return message; } -async function Generate(type, { automatic_trigger, force_name2, resolve, reject, quiet_prompt, quietToLoud, skipWIAN, force_chid, signal } = {}, dryRun = false) { +async function Generate(type, { automatic_trigger, force_name2, resolve, reject, quiet_prompt, quietToLoud, skipWIAN, force_chid, signal, quietImage } = {}, dryRun = false) { console.log('Generate entered'); setGenerationProgress(0); generation_started = new Date(); @@ -2670,10 +2821,10 @@ async function Generate(type, { automatic_trigger, force_name2, resolve, reject, message_already_generated = isImpersonate ? `${name1}: ` : `${name2}: `; - const interruptedByCommand = await processCommands($("#send_textarea").val(), type); + const interruptedByCommand = await processCommands($("#send_textarea").val(), type, dryRun); if (interruptedByCommand) { - $("#send_textarea").val('').trigger('input'); + //$("#send_textarea").val('').trigger('input'); unblockGeneration(); return; } @@ -2709,7 +2860,7 @@ async function Generate(type, { automatic_trigger, force_name2, resolve, reject, } if (selected_group && !is_group_generating && !dryRun) { - generateGroupWrapper(false, type, { resolve, reject, quiet_prompt, force_chid, signal: abortController.signal }); + generateGroupWrapper(false, type, { resolve, reject, quiet_prompt, force_chid, signal: abortController.signal, quietImage }); return; } else if (selected_group && !is_group_generating && dryRun) { const characterIndexMap = new Map(characters.map((char, index) => [char.avatar, index])); @@ -2794,7 +2945,7 @@ async function Generate(type, { automatic_trigger, force_name2, resolve, reject, //********************************* //for normal messages sent from user.. - if (textareaText != "" && !automatic_trigger && type !== 'quiet') { + if ((textareaText != "" || hasPendingFileAttachment()) && !automatic_trigger && type !== 'quiet') { // If user message contains no text other than bias - send as a system message if (messageBias && replaceBiasMarkup(textareaText).trim().length === 0) { sendSystemMessage(system_message_types.GENERIC, ' ', { bias: messageBias }); @@ -2866,12 +3017,22 @@ async function Generate(type, { automatic_trigger, force_name2, resolve, reject, coreChat.pop(); } - coreChat = coreChat.map(x => ({ - ...x, - mes: getRegexedString(x.mes, x.is_user ? regex_placement.USER_INPUT : regex_placement.AI_OUTPUT, { - isPrompt: true, - }), - })) + coreChat = coreChat.map(chatItem => { + let message = chatItem.mes; + let regexType = chatItem.is_user ? regex_placement.USER_INPUT : regex_placement.AI_OUTPUT; + let options = { isPrompt: true }; + + let regexedMessage = getRegexedString(message, regexType, options); + + if (chatItem.extra?.file?.text) { + regexedMessage += `\n\n${chatItem.extra.file.text}`; + } + + return { + ...chatItem, + mes: regexedMessage, + }; + }); // Determine token limit let this_max_context = getMaxContextSize(); @@ -2985,14 +3146,18 @@ async function Generate(type, { automatic_trigger, force_name2, resolve, reject, wiAfter: worldInfoAfter, loreBefore: worldInfoBefore, loreAfter: worldInfoAfter, + mesExamples: mesExamplesArray.join(''), }; const storyString = renderStoryString(storyStringParams); + let oaiMessages = []; + let oaiMessageExamples = []; + if (main_api === 'openai') { message_already_generated = ''; - setOpenAIMessages(coreChat); - setOpenAIMessageExamples(mesExamplesArray); + oaiMessages = setOpenAIMessages(coreChat); + oaiMessageExamples = setOpenAIMessageExamples(mesExamplesArray); } // hack for regeneration of the first message @@ -3072,7 +3237,7 @@ async function Generate(type, { automatic_trigger, force_name2, resolve, reject, console.debug('calling runGenerate'); if (!dryRun) { - streamingProcessor = isStreamingEnabled() ? new StreamingProcessor(type, force_name2, generation_started) : false; + streamingProcessor = isStreamingEnabled() && type !== 'quiet' ? new StreamingProcessor(type, force_name2, generation_started) : false; } if (isContinue) { @@ -3182,12 +3347,14 @@ async function Generate(type, { automatic_trigger, force_name2, resolve, reject, } // Add character's name - // Force name append on continue + // Force name append on continue (if not continuing on user message) if (!isInstruct && force_name2) { if (!lastMesString.endsWith('\n')) { lastMesString += '\n'; } - lastMesString += `${name2}:`; + if (!isContinue || !(chat[chat.length - 1]?.is_user)) { + lastMesString += `${name2}:`; + } } return lastMesString; @@ -3206,7 +3373,7 @@ async function Generate(type, { automatic_trigger, force_name2, resolve, reject, } // Add a space if prompt cache doesn't start with one - if (!/^\s/.test(promptCache) && !isInstruct) { + if (!/^\s/.test(promptCache) && !isInstruct && !isContinue) { promptCache = ' ' + promptCache; } @@ -3397,11 +3564,11 @@ async function Generate(type, { automatic_trigger, force_name2, resolve, reject, } } else if (main_api == 'textgenerationwebui') { - generate_data = getTextGenGenerationData(finalPrompt, maxLength, isImpersonate, cfgValues); + generate_data = getTextGenGenerationData(finalPrompt, maxLength, isImpersonate, isContinue, cfgValues); } else if (main_api == 'novel') { const presetSettings = novelai_settings[novelai_setting_names[nai_settings.preset_settings_novel]]; - generate_data = getNovelGenerationData(finalPrompt, presetSettings, maxLength, isImpersonate, cfgValues); + generate_data = getNovelGenerationData(finalPrompt, presetSettings, maxLength, isImpersonate, isContinue, cfgValues); } else if (main_api == 'openai') { let [prompt, counts] = await prepareOpenAIMessages({ @@ -3415,10 +3582,13 @@ async function Generate(type, { automatic_trigger, force_name2, resolve, reject, bias: promptBias, type: type, quietPrompt: quiet_prompt, + quietImage: quietImage, cyclePrompt: cyclePrompt, systemPromptOverride: system, jailbreakPromptOverride: jailbreak, - personaDescription: persona + personaDescription: persona, + messages: oaiMessages, + messageExamples: oaiMessageExamples, }, dryRun); generate_data = { prompt: prompt }; @@ -3566,7 +3736,7 @@ async function Generate(type, { automatic_trigger, force_name2, resolve, reject, } //Formating - const displayIncomplete = type == 'quiet'; + const displayIncomplete = type === 'quiet' && !quietToLoud; getMessage = cleanUpMessage(getMessage, isImpersonate, isContinue, displayIncomplete); if (getMessage.length > 0) { @@ -3652,6 +3822,7 @@ async function Generate(type, { automatic_trigger, force_name2, resolve, reject, activateSendButtons(); showSwipeButtons(); setGenerationProgress(0); + streamingProcessor = null; if (type !== 'quiet') { triggerAutoContinue(messageChunk, isImpersonate); @@ -3665,12 +3836,9 @@ async function Generate(type, { automatic_trigger, force_name2, resolve, reject, } reject(exception); - $("#send_textarea").removeAttr('disabled'); - is_send_press = false; - activateSendButtons(); - showSwipeButtons(); - setGenerationProgress(0); + unblockGeneration(); console.log(exception); + streamingProcessor = null; }; } //rungenerate ends @@ -3696,6 +3864,7 @@ function unblockGeneration() { activateSendButtons(); showSwipeButtons(); setGenerationProgress(0); + flushEphemeralStoppingStrings(); $("#send_textarea").removeAttr('disabled'); } @@ -3841,6 +4010,7 @@ export async function sendMessageAsUser(textareaText, messageBias) { console.debug('checking bias'); chat[chat.length - 1]['extra']['bias'] = messageBias; } + await populateFileAttachment(chat[chat.length - 1]); statMesProcess(chat[chat.length - 1], 'user', characters, this_chid, ''); // Wait for all handlers to finish before continuing with the prompt const chat_id = (chat.length - 1); @@ -4202,7 +4372,7 @@ function cleanUpMessage(getMessage, isImpersonate, isContinue, displayIncomplete getMessage = substituteParams(power_user.user_prompt_bias) + getMessage; } - const stoppingStrings = getStoppingStrings(isImpersonate); + const stoppingStrings = getStoppingStrings(isImpersonate, isContinue); for (const stoppingString of stoppingStrings) { if (stoppingString.length) { @@ -4244,13 +4414,13 @@ function cleanUpMessage(getMessage, isImpersonate, isContinue, displayIncomplete } if (nameToTrim && getMessage.indexOf(`${nameToTrim}:`) == 0) { - getMessage = getMessage.substr(0, getMessage.indexOf(`${nameToTrim}:`)); + getMessage = getMessage.substring(0, getMessage.indexOf(`${nameToTrim}:`)); } if (nameToTrim && getMessage.indexOf(`\n${nameToTrim}:`) >= 0) { - getMessage = getMessage.substr(0, getMessage.indexOf(`\n${nameToTrim}:`)); + getMessage = getMessage.substring(0, getMessage.indexOf(`\n${nameToTrim}:`)); } if (getMessage.indexOf('<|endoftext|>') != -1) { - getMessage = getMessage.substr(0, getMessage.indexOf('<|endoftext|>')); + getMessage = getMessage.substring(0, getMessage.indexOf('<|endoftext|>')); } const isInstruct = power_user.instruct.enabled && main_api !== 'openai'; if (isInstruct && power_user.instruct.stop_sequence) { @@ -4295,7 +4465,8 @@ function cleanUpMessage(getMessage, isImpersonate, isContinue, displayIncomplete } if (!power_user.allow_name2_display) { - getMessage = getMessage.replace(new RegExp(`(^|\n)${name2}:`, 'g'), "$1"); + const name2Escaped = escapeRegex(name2); + getMessage = getMessage.replace(new RegExp(`(^|\n)${name2Escaped}:\\s*`, 'g'), "$1"); } if (isImpersonate) { @@ -4685,7 +4856,7 @@ async function renamePastChats(newAvatar, newValue) { } } -function saveChatDebounced() { +export function saveChatDebounced() { const chid = this_chid; const selectedGroup = selected_group; @@ -5083,11 +5254,10 @@ export async function getUserAvatars() { $("#user_avatar_block").append('
+
'); for (var i = 0; i < getData.length; i++) { - //console.log(1); appendUserAvatar(getData[i]); } - //var aa = JSON.parse(getData[0]); - //const load_ch_coint = Object.getOwnPropertyNames(getData); + + return getData; } } @@ -5187,7 +5357,7 @@ async function uploadUserAvatar(e) { reloadUserAvatar(true); } - if (data.path) { + if (!name && data.path) { await getUserAvatars(); await delay(500); await createPersona(data.path); @@ -5393,6 +5563,8 @@ async function getSettings() { api_server_textgenerationwebui = settings.api_server_textgenerationwebui; $("#textgenerationwebui_api_url_text").val(api_server_textgenerationwebui); $("#aphrodite_api_url_text").val(api_server_textgenerationwebui); + $("#tabby_api_url_text").val(api_server_textgenerationwebui); + $('#koboldcpp_api_url_text').val(api_server_textgenerationwebui); selected_button = settings.selected_button; @@ -5546,6 +5718,7 @@ function openMessageDelete(fromSlashCommand) { selected_group: ${selected_group} is_group_generating: ${is_group_generating}`); } + this_del_mes = -1; is_delete_mode = true; } @@ -5581,7 +5754,7 @@ async function messageEditDone(div) { ); mesBlock.find(".mes_bias").empty(); mesBlock.find(".mes_bias").append(messageFormatting(bias)); - appendImageToMessage(mes, div.closest(".mes")); + appendMediaToMessage(mes, div.closest(".mes")); addCopyToCodeBlocks(div.closest(".mes")); await eventSource.emit(event_types.MESSAGE_EDITED, this_edit_mes_id); @@ -6112,13 +6285,9 @@ function callPopup(text, type, inputValue = '', { okButton, rows, wide, large } popup_type = type; } - if (wide) { - $("#dialogue_popup").addClass("wide_dialogue_popup"); - } + $('#dialogue_popup').toggleClass('wide_dialogue_popup', !!wide); - if (large) { - $("#dialogue_popup").addClass("large_dialogue_popup"); - } + $('#dialogue_popup').toggleClass('large_dialogue_popup', !!large); $("#dialogue_popup_cancel").css("display", "inline-block"); switch (popup_type) { @@ -6414,6 +6583,8 @@ function enlargeMessageImage() { imgContainer.prepend(img); imgContainer.addClass('img_enlarged_container'); imgContainer.find('code').addClass('txt').text(title); + const titleEmpty = !title || title.trim().length === 0; + imgContainer.find('pre').toggle(!titleEmpty); addCopyToCodeBlocks(imgContainer); callPopup(imgContainer, 'text', '', { wide: true, large: true }); } @@ -6795,6 +6966,9 @@ window["SillyTavern"].getContext = function () { extensionSettings: extension_settings, ModuleWorkerWrapper: ModuleWorkerWrapper, getTokenizerModel: getTokenizerModel, + generateQuietPrompt: generateQuietPrompt, + tags: tags, + tagMap: tag_map, }; }; @@ -7421,7 +7595,7 @@ function addDebugFunctions() { registerDebugFunction('generationTest', 'Send a generation request', 'Generates text using the currently selected API.', async () => { const text = prompt('Input text:', 'Hello'); toastr.info('Working on it...'); - const message = await generateRaw(text, null); + const message = await generateRaw(text, null, ''); alert(message); }); @@ -7693,20 +7867,22 @@ jQuery(async function () { if (popup_type == "del_chat") { //close past chat popup - $("#select_chat_cross").click(); - + $("#select_chat_cross").trigger('click'); + showLoader() if (selected_group) { await deleteGroupChat(selected_group, chat_file_for_del); } else { await delChat(chat_file_for_del); } - //open the history view again after 100ms + //open the history view again after 2seconds (delay to avoid edge cases for deleting last chat) //hide option popup menu setTimeout(function () { $("#option_select_chat").click(); $("#options").hide(); + hideLoader() }, 2000); + } if (popup_type == "del_ch") { const deleteChats = !!$("#del_char_checkbox").prop("checked"); @@ -7986,6 +8162,11 @@ jQuery(async function () { await writeSecret(SECRET_KEYS.APHRODITE, aphroditeKey); } + const tabbyKey = String($("#api_key_tabby").val()).trim(); + if (tabbyKey.length) { + await writeSecret(SECRET_KEYS.TABBY, tabbyKey) + } + const urlSourceId = getTextGenUrlSourceId(); if (urlSourceId && $(urlSourceId).val() !== "") { @@ -8010,12 +8191,14 @@ jQuery(async function () { function showMenu() { showBookmarksButtons(); - menu.stop().fadeIn(250); + // menu.stop() + menu.fadeIn(animation_duration); optionsPopper.update(); } function hideMenu() { - menu.stop().fadeOut(250); + // menu.stop(); + menu.fadeOut(animation_duration); optionsPopper.update(); } @@ -8023,14 +8206,20 @@ jQuery(async function () { return menu.is(':hover') || button.is(':hover'); } - button.on('mouseenter click', function () { showMenu(); }); - button.on('mouseleave', function () { + button.on('click', function () { + if (menu.is(':visible')) { + hideMenu(); + } else { + showMenu(); + } + }); + button.on('blur', function () { //delay to prevent menu hiding when mouse leaves button into menu setTimeout(() => { if (!isMouseOverButtonOrMenu()) { hideMenu(); } }, 100) }); - menu.on('mouseleave', function () { + menu.on('blur', function () { //delay to prevent menu hide when mouseleaves menu into button setTimeout(() => { if (!isMouseOverButtonOrMenu()) { hideMenu(); } @@ -8153,6 +8342,17 @@ jQuery(async function () { hideMenu(); }); + $("#newChatFromManageScreenButton").on('click', function () { + setTimeout(() => { + $("#option_start_new_chat").trigger('click'); + }, 1); + setTimeout(() => { + $("#dialogue_popup_ok").trigger('click'); + }, 1); + $("#select_chat_cross").trigger('click') + + }) + ////////////////////////////////////////////////////////////////////////////////////////////// //functionality for the cancel delete messages button, reverts to normal display of input form @@ -8165,9 +8365,8 @@ jQuery(async function () { $(this).parent().css("background", css_mes_bg); $(this).prop("checked", false); }); - this_del_mes = 0; - console.debug('canceled del msgs, calling showswipesbtns'); showSwipeButtons(); + this_del_mes = -1; is_delete_mode = false; }); @@ -8181,21 +8380,26 @@ jQuery(async function () { $(this).parent().css("background", css_mes_bg); $(this).prop("checked", false); }); - $(".mes[mesid='" + this_del_mes + "']") - .nextAll("div") - .remove(); - $(".mes[mesid='" + this_del_mes + "']").remove(); - chat.length = this_del_mes; - count_view_mes = this_del_mes; - await saveChatConditional(); - var $textchat = $("#chat"); - $textchat.scrollTop($textchat[0].scrollHeight); - eventSource.emit(event_types.MESSAGE_DELETED, chat.length); - this_del_mes = 0; - $('#chat .mes').last().addClass('last_mes'); - $('#chat .mes').eq(-2).removeClass('last_mes'); - console.debug('confirmed del msgs, calling showswipesbtns'); + + if (this_del_mes >= 0) { + $(".mes[mesid='" + this_del_mes + "']") + .nextAll("div") + .remove(); + $(".mes[mesid='" + this_del_mes + "']").remove(); + chat.length = this_del_mes; + count_view_mes = this_del_mes; + await saveChatConditional(); + var $textchat = $("#chat"); + $textchat.scrollTop($textchat[0].scrollHeight); + eventSource.emit(event_types.MESSAGE_DELETED, chat.length); + $('#chat .mes').last().addClass('last_mes'); + $('#chat .mes').eq(-2).removeClass('last_mes'); + } else { + console.log('this_del_mes is not >= 0, not deleting'); + } + showSwipeButtons(); + this_del_mes = -1; is_delete_mode = false; }); @@ -8504,7 +8708,7 @@ jQuery(async function () { chat[this_edit_mes_id].is_system, chat[this_edit_mes_id].is_user, )); - appendImageToMessage(chat[this_edit_mes_id], $(this).closest(".mes")); + appendMediaToMessage(chat[this_edit_mes_id], $(this).closest(".mes")); addCopyToCodeBlocks($(this).closest(".mes")); this_edit_mes_id = undefined; }); @@ -8952,19 +9156,12 @@ jQuery(async function () { }); $(document).on('click', '.mes .avatar', function () { - - //console.log(isMobile()); - //console.log($('body').hasClass('waifuMode')); - - /* if (isMobile() === true && !$('body').hasClass('waifuMode')) { - console.debug('saw mobile regular mode, returning'); - return; - } else { console.debug('saw valid env for zoomed display') } */ - - let thumbURL = $(this).children('img').attr('src'); - let charsPath = '/characters/' - let targetAvatarImg = thumbURL.substring(thumbURL.lastIndexOf("=") + 1); - let charname = targetAvatarImg.replace('.png', ''); + const messageElement = $(this).closest('.mes'); + const thumbURL = $(this).children('img').attr('src'); + const charsPath = '/characters/' + const targetAvatarImg = thumbURL.substring(thumbURL.lastIndexOf("=") + 1); + const charname = targetAvatarImg.replace('.png', ''); + const isValidCharacter = characters.some(x => x.avatar === decodeURIComponent(targetAvatarImg)); // Remove existing zoomed avatars for characters that are not the clicked character when moving UI is not enabled if (!power_user.movingUI) { @@ -8977,7 +9174,7 @@ jQuery(async function () { }); } - let avatarSrc = isDataURL(thumbURL) ? thumbURL : charsPath + targetAvatarImg; + const avatarSrc = isDataURL(thumbURL) ? thumbURL : charsPath + targetAvatarImg; if ($(`.zoomed_avatar[forChar="${charname}"]`).length) { console.debug('removing container as it already existed') $(`.zoomed_avatar[forChar="${charname}"]`).remove(); @@ -8991,11 +9188,11 @@ jQuery(async function () { newElement.find('.drag-grabber').attr('id', `zoomFor_${charname}header`); $('body').append(newElement); - if ($(this).parent().parent().attr('is_user') == 'true') { //handle user avatars + if (messageElement.attr('is_user') == 'true') { //handle user avatars $(`.zoomed_avatar[forChar="${charname}"] img`).attr('src', thumbURL); - } else if ($(this).parent().parent().attr('is_system') == 'true') { //handle system avatars + } else if (messageElement.attr('is_system') == 'true' && !isValidCharacter) { //handle system avatars $(`.zoomed_avatar[forChar="${charname}"] img`).attr('src', thumbURL); - } else if ($(this).parent().parent().attr('is_user') == 'false') { //handle char avatars + } else if (messageElement.attr('is_user') == 'false') { //handle char avatars $(`.zoomed_avatar[forChar="${charname}"] img`).attr('src', avatarSrc); } loadMovingUIState(); diff --git a/public/scripts/PromptManager.js b/public/scripts/PromptManager.js index dce7b7f8..6fa24c0a 100644 --- a/public/scripts/PromptManager.js +++ b/public/scripts/PromptManager.js @@ -179,6 +179,13 @@ class PromptCollection { } function PromptManagerModule() { + this.systemPrompts = [ + 'main', + 'nsfw', + 'jailbreak', + 'enhanceDefinitions', + ]; + this.configuration = { version: 1, prefix: '', @@ -398,6 +405,10 @@ PromptManagerModule.prototype.init = function (moduleConfiguration, serviceSetti document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_injection_position').value = prompt.injection_position ?? 0; document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_injection_depth').value = prompt.injection_depth ?? DEFAULT_DEPTH; document.getElementById(this.configuration.prefix + 'prompt_manager_depth_block').style.visibility = prompt.injection_position === INJECTION_POSITION.ABSOLUTE ? 'visible' : 'hidden'; + + if (!this.systemPrompts.includes(promptId)) { + document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_injection_position').removeAttribute('disabled'); + } } // Append prompt to selected character @@ -721,6 +732,12 @@ PromptManagerModule.prototype.getTokenHandler = function () { return this.tokenHandler; } +PromptManagerModule.prototype.isPromptDisabledForActiveCharacter = function (identifier) { + const promptOrderEntry = this.getPromptOrderEntry(this.activeCharacter, identifier); + if (promptOrderEntry) return !promptOrderEntry.enabled; + return false; +} + /** * Add a prompt to the current character's prompt list. * @param {object} prompt - The prompt to be added. @@ -859,7 +876,8 @@ PromptManagerModule.prototype.isPromptEditAllowed = function (prompt) { * @returns {boolean} True if the prompt can be deleted, false otherwise. */ PromptManagerModule.prototype.isPromptToggleAllowed = function (prompt) { - return prompt.marker ? false : !this.configuration.toggleDisabled.includes(prompt.identifier); + const forceTogglePrompts = ['charDescription', 'charPersonality', 'scenario', 'personaDescription', 'worldInfoBefore', 'worldInfoAfter']; + return prompt.marker && !forceTogglePrompts.includes(prompt.identifier) ? false : !this.configuration.toggleDisabled.includes(prompt.identifier); } /** @@ -1114,6 +1132,11 @@ PromptManagerModule.prototype.loadPromptIntoEditForm = function (prompt) { injectionPositionField.value = prompt.injection_position ?? INJECTION_POSITION.RELATIVE; injectionDepthField.value = prompt.injection_depth ?? DEFAULT_DEPTH; injectionDepthBlock.style.visibility = prompt.injection_position === INJECTION_POSITION.ABSOLUTE ? 'visible' : 'hidden'; + injectionPositionField.removeAttribute('disabled'); + + if (this.systemPrompts.includes(prompt.identifier)) { + injectionPositionField.setAttribute('disabled', 'disabled'); + } const resetPromptButton = document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_reset'); if (true === prompt.system_prompt) { @@ -1198,6 +1221,7 @@ PromptManagerModule.prototype.clearEditForm = function () { roleField.selectedIndex = 0; promptField.value = ''; injectionPositionField.selectedIndex = 0; + injectionPositionField.removeAttribute('disabled'); injectionDepthField.value = DEFAULT_DEPTH; injectionDepthBlock.style.visibility = 'unset'; diff --git a/public/scripts/RossAscends-mods.js b/public/scripts/RossAscends-mods.js index 35f7b1c2..de635b65 100644 --- a/public/scripts/RossAscends-mods.js +++ b/public/scripts/RossAscends-mods.js @@ -906,7 +906,7 @@ export function initRossMods() { if (power_user.gestures === false) { return } - if ($(".mes_edit_buttons, #character_popup, #dialogue_popup, #WorldInfo").is(":visible")) { + if ($(".mes_edit_buttons, .drawer-content, #character_popup, #dialogue_popup, #WorldInfo, #right-nav-panel, #left-nav-panel, #select_chat_popup, #floatingPrompt").is(":visible")) { return } var SwipeButR = $('.swipe_right:last'); @@ -921,7 +921,7 @@ export function initRossMods() { if (power_user.gestures === false) { return } - if ($(".mes_edit_buttons, #character_popup, #dialogue_popup, #WorldInfo").is(":visible")) { + if ($(".mes_edit_buttons, .drawer-content, #character_popup, #dialogue_popup, #WorldInfo, #right-nav-panel, #left-nav-panel, #select_chat_popup, #floatingPrompt").is(":visible")) { return } var SwipeButL = $('.swipe_left:last'); diff --git a/public/scripts/chats.js b/public/scripts/chats.js index d716a968..8ecc774e 100644 --- a/public/scripts/chats.js +++ b/public/scripts/chats.js @@ -1,12 +1,21 @@ // Move chat functions here from script.js (eventually) import { + addCopyToCodeBlocks, + appendMediaToMessage, + callPopup, chat, + eventSource, + event_types, getCurrentChatId, hideSwipeButtons, - saveChatConditional, + name2, + saveChatDebounced, showSwipeButtons, } from "../script.js"; +import { getBase64Async, humanFileSize, saveBase64AsFile } from "./utils.js"; + +const fileSizeLimit = 1024 * 1024 * 1; // 1 MB /** * Mark message as hidden (system message). @@ -30,7 +39,7 @@ export async function hideChatMessage(messageId, messageBlock) { hideSwipeButtons(); showSwipeButtons(); - await saveChatConditional(); + saveChatDebounced(); } /** @@ -55,19 +64,225 @@ export async function unhideChatMessage(messageId, messageBlock) { hideSwipeButtons(); showSwipeButtons(); - await saveChatConditional(); + saveChatDebounced(); } -jQuery(function() { - $(document).on('click', '.mes_hide', async function() { +/** + * Adds a file attachment to the message. + * @param {object} message Message object + * @returns {Promise} + */ +export async function populateFileAttachment(message, inputId = 'file_form_input') { + try { + if (!message) return; + if (!message.extra) message.extra = {}; + const fileInput = document.getElementById(inputId); + if (!(fileInput instanceof HTMLInputElement)) return; + const file = fileInput.files[0]; + if (!file) return; + + // If file is image + if (file.type.startsWith('image/')) { + const base64Img = await getBase64Async(file); + const base64ImgData = base64Img.split(',')[1]; + const extension = file.type.split('/')[1]; + const imageUrl = await saveBase64AsFile(base64ImgData, name2, file.name, extension); + message.extra.image = imageUrl; + message.extra.inline_image = true; + } else { + const fileText = await file.text(); + message.extra.file = { + text: fileText, + size: file.size, + name: file.name, + }; + } + + } catch (error) { + console.error('Could not upload file', error); + } finally { + $('#file_form').trigger('reset'); + } +} + +/** + * Validates file to make sure it is not binary or not image. + * @param {File} file File object + * @returns {Promise} True if file is valid, false otherwise. + */ +async function validateFile(file) { + const fileText = await file.text(); + const isImage = file.type.startsWith('image/'); + const isBinary = /^[\x00-\x08\x0E-\x1F\x7F-\xFF]*$/.test(fileText); + + if (!isImage && file.size > fileSizeLimit) { + toastr.error(`File is too big. Maximum size is ${humanFileSize(fileSizeLimit)}.`); + return false; + } + + // If file is binary + if (isBinary && !isImage) { + toastr.error('Binary files are not supported. Select a text file or image.'); + return false; + } + + return true; +} + +export function hasPendingFileAttachment() { + const fileInput = document.getElementById('file_form_input'); + if (!(fileInput instanceof HTMLInputElement)) return false; + const file = fileInput.files[0]; + return !!file; +} + +/** + * Displays file information in the message sending form. + * @returns {Promise} + */ +async function onFileAttach() { + const fileInput = document.getElementById('file_form_input'); + if (!(fileInput instanceof HTMLInputElement)) return; + const file = fileInput.files[0]; + if (!file) return; + + const isValid = await validateFile(file); + + // If file is binary + if (!isValid) { + $('#file_form').trigger('reset'); + return; + } + + $('#file_form .file_name').text(file.name); + $('#file_form .file_size').text(humanFileSize(file.size)); + $('#file_form').removeClass('displayNone'); + + // Reset form on chat change + eventSource.once(event_types.CHAT_CHANGED, () => { + $('#file_form').trigger('reset'); + }); +} + +/** + * Deletes file from message. + * @param {number} messageId Message ID + */ +async function deleteMessageFile(messageId) { + const confirm = await callPopup('Are you sure you want to delete this file?', 'confirm'); + + if (!confirm) { + console.debug('Delete file cancelled'); + return; + } + + const message = chat[messageId]; + + if (!message?.extra?.file) { + console.debug('Message has no file'); + return; + } + + delete message.extra.file; + $(`.mes[mesid="${messageId}"] .mes_file_container`).remove(); + saveChatDebounced(); +} + +/** + * Opens file from message in a modal. + * @param {number} messageId Message ID + */ +async function viewMessageFile(messageId) { + const messageText = chat[messageId]?.extra?.file?.text; + + if (!messageText) { + console.debug('Message has no file or it is empty'); + return; + } + + const modalTemplate = $('
'); + modalTemplate.find('code').addClass('txt').text(messageText); + modalTemplate.addClass('file_modal'); + addCopyToCodeBlocks(modalTemplate); + + callPopup(modalTemplate, 'text'); +} + + +/** + * Inserts a file embed into the message. + * @param {number} messageId + * @param {JQuery} messageBlock + * @returns {Promise} + */ +function embedMessageFile(messageId, messageBlock) { + const message = chat[messageId]; + + if (!message) { + console.warn('Failed to find message with id', messageId); + return; + } + + $('#embed_file_input') + .off('change') + .on('change', parseAndUploadEmbed) + .trigger('click'); + + async function parseAndUploadEmbed(e) { + const file = e.target.files[0]; + if (!file) return; + + const isValid = await validateFile(file); + + if (!isValid) { + $('#file_form').trigger('reset'); + return; + } + + await populateFileAttachment(message, 'embed_file_input'); + appendMediaToMessage(message, messageBlock); + saveChatDebounced(); + } +} + +jQuery(function () { + $(document).on('click', '.mes_hide', async function () { const messageBlock = $(this).closest('.mes'); const messageId = Number(messageBlock.attr('mesid')); await hideChatMessage(messageId, messageBlock); }); - $(document).on('click', '.mes_unhide', async function() { + $(document).on('click', '.mes_unhide', async function () { const messageBlock = $(this).closest('.mes'); const messageId = Number(messageBlock.attr('mesid')); await unhideChatMessage(messageId, messageBlock); }); + + $(document).on('click', '.mes_file_delete', async function () { + const messageBlock = $(this).closest('.mes'); + const messageId = Number(messageBlock.attr('mesid')); + await deleteMessageFile(messageId); + }); + + $(document).on('click', '.mes_file_open', async function () { + const messageBlock = $(this).closest('.mes'); + const messageId = Number(messageBlock.attr('mesid')); + await viewMessageFile(messageId); + }); + + // Do not change. #attachFile is added by extension. + $(document).on('click', '#attachFile', function () { + $('#file_form_input').trigger('click'); + }); + + $(document).on('click', '.mes_embed', function () { + const messageBlock = $(this).closest('.mes'); + const messageId = Number(messageBlock.attr('mesid')); + embedMessageFile(messageId, messageBlock); + }); + + $('#file_form_input').on('change', onFileAttach); + $('#file_form').on('reset', function () { + $('#file_form').addClass('displayNone'); + }); }) diff --git a/public/scripts/extensions.js b/public/scripts/extensions.js index 19509085..08656b55 100644 --- a/public/scripts/extensions.js +++ b/public/scripts/extensions.js @@ -1,4 +1,4 @@ -import { callPopup, eventSource, event_types, saveSettings, saveSettingsDebounced, getRequestHeaders, substituteParams, renderTemplate } from "../script.js"; +import { callPopup, eventSource, event_types, saveSettings, saveSettingsDebounced, getRequestHeaders, substituteParams, renderTemplate, animation_duration } from "../script.js"; import { hideLoader, showLoader } from "./loader.js"; import { isSubsetOf } from "./utils.js"; export { @@ -103,7 +103,7 @@ class ModuleWorkerWrapper { } // Called by the extension - async update() { + async update(...args) { // Don't touch me I'm busy... if (this.isBusy) { return; @@ -112,7 +112,7 @@ class ModuleWorkerWrapper { // I'm free. Let's update! try { this.isBusy = true; - await this.callback(); + await this.callback(...args); } finally { this.isBusy = false; @@ -347,27 +347,28 @@ function addExtensionsButtonAndMenu() { $(document.body).append(extensionsMenuHTML); - $('#send_but_sheld').prepend(buttonHTML); + $('#leftSendForm').prepend(buttonHTML); const button = $('#extensionsMenuButton'); const dropdown = $('#extensionsMenu'); //dropdown.hide(); let popper = Popper.createPopper(button.get(0), dropdown.get(0), { - placement: 'top-end', + placement: 'top-start', }); $(button).on('click', function () { popper.update() - dropdown.fadeIn(250); + if (!dropdown.is(':visible')) { + dropdown.fadeIn(animation_duration); + } }); $("html").on('touchstart mousedown', function (e) { - let clickTarget = $(e.target); - if (dropdown.is(':visible') - && clickTarget.closest(button).length == 0 - && clickTarget.closest(dropdown).length == 0) { - $(dropdown).fadeOut(250); + const clickTarget = $(e.target); + const noCloseTargets = ['#sd_gen']; + if (dropdown.is(':visible') && !noCloseTargets.some(id => clickTarget.closest(id).length > 0)) { + $(dropdown).fadeOut(animation_duration); } }); } @@ -511,8 +512,8 @@ async function generateExtensionHtml(name, manifest, isActive, isDisabled, isExt isUpToDate = data.isUpToDate; displayVersion = ` (${branch}-${commitHash.substring(0, 7)})`; updateButton = isUpToDate ? - `` : - ``; + `` : + ``; originHtml = ``; } @@ -592,7 +593,7 @@ function getModuleInformation() { * Generates the HTML strings for all extensions and displays them in a popup. */ async function showExtensionsDetails() { - try{ + try { showLoader(); let htmlDefault = '

Built-in Extensions:

'; let htmlExternal = '

Installed Extensions:

'; @@ -640,6 +641,7 @@ async function showExtensionsDetails() { */ async function onUpdateClick() { const extensionName = $(this).data('name'); + $(this).find('i').addClass('fa-spin'); await updateExtension(extensionName, false); } @@ -657,6 +659,11 @@ async function updateExtension(extensionName, quiet) { }); const data = await response.json(); + + if (!quiet) { + showExtensionsDetails(); + } + if (data.isUpToDate) { if (!quiet) { toastr.success('Extension is already up to date'); @@ -664,10 +671,6 @@ async function updateExtension(extensionName, quiet) { } else { toastr.success(`Extension ${extensionName} updated to ${data.shortCommitHash}`); } - - if (!quiet) { - showExtensionsDetails(); - } } catch (error) { console.error('Error:', error); } @@ -843,12 +846,19 @@ async function checkForExtensionUpdates(force) { } async function autoUpdateExtensions() { + if (!Object.values(manifests).some(x => x.auto_update)) { + return; + } + + toastr.info('Auto-updating extensions. This may take several minutes.', 'Please wait...', { timeOut: 10000, extendedTimeOut: 20000 }); + const promises = []; for (const [id, manifest] of Object.entries(manifests)) { if (manifest.auto_update && id.startsWith('third-party')) { console.debug(`Auto-updating 3rd-party extension: ${manifest.display_name} (${id})`); - await updateExtension(id.replace('third-party', ''), true); + promises.push(updateExtension(id.replace('third-party', ''), true)); } } + await Promise.allSettled(promises); } /** diff --git a/public/scripts/extensions/assets/index.js b/public/scripts/extensions/assets/index.js index fe6cd12e..1c49917c 100644 --- a/public/scripts/extensions/assets/index.js +++ b/public/scripts/extensions/assets/index.js @@ -67,7 +67,7 @@ function downloadAssetsList(url) { const asset = availableAssets[assetType][i]; const elemId = `assets_install_${assetType}_${i}`; let element = $('
@@ -379,7 +371,7 @@ jQuery(function () { addSendPictureButton(); setImageIcon(); migrateSettings(); - moduleWorker(); + switchMultimodalBlocks(); $('#caption_refine_mode').prop('checked', !!(extension_settings.caption.refine_mode)); $('#caption_source').val(extension_settings.caption.source); @@ -388,6 +380,7 @@ jQuery(function () { $('#caption_refine_mode').on('input', onRefineModeInput); $('#caption_source').on('change', () => { extension_settings.caption.source = String($('#caption_source').val()); + switchMultimodalBlocks(); saveSettingsDebounced(); }); $('#caption_prompt').on('input', () => { @@ -398,6 +391,4 @@ jQuery(function () { extension_settings.caption.template = String($('#caption_template').val()); saveSettingsDebounced(); }); - $(document).on('click', '.mes_embed', onImageEmbedClicked); - setInterval(moduleWorker, UPDATE_INTERVAL); }); diff --git a/public/scripts/extensions/expressions/index.js b/public/scripts/extensions/expressions/index.js index 40e0c36c..eea4a865 100644 --- a/public/scripts/extensions/expressions/index.js +++ b/public/scripts/extensions/expressions/index.js @@ -4,6 +4,7 @@ import { getContext, getApiUrl, modules, extension_settings, ModuleWorkerWrapper import { loadMovingUIState, power_user } from "../../power-user.js"; import { registerSlashCommand } from "../../slash-commands.js"; import { onlyUnique, debounce, getCharaFilename, trimToEndSentence, trimToStartSentence } from "../../utils.js"; +import { hideMutedSprites } from "../../group-chats.js"; export { MODULE_NAME }; const MODULE_NAME = 'expressions'; @@ -118,7 +119,7 @@ async function visualNovelSetCharacterSprites(container, name, expression) { const isDisabled = group.disabled_members.includes(avatar); // skip disabled characters - if (isDisabled) { + if (isDisabled && hideMutedSprites) { continue; } diff --git a/public/scripts/extensions/memory/index.js b/public/scripts/extensions/memory/index.js index 6ed2561a..d60d8162 100644 --- a/public/scripts/extensions/memory/index.js +++ b/public/scripts/extensions/memory/index.js @@ -651,14 +651,12 @@ jQuery(function () {
- +
- Current summary: -
- +
- +
-