Merge branch 'staging' into silerott-add-session-handling

This commit is contained in:
Cohee 2023-11-28 18:25:31 +02:00
commit a1098a4f31
118 changed files with 13535 additions and 3921 deletions

View File

@ -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 非常重要,否则即使添加了上述防火墙规则也无法连接。
### 性能问题?

12
.github/readme.md vendored
View File

@ -173,7 +173,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
@ -211,7 +211,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
@ -223,7 +223,7 @@ If the ST-hosting device is on the same wifi network, you will use the ST-host's
If you (or someone else) want to connect to your hosted ST while not being on the same network, you will need the public IP of your ST-hosting device.
* While using the ST-hosting device, access [this page](https://whatismyipaddress.com/) and look for for `IPv4`. This is what you would use to connect from the remote device.
* While using the ST-hosting device, access [this page](https://whatismyipaddress.com/) and look for `IPv4`. This is what you would use to connect from the remote device.
### 3. Connect the remote device to the ST host machine
@ -237,19 +237,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?

3
.gitignore vendored
View File

@ -20,6 +20,8 @@ public/stats.json
/uploads/
*.jsonl
/config.conf
/config.yaml
/config.conf.bak
/docker/config
.DS_Store
public/settings.json
@ -38,3 +40,4 @@ public/assets/
access.log
/vectors/
/cache/
public/css/user.css

View File

@ -23,18 +23,19 @@ COPY . ./
# Copy default chats, characters and user avatars to <folder>.default folder
RUN \
IFS="," RESOURCES="characters,chats,groups,group chats,User Avatars,worlds,OpenAI Settings,NovelAI Settings,KoboldAI Settings,TextGen Settings" && \
IFS="," RESOURCES="assets,backgrounds,user,context,instruct,QuickReplies,movingUI,themes,characters,chats,groups,group chats,User Avatars,worlds,OpenAI Settings,NovelAI Settings,KoboldAI Settings,TextGen Settings" && \
\
echo "*** Store default $RESOURCES in <folder>.default ***" && \
for R in $RESOURCES; do mv "public/$R" "public/$R.default"; done && \
for R in $RESOURCES; do mv "public/$R" "public/$R.default"; done || true && \
\
echo "*** Create symbolic links to config directory ***" && \
for R in $RESOURCES; do ln -s "../config/$R" "public/$R"; done && \
# rm "config.conf" "public/settings.json" "public/css/bg_load.css" && \
ln -s "./config/config.conf" "config.conf" && \
ln -s "../config/settings.json" "public/settings.json" && \
ln -s "../../config/bg_load.css" "public/css/bg_load.css" && \
mkdir "config"
for R in $RESOURCES; do ln -s "../config/$R" "public/$R"; done || 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
# Cleanup unnecessary files
RUN \

View File

@ -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.

View File

@ -34,43 +34,56 @@
"source": [
"#@markdown (RECOMMENDED) Generates an API key for you to use with the API\n",
"secure = False #@param {type:\"boolean\"}\n",
"#@markdown Enables hosting of extensions backend for SillyTavern Extras\n",
"use_cpu = False #@param {type:\"boolean\"}\n",
"#@markdown Allows to run SillyTavern Extras on CPU (use if you're out of daily GPU allowance)\n",
"use_sd_cpu = False #@param {type:\"boolean\"}\n",
"use_cpu = False #@param {type:\"boolean\"}\n",
"#@markdown Allows to run Stable Diffusion pipeline on CPU (slow!)\n",
"extras_enable_captioning = True #@param {type:\"boolean\"}\n",
"use_sd_cpu = False #@param {type:\"boolean\"}\n",
"#@markdown ***\n",
"#@markdown Loads the image captioning module\n",
"Captions_Model = \"Salesforce/blip-image-captioning-large\" #@param [ \"Salesforce/blip-image-captioning-large\", \"Salesforce/blip-image-captioning-base\" ]\n",
"extras_enable_caption = True #@param {type:\"boolean\"}\n",
"captioning_model = \"Salesforce/blip-image-captioning-large\" #@param [ \"Salesforce/blip-image-captioning-large\", \"Salesforce/blip-image-captioning-base\" ]\n",
"#@markdown * Salesforce/blip-image-captioning-large - good base model\n",
"#@markdown * Salesforce/blip-image-captioning-base - slightly faster but less accurate\n",
"extras_enable_emotions = True #@param {type:\"boolean\"}\n",
"#@markdown ***\n",
"#@markdown Loads the sentiment classification model\n",
"Emotions_Model = \"nateraw/bert-base-uncased-emotion\" #@param [\"nateraw/bert-base-uncased-emotion\", \"joeddav/distilbert-base-uncased-go-emotions-student\"]\n",
"extras_enable_classify = True #@param {type:\"boolean\"}\n",
"classification_model = \"nateraw/bert-base-uncased-emotion\" #@param [\"nateraw/bert-base-uncased-emotion\", \"joeddav/distilbert-base-uncased-go-emotions-student\"]\n",
"#@markdown * nateraw/bert-base-uncased-emotion = 6 supported emotions<br>\n",
"#@markdown * joeddav/distilbert-base-uncased-go-emotions-student = 28 supported emotions\n",
"extras_enable_memory = True #@param {type:\"boolean\"}\n",
"#@markdown ***\n",
"#@markdown Loads the story summarization module\n",
"Memory_Model = \"slauw87/bart_summarisation\" #@param [ \"slauw87/bart_summarisation\", \"Qiliang/bart-large-cnn-samsum-ChatGPT_v3\", \"Qiliang/bart-large-cnn-samsum-ElectrifAi_v10\", \"distilbart-xsum-12-3\" ]\n",
"extras_enable_summarize = True #@param {type:\"boolean\"}\n",
"summarization_model = \"slauw87/bart_summarisation\" #@param [ \"slauw87/bart_summarisation\", \"Qiliang/bart-large-cnn-samsum-ChatGPT_v3\", \"Qiliang/bart-large-cnn-samsum-ElectrifAi_v10\", \"distilbart-xsum-12-3\" ]\n",
"#@markdown * slauw87/bart_summarisation - general purpose summarization model\n",
"#@markdown * Qiliang/bart-large-cnn-samsum-ChatGPT_v3 - summarization model optimized for chats\n",
"#@markdown * Qiliang/bart-large-cnn-samsum-ElectrifAi_v10 - nice results so far, but still being evaluated\n",
"#@markdown * distilbart-xsum-12-3 - faster, but pretty basic alternative\n",
"extras_enable_silero_tts = True #@param {type:\"boolean\"}\n",
"#@markdown ***\n",
"#@markdown Enables Silero text-to-speech module\n",
"extras_enable_edge_tts = True #@param {type:\"boolean\"}\n",
"extras_enable_silero_tts = True #@param {type:\"boolean\"}\n",
"#@markdown Enables Microsoft Edge text-to-speech module\n",
"extras_enable_sd = True #@param {type:\"boolean\"}\n",
"extras_enable_edge_tts = True #@param {type:\"boolean\"}\n",
"#@markdown Enables RVC module\n",
"extras_enable_rvc = False #@param {type:\"boolean\"}\n",
"#@markdown ***\n",
"#@markdown Enables Whisper speech recognition module\n",
"extras_enable_whisper_stt = True #@param {type:\"boolean\"}\n",
"whisper_model = \"base.en\" #@param [ \"tiny.en\", \"base.en\", \"small.en\", \"medium.en\", \"tiny\", \"base\", \"small\", \"medium\", \"large\" ]\n",
"#@markdown There are five model sizes, four with English-only versions, offering speed and accuracy tradeoffs.\n",
"#@markdown The .en models for English-only applications tend to perform better, especially for the tiny.en and base.en models.\n",
"#@markdown ***\n",
"#@markdown Enables SD picture generation\n",
"SD_Model = \"ckpt/anything-v4.5-vae-swapped\" #@param [ \"ckpt/anything-v4.5-vae-swapped\", \"hakurei/waifu-diffusion\", \"philz1337/clarity\", \"prompthero/openjourney\", \"ckpt/sd15\", \"stabilityai/stable-diffusion-2-1-base\" ]\n",
"extras_enable_sd = True #@param {type:\"boolean\"}\n",
"sd_model = \"ckpt/anything-v4.5-vae-swapped\" #@param [ \"ckpt/anything-v4.5-vae-swapped\", \"hakurei/waifu-diffusion\", \"philz1337/clarity\", \"prompthero/openjourney\", \"ckpt/sd15\", \"stabilityai/stable-diffusion-2-1-base\" ]\n",
"#@markdown * ckpt/anything-v4.5-vae-swapped - anime style model\n",
"#@markdown * hakurei/waifu-diffusion - anime style model\n",
"#@markdown * philz1337/clarity - realistic style model\n",
"#@markdown * prompthero/openjourney - midjourney style model\n",
"#@markdown * ckpt/sd15 - base SD 1.5\n",
"#@markdown * stabilityai/stable-diffusion-2-1-base - base SD 2.1\n",
"extras_enable_chromadb = True #@param {type:\"boolean\"}\n",
"#@markdown ***\n",
"#@markdown Enables ChromaDB module\n",
"extras_enable_chromadb = True #@param {type:\"boolean\"}\n",
"\n",
"import subprocess\n",
"import secrets\n",
@ -86,28 +99,36 @@
"if secure:\n",
" params.append('--secure')\n",
"params.append('--share')\n",
"ExtrasModules = []\n",
"modules = []\n",
"\n",
"if (extras_enable_captioning):\n",
" ExtrasModules.append('caption')\n",
"if (extras_enable_memory):\n",
" ExtrasModules.append('summarize')\n",
"if (extras_enable_emotions):\n",
" ExtrasModules.append('classify')\n",
"if (extras_enable_sd):\n",
" ExtrasModules.append('sd')\n",
"if (extras_enable_silero_tts):\n",
" ExtrasModules.append('silero-tts')\n",
"if extras_enable_caption:\n",
" modules.append('caption')\n",
"if extras_enable_summarize:\n",
" modules.append('summarize')\n",
"if extras_enable_classify:\n",
" modules.append('classify')\n",
"if extras_enable_sd:\n",
" modules.append('sd')\n",
"if extras_enable_silero_tts:\n",
" modules.append('silero-tts')\n",
"if extras_enable_edge_tts:\n",
" ExtrasModules.append('edge-tts')\n",
"if (extras_enable_chromadb):\n",
" ExtrasModules.append('chromadb')\n",
" modules.append('edge-tts')\n",
"if extras_enable_chromadb:\n",
" modules.append('chromadb')\n",
"if extras_enable_whisper_stt:\n",
" modules.append('whisper-stt')\n",
" params.append(f'--stt-whisper-model-path={whisper_model}')\n",
"if extras_enable_rvc:\n",
" modules.append('rvc')\n",
" params.append('--max-content-length=2000')\n",
" params.append('--rvc-save-file')\n",
"\n",
"params.append(f'--classification-model={Emotions_Model}')\n",
"params.append(f'--summarization-model={Memory_Model}')\n",
"params.append(f'--captioning-model={Captions_Model}')\n",
"params.append(f'--sd-model={SD_Model}')\n",
"params.append(f'--enable-modules={\",\".join(ExtrasModules)}')\n",
"\n",
"params.append(f'--classification-model={classification_model}')\n",
"params.append(f'--summarization-model={summarization_model}')\n",
"params.append(f'--captioning-model={captioning_model}')\n",
"params.append(f'--sd-model={sd_model}')\n",
"params.append(f'--enable-modules={\",\".join(modules)}')\n",
"\n",
"\n",
"%cd /\n",
@ -115,23 +136,14 @@
"%cd /SillyTavern-extras\n",
"!git clone https://github.com/Cohee1207/tts_samples\n",
"!npm install -g localtunnel\n",
"!pip install -r requirements-complete.txt\n",
"!pip install tensorflow==2.14\n",
"!pip install colorama\n",
"!pip install Flask-Cors\n",
"!pip install Flask-Compress\n",
"!pip install transformers\n",
"!pip install Flask_Cloudflared\n",
"!pip install webuiapi\n",
"!pip install diffusers\n",
"!pip install accelerate\n",
"!pip install silero_api_server\n",
"!pip install edge_tts\n",
"!pip install chromadb\n",
"!pip install sentence_transformers\n",
"%pip install -r requirements.txt\n",
"!wget https://github.com/cloudflare/cloudflared/releases/download/2023.5.0/cloudflared-linux-amd64 -O /tmp/cloudflared-linux-amd64\n",
"!chmod +x /tmp/cloudflared-linux-amd64\n",
"\n",
"if extras_enable_rvc:\n",
" print(\"Installing RVC requirements\")\n",
" !pip install -r requirements-rvc.txt\n",
"\n",
"# Generate a random API key\n",
"api_key = secrets.token_hex(5)\n",
"\n",

View File

@ -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: [ "<url>" ], headers: { <header>: "<value>" } }
const requestOverrides = [];
module.exports = {
port,
whitelist,
whitelistMode,
basicAuthMode,
basicAuthUser,
autorun,
enableExtensions,
listen,
disableThumbnails,
allowKeysExposure,
securityOverride,
skipContentCheck,
requestOverrides,
thumbnailsQuality,
extras,
disableChatBackup,
};

53
default/config.yaml Normal file
View File

@ -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

View File

@ -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
]
}
}
}

View File

@ -22,5 +22,9 @@
{
"filename": "user-default.png",
"type": "avatar"
},
{
"filename": "Default_Comfy_Workflow.json",
"type": "workflow"
}
]

View File

@ -49,7 +49,6 @@
"ban_eos_token": false,
"skip_special_tokens": true,
"streaming": false,
"streaming_url": "ws://127.0.0.1:5005/api/v1/stream",
"mirostat_mode": 0,
"mirostat_tau": 5,
"mirostat_eta": 0.1,
@ -164,6 +163,8 @@
"custom_stopping_strings_macro": true,
"fuzzy_search": true,
"encode_tags": false,
"enableLabMode": false,
"enableZenSliders": false,
"ui_mode": 1
},
"extension_settings": {

1
default/user.css Normal file
View File

@ -0,0 +1 @@
/* Put custom styles here. */

View File

@ -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:

View File

@ -1,7 +1,7 @@
#!/bin/sh
# Initialize missing user files
IFS="," RESOURCES="characters,chats,groups,group chats,User Avatars,worlds,OpenAI Settings,NovelAI Settings,KoboldAI Settings,TextGen Settings"
IFS="," RESOURCES="assets,backgrounds,user,context,instruct,QuickReplies,movingUI,themes,characters,chats,groups,group chats,User Avatars,worlds,OpenAI Settings,NovelAI Settings,KoboldAI Settings,TextGen Settings"
for R in $RESOURCES; do
if [ ! -e "config/$R" ]; then
echo "Resource not found, copying from defaults: $R"
@ -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,5 +24,20 @@ if [ ! -e "config/bg_load.css" ]; then
cp -r "default/bg_load.css" "config/bg_load.css"
fi
CONFIG_FILE="config.yaml"
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.yaml\" file and restart the container.\033[0m"
sleep 5
fi
# Start the server
exec node server.js

198
package-lock.json generated
View File

@ -1,12 +1,12 @@
{
"name": "sillytavern",
"version": "1.10.7",
"version": "1.10.10",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "sillytavern",
"version": "1.10.7",
"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"
},
@ -756,6 +758,15 @@
"resolved": "https://registry.npmjs.org/@types/node/-/node-16.9.1.tgz",
"integrity": "sha512-QpLcX9ZSsq3YYUUnD3nFDY8H7wctAhQj/TFKL8Ya8v5fMm3CFXxo8zStsLAl780ltoYoo1WvKUVGBQK+1ifr7g=="
},
"node_modules/@types/node-fetch": {
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.9.tgz",
"integrity": "sha512-bQVlnMLFJ2d35DkPNjEPmd9ueO/rh5EiaZt2bhqiSarPjZIuIV6bPQVqcrEyvNo+AfTrRGVazle1tl597w3gfA==",
"dependencies": {
"@types/node": "*",
"form-data": "^4.0.0"
}
},
"node_modules/@types/responselike": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.1.tgz",
@ -764,6 +775,17 @@
"@types/node": "*"
}
},
"node_modules/abort-controller": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz",
"integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==",
"dependencies": {
"event-target-shim": "^5.0.0"
},
"engines": {
"node": ">=6.5"
}
},
"node_modules/accepts": {
"version": "1.3.8",
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
@ -811,6 +833,17 @@
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
"dev": true
},
"node_modules/agentkeepalive": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.5.0.tgz",
"integrity": "sha512-5GG/5IbQQpC9FpkRGsSvZI5QYeSCzlJHdpBQntCsuTOxhKD8lqKhrleg2Yi7yvMIf82Ycmmqln9U8V9qwEiJew==",
"dependencies": {
"humanize-ms": "^1.2.1"
},
"engines": {
"node": ">= 8.0.0"
}
},
"node_modules/ansi-regex": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
@ -885,15 +918,20 @@
}
},
"node_modules/axios": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.5.0.tgz",
"integrity": "sha512-D4DdjDo5CY50Qms0qGQTTw6Q44jl7zRwY7bthds06pUGfChBCTcQs+N743eFWGEd6pRTMd6A+I87aWyFV5wiZQ==",
"version": "1.6.1",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.6.1.tgz",
"integrity": "sha512-vfBmhDpKafglh0EldBEbVuoe7DyAavGSLWhuSm5ZSEKQnHhBf0xAAwybbNH1IkrJNGnS/VG4I5yxig1pCEXE4g==",
"dependencies": {
"follow-redirects": "^1.15.0",
"form-data": "^4.0.0",
"proxy-from-env": "^1.1.0"
}
},
"node_modules/base-64": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/base-64/-/base-64-0.1.0.tgz",
"integrity": "sha512-Y5gU45svrR5tI2Vt/X9GPd3L0HNIKzGu202EjxrXMpuc2V2CiKgemAbUUsqYmZJvPtCXoUKjNZwBJzsNScUbXA=="
},
"node_modules/base64-js": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
@ -1115,6 +1153,14 @@
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
"node_modules/charenc": {
"version": "0.0.2",
"resolved": "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz",
"integrity": "sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA==",
"engines": {
"node": "*"
}
},
"node_modules/cheerio": {
"version": "1.0.0-rc.12",
"resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0-rc.12.tgz",
@ -1351,6 +1397,14 @@
"node": ">=0.8"
}
},
"node_modules/crypt": {
"version": "0.0.2",
"resolved": "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz",
"integrity": "sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow==",
"engines": {
"node": "*"
}
},
"node_modules/csrf-csrf": {
"version": "2.2.4",
"resolved": "https://registry.npmjs.org/csrf-csrf/-/csrf-csrf-2.2.4.tgz",
@ -1474,6 +1528,15 @@
"node": ">= 8.11.4"
}
},
"node_modules/digest-fetch": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/digest-fetch/-/digest-fetch-1.3.0.tgz",
"integrity": "sha512-CGJuv6iKNM7QyZlM2T3sPAdZWd/p9zQiRNS9G+9COUCwzWFTs0Xp8NF5iePx7wtvhDykReiRRrSeNb4oMmB8lA==",
"dependencies": {
"base-64": "^0.1.0",
"md5": "^2.3.0"
}
},
"node_modules/dir-glob": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz",
@ -1616,6 +1679,14 @@
"node": ">= 0.6"
}
},
"node_modules/event-target-shim": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz",
"integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==",
"engines": {
"node": ">=6"
}
},
"node_modules/exif-parser": {
"version": "0.1.12",
"resolved": "https://registry.npmjs.org/exif-parser/-/exif-parser-0.1.12.tgz",
@ -1813,6 +1884,31 @@
"node": ">= 6"
}
},
"node_modules/form-data-encoder": {
"version": "1.7.2",
"resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-1.7.2.tgz",
"integrity": "sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A=="
},
"node_modules/formdata-node": {
"version": "4.4.1",
"resolved": "https://registry.npmjs.org/formdata-node/-/formdata-node-4.4.1.tgz",
"integrity": "sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==",
"dependencies": {
"node-domexception": "1.0.0",
"web-streams-polyfill": "4.0.0-beta.3"
},
"engines": {
"node": ">= 12.20"
}
},
"node_modules/formdata-node/node_modules/web-streams-polyfill": {
"version": "4.0.0-beta.3",
"resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-4.0.0-beta.3.tgz",
"integrity": "sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==",
"engines": {
"node": ">= 14"
}
},
"node_modules/forwarded": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
@ -2141,6 +2237,14 @@
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
"dev": true
},
"node_modules/humanize-ms": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz",
"integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==",
"dependencies": {
"ms": "^2.0.0"
}
},
"node_modules/iconv-lite": {
"version": "0.4.24",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
@ -2236,6 +2340,11 @@
"node": ">= 10"
}
},
"node_modules/is-buffer": {
"version": "1.1.6",
"resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz",
"integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w=="
},
"node_modules/is-core-module": {
"version": "2.9.0",
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.9.0.tgz",
@ -2515,6 +2624,16 @@
"node": ">=10"
}
},
"node_modules/md5": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/md5/-/md5-2.3.0.tgz",
"integrity": "sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==",
"dependencies": {
"charenc": "0.0.2",
"crypt": "0.0.2",
"is-buffer": "~1.1.6"
}
},
"node_modules/media-typer": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
@ -2718,6 +2837,24 @@
"node": ">=10"
}
},
"node_modules/node-domexception": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz",
"integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/jimmywarting"
},
{
"type": "github",
"url": "https://paypal.me/jimmywarting"
}
],
"engines": {
"node": ">=10.5.0"
}
},
"node_modules/node-fetch": {
"version": "2.6.12",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.12.tgz",
@ -2864,20 +3001,30 @@
}
},
"node_modules/openai": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/openai/-/openai-3.3.0.tgz",
"integrity": "sha512-uqxI/Au+aPRnsaQRe8CojU0eCR7I0mBiKjD3sNMzY6DaC1ZVrc85u98mtJW6voDug8fgGN+DIZmTDxTthxb7dQ==",
"version": "4.17.4",
"resolved": "https://registry.npmjs.org/openai/-/openai-4.17.4.tgz",
"integrity": "sha512-ThRFkl6snLbcAKS58St7N3CaKuI5WdYUvIjPvf4s+8SdymgNtOfzmZcZXVcCefx04oKFnvZJvIcTh3eAFUUhAQ==",
"dependencies": {
"axios": "^0.26.0",
"form-data": "^4.0.0"
"@types/node": "^18.11.18",
"@types/node-fetch": "^2.6.4",
"abort-controller": "^3.0.0",
"agentkeepalive": "^4.2.1",
"digest-fetch": "^1.3.0",
"form-data-encoder": "1.7.2",
"formdata-node": "^4.3.2",
"node-fetch": "^2.6.7",
"web-streams-polyfill": "^3.2.1"
},
"bin": {
"openai": "bin/cli"
}
},
"node_modules/openai/node_modules/axios": {
"version": "0.26.1",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.26.1.tgz",
"integrity": "sha512-fPwcX4EvnSHuInCMItEhAGnaSEXRBjtzh9fOtsE6E1G6p7vl7edEeZe11QHf18+6+9gR5PbKV/sGKNaD8YaMeA==",
"node_modules/openai/node_modules/@types/node": {
"version": "18.18.9",
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.18.9.tgz",
"integrity": "sha512-0f5klcuImLnG4Qreu9hPj/rEfFq6YRc5n2mAjSsH+ec/mJL+3voBH0+8T7o8RpFjH7ovc+TRsL/c7OYIQsPTfQ==",
"dependencies": {
"follow-redirects": "^1.14.8"
"undici-types": "~5.26.4"
}
},
"node_modules/p-cancelable": {
@ -4022,6 +4169,11 @@
"resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz",
"integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA=="
},
"node_modules/undici-types": {
"version": "5.26.5",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="
},
"node_modules/universalify": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz",
@ -4099,6 +4251,14 @@
"vectra": "bin/vectra.js"
}
},
"node_modules/web-streams-polyfill": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.2.1.tgz",
"integrity": "sha512-e0MO3wdXWKrLbL0DgGnUV7WHVuw9OUvL4hjgnPkIeEvESk74gAITi5G606JtZPp39cd8HA9VQzCIvA49LpPN5Q==",
"engines": {
"node": ">= 8"
}
},
"node_modules/webidl-conversions": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
@ -4229,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",

View File

@ -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,12 +33,16 @@
"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"
},
"overrides": {
"parse-bmfont-xml": {
"xml2js": "^0.5.0"
},
"vectra": {
"openai": "^4.17.0"
}
},
"name": "sillytavern",
@ -47,7 +52,7 @@
"type": "git",
"url": "https://github.com/SillyTavern/SillyTavern.git"
},
"version": "1.10.7",
"version": "1.10.10",
"scripts": {
"start": "node server.js",
"start-multi": "node server.js --disableCsrf",

View File

@ -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,8 @@ 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',
};
for (const file of Object.values(files)) {
@ -20,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);
}
}
}
@ -72,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);
}

View File

@ -0,0 +1,105 @@
#rm_print_characters_block.group_overlay_mode_select .character_select {
transition: background-color 0.4s ease;
margin-bottom: 1px;
background-color: rgba(170, 170, 170, 0.15);
}
#rm_print_characters_block.group_overlay_mode_select .bogus_folder_select,
#rm_print_characters_block.group_overlay_mode_select .group_select {
cursor: auto;
filter: saturate(0.3);
}
#rm_print_characters_block.group_overlay_mode_select .bogus_folder_select:hover,
#rm_print_characters_block.group_overlay_mode_select .group_select:hover {
background: none;
}
#rm_print_characters_block.group_overlay_mode_select .character_select input.bulk_select_checkbox {
display: none !important;
}
#rm_print_characters_block.group_overlay_mode_select .character_select.character_selected {
background-color: var(--SmartThemeQuoteColor);
}
#rm_print_characters_block.group_overlay_mode_select .character_select .bulk_select_checkbox {
visibility: hidden;
height: 0 !important;
}
#character_context_menu.hidden { display: none; }
#character_context_menu {
position: absolute;
padding: 3px;
z-index: 9998;
background-color: var(--black90a);
border: 1px solid var(--black90a);
border-radius: 10px;
}
#character_context_menu ul li button {
border: 0;
border-bottom-color: currentcolor;
color: var(--SmartThemeQuoteColor);
background-color: transparent;
font-weight: bold;
font-size: 1em;
padding: 0.5em;
border-bottom: 1px dotted var(--SmartThemeQuoteColor);
width: 100%;
cursor: pointer;
}
#character_context_menu ul li button:hover {
background-color: var(--SmartThemeBlurTintColor);
}
#character_context_menu ul li:last-child button {
border-bottom: 0;
}
#character_context_menu ul li #character_context_menu_delete {
color: var(--fullred);
}
#character_context_menu ul {
list-style-type: none;
padding: 0;
margin: 0;
}
#character_context_menu .character_context_menu_separator {
height: 1px;
background-color: var(--SmartThemeBotMesBlurTintColor);
}
#character_context_menu li:hover {
background-color: var(--SmartThemeBotMesBlurTintColor);
}
#bulkEditButton.bulk_edit_overlay_active {
color: var(--golden);
}
#bulk_tag_shadow_popup {
backdrop-filter: blur(calc(var(--SmartThemeBlurStrength) * 2));
-webkit-backdrop-filter: blur(calc(var(--SmartThemeBlurStrength) * 2));
background-color: var(--black30a);
position: absolute;
width: 100%;
height: 100vh;
height: 100svh;
z-index: 9998;
top: 0;
}
#bulk_tag_shadow_popup #bulk_tag_popup {
padding: 1em;
}
#bulk_tag_shadow_popup #bulk_tag_popup #dialogue_popup_controls .menu_button {
width: 100px;
padding: 0.25em;
}

View File

@ -3,11 +3,6 @@
display: block;
}
#extensions_status {
/* margin-bottom: 10px; */
font-weight: 700;
}
.extensions_block input[type="submit"]:hover {
background-color: green;
}
@ -103,8 +98,9 @@ input.extension_missing[type="checkbox"] {
}
/** LEFT COLUMN **/
/* Must be always on top */
#extensions_settings>#assets_ui {
order: 1;
order: -1;
}
#extensions_settings>.expression_settings {

57
public/css/file-form.css Normal file
View File

@ -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;
}

25
public/css/loader.css Normal file
View File

@ -0,0 +1,25 @@
#loader {
position: fixed;
margin: 0;
padding: 0;
top: 0;
left: 0;
display: flex;
align-items: center;
justify-content: center;
z-index: 999999;
width: 100vw;
height: 100vh;
width: 100svw;
height: 100svh;
background-color: var(--SmartThemeBlurTintColor);
/*for some reason the full screen blur does not work on iOS*/
backdrop-filter: blur(30px);
color: var(--SmartThemeBodyColor);
opacity: 1;
}
#load-spinner {
transition: all 300ms ease-out;
opacity: 1;
}

View File

@ -256,6 +256,14 @@
bottom: 0 !important;
}
body:not(.waifuMode) #expression-wrapper {
visibility: hidden;
}
#visual-novel-wrapper {
position: unset !important;
}
body.waifuMode .expression-holder {
/*display: inline;*/
@ -267,7 +275,7 @@
left: 0;
right: 0;
filter: drop-shadow(2px 2px 2px #51515199);
z-index: 1;
z-index: 1 !important;
}
body.waifuMode img.expression {
@ -369,6 +377,18 @@
top: unset;
bottom: unset;
}
#leftSendForm,
#rightSendForm {
width: 1.15em;
flex-wrap: wrap;
height: unset;
}
#extensionsMenuButton {
order: 1;
}
}
/*iOS specific*/

View File

@ -62,23 +62,24 @@
.margin-bot-10px,
.marginBot10 {
margin-bottom: 10px;
margin-bottom: 10px !important;
}
.marginTop10 {
margin-top: 10px;
margin-top: 10px !important;
}
.marginBot5 {
margin-bottom: 5px;
margin-bottom: 5px !important;
}
.marginTop5 {
margin-top: 5px;
margin-top: 5px !important;
}
.marginTopBot5 {
margin: 5px 0;
margin-top: 5px !important;
margin-bottom: 5px !important;
}
.margin5 {
@ -113,6 +114,10 @@
align-self: start;
}
.gap0 {
gap: 0 !important;
}
.gap3px {
gap: 3px !important;
}
@ -125,6 +130,14 @@
gap: 10px !important;
}
.gap10h20v {
gap: 10px 20px !important;
}
.gap10h5v {
gap: 5px 10px !important;
}
.wide10pMinFit {
width: 10%;
min-width: fit-content;
@ -154,6 +167,10 @@
box-shadow: none !important;
}
.height100p {
height: 100%;
}
.height100pSpaceEvenly {
align-content: space-evenly;
height: 100%;
@ -212,6 +229,26 @@
display: flex;
}
.flexBasis100p {
flex-basis: 100%;
}
.flexBasis50p {
flex-basis: 50%
}
.flexBasis25p {
flex-basis: 25%
}
.flexBasis200px {
flex-basis: 200px
}
.flexBasis48p {
flex-basis: 48%
}
.flex-container {
display: flex;
gap: 5px;
@ -226,6 +263,14 @@
flex-grow: 1;
}
.flexShrink {
flex-shrink: 1
}
.flexWrap {
flex-wrap: wrap;
}
.flexnowrap {
flex-wrap: nowrap;
}
@ -252,6 +297,10 @@
align-content: flex-start;
}
.alignContentCenter {
align-content: center;
}
.overflowHidden {
overflow: hidden;
}
@ -304,10 +353,6 @@
flex: 50%;
}
.wide50p {
width: 50% !important;
}
.wide25p {
width: 25%;
}
@ -391,6 +436,10 @@
display: none;
}
.hoverglow {
transition: opacity 200ms;
}
.hoverglow:hover {
opacity: 1 !important;
cursor: pointer;
@ -421,6 +470,10 @@ textarea:disabled {
border: 1px solid purple !important;
}
.fontsize120p {
font-size: calc(var(--mainFontSize) * 1.2) !important;
}
.fontsize80p {
font-size: calc(var(--mainFontSize) * 0.8) !important;
}
@ -459,6 +512,22 @@ textarea:disabled {
gap: 10px;
}
.opacity50p {
opacity: 0.5
}
.opacity1 {
opacity: 1 !important;
}
.circleborder30px {
right: 30px;
top: 10px;
position: absolute;
border: 1px solid var(--SmartThemeBodyColor);
border-radius: 100%;
aspect-ratio: 1 / 1;
height: 30px;
text-align: center;
padding: 5px;
}

View File

@ -1,3 +1,4 @@
#bulk_tags_div,
#tags_div {
min-width: 0;
}
@ -12,7 +13,7 @@
.tag_view_item {
display: flex;
flex-direction: row;
align-items: baseline;
align-items: center;
gap: 10px;
margin-bottom: 5px;
}
@ -86,10 +87,12 @@
align-items: flex-end;
}
#bulkTagsList,
#tagList.tags {
margin: 5px 0;
}
#bulkTagsList,
#tagList .tag {
opacity: 0.6;
}

View File

@ -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 {
@ -28,6 +35,7 @@ body.charListGrid #rm_print_characters_block {
align-content: flex-start;
}
body.charListGrid #rm_print_characters_block .bogus_folder_select,
body.charListGrid #rm_print_characters_block .character_select {
width: 30%;
align-items: flex-start;
@ -37,6 +45,7 @@ body.charListGrid #rm_print_characters_block .character_select {
max-width: 100px;
}
body.charListGrid #rm_print_characters_block .bogus_folder_select .ch_name,
body.charListGrid #rm_print_characters_block .character_select .ch_name,
body.charListGrid #rm_print_characters_block .group_select .ch_name {
width: 100%;
@ -45,10 +54,12 @@ body.charListGrid #rm_print_characters_block .group_select .ch_name {
font-size: calc(var(--mainFontSize) * .8);
}
body.charListGrid #rm_print_characters_block .bogus_folder_select .character_name_block,
body.charListGrid #rm_print_characters_block .character_select .character_name_block {
width: 100%;
}
body.charListGrid #rm_print_characters_block .bogus_folder_select .character_select_container,
body.charListGrid #rm_print_characters_block .character_select .character_select_container {
width: 100%;
justify-content: center;
@ -68,6 +79,7 @@ body.charListGrid #rm_print_characters_block .group_select .group_name_block {
width: 100%;
}
body.charListGrid #rm_print_characters_block .bogus_folder_counter_block,
body.charListGrid #rm_print_characters_block .ch_description,
body.charListGrid #rm_print_characters_block .tags_inline,
body.charListGrid #rm_print_characters_block .character_version,
@ -352,4 +364,12 @@ body.expandMessageActions .mes .mes_buttons .extraMesButtons {
body.expandMessageActions .mes .mes_buttons .extraMesButtonsHint {
display: none !important;
}
}
#openai_image_inlining:not(:checked)~#image_inlining_hint {
display: none;
}
#openai_image_inlining:checked~#image_inlining_hint {
display: block;
}

View File

@ -190,4 +190,4 @@
.WIEntryHeaderTitleMobile {
display: none;
}
}

View File

@ -12,66 +12,66 @@
"clickslidertips": "点击滑块右侧数字可手动输入",
"kobldpresets": "Kobold 预设",
"guikoboldaisettings": "KoboldAI GUI 设置",
"novelaipreserts": "NovelAI预设",
"novelaipreserts": "NovelAI 预设",
"default": "默认",
"openaipresets": "OpenAI 预设",
"text gen webio(ooba) presets": "Text generation web UI 预设",
"response legth(tokens)": "响应长度 (Toekns)",
"response legth(tokens)": "响应长度Tokens",
"select": "选择 ",
"context size(tokens)": "上下文大小 (Toekns)",
"context size(tokens)": "上下文大小Tokens",
"unlocked": "解锁",
"Only select models support context sizes greater than 4096 tokens. Increase only if you know what you're doing.": "只有在选定的模型支持大于 4096 个Toekn 时可以选择启用,在启用该选项时,你应该知道自己在做什么。",
"rep.pen": "频率惩罚",
"rep.pen range": "存在惩罚",
"temperature": "温度设置",
"Encoder Rep. Pen.": "Encoder Rep. Pen.",
"No Repeat Ngram Size": "不需要重复Ngram大小",
"Only select models support context sizes greater than 4096 tokens. Increase only if you know what you're doing.": "只有在选定的模型支持大于 4096 个Token时可以选择启用。在启用该选项时,你应该知道自己在做什么。",
"rep.pen": "重复惩罚",
"rep.pen range": "重复惩罚范围",
"temperature": "温度",
"Encoder Rep. Pen.": "编码器重复惩罚",
"No Repeat Ngram Size": "不重复N元语法大小",
"Min Length": "最小长度",
"OpenAI Reverse Proxy": "OpenAI API 反向代理",
"Alternative server URL (leave empty to use the default value).": "自定义 OpenAI API 的反向代理地址 (留空时使用 OpenAI 默认服务器)。",
"Remove your real OAI API Key from the API panel BEFORE typing anything into this box": "在输入内容之前,从 API 面板中删除 OpenAI API 密钥",
"We cannot provide support for problems encountered while using an unofficial OpenAI proxy": "我们无法为使用自定义 OpenAI API 反向代理时遇到的问题提供支持",
"Legacy Streaming Processing": "传统流式处理",
"Enable this if the streaming doesn't work with your proxy": "如果流式响应与您的代理不兼容,请启用此功能",
"Context Size (tokens)": "上下文大小(Tokens)",
"Max Response Length (tokens)": "最大响应长度(Tokens)",
"Temperature": "温度设置",
"Enable this if the streaming doesn't work with your proxy": "如果流式回复与您的代理不兼容,请启用此功能",
"Context Size (tokens)": "上下文大小Tokens",
"Max Response Length (tokens)": "最大响应长度Tokens",
"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",
"Rep. Pen. Slope": "重复惩罚梯度",
"Single-line mode": "单行模式",
"Top K": "Top-k",
"Top P": "Top-p",
"Typical P": "典型P",
"Do Sample": "样",
"Add BOS Token": "添加BOS标记",
"Add the bos_token to the beginning of prompts. Disabling this can make the replies more creative.": "在提示的开头添加bos_token标记。禁用此功能可以让回复更加创造性.",
"Ban EOS Token": "禁止EOS标记",
"Ban the eos_token. This forces the model to never end the generation prematurely": "禁止eos_token标记。这会迫使模型不会过早结束生成",
"Skip Special 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": "种子",
"Inserts jailbreak as a last system message.": "在最后一个系统消息中插入越狱",
"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": "流式响应",
"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.": "关闭此选项后,响应将在完成后立即显示所有响应。",
"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 忽略)。",
"Ban the End-of-Sequence (EOS) token (with KoboldCpp, and possibly also other tokens with KoboldAI).": "禁止序列结束 (EOS) 代币(使用 KoboldCpp也可能使用 KoboldAI 禁止其他代币)。",
"Ban the End-of-Sequence (EOS) token (with KoboldCpp, and possibly also other tokens with KoboldAI).": "禁止序列结束 (EOS) token使用 KoboldCpp也可能使用 KoboldAI 禁止其他 token)。",
"Good for story writing, but should not be used for chat and instruct mode.": "适合故事写作,但不应用于聊天和指导模式。",
"Enhance Definitions": "增强定义",
"Use OAI knowledge base to enhance definitions for public figures and known fictional characters": "使用 OpenAI 知识库增强公众人物和已知虚构人物的定义",
@ -84,9 +84,9 @@
"Prompt that is used when the NSFW toggle is on": "NSFW 打开时使用的提示",
"Jailbreak prompt": "越狱提示",
"Prompt that is used when the Jailbreak toggle is on": "越狱开关打开时使用的提示",
"Impersonation prompt": "模拟提示",
"Prompt that is used for Impersonation function": "用于模拟功能的提示",
"Logit Bias": "对数偏",
"Impersonation prompt": "扮演提示",
"Prompt that is used for Impersonation function": "用于扮演功能的提示",
"Logit Bias": "对数偏",
"Helps to ban or reenforce the usage of certain words": "有助于禁止或加强某些单词的使用",
"View / Edit bias preset": "查看/编辑偏置预设",
"Add bias entry": "添加偏置条目",
@ -108,8 +108,8 @@
"Register": "注册",
"For privacy reasons": "出于隐私原因,您的 API 密钥将在您重新加载页面后隐藏",
"Model": "模型",
"Hold Control / Command key to select multiple models.": "按住控制/命令键选择多个。",
"Horde models not loaded": "按住控制/命令键选择多个型号。",
"Hold Control / Command key to select multiple models.": "按住控制/命令键选择多个型。",
"Horde models not loaded": "未加载 Horde 模型。",
"Not connected": "未连接",
"Novel API key": "NovelAI API 密钥",
"Follow": "跟随",
@ -126,15 +126,15 @@
"OpenAI Model": "OpenAI模型",
"View API Usage Metrics": "查看 API 使用情况",
"Bot": "Bot",
"Connect to the API": "连接到API",
"Auto-connect to Last Server": "自动连接到最后设置的 API 服务",
"Connect to the API": "连接到 API",
"Auto-connect to Last Server": "自动连接到最后设置的 API 服务",
"View hidden API keys": "查看隐藏的 API 密钥",
"Advanced Formatting": "高级格式化",
"AutoFormat Overrides": "自动套用格式替代",
"Disable description formatting": "禁用说明格式",
"Disable personality formatting": "禁用个性化格式",
"Disable scenario formatting": "禁用方案格式",
"Disable example chats formatting": "禁用聊天格式示例",
"AutoFormat Overrides": "覆盖自动格式化",
"Disable description formatting": "禁用描述格式",
"Disable personality formatting": "禁用人设格式",
"Disable scenario formatting": "禁用场景格式",
"Disable example chats formatting": "禁用聊天示例格式",
"Disable chat start formatting": "禁用聊天开始格式",
"Custom Chat Separator": "自定义聊天分隔符",
"Instruct Mode": "指示模式",
@ -142,7 +142,7 @@
"Wrap Sequences with Newline": "用换行符换行序列",
"Include Names": "包括名称",
"System Prompt": "系统提示",
"Instruct Mode Sequences": "指模式序列",
"Instruct Mode Sequences": "指模式序列",
"Input Sequence": "输入序列",
"Output Sequence": "输出序列",
"First Output Sequence": "第一个输出序列",
@ -151,57 +151,54 @@
"System Sequence Suffix": "系统序列后缀",
"Stop Sequence": "停止序列",
"Context Formatting": "上下文格式",
"Tokenizer": "分词器",
"Tokenizer": "Tokenizer",
"None / Estimated": "无/估计",
"Sentencepiece (LLaMA)": "Sentencepiece (LLaMA)",
"Token Padding": "令牌填充",
"Always add character's name to prompt": "始终将角色名称添加到提示中",
"Keep Example Messages in Prompt": "保持示例消息提示",
"Token Padding": "Token 填充",
"Always add character's name to prompt": "始终将角色名称添加到提示中",
"Keep Example Messages in Prompt": "在提示中保留示例消息",
"Remove Empty New Lines from Output": "从输出中删除空的新行",
"Disabled for all models": "对所有模型禁用",
"Automatic (based on model name)": "自动(基于名称)",
"Enabled for all models": "所有模型启用",
"Automatic (based on model name)": "自动(基于型名称)",
"Enabled for all models": "所有模型启用",
"Anchors Order": "锚点顺序",
"Character then Style": "字符然后样式",
"Style then Character": "样式然后字符",
"Character Anchor": "角色锚点",
"Character Anchor": "字符锚点",
"Style Anchor": "样式锚点",
"World Info": "",
"Scan Depth": "扫描深度",
"depth": "深度",
"Token Budget": "Token 预算",
"budget": "预算",
"Recursive scanning": "递归扫描",
"Soft Prompt": "软提示",
"About soft prompts": "关于软提示",
"None": "没有",
"User Settings": "聊天窗口设置",
"UI Customization": "聊天窗口定制",
"UI Customization": "聊天窗口自定义",
"Avatar Style": "头像风格",
"Circle": "圆形",
"Rectangle": "长方形",
"Chat Style": "聊天式:",
"Chat Style": "聊天窗口样式:",
"Default": "默认",
"Bubbles": "气泡",
"Chat Width (PC)": "聊天窗口宽度(电脑):",
"No Blur Effect": "关糊效果",
"No Text Shadows": "关文字阴影",
"No Blur Effect": "闭模糊效果",
"No Text Shadows": "文字阴影",
"Waifu Mode": "♡ 老 婆 模 式 ♡",
"Message Timer": "消息计时器",
"Characters Hotswap": "角色热插拔",
"Movable UI Panels": "可移动的聊天窗口",
"Reset Panels": "恢复默认设置",
"UI Colors": "聊天窗口字体颜色",
"Movable UI Panels": "可移动的UI面板",
"Reset Panels": "重置面板",
"UI Colors": "UI颜色",
"Main Text": "正文",
"Italics Text": "斜体文字",
"Quote Text": "引用文字",
"Shadow Color": "阴影颜色",
"FastUI BG": "快界面 BG",
"Blur Tint": "背景颜色",
"Font Scale": "字体比例",
"FastUI BG": "快速UI背景",
"Blur Tint": "模糊色调",
"Font Scale": "字体缩放",
"Blur Strength": "模糊强度",
"Text Shadow Width": "文字阴影宽度",
"UI Theme Preset": "界面主题预设",
"UI Theme Preset": "UI主题预设",
"Power User Options": "高级用户选项",
"Swipes": "滑动",
"Background Sound Only": "仅背景声音",
@ -210,23 +207,23 @@
"Auto-fix Markdown": "自动修复 Markdown 格式",
"Allow {{char}}: in bot messages": "允许 {{char}}:在机器人消息中",
"Allow {{user}}: in bot messages": "允许 {{user}}:在机器人消息中",
"Auto-scroll Chat": "自动滚动聊天信息",
"Auto-scroll Chat": "自动滚动聊天界面",
"Render Formulas": "渲染公式",
"Send on Enter": "按下回车键发送",
"Always disabled": "始终禁用",
"Automatic (desktop)": "自动(电脑)",
"Always enabled": "始终启用",
"Name": "用户名称",
"Your Avatar": "用户角色头像",
"Extensions API:": "扩展接口",
"Your Avatar": "用户头像",
"Extensions API:": "扩展API",
"SillyTavern-extras": "SillyTavern 扩展",
"Auto-connect": "自动连接",
"Active extensions": "活动扩展",
"Active extensions": "启用扩展",
"Extension settings": "扩展设置",
"Description": "描述",
"First message": "第一条消息",
"Group Controls": "组控件",
"Group reply strategy": "组回复策略",
"Group Controls": "群组控制",
"Group reply strategy": "组回复策略",
"Natural order": "自然顺序",
"List order": "列表顺序",
"Allow self responses": "允许自我响应",
@ -236,9 +233,9 @@
"text": "文本",
"Delete": "删除",
"Cancel": "取消",
"Advanced Defininitions": "- 高级定义",
"Personality summary": "性格总结",
"A brief description of the personality": "个性的简要描述",
"Advanced Defininitions": "高级定义",
"Personality summary": "人设总结",
"A brief description of the personality": "人设的简要描述",
"Scenario": "场景",
"Circumstances and context of the dialogue": "对话的情况和背景",
"Talkativeness": "回复频率",
@ -246,17 +243,17 @@
"group chats!": "群聊!",
"Shy": "羞涩 ",
"Normal": "正常",
"Chatty": "",
"Chatty": "健谈",
"Examples of dialogue": "对话示例",
"Forms a personality more clearly": "更清晰地形成个性",
"Forms a personality more clearly": "更清晰地形成人设",
"Save": "保存",
"World Info Editor": "信息编辑器",
"New Entry": "新一行",
"World Info Editor": "世界背景编辑器",
"New Entry": "新条目",
"Export": "导出",
"Delete World": "删除文本",
"Chat History": "聊天记录",
"Group Chat Scenario Override": "群聊方案覆盖",
"All group members will use the following scenario text instead of what is specified in their character cards.": "所有组成员都将使用以下方案文本,而不是其角色卡中指定的内容。",
"Group Chat Scenario Override": "群聊场景覆盖",
"All group members will use the following scenario text instead of what is specified in their character cards.": "所有群组成员都将使用以下场景文本,而不是其角色卡中指定的内容。",
"Keywords": "关键字",
"Separate with commas": "用逗号分隔",
"Secondary Required Keywords": "次要必填关键字",
@ -268,15 +265,15 @@
"Selective": "选择",
"Before Char": "在Char之前",
"After Char": "在Char之后",
"Insertion Order": "顺序",
"Insertion Order": "插入顺序",
"Tokens:": "Tokens",
"Disable": "禁用",
"${characterName}": "${字符名称}",
"CHAR": "字符",
"${characterName}": "${角色名}",
"CHAR": "角色",
"is typing": "正在输入...",
"Back to parent chat": "返回聊天",
"Save bookmark": "保存书签",
"Convert to group": "转换为组",
"Convert to group": "转换为组",
"Start new chat": "开始新聊天",
"View past chats": "查看过去的聊天",
"Delete messages": "删除消息",
@ -294,9 +291,9 @@
"Prompt that is used when the NSFW toggle is off": "当 NSFW 切换关闭时使用的提示",
"Advanced prompt bits": "高级提示",
"World Info format template": "世界背景格式模板",
"Wraps activated World Info entries before inserting into the prompt. Use {0} to mark a place where the content is inserted.": "在插入提示之前,激活世界背景条目的包装。使用 {0} 来标记内容插入的位置。",
"Unrestricted maximum value for the context slider": "上下文滑块的无限最大值",
"Chat Completion Source": "Chat Completion",
"Wraps activated World Info entries before inserting into the prompt. Use {0} to mark a place where the content is inserted.": "在插入提示之前,包装已激活世界背景条目。使用 {0} 来标记内容插入的位置。",
"Unrestricted maximum value for the context slider": "上下文滑块的无限最大值",
"Chat Completion Source": "聊天补全源",
"Avoid sending sensitive information to the Horde.": "避免向 Horde 发送敏感信息",
"Review the Privacy statement": "查看隐私声明",
"Learn how to contribute your idel GPU cycles to the Horde": "学习如何将闲置的显卡计算资源贡献给Horde",
@ -310,15 +307,15 @@
"Text Gen WebUI (ooba)": "Text Gen WebUI (ooba)",
"NovelAI": "NovelAI",
"Chat Completion (OpenAI, Claude, Window/OpenRouter, Scale)": "Chat Completion (OpenAI, Claude, Window/OpenRouter, Scale)",
"OpenAI API key": "OenAI API 密钥",
"OpenAI API key": "OpenAI API 密钥",
"Trim spaces": "修剪空格",
"Trim Incomplete Sentences": "修剪不完整的句子",
"Include Newline": "包括换行",
"Non-markdown strings": "非标记字串符",
"Non-markdown strings": "非markdown字符串",
"Replace Macro in Sequences": "替换序列中的宏",
"Presets": "预设",
"Separator": "分隔符",
"Start Reply With": "开始回复",
"Start Reply With": "回复前缀",
"Show reply prefix in chat": "在聊天中显示回复前缀",
"Worlds/Lorebooks": "世界/Lorebooks",
"Active World(s)": "激活的世界",
@ -334,40 +331,40 @@
"--- None ---": "--- 全无 ---",
"Comma seperated (ignored if empty)": "逗号分隔 (如果为空则忽略)",
"Use Probability": "使用概率",
"Exclude from recursion": "排除递归",
"Position:": "插入位置",
"Before Char Defs": "字符定义之前",
"After Char Defs": "字符定义之后",
"Exclude from recursion": "从递归中排除",
"Position:": "插入位置",
"Before Char Defs": "角色定义之前",
"After Char Defs": "角色定义之后",
"Before AN": "作者注释之前",
"After AN": "作者注释之后",
"Order:": "排序",
"Probability:": "概率",
"Delete Entry": "删除条目",
"User Message Blur Tint": "用户消息",
"AI Message Blur Tint": "AI 消息",
"Chat Style:": "聊天窗口样式",
"Chat Width (PC):": "聊天窗口宽度 (电脑)",
"User Message Blur Tint": "用户消息模糊颜色",
"AI Message Blur Tint": "AI 消息模糊颜色",
"Chat Style:": "聊天窗口样式",
"Chat Width (PC):": "聊天窗口宽度 (电脑)",
"Chat Timestamps": "聊天时间戳",
"Message IDs": "消息 ID",
"Prefer Character Card Prompt": "首选角色卡提示",
"Prefer Character Card Jailbreak": "首选角色卡越狱",
"Press Send to continue": "按下发送消息继续",
"Press Send to continue": "按下发送按钮继续",
"Log prompts to console": "将提示记录到控制台",
"Never resize avatars": "不要调整头像大小",
"Show avatar filenames": "显示头像文件名",
"Import Card Tags": "导入卡片标签",
"Confirm message deletion": "确认删除消息",
"Spoiler Free Mode": "自由剧透模式",
"Auto-swipe": "自动重新生成",
"Spoiler Free Mode": "剧透模式",
"Auto-swipe": "自动右滑生成",
"Minimum generated message length": "消息生成的最小长度",
"Blacklisted words": "黑名单词汇",
"Blacklisted word count to swipe": "黑名单词汇",
"Blacklisted word count to swipe": "自动滑动触发的累计黑名单词汇数",
"Reload Chat": "重新加载聊天窗口",
"Not Connected": "未连接",
"Persona Management": "用户角色设置",
"Persona Description": "用户角色描述",
"In Story String / Chat Completion: Before Character Card": "在故事串中 / Chat Completion: 角色卡之前",
"In Story String / Chat Completion: After Character Card": "在故事串中 / Chat Completion: 角色卡之后",
"In Story String / Chat Completion: Before Character Card": "在故事字符串 / 聊天补全中: 角色卡之前",
"In Story String / Chat Completion: After Character Card": "在故事字符串 / 聊天补全中: 角色卡之后",
"Top of Author's Note": "作者注释之前",
"Bottom of Author's Note": "作者注释之后",
"How do I use this?": "用户角色设置说明",
@ -384,8 +381,8 @@
"Oldest": "最旧",
"Favorites": "收藏",
"Recent": "最近",
"Most chats": "大多数聊天",
"Least chats": "最少聊天",
"Most chats": "聊天次数最多",
"Least chats": "聊天次数最少",
"Back": "返回",
"Prompt Overrides (For OpenAI/Claude/Scale APIs, Window/OpenRouter, and Instruct mode)": "提示覆盖适用于OpenAI/Claude/Scale APIs、Window/OpenRouter和Instruct模式",
"Insert {{original}} into either box to include the respective default prompt from system settings.": "将{{original}}插入任意一个框中,即可包含来自系统设置的默认提示。",
@ -397,16 +394,16 @@
"Character Version": "角色版本",
"Tags to Embed": "要嵌入的标签",
"How often the character speaks in group chats!": "角色在群聊中说话的频率!",
"Important to set the character's writing style.": "的是设置角色的写作风格。",
"Important to set the character's writing style.": "要设置角色的写作风格,它很重要。",
"ATTENTION!": "注意!",
"Samplers Order": "采样器顺序",
"Samplers will be applied in a top-down order. Use with caution.": "采样器将按从上到下的顺序应用。谨慎使用。",
"Repetition Penalty": "重复惩罚",
"Epsilon Cutoff": "Epsilon切断",
"Eta Cutoff": "Eta切断",
"Rep. Pen. Range.": "代表范围的惩罚。",
"Rep. Pen. Freq.": "代表鹰频",
"Rep. Pen. Presence": "重复惩罚出现",
"Epsilon Cutoff": "Epsilon 切断",
"Eta Cutoff": "Eta 切断",
"Rep. Pen. Range.": "重复惩罚范围",
"Rep. Pen. Freq.": "重复频率惩罚",
"Rep. Pen. Presence": "重复存在惩罚",
"Enter it in the box below:": "在下面的框中输入:",
"separate with commas w/o space between": "用逗号分隔,不要空格",
"Document": "文档",
@ -430,9 +427,9 @@
"Any contents here will replace the default Jailbreak Prompt used for this character. (v2 spec: post_history_instructions)": "这里的任何内容都将替换用于此角色的默认越狱提示。 v2规范post_history_instructions",
"(Botmaker's name / Contact Info)": "Bot制作者的名字/联系信息)",
"(If you want to track character versions)": "(如果你想跟踪角色版本)",
"(Describe the bot, give use tips, or list the chat models it has been tested on. This will be displayed in the character list.)": "(描述机器人,给出使用提示,或列出测试过的聊天模型。这将显示在角色列表中)",
"(Describe the bot, give use tips, or list the chat models it has been tested on. This will be displayed in the character list.)": "(描述机器人,给出使用提示,或列出它测试过的聊天模型。这将显示在角色列表中)",
"(Write a comma-separated list of tags)": "(编写逗号分隔的标签列表)",
"(A brief description of the personality)": "个性的简要描述)",
"(A brief description of the personality)": "人设的简要描述)",
"(Circumstances and context of the interaction)": "(互动的情况和上下文)",
"(Examples of chat dialog. Begin each example with START on a new line.)": "聊天对话的示例。每个示例都以新行的START开始",
"Injection text (supports parameters)": "注入文本(支持参数)",
@ -452,8 +449,8 @@
"Export preset": "导出预设",
"Delete the preset": "删除该预设",
"Inserts jailbreak as a last system message": "将越狱插入为最后一个系统消息",
"NSFW block goes first in the resulting prompt": "在生成的提示中,首先是NSFW块",
"Enables OpenAI completion streaming": "启用OpenAI完成流",
"NSFW block goes first in the resulting prompt": "在生成的提示中,NSFW部分排在首位",
"Enables OpenAI completion streaming": "启用OpenAI补全流",
"Wrap user messages in quotes before sending": "发送前用引号括起用户消息",
"Restore default prompt": "恢复默认提示",
"New preset": "新预设",
@ -462,17 +459,17 @@
"Restore default reply": "恢复默认回复",
"Restore defaul note": "恢复默认注释",
"API Connections": "API连接",
"Can help with bad responses by queueing only the approved workers. May slowdown the response time.": "只通过排队已批准的工作人员来帮助处理不良响应。可能会减慢响应时间。",
"Can help with bad responses by queueing only the approved workers. May slowdown the response time.": "通过只排队已批准的worker来帮助处理不良响应。可能会减慢响应时间。",
"Clear your API key": "清除你的API密钥",
"Refresh models": "刷新模型",
"Get your OpenRouter API token using OAuth flow. You will be redirected to openrouter.ai": "使用OAuth流程获取您的OpenRouter API令牌。您将被重定向到openrouter.ai",
"Verifies your API connection by sending a short test message. Be aware that you'll be credited for it!": "通过发送一个短测试消息验证您的API连接。请注意您会获得相应的积分",
"Verifies your API connection by sending a short test message. Be aware that you'll be credited for it!": "通过发送一个短测试消息验证您的API连接。请注意,这将会计入你的使用额度",
"Create New": "创建新的",
"Edit": "编辑",
"World Info & Soft Prompts": "世界背景 & 软提示",
"World Info": "世界背景",
"Locked = World Editor will stay open": "锁定=世界编辑器将保持打开状态",
"Entries can activate other entries by mentioning their keywords": "条目可以通过提及其关键字来激活其他条目",
"Lookup for the entry keys in the context will respect the case": "在上下文中查找条目键将遵守大小写",
"Lookup for the entry keys in the context will respect the case": "在上下文中查找条目将遵守大小写",
"If the entry key consists of only one word, it would not be matched as part of other words": "如果条目键仅包含一个词,它将不会被匹配为其他词汇的一部分",
"Open all Entries": "打开所有条目",
"Close all Entries": "关闭所有条目",
@ -485,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": "点击设置新用户名",
@ -495,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 counts may be inaccurate and provided just for reference.": "Token 计数可能不准确,仅供参考。",
"Click to select a new avatar for this character": "点击选择此角色的新头像",
"Add to Favorites": "添加到收藏夹",
"Advanced Definition": "高级定义",
@ -527,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": "删除标签",
@ -555,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.": "将此选择为新聊天的默认角色。",
@ -672,7 +669,7 @@
"Novel AI Model": "NovelAI モデル",
"No connection": "接続なし",
"oobabooga/text-generation-webui": "",
"Make sure you run it with": "必ず --api の引数を含めて起動してください",
"Make sure you run it with": "必ず --extensions openai の引数を含めて起動してください",
"Blocking API url": "ブロッキング API URL",
"Streaming API url": "ストリーミング API URL",
"to get your OpenAI API key.": "あなたの OpenAI API キーを取得するために。",
@ -724,8 +721,6 @@
"Token Budget": "トークン予算",
"budget": "予算",
"Recursive scanning": "再帰的スキャン",
"Soft Prompt": "ソフトプロンプト",
"About soft prompts": "ソフトプロンプトについて",
"None": "なし",
"User Settings": "ユーザー設定",
"UI Customization": "UIカスタマイズ",
@ -1023,7 +1018,7 @@
"Verifies your API connection by sending a short test message. Be aware that you'll be credited for it!": "NEEDS TRANSLATION",
"Create New": "NEEDS TRANSLATION",
"Edit": "NEEDS TRANSLATION",
"World Info & Soft Prompts": "NEEDS TRANSLATION",
"World Info": "NEEDS TRANSLATION",
"Locked = World Editor will stay open": "NEEDS TRANSLATION",
"Entries can activate other entries by mentioning their keywords": "NEEDS TRANSLATION",
"Lookup for the entry keys in the context will respect the case": "NEEDS TRANSLATION",
@ -1227,7 +1222,7 @@
"Novel AI Model": "NovelAI 모델",
"No connection": "접속 실패",
"oobabooga/text-generation-webui": "oobabooga/text-generation-webui",
"Make sure you run it with": "--api 인수를 반드시 사용해야 합니다.",
"Make sure you run it with": "--extensions openai 인수를 반드시 사용해야 합니다.",
"Blocking API url": "API URL을 막는 중",
"Streaming API url": "API URL에서 스트리밍 중",
"OpenAI Model": "OpenAI 모델",
@ -1278,8 +1273,6 @@
"Token Budget": "토큰 예산",
"budget": "예산",
"Recursive scanning": "되풀이 검색",
"Soft Prompt": "Soft Prompt",
"About soft prompts": "Soft prompt란?",
"None": "없음",
"User Settings": "사용자 설정",
"UI Customization": "UI 꾸미기",
@ -1581,7 +1574,7 @@
"Verifies your API connection by sending a short test message. Be aware that you'll be credited for it!": "짧은 시험 메시지를 보내서 API 접속 상태를 확인합니다. 서비스 사용으로 취급됩니다!",
"Create New": "새로 만들기",
"Edit": "수정하기",
"World Info & Soft Prompts": "세계관 & 소프트 프롬프트",
"World Info": "세계관",
"Locked = World Editor will stay open": "세계관 설정 패널 열림을 고정합니다",
"Entries can activate other entries by mentioning their keywords": "설정 내용에 다른 설정의 키워드가 있다면 연속으로 발동하게 합니다",
"Lookup for the entry keys in the context will respect the case": "설정 발동 키워드가 대소문자를 구분합니다",
@ -1803,7 +1796,7 @@
"Novel AI Model": "Модель NovelAI",
"If you are using:": "Если вы используете:",
"oobabooga/text-generation-webui": "",
"Make sure you run it with": "Убедитесь, что при запуске указали аргумент --api",
"Make sure you run it with": "Убедитесь, что при запуске указали аргумент --extensions openai",
"Mancer AI": "",
"Use API key (Only required for Mancer)": "Нажмите на ячейку (и добавьте свой API ключ!):",
"Blocking API url": "Блокирующий API url",
@ -1890,8 +1883,6 @@
"Token Budget": "Объем токенов",
"budget": "объем",
"Recursive scanning": "Рекурсивное сканирование",
"Soft Prompt": "Мягкая инструкция",
"About soft prompts": "О мягких инструкциях",
"None": "Отсутствует",
"User Settings": "Настройки пользователя",
"UI Mode": "Режим интерфейса",
@ -2207,7 +2198,7 @@
"Verifies your API connection by sending a short test message. Be aware that you'll be credited for it!": "Подверждает ваше соединение к API. Знайте, что за это снимут деньги с вашего счета.",
"Create New": "Создать новое",
"Edit": "Изменить",
"World Info & Soft Prompts": "Информация о Мире & Мягкий Промт",
"World Info": "Информация о Мире",
"Locked = World Editor will stay open": "Закреплено = Редактирование Мира останется открытым",
"Entries can activate other entries by mentioning their keywords": "Записи могут активировать другие записи если в них содержаться ключевые слова",
"Lookup for the entry keys in the context will respect the case": "Большая буква имеет значение при активации ключевого слова",
@ -2413,7 +2404,7 @@
"Krake": "Krake",
"No connection": "Nessuna connessione",
"oobabooga/text-generation-webui": "oobabooga/text-generation-webui",
"Make sure you run it with": "assicurati di farlo partire con",
"Make sure you run it with": "assicurati di farlo partire con --extensions openai",
"Blocking API url": "Blocca l'indirizzo API",
"Streaming API url": "Streaming dell'indirizzo API",
"to get your OpenAI API key.": "per ottenere la tua chiave API di OpenAI.",
@ -2469,8 +2460,6 @@
"Token Budget": "Budget per i Token",
"budget": "budget",
"Recursive scanning": "Analisi ricorsiva",
"Soft Prompt": "Prompt leggero",
"About soft prompts": "Riguardo i prompt leggeri",
"None": "None",
"User Settings": "Settaggi utente",
"UI Customization": "Personalizzazione UI",
@ -2767,7 +2756,7 @@
"Verifies your API connection by sending a short test message. Be aware that you'll be credited for it!": "Verifica la connessione all'API inviando un breve messaggio. Devi comprendere che il messaggio verrà addebitato come tutti gli altri!",
"Create New": "Crea nuovo",
"Edit": "Edita",
"World Info & Soft Prompts": "'Info Mondo' & Soft Prompt",
"World Info": "'Info Mondo'",
"Locked = World Editor will stay open": "Se clicchi il lucchetto, l'editor del mondo rimarrà aperto",
"Entries can activate other entries by mentioning their keywords": "Le voci possono attivare altre voci menzionando le loro parole chiave",
"Lookup for the entry keys in the context will respect the case": "Fai attenzione alle parole chiave usate, esse rispetteranno le maiuscole",
@ -2963,7 +2952,7 @@
"Show tags in responses": "Mostra i tag nelle risposte",
"Story String": "Stringa narrativa",
"Text Adventure": "Avventura testuale",
"Text Gen WebUI (ooba/Mancer) presets": "Preset Text Gen WebUI (ooba/Mancer)",
"Text Gen WebUI presets": "Preset Text Gen WebUI",
"Toggle Panels": "Interruttore pannelli",
"Top A Sampling": "Top A Sampling",
"Top K Sampling": "Top K Sampling",
@ -3174,7 +3163,7 @@
"Novel AI Model": "NovelAI-model",
"No connection": "Geen verbinding",
"oobabooga/text-generation-webui": "oobabooga/text-generation-webui",
"Make sure you run it with": "Zorg ervoor dat je het uitvoert met",
"Make sure you run it with": "Zorg ervoor dat je het uitvoert met --extensions openai",
"Blocking API url": "Blokkerende API-url",
"Streaming API url": "Streaming API-url",
"to get your OpenAI API key.": "om je OpenAI API-sleutel te verkrijgen.",
@ -3226,8 +3215,6 @@
"Token Budget": "Token-budget",
"budget": "budget",
"Recursive scanning": "Recursieve scanning",
"Soft Prompt": "Zachte prompt",
"About soft prompts": "Over zachte prompts",
"None": "Geen",
"User Settings": "Gebruikersinstellingen",
"UI Customization": "UI-aanpassing",
@ -3523,7 +3510,7 @@
"Verifies your API connection by sending a short test message. Be aware that you'll be credited for it!": "Verifieert je API-verbinding door een kort testbericht te sturen. Wees je ervan bewust dat je hiervoor wordt gecrediteerd!",
"Create New": "Nieuw aanmaken",
"Edit": "Bewerken",
"World Info & Soft Prompts": "Wereldinformatie & Zachte Prompts",
"World Info": "Wereldinformatie",
"Locked = World Editor will stay open": "Vergrendeld = Wereld Editor blijft open",
"Entries can activate other entries by mentioning their keywords": "Invoeren kunnen andere invoeren activeren door hun trefwoorden te noemen",
"Lookup for the entry keys in the context will respect the case": "Zoeken naar de toetsen van de invoer in de context zal de hoofdlettergevoeligheid respecteren",
@ -3727,7 +3714,7 @@
"Novel AI Model": "Modelo IA de NovelAI",
"No connection": "Desconectado",
"oobabooga/text-generation-webui": "oobabooga/text-generation-webui",
"Make sure you run it with": "Asegúrate de usar el argumento --api cuando se ejecute",
"Make sure you run it with": "Asegúrate de usar el argumento --extensions openai cuando se ejecute",
"Blocking API url": "API URL",
"Streaming API url": "Streaming API URL",
"to get your OpenAI API key.": "para conseguir tu clave API de OpenAI",

71
public/img/aphrodite.svg Normal file
View File

@ -0,0 +1,71 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
viewBox="0 0 500 500"
version="1.1"
id="svg6"
sodipodi:docname="aphrodite.svg"
inkscape:version="1.3 (0e150ed, 2023-07-21)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs6" />
<sodipodi:namedview
id="namedview6"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="0.472"
inkscape:cx="251.05932"
inkscape:cy="250"
inkscape:window-width="1280"
inkscape:window-height="449"
inkscape:window-x="0"
inkscape:window-y="25"
inkscape:window-maximized="0"
inkscape:current-layer="svg6" />
<g
transform="matrix(1.3637143,0,0,1.2306337,286.98714,309.0439)"
id="b08450db-4034-4e8d-9232-9d086fc10fd0" />
<g
transform="matrix(1.3637143,0,0,1.2306337,286.98714,309.0439)"
id="54daa6c1-4b17-4e19-b0bb-42d1bcbfe659" />
<g
transform="matrix(1.3637143,0,0,1.2306337,186.0314,431.30731)"
id="g2" />
<g
transform="matrix(1.3637143,0,0,1.2306337,288.29633,320.27957)"
id="g3" />
<g
transform="matrix(1.686936,0,0,1.507445,388.05263,106.65182)"
id="g6"
style="">
<g
id="g5"
style="">
<path
style="opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;"
vector-effect="non-scaling-stroke"
d="m -189.927,161.041 32.809,-32.022 47.368,38.876 -32.619,43.738 -87.665,49.304 z"
stroke-linecap="round"
id="path3" />
<path
style="opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;"
vector-effect="non-scaling-stroke"
d="m -64.913,42.392 32.651,28.068 -77.49,97.438 -47.367,-38.878 91.346,-87.359 z"
stroke-linecap="round"
id="path4" />
<path
style="opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;"
vector-effect="non-scaling-stroke"
d="m 46.895,-67.722 -2.202,2.004 -110.467,107.379 33.512,28.799 95.769,-121.944 0.023,-0.025 c 2.011,-2.328 2.952,-5.03 2.819,-8.105 -0.131,-3.074 -1.3,-5.686 -3.502,-7.834 -2.205,-2.148 -4.846,-3.248 -7.922,-3.3 -3.077,-0.054 -5.754,0.955 -8.03,3.026 z"
stroke-linecap="round"
id="path5" />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.8 KiB

3
public/img/mancer.svg Normal file
View File

@ -0,0 +1,3 @@
<svg width="128" height="128" viewBox="0 0 128 128" style="enable-background:new 0 0 128 128;" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<path d="M115.36,61.84L70.22,50.49L114.45,2.4c0.41-0.45,0.43-1.13,0.05-1.6c-0.39-0.48-1.07-0.59-1.59-0.27 L12.3,61.98c-0.41,0.25-0.64,0.72-0.57,1.2c0.06,0.48,0.4,0.87,0.87,1.01l45.07,13.25L13.38,125.6c-0.42,0.46-0.44,1.15-0.04,1.61 c0.24,0.29,0.58,0.44,0.94,0.44c0.22,0,0.45-0.06,0.65-0.19l100.78-63.41c0.42-0.26,0.64-0.75,0.56-1.22 C116.19,62.34,115.84,61.95,115.36,61.84z" />
</svg>

After

Width:  |  Height:  |  Size: 561 B

View File

@ -1,3 +1,3 @@
<svg width="33" height="41" viewBox="0 0 33 41" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M5.89418 31.9285C4.51814 29.6818 2.83212 27.8112 0.836131 26.521C0.26793 26.1537 0.124452 25.3382 0.540438 24.8047C4.15593 20.1672 9.79294 8.01868 12.7415 1.40215C13.181 0.416062 14.6883 0.738582 14.6883 1.81816V19.44C13.1242 20.1331 12.0332 21.6992 12.0332 23.5201C12.0332 24.1851 12.1787 24.8161 12.4397 25.383L5.89418 31.9285ZM7.34675 34.6814C8.03773 36.2042 8.61427 37.8368 9.07635 39.5334C9.19588 39.9722 9.59101 40.2824 10.0459 40.2824H16.4937H22.9416C23.3964 40.2824 23.7916 39.9722 23.9111 39.5334C24.3732 37.8368 24.9497 36.2042 25.6407 34.6814L22.211 31.2516L19.3551 34.1075C19.4281 34.3655 19.4672 34.6378 19.4672 34.9192C19.4672 36.5615 18.1358 37.8928 16.4935 37.8928C14.8512 37.8928 13.5198 36.5615 13.5198 34.9192C13.5198 33.2768 14.8512 31.9455 16.4935 31.9455C16.7448 31.9455 16.9888 31.9766 17.2219 32.0353L20.1083 29.1489L18.4762 27.5169C17.879 27.8137 17.2058 27.9806 16.4937 27.9806C15.7816 27.9806 15.1084 27.8137 14.5112 27.5169L7.34675 34.6814ZM27.0933 31.9285C28.4693 29.6818 30.1553 27.8112 32.1513 26.521C32.7195 26.1537 32.863 25.3382 32.447 24.8047C28.8315 20.1672 23.1945 8.01868 20.2459 1.40215C19.8065 0.416062 18.2992 0.738582 18.2992 1.81816V19.44C19.8632 20.1332 20.9542 21.6992 20.9542 23.5201C20.9542 24.1851 20.8087 24.8161 20.5478 25.383L27.0933 31.9285Z" fill="white"/>
<svg width="33" height="41" viewBox="0 0 33 41" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M5.89418 31.9285C4.51814 29.6818 2.83212 27.8112 0.836131 26.521C0.26793 26.1537 0.124452 25.3382 0.540438 24.8047C4.15593 20.1672 9.79294 8.01868 12.7415 1.40215C13.181 0.416062 14.6883 0.738582 14.6883 1.81816V19.44C13.1242 20.1331 12.0332 21.6992 12.0332 23.5201C12.0332 24.1851 12.1787 24.8161 12.4397 25.383L5.89418 31.9285ZM7.34675 34.6814C8.03773 36.2042 8.61427 37.8368 9.07635 39.5334C9.19588 39.9722 9.59101 40.2824 10.0459 40.2824H16.4937H22.9416C23.3964 40.2824 23.7916 39.9722 23.9111 39.5334C24.3732 37.8368 24.9497 36.2042 25.6407 34.6814L22.211 31.2516L19.3551 34.1075C19.4281 34.3655 19.4672 34.6378 19.4672 34.9192C19.4672 36.5615 18.1358 37.8928 16.4935 37.8928C14.8512 37.8928 13.5198 36.5615 13.5198 34.9192C13.5198 33.2768 14.8512 31.9455 16.4935 31.9455C16.7448 31.9455 16.9888 31.9766 17.2219 32.0353L20.1083 29.1489L18.4762 27.5169C17.879 27.8137 17.2058 27.9806 16.4937 27.9806C15.7816 27.9806 15.1084 27.8137 14.5112 27.5169L7.34675 34.6814ZM27.0933 31.9285C28.4693 29.6818 30.1553 27.8112 32.1513 26.521C32.7195 26.1537 32.863 25.3382 32.447 24.8047C28.8315 20.1672 23.1945 8.01868 20.2459 1.40215C19.8065 0.416062 18.2992 0.738582 18.2992 1.81816V19.44C19.8632 20.1332 20.9542 21.6992 20.9542 23.5201C20.9542 24.1851 20.8087 24.8161 20.5478 25.383L27.0933 31.9285Z" />
</svg>

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

File diff suppressed because it is too large Load Diff

30
public/manifest.json Normal file
View File

@ -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"
}
]
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,652 @@
"use strict";
import {
callPopup,
characters,
deleteCharacter,
event_types,
eventSource,
getCharacters,
getRequestHeaders,
printCharacters,
this_chid
} from "../script.js";
import { favsToHotswap } from "./RossAscends-mods.js";
import { hideLoader, showLoader } from "./loader.js";
import { convertCharacterToPersona } from "./personas.js";
import { createTagInput, getTagKeyForCharacter, tag_map } from "./tags.js";
// Utility object for popup messages.
const popupMessage = {
deleteChat(characterCount) {
return `<h3>Delete ${characterCount} characters?</h3>
<b>THIS IS PERMANENT!<br><br>
<label for="del_char_checkbox" class="checkbox_label justifyCenter">
<input type="checkbox" id="del_char_checkbox" />
<span>Also delete the chat files</span>
</label><br></b>`;
},
}
/**
* Static object representing the actions of the
* character context menu override.
*/
class CharacterContextMenu {
/**
* Tag one or more characters,
* opens a popup.
*
* @param selectedCharacters
*/
static tag = (selectedCharacters) => {
BulkTagPopupHandler.show(selectedCharacters);
}
/**
* Duplicate one or more characters
*
* @param characterId
* @returns {Promise<Response>}
*/
static duplicate = async (characterId) => {
const character = CharacterContextMenu.#getCharacter(characterId);
return fetch('/dupecharacter', {
method: 'POST',
headers: getRequestHeaders(),
body: JSON.stringify({ avatar_url: character.avatar }),
});
}
/**
* Favorite a character
* and highlight it.
*
* @param characterId
* @returns {Promise<void>}
*/
static favorite = async (characterId) => {
const character = CharacterContextMenu.#getCharacter(characterId);
// Only set fav for V2 spec
const data = {
name: character.name,
avatar: character.avatar,
data: {
extensions: {
fav: !character.data.extensions.fav
}
}
};
return fetch('/v2/editcharacterattribute', {
method: "POST",
headers: getRequestHeaders(),
body: JSON.stringify(data),
}).then((response) => {
if (response.ok) {
const element = document.getElementById(`CharID${characterId}`);
element.classList.toggle('is_fav');
} else {
response.json().then(json => toastr.error('Character not saved. Error: ' + json.message + '. Field: ' + json.error));
}
});
}
/**
* Convert one or more characters to persona,
* may open a popup for one or more characters.
*
* @param characterId
* @returns {Promise<void>}
*/
static persona = async (characterId) => await convertCharacterToPersona(characterId);
/**
* Delete one or more characters,
* opens a popup.
*
* @param characterId
* @param deleteChats
* @returns {Promise<void>}
*/
static delete = async (characterId, deleteChats = false) => {
const character = CharacterContextMenu.#getCharacter(characterId);
return fetch('/deletecharacter', {
method: 'POST',
headers: getRequestHeaders(),
body: JSON.stringify({ avatar_url: character.avatar, delete_chats: deleteChats }),
cache: 'no-cache',
}).then(response => {
if (response.ok) {
deleteCharacter(character.name, character.avatar).then(() => {
if (deleteChats) {
fetch("/getallchatsofcharacter", {
method: 'POST',
body: JSON.stringify({ avatar_url: character.avatar }),
headers: getRequestHeaders(),
}).then((response) => {
let data = response.json();
data = Object.values(data);
const pastChats = data.sort((a, b) => a["file_name"].localeCompare(b["file_name"])).reverse();
for (const chat of pastChats) {
const name = chat.file_name.replace('.jsonl', '');
eventSource.emit(event_types.CHAT_DELETED, name);
}
});
}
})
}
eventSource.emit('characterDeleted', { id: this_chid, character: characters[this_chid] });
});
}
static #getCharacter = (characterId) => characters[characterId] ?? null;
/**
* Show the context menu at the given position
*
* @param positionX
* @param positionY
*/
static show = (positionX, positionY) => {
let contextMenu = document.getElementById(BulkEditOverlay.contextMenuId);
contextMenu.style.left = `${positionX}px`;
contextMenu.style.top = `${positionY}px`;
document.getElementById(BulkEditOverlay.contextMenuId).classList.remove('hidden');
// Adjust position if context menu is outside of viewport
const boundingRect = contextMenu.getBoundingClientRect();
if (boundingRect.right > window.innerWidth) {
contextMenu.style.left = `${positionX - (boundingRect.right - window.innerWidth)}px`;
}
if (boundingRect.bottom > window.innerHeight) {
contextMenu.style.top = `${positionY - (boundingRect.bottom - window.innerHeight)}px`;
}
}
/**
* Hide the context menu
*/
static hide = () => document.getElementById(BulkEditOverlay.contextMenuId).classList.add('hidden');
/**
* Sets up the context menu for the given overlay
*
* @param characterGroupOverlay
*/
constructor(characterGroupOverlay) {
const contextMenuItems = [
{ id: 'character_context_menu_favorite', callback: characterGroupOverlay.handleContextMenuFavorite },
{ id: 'character_context_menu_duplicate', callback: characterGroupOverlay.handleContextMenuDuplicate },
{ id: 'character_context_menu_delete', callback: characterGroupOverlay.handleContextMenuDelete },
{ id: 'character_context_menu_persona', callback: characterGroupOverlay.handleContextMenuPersona },
{ id: 'character_context_menu_tag', callback: characterGroupOverlay.handleContextMenuTag }
];
contextMenuItems.forEach(contextMenuItem => document.getElementById(contextMenuItem.id).addEventListener('click', contextMenuItem.callback))
}
}
/**
* Represents a tag control not bound to a single character
*/
class BulkTagPopupHandler {
static #getHtml = (characterIds) => {
const characterData = JSON.stringify({ characterIds: characterIds });
return `<div id="bulk_tag_shadow_popup">
<div id="bulk_tag_popup">
<div id="bulk_tag_popup_holder">
<h3 class="m-b-1">Add tags to ${characterIds.length} characters</h3>
<br>
<div id="bulk_tags_div" class="marginBot5" data-characters='${characterData}'>
<div class="tag_controls">
<input id="bulkTagInput" class="text_pole tag_input wide100p margin0" data-i18n="[placeholder]Search / Create Tags" placeholder="Search / Create tags" maxlength="25" />
<div class="tags_view menu_button fa-solid fa-tags" title="View all tags" data-i18n="[title]View all tags"></div>
</div>
<div id="bulkTagList" class="m-t-1 tags"></div>
</div>
<div id="dialogue_popup_controls" class="m-t-1">
<div id="bulk_tag_popup_cancel" class="menu_button" data-i18n="Cancel">Close</div>
<div id="bulk_tag_popup_reset" class="menu_button" data-i18n="Cancel">Remove all</div>
</div>
</div>
</div>
</div>
`
};
/**
* Append and show the tag control
*
* @param characters - The characters assigned to this control
*/
static show(characters) {
document.body.insertAdjacentHTML('beforeend', this.#getHtml(characters));
createTagInput('#bulkTagInput', '#bulkTagList');
document.querySelector('#bulk_tag_popup_cancel').addEventListener('click', this.hide.bind(this));
document.querySelector('#bulk_tag_popup_reset').addEventListener('click', this.resetTags.bind(this, characters));
}
/**
* Hide and remove the tag control
*/
static hide() {
let popupElement = document.querySelector('#bulk_tag_shadow_popup');
if (popupElement) {
document.body.removeChild(popupElement);
}
printCharacters(true);
}
/**
* Empty the tag map for the given characters
*
* @param characterIds
*/
static resetTags(characterIds) {
characterIds.forEach((characterId) => {
const key = getTagKeyForCharacter(characterId);
if (key) tag_map[key] = [];
});
printCharacters(true);
}
}
class BulkEditOverlayState {
/**
*
* @type {number}
*/
static browse = 0;
/**
*
* @type {number}
*/
static select = 1;
}
/**
* Implement a SingletonPattern, allowing access to the group overlay instance
* from everywhere via (new CharacterGroupOverlay())
*
* @type BulkEditOverlay
*/
let bulkEditOverlayInstance = null;
class BulkEditOverlay {
static containerId = 'rm_print_characters_block';
static contextMenuId = 'character_context_menu';
static characterClass = 'character_select';
static groupClass = 'group_select';
static bogusFolderClass = 'bogus_folder_select';
static selectModeClass = 'group_overlay_mode_select';
static selectedClass = 'character_selected';
static legacySelectedClass = 'bulk_select_checkbox';
static longPressDelay = 2500;
#state = BulkEditOverlayState.browse;
#longPress = false;
#stateChangeCallbacks = [];
#selectedCharacters = [];
/**
* Locks other pointer actions when the context menu is open
*
* @type {boolean}
*/
#contextMenuOpen = false;
/**
* Whether the next character select should be skipped
*
* @type {boolean}
*/
#cancelNextToggle = false;
/**
* @type HTMLElement
*/
container = null;
get state() {
return this.#state;
}
set state(newState) {
if (this.#state === newState) return;
eventSource.emit(event_types.CHARACTER_GROUP_OVERLAY_STATE_CHANGE_BEFORE, newState)
.then(() => {
this.#state = newState;
eventSource.emit(event_types.CHARACTER_GROUP_OVERLAY_STATE_CHANGE_AFTER, this.state)
});
}
get isLongPress() {
return this.#longPress;
}
set isLongPress(longPress) {
this.#longPress = longPress;
}
get stateChangeCallbacks() {
return this.#stateChangeCallbacks;
}
/**
*
* @returns {*[]}
*/
get selectedCharacters() {
return this.#selectedCharacters;
}
constructor() {
if (bulkEditOverlayInstance instanceof BulkEditOverlay)
return bulkEditOverlayInstance
this.container = document.getElementById(BulkEditOverlay.containerId);
eventSource.on(event_types.CHARACTER_GROUP_OVERLAY_STATE_CHANGE_AFTER, this.handleStateChange);
bulkEditOverlayInstance = Object.freeze(this);
}
/**
* Set the overlay to browse mode
*/
browseState = () => this.state = BulkEditOverlayState.browse;
/**
* Set the overlay to select mode
*/
selectState = () => this.state = BulkEditOverlayState.select;
/**
* Set up a Sortable grid for the loaded page
*/
onPageLoad = () => {
this.browseState();
const elements = this.#getEnabledElements();
elements.forEach(element => element.addEventListener('touchstart', this.handleHold));
elements.forEach(element => element.addEventListener('mousedown', this.handleHold));
elements.forEach(element => element.addEventListener('contextmenu', this.handleDefaultContextMenu));
elements.forEach(element => element.addEventListener('touchend', this.handleLongPressEnd));
elements.forEach(element => element.addEventListener('mouseup', this.handleLongPressEnd));
elements.forEach(element => element.addEventListener('dragend', this.handleLongPressEnd));
elements.forEach(element => element.addEventListener('touchmove', this.handleLongPressEnd));
// Cohee: It only triggers when clicking on a margin between the elements?
// Feel free to fix or remove this, I'm not sure how to.
//this.container.addEventListener('click', this.handleCancelClick);
}
/**
* Handle state changes
*
*
*/
handleStateChange = () => {
switch (this.state) {
case BulkEditOverlayState.browse:
this.container.classList.remove(BulkEditOverlay.selectModeClass);
this.#contextMenuOpen = false;
this.#enableClickEventsForCharacters();
this.#enableClickEventsForGroups();
this.clearSelectedCharacters();
this.disableContextMenu();
this.#disableBulkEditButtonHighlight();
CharacterContextMenu.hide();
break;
case BulkEditOverlayState.select:
this.container.classList.add(BulkEditOverlay.selectModeClass);
this.#disableClickEventsForCharacters();
this.#disableClickEventsForGroups();
this.enableContextMenu();
this.#enableBulkEditButtonHighlight();
break;
}
this.stateChangeCallbacks.forEach(callback => callback(this.state));
}
/**
* Block the browsers native context menu and
* set a click event to hide the custom context menu.
*/
enableContextMenu = () => {
this.container.addEventListener('contextmenu', this.handleContextMenuShow);
document.addEventListener('click', this.handleContextMenuHide);
}
/**
* Remove event listeners, allowing the native browser context
* menu to be opened.
*/
disableContextMenu = () => {
this.container.removeEventListener('contextmenu', this.handleContextMenuShow);
document.removeEventListener('click', this.handleContextMenuHide);
}
handleDefaultContextMenu = (event) => {
if (this.isLongPress) {
event.preventDefault();
event.stopPropagation();
return false;
}
}
/**
* Opens menu on long-press.
*
* @param event - Pointer event
*/
handleHold = (event) => {
if (0 !== event.button && event.type !== 'touchstart') return;
if (this.#contextMenuOpen) {
this.#contextMenuOpen = false;
this.#cancelNextToggle = true;
CharacterContextMenu.hide();
return;
}
let cancel = false;
const cancelHold = (event) => cancel = true;
this.container.addEventListener('mouseup', cancelHold);
this.container.addEventListener('touchend', cancelHold);
this.isLongPress = true;
setTimeout(() => {
if (this.isLongPress && !cancel) {
if (this.state === BulkEditOverlayState.browse) {
this.selectState();
} else if (this.state === BulkEditOverlayState.select) {
this.#contextMenuOpen = true;
CharacterContextMenu.show(...this.#getContextMenuPosition(event));
}
}
this.container.removeEventListener('mouseup', cancelHold);
this.container.removeEventListener('touchend', cancelHold);
},
BulkEditOverlay.longPressDelay);
}
handleLongPressEnd = (event) => {
this.isLongPress = false;
if (this.#contextMenuOpen) event.stopPropagation();
}
handleCancelClick = () => {
if (false === this.#contextMenuOpen) this.state = BulkEditOverlayState.browse;
this.#contextMenuOpen = false;
}
/**
* Returns the position of the mouse/touch location
*
* @param event
* @returns {(boolean|number|*)[]}
*/
#getContextMenuPosition = (event) => [
event.clientX || event.touches[0].clientX,
event.clientY || event.touches[0].clientY,
];
#stopEventPropagation = (event) => {
if (this.#contextMenuOpen) {
this.handleContextMenuHide(event);
}
event.stopPropagation();
}
#enableClickEventsForGroups = () => this.#getDisabledElements().forEach((element) => element.removeEventListener('click', this.#stopEventPropagation));
#disableClickEventsForGroups = () => this.#getDisabledElements().forEach((element) => element.addEventListener('click', this.#stopEventPropagation));
#enableClickEventsForCharacters = () => this.#getEnabledElements().forEach(element => element.removeEventListener('click', this.toggleCharacterSelected));
#disableClickEventsForCharacters = () => this.#getEnabledElements().forEach(element => element.addEventListener('click', this.toggleCharacterSelected));
#enableBulkEditButtonHighlight = () => document.getElementById('bulkEditButton').classList.add('bulk_edit_overlay_active');
#disableBulkEditButtonHighlight = () => document.getElementById('bulkEditButton').classList.remove('bulk_edit_overlay_active');
#getEnabledElements = () => [...this.container.getElementsByClassName(BulkEditOverlay.characterClass)];
#getDisabledElements = () =>[...this.container.getElementsByClassName(BulkEditOverlay.groupClass), ...this.container.getElementsByClassName(BulkEditOverlay.bogusFolderClass)];
toggleCharacterSelected = event => {
event.stopPropagation();
const character = event.currentTarget;
const characterId = character.getAttribute('chid');
const alreadySelected = this.selectedCharacters.includes(characterId)
const legacyBulkEditCheckbox = character.querySelector('.' + BulkEditOverlay.legacySelectedClass);
// Only toggle when context menu is closed and wasn't just closed.
if (!this.#contextMenuOpen && !this.#cancelNextToggle)
if (alreadySelected) {
character.classList.remove(BulkEditOverlay.selectedClass);
if (legacyBulkEditCheckbox) legacyBulkEditCheckbox.checked = false;
this.dismissCharacter(characterId);
} else {
character.classList.add(BulkEditOverlay.selectedClass)
if (legacyBulkEditCheckbox) legacyBulkEditCheckbox.checked = true;
this.selectCharacter(characterId);
}
this.#cancelNextToggle = false;
}
handleContextMenuShow = (event) => {
event.preventDefault();
CharacterContextMenu.show(...this.#getContextMenuPosition(event));
this.#contextMenuOpen = true;
}
handleContextMenuHide = (event) => {
let contextMenu = document.getElementById(BulkEditOverlay.contextMenuId);
if (false === contextMenu.contains(event.target)) {
CharacterContextMenu.hide();
}
}
/**
* Concurrently handle character favorite requests.
*
* @returns {Promise<number>}
*/
handleContextMenuFavorite = () => Promise.all(this.selectedCharacters.map(async characterId => CharacterContextMenu.favorite(characterId)))
.then(() => getCharacters())
.then(() => favsToHotswap())
.then(() => this.browseState())
/**
* Concurrently handle character duplicate requests.
*
* @returns {Promise<number>}
*/
handleContextMenuDuplicate = () => Promise.all(this.selectedCharacters.map(async characterId => CharacterContextMenu.duplicate(characterId)))
.then(() => getCharacters())
.then(() => this.browseState())
/**
* Sequentially handle all character-to-persona conversions.
*
* @returns {Promise<void>}
*/
handleContextMenuPersona = async () => {
for (const characterId of this.selectedCharacters) {
await CharacterContextMenu.persona(characterId)
}
this.browseState();
}
/**
* Request user input before concurrently handle deletion
* requests.
*
* @returns {Promise<number>}
*/
handleContextMenuDelete = () => {
callPopup(
popupMessage.deleteChat(this.selectedCharacters.length), null)
.then((accept) => {
if (true !== accept) return;
const deleteChats = document.getElementById('del_char_checkbox').checked ?? false;
showLoader();
toastr.info("We're deleting your characters, please wait...", 'Working on it');
Promise.all(this.selectedCharacters.map(async characterId => CharacterContextMenu.delete(characterId, deleteChats)))
.then(() => getCharacters())
.then(() => this.browseState())
.finally(() => hideLoader());
}
);
}
/**
* Attaches and opens the tag menu
*/
handleContextMenuTag = () => {
CharacterContextMenu.tag(this.selectedCharacters);
}
addStateChangeCallback = callback => this.stateChangeCallbacks.push(callback);
selectCharacter = characterId => this.selectedCharacters.push(String(characterId));
dismissCharacter = characterId => this.#selectedCharacters = this.selectedCharacters.filter(item => String(characterId) !== item);
/**
* Clears internal character storage and
* removes visual highlight.
*/
clearSelectedCharacters = () => {
document.querySelectorAll('#' + BulkEditOverlay.containerId + ' .' + BulkEditOverlay.selectedClass)
.forEach(element => element.classList.remove(BulkEditOverlay.selectedClass));
this.selectedCharacters.length = 0;
}
}
export { BulkEditOverlayState, CharacterContextMenu, BulkEditOverlay };

View File

@ -2,7 +2,7 @@
import { callPopup, event_types, eventSource, is_send_press, main_api, substituteParams } from "../script.js";
import { is_group_generating } from "./group-chats.js";
import { TokenHandler } from "./openai.js";
import { Message, TokenHandler } from "./openai.js";
import { power_user } from "./power-user.js";
import { debounce, waitUntilCondition, escapeHtml } from "./utils.js";
@ -26,7 +26,7 @@ const DEFAULT_DEPTH = 4;
/**
* @enum {number}
*/
export const INJECTION_POSITION ={
export const INJECTION_POSITION = {
RELATIVE: 0,
ABSOLUTE: 1,
}
@ -179,6 +179,13 @@ class PromptCollection {
}
function PromptManagerModule() {
this.systemPrompts = [
'main',
'nsfw',
'jailbreak',
'enhanceDefinitions',
];
this.configuration = {
version: 1,
prefix: '',
@ -289,7 +296,7 @@ PromptManagerModule.prototype.init = function (moduleConfiguration, serviceSetti
this.serviceSettings = serviceSettings;
this.containerElement = document.getElementById(this.configuration.containerIdentifier);
if ('global' === this.configuration.promptOrder.strategy) this.activeCharacter = {id: this.configuration.promptOrder.dummyId};
if ('global' === this.configuration.promptOrder.strategy) this.activeCharacter = { id: this.configuration.promptOrder.dummyId };
this.sanitizeServiceSettings();
@ -397,6 +404,11 @@ PromptManagerModule.prototype.init = function (moduleConfiguration, serviceSetti
document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_prompt').value = prompt.content;
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
@ -720,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.
@ -858,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);
}
/**
@ -1105,12 +1124,19 @@ PromptManagerModule.prototype.loadPromptIntoEditForm = function (prompt) {
const promptField = document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_prompt');
const injectionPositionField = document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_injection_position');
const injectionDepthField = document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_injection_depth');
const injectionDepthBlock = document.getElementById(this.configuration.prefix + 'prompt_manager_depth_block');
nameField.value = prompt.name ?? '';
roleField.value = prompt.role ?? '';
promptField.value = prompt.content ?? '';
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) {
@ -1120,10 +1146,23 @@ PromptManagerModule.prototype.loadPromptIntoEditForm = function (prompt) {
resetPromptButton.style.display = 'none';
}
injectionPositionField.removeEventListener('change', (e) => this.handleInjectionPositionChange(e));
injectionPositionField.addEventListener('change', (e) => this.handleInjectionPositionChange(e));
const savePromptButton = document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_save');
savePromptButton.dataset.pmPrompt = prompt.identifier;
}
PromptManagerModule.prototype.handleInjectionPositionChange = function (event) {
const injectionDepthBlock = document.getElementById(this.configuration.prefix + 'prompt_manager_depth_block');
const injectionPosition = Number(event.target.value);
if (injectionPosition === INJECTION_POSITION.ABSOLUTE) {
injectionDepthBlock.style.visibility = 'visible';
} else {
injectionDepthBlock.style.visibility = 'hidden';
}
}
/**
* Loads a given prompt into the inspect form
* @param {MessageCollection} messages - Prompt object with properties 'name', 'role', 'content', and 'system_prompt'
@ -1141,12 +1180,10 @@ PromptManagerModule.prototype.loadMessagesIntoInspectForm = function (messages)
let drawerHTML = `
<div class="inline-drawer ${this.configuration.prefix}prompt_manager_prompt">
<div class="inline-drawer-toggle inline-drawer-header">
<span>Name: ${title}, Role: ${role}, Tokens: ${tokens}</span>
<span>Name: ${escapeHtml(title)}, Role: ${role}, Tokens: ${tokens}</span>
<div class="fa-solid fa-circle-chevron-down inline-drawer-icon down"></div>
</div>
<div class="inline-drawer-content">
${content}
</div>
<div class="inline-drawer-content" style="white-space: pre-wrap;">${escapeHtml(content)}</div>
</div>
`;
@ -1157,9 +1194,11 @@ PromptManagerModule.prototype.loadMessagesIntoInspectForm = function (messages)
const messageList = document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_inspect_list');
if (0 === messages.getCollection().length) messageList.innerHTML = `<span>This marker does not contain any prompts.</span>`;
const messagesCollection = messages instanceof Message ? [messages] : messages.getCollection();
messages.getCollection().forEach(message => {
if (0 === messagesCollection.length) messageList.innerHTML = `<span>This marker does not contain any prompts.</span>`;
messagesCollection.forEach(message => {
messageList.append(createInlineDrawer(message));
});
}
@ -1176,12 +1215,15 @@ PromptManagerModule.prototype.clearEditForm = function () {
const promptField = document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_prompt');
const injectionPositionField = document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_injection_position');
const injectionDepthField = document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_injection_depth');
const injectionDepthBlock = document.getElementById(this.configuration.prefix + 'prompt_manager_depth_block');
nameField.value = '';
roleField.selectedIndex = 0;
promptField.value = '';
injectionPositionField.selectedIndex = 0;
injectionPositionField.removeAttribute('disabled');
injectionDepthField.value = DEFAULT_DEPTH;
injectionDepthBlock.style.visibility = 'unset';
roleField.disabled = false;
}

View File

@ -36,6 +36,7 @@ import {
import { debounce, delay, getStringHash, isValidUrl, waitUntilCondition } from "./utils.js";
import { chat_completion_sources, oai_settings } from "./openai.js";
import { getTokenCount } from "./tokenizers.js";
import { isMancer } from "./textgen-settings.js";
var RPanelPin = document.getElementById("rm_button_panel_pin");
@ -59,9 +60,7 @@ const countTokensDebounced = debounce(RA_CountCharTokens, 1000);
const observer = new MutationObserver(function (mutations) {
mutations.forEach(function (mutation) {
if (mutation.target.id === "online_status_text2" ||
mutation.target.id === "online_status_text3" ||
mutation.target.classList.contains("online_status_text4")) {
if (mutation.target.classList.contains("online_status_text")) {
RA_checkOnlineStatus();
} else if (mutation.target.parentNode === SelectedCharacterTab) {
setTimeout(RA_CountCharTokens, 200);
@ -173,7 +172,7 @@ export function humanizedDateTime() {
let humanMillisecond =
(baseDate.getMilliseconds() < 10 ? "0" : "") + baseDate.getMilliseconds();
let HumanizedDateTime =
humanYear + "-" + humanMonth + "-" + humanDate + " @" + humanHour + "h " + humanMinute + "m " + humanSecond + "s " + humanMillisecond + "ms";
humanYear + "-" + humanMonth + "-" + humanDate + "@" + humanHour + "h" + humanMinute + "m" + humanSecond + "s";
return HumanizedDateTime;
}
@ -268,11 +267,11 @@ async function RA_autoloadchat() {
let active_character_id = Object.keys(characters).find(key => characters[key].avatar === active_character);
if (active_character_id !== null) {
selectCharacterById(String(active_character_id));
await selectCharacterById(String(active_character_id));
}
if (active_group != null) {
openGroupById(String(active_group));
await openGroupById(String(active_group));
}
// if the character list hadn't been loaded yet, try again.
@ -372,7 +371,7 @@ function RA_checkOnlineStatus() {
connection_made = false;
} else {
if (online_status !== undefined && online_status !== "no_connection") {
$("#send_textarea").attr("placeholder", `Type a message, or /? for command list`); //on connect, placeholder tells user to type message
$("#send_textarea").attr("placeholder", `Type a message, or /? for help`); //on connect, placeholder tells user to type message
$('#send_form').removeClass("no-connection");
$("#API-status-top").removeClass("fa-plug-circle-exclamation redOverlayGlow");
$("#API-status-top").addClass("fa-plug");
@ -399,17 +398,20 @@ function RA_autoconnect(PrevApi) {
switch (main_api) {
case 'kobold':
if (api_server && isValidUrl(api_server)) {
$("#api_button").click();
$("#api_button").trigger('click');
}
break;
case 'novel':
if (secret_state[SECRET_KEYS.NOVEL]) {
$("#api_button_novel").click();
$("#api_button_novel").trigger('click');
}
break;
case 'textgenerationwebui':
if (api_server_textgenerationwebui && isValidUrl(api_server_textgenerationwebui)) {
$("#api_button_textgenerationwebui").click();
if (isMancer() && secret_state[SECRET_KEYS.MANCER]) {
$("#api_button_textgenerationwebui").trigger('click');
}
else if (api_server_textgenerationwebui && isValidUrl(api_server_textgenerationwebui)) {
$("#api_button_textgenerationwebui").trigger('click');
}
break;
case 'openai':
@ -421,7 +423,7 @@ function RA_autoconnect(PrevApi) {
|| (secret_state[SECRET_KEYS.AI21] && oai_settings.chat_completion_source == chat_completion_sources.AI21)
|| (secret_state[SECRET_KEYS.PALM] && oai_settings.chat_completion_source == chat_completion_sources.PALM)
) {
$("#api_button_openai").click();
$("#api_button_openai").trigger('click');
}
break;
}
@ -429,8 +431,8 @@ function RA_autoconnect(PrevApi) {
if (!connection_made) {
RA_AC_retries++;
retry_delay = Math.min(retry_delay * 2, 30000); // double retry delay up to to 30 secs
//console.log('connection attempts: ' + RA_AC_retries + ' delay: ' + (retry_delay / 1000) + 's');
setTimeout(RA_autoconnect, retry_delay);
// console.log('connection attempts: ' + RA_AC_retries + ' delay: ' + (retry_delay / 1000) + 's');
// setTimeout(RA_autoconnect, retry_delay);
}
}
}
@ -458,6 +460,22 @@ function OpenNavPanels() {
}
}
function restoreUserInput() {
if (!power_user.restore_user_input) {
console.debug('restoreUserInput disabled');
return;
}
const userInput = LoadLocal("userInput");
if (userInput) {
$("#send_textarea").val(userInput).trigger('input');
}
}
function saveUserInput() {
const userInput = String($("#send_textarea").val());
SaveLocal("userInput", userInput);
}
// Make the DIV element draggable:
@ -893,17 +911,23 @@ export function initRossMods() {
const chatBlock = $('#chat');
const originalScrollBottom = chatBlock[0].scrollHeight - (chatBlock.scrollTop() + chatBlock.outerHeight());
this.style.height = window.getComputedStyle(this).getPropertyValue('min-height');
this.style.height = (this.scrollHeight) + 'px';
const newScrollTop = chatBlock[0].scrollHeight - (chatBlock.outerHeight() + originalScrollBottom);
this.style.height = this.scrollHeight + 0.1 + 'px';
const newScrollTop = Math.round(chatBlock[0].scrollHeight - (chatBlock.outerHeight() + originalScrollBottom));
chatBlock.scrollTop(newScrollTop);
saveUserInput();
});
restoreUserInput();
//Regenerate if user swipes on the last mesage in chat
document.addEventListener('swiped-left', function (e) {
if (power_user.gestures === false) {
return
}
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');
var SwipeTargetMesClassParent = $(e.target).closest('.last_mes');
if (SwipeTargetMesClassParent !== null) {
@ -916,6 +940,9 @@ export function initRossMods() {
if (power_user.gestures === false) {
return
}
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');
var SwipeTargetMesClassParent = $(e.target).closest('.last_mes');
if (SwipeTargetMesClassParent !== null) {

View File

@ -1,24 +1,44 @@
import { characters, getCharacters, handleDeleteCharacter, callPopup } from "../script.js";
import {BulkEditOverlay, BulkEditOverlayState} from "./BulkEditOverlay.js";
let is_bulk_edit = false;
const enableBulkEdit = () => {
enableBulkSelect();
(new BulkEditOverlay()).selectState();
// show the delete button
$("#bulkDeleteButton").show();
is_bulk_edit = true;
}
const disableBulkEdit = () => {
disableBulkSelect();
(new BulkEditOverlay()).browseState();
// hide the delete button
$("#bulkDeleteButton").hide();
is_bulk_edit = false;
}
const toggleBulkEditMode = (isBulkEdit) => {
if (isBulkEdit) {
disableBulkEdit();
} else {
enableBulkEdit();
}
}
(new BulkEditOverlay()).addStateChangeCallback((state) => {
if (state === BulkEditOverlayState.select) enableBulkEdit();
if (state === BulkEditOverlayState.browse) disableBulkEdit();
});
/**
* Toggles bulk edit mode on/off when the edit button is clicked.
*/
function onEditButtonClick() {
console.log("Edit button clicked");
// toggle bulk edit mode
if (is_bulk_edit) {
disableBulkSelect();
// hide the delete button
$("#bulkDeleteButton").hide();
is_bulk_edit = false;
} else {
enableBulkSelect();
// show the delete button
$("#bulkDeleteButton").show();
is_bulk_edit = true;
}
toggleBulkEditMode(is_bulk_edit);
}
/**

View File

@ -178,7 +178,7 @@ async function modifyCharaHtml() {
function loadSettings() {
// Set chat CFG if it exists
$('#chat_cfg_guidance_scale').val(chat_metadata[metadataKeys.guidance_scale] ?? 1.0.toFixed(2));
$('#chat_cfg_guidance_scale_counter').text(chat_metadata[metadataKeys.guidance_scale]?.toFixed(2) ?? 1.0.toFixed(2));
$('#chat_cfg_guidance_scale_counter').val(chat_metadata[metadataKeys.guidance_scale]?.toFixed(2) ?? 1.0.toFixed(2));
$('#chat_cfg_negative_prompt').val(chat_metadata[metadataKeys.negative_prompt] ?? '');
$('#chat_cfg_positive_prompt').val(chat_metadata[metadataKeys.positive_prompt] ?? '');
$('#groupchat_cfg_use_chara').prop('checked', chat_metadata[metadataKeys.groupchat_individual_chars] ?? false);
@ -211,7 +211,7 @@ function loadSettings() {
if (!selected_group) {
const charaCfg = extension_settings.cfg.chara.find((e) => e.name === getCharaFilename());
$('#chara_cfg_guidance_scale').val(charaCfg?.guidance_scale ?? 1.00);
$('#chara_cfg_guidance_scale_counter').text(charaCfg?.guidance_scale?.toFixed(2) ?? 1.0.toFixed(2));
$('#chara_cfg_guidance_scale_counter').val(charaCfg?.guidance_scale?.toFixed(2) ?? 1.0.toFixed(2));
$('#chara_cfg_negative_prompt').val(charaCfg?.negative_prompt ?? '');
$('#chara_cfg_positive_prompt').val(charaCfg?.positive_prompt ?? '');
}
@ -228,7 +228,7 @@ async function initialLoadSettings() {
// Set global CFG values on load
$('#global_cfg_guidance_scale').val(extension_settings.cfg.global.guidance_scale);
$('#global_cfg_guidance_scale_counter').text(extension_settings.cfg.global.guidance_scale.toFixed(2));
$('#global_cfg_guidance_scale_counter').val(extension_settings.cfg.global.guidance_scale.toFixed(2));
$('#global_cfg_negative_prompt').val(extension_settings.cfg.global.negative_prompt);
$('#global_cfg_positive_prompt').val(extension_settings.cfg.global.positive_prompt);
}
@ -291,7 +291,7 @@ export function initCfg() {
const numberValue = Number($(this).val());
const success = setChatCfg(numberValue, settingType.guidance_scale);
if (success) {
$('#chat_cfg_guidance_scale_counter').text(numberValue.toFixed(2));
$('#chat_cfg_guidance_scale_counter').val(numberValue.toFixed(2));
}
});
@ -307,7 +307,7 @@ export function initCfg() {
const value = $(this).val();
const success = setCharCfg(value, settingType.guidance_scale);
if (success) {
$('#chara_cfg_guidance_scale_counter').text(Number(value).toFixed(2));
$('#chara_cfg_guidance_scale_counter').val(Number(value).toFixed(2));
}
});
@ -321,7 +321,7 @@ export function initCfg() {
$('#global_cfg_guidance_scale').on('input', function() {
extension_settings.cfg.global.guidance_scale = Number($(this).val());
$('#global_cfg_guidance_scale_counter').text(extension_settings.cfg.global.guidance_scale.toFixed(2));
$('#global_cfg_guidance_scale_counter').val(extension_settings.cfg.global.guidance_scale.toFixed(2));
saveSettingsDebounced();
});

288
public/scripts/chats.js Normal file
View File

@ -0,0 +1,288 @@
// Move chat functions here from script.js (eventually)
import {
addCopyToCodeBlocks,
appendMediaToMessage,
callPopup,
chat,
eventSource,
event_types,
getCurrentChatId,
hideSwipeButtons,
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).
* @param {number} messageId Message ID
* @param {JQuery<Element>} messageBlock Message UI element
* @returns
*/
export async function hideChatMessage(messageId, messageBlock) {
const chatId = getCurrentChatId();
if (!chatId || isNaN(messageId)) return;
const message = chat[messageId];
if (!message) return;
message.is_system = true;
messageBlock.attr('is_system', String(true));
// Reload swipes. Useful when a last message is hidden.
hideSwipeButtons();
showSwipeButtons();
saveChatDebounced();
}
/**
* Mark message as visible (non-system message).
* @param {number} messageId Message ID
* @param {JQuery<Element>} messageBlock Message UI element
* @returns
*/
export async function unhideChatMessage(messageId, messageBlock) {
const chatId = getCurrentChatId();
if (!chatId || isNaN(messageId)) return;
const message = chat[messageId];
if (!message) return;
message.is_system = false;
messageBlock.attr('is_system', String(false));
// Reload swipes. Useful when a last message is hidden.
hideSwipeButtons();
showSwipeButtons();
saveChatDebounced();
}
/**
* Adds a file attachment to the message.
* @param {object} message Message object
* @returns {Promise<void>}
*/
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<boolean>} 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<void>}
*/
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 = $('<div><pre><code></code></pre></div>');
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<HTMLElement>} messageBlock
* @returns {Promise<void>}
*/
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 () {
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');
});
})

View File

@ -1,4 +1,5 @@
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 {
getContext,
@ -102,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;
@ -111,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;
@ -159,6 +160,9 @@ const extension_settings = {
rvc: {},
hypebot: {},
vectors: {},
variables: {
global: {},
},
};
let modules = [];
@ -343,27 +347,30 @@ 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.fadeOut(animation_duration);
} else {
dropdown.fadeIn(animation_duration);
}
popper.update();
});
$("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);
$("html").on('click', function (e) {
const clickTarget = $(e.target);
const noCloseTargets = ['#sd_gen', '#extensionsMenuButton'];
if (dropdown.is(':visible') && !noCloseTargets.some(id => clickTarget.closest(id).length > 0)) {
$(dropdown).fadeOut(animation_duration);
}
});
}
@ -507,8 +514,8 @@ async function generateExtensionHtml(name, manifest, isActive, isDisabled, isExt
isUpToDate = data.isUpToDate;
displayVersion = ` (${branch}-${commitHash.substring(0, 7)})`;
updateButton = isUpToDate ?
`<span class="update-button"><button class="btn_update menu_button" data-name="${name.replace('third-party', '')}" title="Up to date"><i class="fa-solid fa-code-commit"></i></button></span>` :
`<span class="update-button"><button class="btn_update menu_button" data-name="${name.replace('third-party', '')}" title="Update available"><i class="fa-solid fa-download"></i></button></span>`;
`<span class="update-button"><button class="btn_update menu_button" data-name="${name.replace('third-party', '')}" title="Up to date"><i class="fa-solid fa-code-commit fa-fw"></i></button></span>` :
`<span class="update-button"><button class="btn_update menu_button" data-name="${name.replace('third-party', '')}" title="Update available"><i class="fa-solid fa-download fa-fw"></i></button></span>`;
originHtml = `<a href="${origin}" target="_blank" rel="noopener noreferrer">`;
}
@ -579,7 +586,7 @@ async function getExtensionData(extension) {
function getModuleInformation() {
let moduleInfo = modules.length ? `<p>${DOMPurify.sanitize(modules.join(', '))}</p>` : '<p class="failure">Not connected to the API!</p>';
return `
<h3>Modules provided by your Extensions API:</h3>
<h3>Modules provided by your Extras API:</h3>
${moduleInfo}
`;
}
@ -588,35 +595,43 @@ function getModuleInformation() {
* Generates the HTML strings for all extensions and displays them in a popup.
*/
async function showExtensionsDetails() {
let htmlDefault = '<h3>Default Extensions:</h3>';
let htmlExternal = '<h3>External Extensions:</h3>';
try {
showLoader();
let htmlDefault = '<h3>Built-in Extensions:</h3>';
let htmlExternal = '<h3>Installed Extensions:</h3>';
const extensions = Object.entries(manifests).sort((a, b) => a[1].loading_order - b[1].loading_order);
const promises = [];
const extensions = Object.entries(manifests).sort((a, b) => a[1].loading_order - b[1].loading_order);
const promises = [];
for (const extension of extensions) {
promises.push(getExtensionData(extension));
}
const settledPromises = await Promise.allSettled(promises);
settledPromises.forEach(promise => {
if (promise.status === 'fulfilled') {
const { isExternal, extensionHtml } = promise.value;
if (isExternal) {
htmlExternal += extensionHtml;
} else {
htmlDefault += extensionHtml;
}
for (const extension of extensions) {
promises.push(getExtensionData(extension));
}
});
const html = `
${getModuleInformation()}
${htmlDefault}
${htmlExternal}
`;
callPopup(`<div class="extensions_info">${html}</div>`, 'text');
const settledPromises = await Promise.allSettled(promises);
settledPromises.forEach(promise => {
if (promise.status === 'fulfilled') {
const { isExternal, extensionHtml } = promise.value;
if (isExternal) {
htmlExternal += extensionHtml;
} else {
htmlDefault += extensionHtml;
}
}
});
const html = `
${getModuleInformation()}
${htmlDefault}
${htmlExternal}
`;
callPopup(`<div class="extensions_info">${html}</div>`, 'text');
} catch (error) {
toastr.error('Error loading extensions. See browser console for details.');
console.error(error);
} finally {
hideLoader();
}
}
@ -628,6 +643,7 @@ async function showExtensionsDetails() {
*/
async function onUpdateClick() {
const extensionName = $(this).data('name');
$(this).find('i').addClass('fa-spin');
await updateExtension(extensionName, false);
}
@ -645,6 +661,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');
@ -652,10 +673,6 @@ async function updateExtension(extensionName, quiet) {
} else {
toastr.success(`Extension ${extensionName} updated to ${data.shortCommitHash}`);
}
if (!quiet) {
showExtensionsDetails();
}
} catch (error) {
console.error('Error:', error);
}
@ -831,25 +848,52 @@ 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);
}
/**
* Runs the generate interceptors for all extensions.
* @param {any[]} chat Chat array
* @param {number} contextSize Context size
* @returns {Promise<boolean>} True if generation should be aborted
*/
async function runGenerationInterceptors(chat, contextSize) {
let aborted = false;
let exitImmediately = false;
const abort = (/** @type {boolean} */ immediately) => {
aborted = true;
exitImmediately = immediately;
};
for (const manifest of Object.values(manifests)) {
const interceptorKey = manifest.generate_interceptor;
if (typeof window[interceptorKey] === 'function') {
try {
await window[interceptorKey](chat, contextSize);
await window[interceptorKey](chat, contextSize, abort);
} catch (e) {
console.error(`Failed running interceptor for ${manifest.display_name}`, e);
}
}
if (exitImmediately) {
break;
}
}
return aborted;
}
jQuery(function () {

View File

@ -67,7 +67,7 @@ function downloadAssetsList(url) {
const asset = availableAssets[assetType][i];
const elemId = `assets_install_${assetType}_${i}`;
let element = $('<button />', { id: elemId, type: "button", class: "asset-download-button menu_button" })
const label = $("<i class=\"fa-solid fa-download fa-xl\"></i>");
const label = $("<i class=\"fa-fw fa-solid fa-download fa-xl\"></i>");
element.append(label);
//if (DEBUG_TONY_SAMA_FORK_MODE)

View File

@ -1,17 +1,57 @@
import { getBase64Async, saveBase64AsFile } from "../../utils.js";
import { getContext, getApiUrl, doExtrasFetch, extension_settings, modules } from "../../extensions.js";
import { callPopup, getRequestHeaders, saveSettingsDebounced } from "../../../script.js";
import { callPopup, getRequestHeaders, saveSettingsDebounced, substituteParams } from "../../../script.js";
import { getMessageTimeStamp } from "../../RossAscends-mods.js";
import { SECRET_KEYS, secret_state } from "../../secrets.js";
import { getMultimodalCaption } from "../shared.js";
export { MODULE_NAME };
const MODULE_NAME = 'caption';
const UPDATE_INTERVAL = 1000;
async function moduleWorker() {
const hasConnection = getContext().onlineStatus !== 'no_connection';
$('#send_picture').toggle(hasConnection);
const PROMPT_DEFAULT = 'Whats in this image?';
const TEMPLATE_DEFAULT = '[{{user}} sends {{char}} a picture that contains: {{caption}}]';
/**
* Migrates old extension settings to the new format.
* Must keep this function for compatibility with old settings.
*/
function migrateSettings() {
if (extension_settings.caption.local !== undefined) {
extension_settings.caption.source = extension_settings.caption.local ? 'local' : 'extras';
}
delete extension_settings.caption.local;
if (!extension_settings.caption.source) {
extension_settings.caption.source = 'extras';
}
if (extension_settings.caption.source === 'openai') {
extension_settings.caption.source = 'multimodal';
extension_settings.caption.multimodal_api = 'openai';
extension_settings.caption.multimodal_model = 'gpt-4-vision-preview';
}
if (!extension_settings.caption.multimodal_api) {
extension_settings.caption.multimodal_api = 'openai';
}
if (!extension_settings.caption.multimodal_model) {
extension_settings.caption.multimodal_model = 'gpt-4-vision-preview';
}
if (!extension_settings.caption.prompt) {
extension_settings.caption.prompt = PROMPT_DEFAULT;
}
if (!extension_settings.caption.template) {
extension_settings.caption.template = TEMPLATE_DEFAULT;
}
}
/**
* Sets an image icon for the send button.
*/
async function setImageIcon() {
try {
const sendButton = $('#send_picture .extensionsMenuExtensionButton');
@ -23,6 +63,9 @@ async function setImageIcon() {
}
}
/**
* Sets a spinner icon for the send button.
*/
async function setSpinnerIcon() {
try {
const sendButton = $('#send_picture .extensionsMenuExtensionButton');
@ -34,9 +77,21 @@ async function setSpinnerIcon() {
}
}
/**
* Sends a captioned message to the chat.
* @param {string} caption Caption text
* @param {string} image Image URL
*/
async function sendCaptionedMessage(caption, image) {
const context = getContext();
let messageText = `[${context.name1} sends ${context.name2 ?? ''} a picture that contains: ${caption}]`;
let template = extension_settings.caption.template || TEMPLATE_DEFAULT;
if (!/{{caption}}/i.test(template)) {
console.warn('Poka-yoke: Caption template does not contain {{caption}}. Appending it.')
template += ' {{caption}}';
}
let messageText = substituteParams(template).replace(/{{caption}}/i, caption);
if (extension_settings.caption.refine_mode) {
messageText = await callPopup(
@ -62,45 +117,108 @@ async function sendCaptionedMessage(caption, image) {
};
context.chat.push(message);
context.addOneMessage(message);
await context.generate('caption');
}
async function doCaptionRequest(base64Img) {
if (extension_settings.caption.local) {
const apiResult = await fetch('/api/extra/caption', {
method: 'POST',
headers: getRequestHeaders(),
body: JSON.stringify({ image: base64Img })
});
/**
* Generates a caption for an image using a selected source.
* @param {string} base64Img Base64 encoded image without the data:image/...;base64, prefix
* @param {string} fileData Base64 encoded image with the data:image/...;base64, prefix
* @returns {Promise<{caption: string}>} Generated caption
*/
async function doCaptionRequest(base64Img, fileData) {
switch (extension_settings.caption.source) {
case 'local':
return await captionLocal(base64Img);
case 'extras':
return await captionExtras(base64Img);
case 'horde':
return await captionHorde(base64Img);
case 'multimodal':
return await captionMultimodal(fileData);
default:
throw new Error('Unknown caption source.');
}
}
if (!apiResult.ok) {
throw new Error('Failed to caption image via local pipeline.');
}
const data = await apiResult.json();
return data;
} else if (modules.includes('caption')) {
const url = new URL(getApiUrl());
url.pathname = '/api/caption';
const apiResult = await doExtrasFetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Bypass-Tunnel-Reminder': 'bypass',
},
body: JSON.stringify({ image: base64Img })
});
if (!apiResult.ok) {
throw new Error('Failed to caption image via Extras.');
}
const data = await apiResult.json();
return data;
} else {
/**
* Generates a caption for an image using Extras API.
* @param {string} base64Img Base64 encoded image without the data:image/...;base64, prefix
* @returns {Promise<{caption: string}>} Generated caption
*/
async function captionExtras(base64Img) {
if (!modules.includes('caption')) {
throw new Error('No captioning module is available.');
}
const url = new URL(getApiUrl());
url.pathname = '/api/caption';
const apiResult = await doExtrasFetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Bypass-Tunnel-Reminder': 'bypass',
},
body: JSON.stringify({ image: base64Img })
});
if (!apiResult.ok) {
throw new Error('Failed to caption image via Extras.');
}
const data = await apiResult.json();
return data;
}
/**
* Generates a caption for an image using a local model.
* @param {string} base64Img Base64 encoded image without the data:image/...;base64, prefix
* @returns {Promise<{caption: string}>} Generated caption
*/
async function captionLocal(base64Img) {
const apiResult = await fetch('/api/extra/caption', {
method: 'POST',
headers: getRequestHeaders(),
body: JSON.stringify({ image: base64Img })
});
if (!apiResult.ok) {
throw new Error('Failed to caption image via local pipeline.');
}
const data = await apiResult.json();
return data;
}
/**
* Generates a caption for an image using a Horde model.
* @param {string} base64Img Base64 encoded image without the data:image/...;base64, prefix
* @returns {Promise<{caption: string}>} Generated caption
*/
async function captionHorde(base64Img) {
const apiResult = await fetch('/api/horde/caption-image', {
method: 'POST',
headers: getRequestHeaders(),
body: JSON.stringify({ image: base64Img })
});
if (!apiResult.ok) {
throw new Error('Failed to caption image via Horde.');
}
const data = await apiResult.json();
return data;
}
/**
* Generates a caption for an image using a multimodal model.
* @param {string} base64Img Base64 encoded image with the data:image/...;base64, prefix
* @returns {Promise<{caption: string}>} Generated caption
*/
async function captionMultimodal(base64Img) {
const prompt = extension_settings.caption.prompt || PROMPT_DEFAULT;
const caption = await getMultimodalCaption(base64Img, prompt);
return { caption };
}
async function onSelectImage(e) {
@ -116,11 +234,8 @@ async function onSelectImage(e) {
const fileData = await getBase64Async(file);
const base64Format = fileData.split(',')[0].split(';')[0].split('/')[1];
const base64Data = fileData.split(',')[1];
const data = await doCaptionRequest(base64Data);
const caption = data.caption;
const imageToSave = data.thumbnail ? data.thumbnail : base64Data;
const format = data.thumbnail ? 'jpeg' : base64Format;
const imagePath = await saveBase64AsFile(imageToSave, context.name2, '', format);
const { caption } = await doCaptionRequest(base64Data, fileData);
const imagePath = await saveBase64AsFile(base64Data, context.name2, '', base64Format);
await sendCaptionedMessage(caption, imagePath);
}
catch (error) {
@ -143,16 +258,26 @@ jQuery(function () {
const sendButton = $(`
<div id="send_picture" class="list-group-item flex-container flexGap5">
<div class="fa-solid fa-image extensionsMenuExtensionButton"></div>
Send a Picture
Generate Caption
</div>`);
const attachFileButton = $(`
<div id="attachFile" class="list-group-item flex-container flexGap5">
<div class="fa-solid fa-paperclip extensionsMenuExtensionButton"></div>
Attach a File
</div>`);
$('#extensionsMenu').prepend(sendButton);
$(sendButton).hide();
$('#extensionsMenu').prepend(attachFileButton);
$(sendButton).on('click', () => {
const hasCaptionModule = modules.includes('caption') || extension_settings.caption.local;
const hasCaptionModule =
(modules.includes('caption') && extension_settings.caption.source === 'extras') ||
(extension_settings.caption.source === 'multimodal' && extension_settings.caption.multimodal_api === 'openai' && secret_state[SECRET_KEYS.OPENAI]) ||
(extension_settings.caption.source === 'multimodal' && extension_settings.caption.multimodal_api === 'openrouter' && secret_state[SECRET_KEYS.OPENROUTER]) ||
extension_settings.caption.source === 'local' ||
extension_settings.caption.source === 'horde';
if (!hasCaptionModule) {
toastr.error('No captioning module is available. Either enable the local captioning pipeline or connect to Extras.');
toastr.error('Choose other captioning source in the extension settings.', 'Captioning is not available');
return;
}
@ -160,7 +285,7 @@ jQuery(function () {
});
}
function addPictureSendForm() {
const inputHtml = `<input id="img_file" type="file" accept="image/*">`;
const inputHtml = `<input id="img_file" type="file" hidden accept="image/*">`;
const imgForm = document.createElement('form');
imgForm.id = 'img_form';
$(imgForm).append(inputHtml);
@ -168,6 +293,29 @@ jQuery(function () {
$('#form_sheld').append(imgForm);
$('#img_file').on('change', onSelectImage);
}
function switchMultimodalBlocks() {
const isMultimodal = extension_settings.caption.source === 'multimodal';
$('#caption_multimodal_block').toggle(isMultimodal);
$('#caption_prompt_block').toggle(isMultimodal);
$('#caption_multimodal_api').val(extension_settings.caption.multimodal_api);
$('#caption_multimodal_model').val(extension_settings.caption.multimodal_model);
$('#caption_multimodal_model option').each(function () {
const type = $(this).data('type');
$(this).toggle(type === extension_settings.caption.multimodal_api);
});
$('#caption_multimodal_api').on('change', () => {
const api = String($('#caption_multimodal_api').val());
const model = String($(`#caption_multimodal_model option[data-type="${api}"]`).first().val());
extension_settings.caption.multimodal_api = api;
extension_settings.caption.multimodal_model = model;
saveSettingsDebounced();
switchMultimodalBlocks();
});
$('#caption_multimodal_model').on('change', () => {
extension_settings.caption.multimodal_model = String($('#caption_multimodal_model').val());
saveSettingsDebounced();
});
}
function addSettings() {
const html = `
<div class="caption_settings">
@ -177,13 +325,39 @@ jQuery(function () {
<div class="inline-drawer-icon fa-solid fa-circle-chevron-down down"></div>
</div>
<div class="inline-drawer-content">
<label class="checkbox_label" for="caption_local">
<input id="caption_local" type="checkbox" class="checkbox">
Use local captioning pipeline
</label>
<label class="checkbox_label" for="caption_refine_mode">
<label for="caption_source">Source</label>
<select id="caption_source" class="text_pole">
<option value="local">Local</option>
<option value="multimodal">Multimodal (OpenAI / OpenRouter)</option>
<option value="extras">Extras</option>
<option value="horde">Horde</option>
</select>
<div id="caption_multimodal_block" class="flex-container wide100p">
<div class="flex1 flex-container flexFlowColumn flexNoGap">
<label for="caption_multimodal_api">API</label>
<select id="caption_multimodal_api" class="flex1 text_pole">
<option value="openai">OpenAI</option>
<option value="openrouter">OpenRouter</option>
</select>
</div>
<div class="flex1 flex-container flexFlowColumn flexNoGap">
<label for="caption_multimodal_model">Model</label>
<select id="caption_multimodal_model" class="flex1 text_pole">
<option data-type="openai" value="gpt-4-vision-preview">gpt-4-vision-preview</option>
<option data-type="openrouter" value="openai/gpt-4-vision-preview">openai/gpt-4-vision-preview</option>
<option data-type="openrouter" value="haotian-liu/llava-13b">haotian-liu/llava-13b</option>
</select>
</div>
</div>
<div id="caption_prompt_block">
<label for="caption_prompt">Caption Prompt</label>
<textarea id="caption_prompt" class="text_pole" rows="1" placeholder="&lt; Use default &gt;">${PROMPT_DEFAULT}</textarea>
</div>
<label for="caption_template">Message Template <small>(use <code>{{caption}}</code> macro)</small></label>
<textarea id="caption_template" class="text_pole" rows="2" placeholder="&lt; Use default &gt;">${TEMPLATE_DEFAULT}</textarea>
<label class="checkbox_label margin-bot-10px" for="caption_refine_mode">
<input id="caption_refine_mode" type="checkbox" class="checkbox">
Edit captions before generation
Edit captions before saving
</label>
</div>
</div>
@ -196,13 +370,25 @@ jQuery(function () {
addPictureSendForm();
addSendPictureButton();
setImageIcon();
moduleWorker();
migrateSettings();
switchMultimodalBlocks();
$('#caption_refine_mode').prop('checked', !!(extension_settings.caption.refine_mode));
$('#caption_local').prop('checked', !!(extension_settings.caption.local));
$('#caption_source').val(extension_settings.caption.source);
$('#caption_prompt').val(extension_settings.caption.prompt);
$('#caption_template').val(extension_settings.caption.template);
$('#caption_refine_mode').on('input', onRefineModeInput);
$('#caption_local').on('input', () => {
extension_settings.caption.local = !!$('#caption_local').prop('checked');
$('#caption_source').on('change', () => {
extension_settings.caption.source = String($('#caption_source').val());
switchMultimodalBlocks();
saveSettingsDebounced();
});
$('#caption_prompt').on('input', () => {
extension_settings.caption.prompt = String($('#caption_prompt').val());
saveSettingsDebounced();
});
$('#caption_template').on('input', () => {
extension_settings.caption.template = String($('#caption_template').val());
saveSettingsDebounced();
});
setInterval(moduleWorker, UPDATE_INTERVAL);
});

View File

@ -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;
}
@ -208,7 +209,7 @@ async function visualNovelUpdateLayers(container) {
const containerWidth = container.width();
const pivotalPoint = containerWidth * 0.5;
let images = $('.expression-holder');
let images = $('#visual-novel-wrapper .expression-holder');
let imagesWidth = [];
images.sort(sortFunction).each(function () {
@ -1475,22 +1476,19 @@ function setExpressionOverrideHtml(forceClear = false) {
dragElement($("#expression-holder"))
eventSource.on(event_types.CHAT_CHANGED, () => {
// character changed
const context = getContext();
if (context.groupId !== lastCharacter && context.characterId !== lastCharacter) {
removeExpression();
spriteCache = {};
removeExpression();
spriteCache = {};
//clear expression
let imgElement = document.getElementById('expression-image');
if (imgElement && imgElement instanceof HTMLImageElement) {
imgElement.src = "";
}
//clear expression
let imgElement = document.getElementById('expression-image');
if (imgElement && imgElement instanceof HTMLImageElement) {
imgElement.src = "";
}
//set checkbox to global var
$('#image_type_toggle').prop('checked', extension_settings.expressions.talkinghead);
if (extension_settings.expressions.talkinghead) {
setTalkingHeadState(extension_settings.expressions.talkinghead);
}
//set checkbox to global var
$('#image_type_toggle').prop('checked', extension_settings.expressions.talkinghead);
if (extension_settings.expressions.talkinghead) {
setTalkingHeadState(extension_settings.expressions.talkinghead);
}
setExpressionOverrideHtml();

View File

@ -190,8 +190,3 @@ img.expression.default {
flex-direction: row;
}
@media screen and (max-width:1200px) {
div.expression {
display: none;
}
}

View File

@ -651,14 +651,12 @@ jQuery(function () {
<option value="main">Main API</option>
<option value="extras">Extras API</option>
</select><br>
<div class="flex-container justifyspacebetween alignitemscenter">
<span class="flex1">Current summary:</span>
<div id="memory_restore" class="menu_button flex1 margin0"><span>Restore Previous</span></div>
</div>
<textarea id="memory_contents" class="text_pole textarea_compact" rows="6" placeholder="Summary will be generated here..."></textarea>
<div class="memory_contents_controls">
<div id="memory_force_summarize" class="menu_button menu_button_icon">
@ -669,14 +667,17 @@ jQuery(function () {
<label for="memory_skipWIAN"><input id="memory_skipWIAN" type="checkbox" />No WI/AN</label>
</div>
<div class="memory_contents_controls">
<div id="summarySettingsBlockToggle" class="menu_button">Settings</div>
<div id="summarySettingsBlockToggle" class="menu_button menu_button_icon" title="Edit summarization prompt, insertion position, etc.">
<i class="fa-solid fa-cog"></i>
<span>Summary Settings</span>
</div>
</div>
<div id="summarySettingsBlock" style="display:none;">
<div id="summarySettingsBlock" style="display:none;">
<div class="memory_template">
<label for="memory_template">Insertion string:</label>
<label for="memory_template">Insertion Template</label>
<textarea id="memory_template" class="text_pole textarea_compact" rows="2" placeholder="{{summary}} will resolve to the current summary contents."></textarea>
</div>
<label for="memory_position">Position:</label>
<label for="memory_position">Injection Position</label>
<div class="radio_group">
<label>
<input type="radio" name="memory_position" value="2" />

View File

@ -0,0 +1,44 @@
<div id="quickReply_contextMenuEditor_template">
<div class="quickReply_contextMenuEditor">
<h3><strong>Context Menu Editor</strong></h3>
<div id="quickReply_contextMenuEditor_content">
<template id="quickReply_contextMenuEditor_itemTemplate">
<div class="quickReplyContextMenuEditor_item flex-container alignitemscenter" data-order="0">
<span class="drag-handle ui-sortable-handle"></span>
<select class="quickReply_contextMenuEditor_preset"></select>
<label class="flex-container" title="When enabled, the current Quick Reply will be sent together with (before) the clicked QR from the context menu.">
Chaining:
<input type="checkbox" class="quickReply_contextMenuEditor_chaining">
</label>
<span class="quickReply_contextMenuEditor_remove menu_button menu_button_icon fa-solid fa-trash-can" title="Remove entry"></span>
</div>
</template>
</div>
<div class="quickReply_contextMenuEditor_actions">
<span id="quickReply_contextMenuEditor_addPreset" class="menu_button menu_button_icon fa-solid fa-plus" title="Add preset to context menu"></span>
</div>
<h3><strong>Auto-Execute</strong></h3>
<div class="flex-container flexFlowColumn">
<label class="checkbox_label" for="quickReply_hidden">
<input type="checkbox" id="quickReply_hidden" >
<span><i class="fa-solid fa-fw fa-eye-slash"></i> Invisible (auto-execute only)</span>
</label>
<label class="checkbox_label" for="quickReply_autoExecute_appStartup">
<input type="checkbox" id="quickReply_autoExecute_appStartup" >
<span><i class="fa-solid fa-fw fa-rocket"></i> Execute on app startup</span>
</label>
<label class="checkbox_label" for="quickReply_autoExecute_userMessage">
<input type="checkbox" id="quickReply_autoExecute_userMessage" >
<span><i class="fa-solid fa-fw fa-user"></i> Execute on user message</span>
</label>
<label class="checkbox_label" for="quickReply_autoExecute_botMessage">
<input type="checkbox" id="quickReply_autoExecute_botMessage" >
<span><i class="fa-solid fa-fw fa-robot"></i> Execute on AI message</span>
</label>
<label class="checkbox_label" for="quickReply_autoExecute_chatLoad">
<input type="checkbox" id="quickReply_autoExecute_chatLoad" >
<span><i class="fa-solid fa-fw fa-message"></i> Execute on opening chat</span>
</label>
</div>
</div>
</div>

View File

@ -1,7 +1,12 @@
import { saveSettingsDebounced, callPopup, getRequestHeaders, substituteParams } from "../../../script.js";
import { saveSettingsDebounced, callPopup, getRequestHeaders, substituteParams, eventSource, event_types } from "../../../script.js";
import { getContext, extension_settings } from "../../extensions.js";
import { initScrollHeight, resetScrollHeight } from "../../utils.js";
import { registerSlashCommand } from "../../slash-commands.js";
import { initScrollHeight, resetScrollHeight, getSortableDelay } from "../../utils.js";
import { executeSlashCommands, registerSlashCommand } from "../../slash-commands.js";
import { ContextMenu } from "./src/ContextMenu.js";
import { MenuItem } from "./src/MenuItem.js";
import { MenuHeader } from "./src/MenuHeader.js";
import { loadMovingUIState } from "../../power-user.js";
import { dragElement } from "../../RossAscends-mods.js";
export { MODULE_NAME };
@ -21,7 +26,7 @@ const defaultSettings = {
//method from worldinfo
async function updateQuickReplyPresetList() {
var result = await fetch("/getsettings", {
const result = await fetch("/getsettings", {
method: "POST",
headers: getRequestHeaders(),
body: JSON.stringify({}),
@ -99,10 +104,105 @@ function onQuickReplyInput(id) {
function onQuickReplyLabelInput(id) {
extension_settings.quickReply.quickReplySlots[id - 1].label = $(`#quickReply${id}Label`).val();
$(`#quickReply${id}`).text(String($(`#quickReply${id}Label`).val()));
addQuickReplyBar();
saveSettingsDebounced();
}
async function onQuickReplyContextMenuChange(id) {
extension_settings.quickReply.quickReplySlots[id - 1].contextMenu = JSON.parse($(`#quickReplyContainer > [data-order="${id}"]`).attr('data-contextMenu'))
saveSettingsDebounced();
}
async function onQuickReplyCtxButtonClick(id) {
const editorHtml = $(await $.get('scripts/extensions/quick-reply/contextMenuEditor.html'));
const popupResult = callPopup(editorHtml, "confirm", undefined, { okButton: "Save", wide: false, large: false, rows: 1 });
const qr = extension_settings.quickReply.quickReplySlots[id - 1];
if (!qr.contextMenu) {
qr.contextMenu = [];
}
/**@type {HTMLTemplateElement}*/
const tpl = document.querySelector('#quickReply_contextMenuEditor_itemTemplate');
const fillPresetSelect = (select, item) => {
[{ name: 'Select a preset', value: '' }, ...presets].forEach(preset => {
const opt = document.createElement('option'); {
opt.value = preset.value ?? preset.name;
opt.textContent = preset.name;
opt.selected = preset.name == item.preset;
select.append(opt);
}
});
};
const addCtxItem = (item, idx) => {
const dom = tpl.content.cloneNode(true);
const ctxItem = dom.querySelector('.quickReplyContextMenuEditor_item');
ctxItem.setAttribute('data-order', idx);
const select = ctxItem.querySelector('.quickReply_contextMenuEditor_preset');
fillPresetSelect(select, item);
dom.querySelector('.quickReply_contextMenuEditor_chaining').checked = item.chain;
$('.quickReply_contextMenuEditor_remove', ctxItem).on('click', () => ctxItem.remove());
document.querySelector('#quickReply_contextMenuEditor_content').append(ctxItem);
}
[...qr.contextMenu, {}].forEach((item, idx) => {
addCtxItem(item, idx)
});
$('#quickReply_contextMenuEditor_addPreset').on('click', () => {
addCtxItem({}, document.querySelector('#quickReply_contextMenuEditor_content').children.length);
});
$('#quickReply_contextMenuEditor_content').sortable({
delay: getSortableDelay(),
stop: () => { },
});
$('#quickReply_autoExecute_userMessage').prop('checked', qr.autoExecute_userMessage ?? false);
$('#quickReply_autoExecute_botMessage').prop('checked', qr.autoExecute_botMessage ?? false);
$('#quickReply_autoExecute_chatLoad').prop('checked', qr.autoExecute_chatLoad ?? false);
$('#quickReply_autoExecute_appStartup').prop('checked', qr.autoExecute_appStartup ?? false);
$('#quickReply_hidden').prop('checked', qr.hidden ?? false);
$('#quickReply_hidden').on('input', () => {
const state = !!$('#quickReply_hidden').prop('checked');
qr.hidden = state;
saveSettingsDebounced();
});
$('#quickReply_autoExecute_appStartup').on('input', () => {
const state = !!$('#quickReply_autoExecute_appStartup').prop('checked');
qr.autoExecute_appStartup = state;
saveSettingsDebounced();
});
$('#quickReply_autoExecute_userMessage').on('input', () => {
const state = !!$('#quickReply_autoExecute_userMessage').prop('checked');
qr.autoExecute_userMessage = state;
saveSettingsDebounced();
});
$('#quickReply_autoExecute_botMessage').on('input', () => {
const state = !!$('#quickReply_autoExecute_botMessage').prop('checked');
qr.autoExecute_botMessage = state;
saveSettingsDebounced();
});
$('#quickReply_autoExecute_chatLoad').on('input', () => {
const state = !!$('#quickReply_autoExecute_chatLoad').prop('checked');
qr.autoExecute_chatLoad = state;
saveSettingsDebounced();
});
if (await popupResult) {
qr.contextMenu = Array.from(document.querySelectorAll('#quickReply_contextMenuEditor_content > .quickReplyContextMenuEditor_item'))
.map(item => ({
preset: item.querySelector('.quickReply_contextMenuEditor_preset').value,
chain: item.querySelector('.quickReply_contextMenuEditor_chaining').checked,
}))
.filter(item => item.preset);
$(`#quickReplyContainer[data-order="${id}"]`).attr('data-contextMenu', JSON.stringify(qr.contextMenu));
updateQuickReplyPreset();
onQuickReplyLabelInput(id);
}
}
async function onQuickReplyEnabledInput() {
let isEnabled = $(this).prop('checked')
extension_settings.quickReply.quickReplyEnabled = !!isEnabled;
@ -129,13 +229,32 @@ async function onAutoInputInject() {
}
async function sendQuickReply(index) {
const existingText = $("#send_textarea").val();
const prompt = extension_settings.quickReply.quickReplySlots[index]?.mes || '';
return await performQuickReply(prompt, index);
}
async function executeQuickReplyByName(name) {
if (!extension_settings.quickReply.quickReplyEnabled) {
throw new Error('Quick Reply is disabled');
}
const qr = extension_settings.quickReply.quickReplySlots.find(x => x.label == name);
if (!qr) {
throw new Error(`Quick Reply "${name}" not found`);
}
return await performQuickReply(qr.mes);
}
window['executeQuickReplyByName'] = executeQuickReplyByName;
async function performQuickReply(prompt, index) {
if (!prompt) {
console.warn(`Quick reply slot ${index} is empty! Aborting.`);
return;
}
const existingText = $("#send_textarea").val();
let newText;
@ -150,6 +269,12 @@ async function sendQuickReply(index) {
newText = `${prompt} `;
}
// the prompt starts with '/' - execute slash commands natively
if (prompt.startsWith('/')) {
const result = await executeSlashCommands(newText);
return result?.pipe;
}
newText = substituteParams(newText);
$("#send_textarea").val(newText);
@ -158,21 +283,161 @@ async function sendQuickReply(index) {
$("#send_textarea").trigger('focus');
// Only trigger send button if quickActionEnabled is not checked or
// the prompt starts with '/'
if (!extension_settings.quickReply.quickActionEnabled || prompt.startsWith('/')) {
if (!extension_settings.quickReply.quickActionEnabled) {
$("#send_but").trigger('click');
}
}
function buildContextMenu(qr, chainMes = null, hierarchy = [], labelHierarchy = []) {
const tree = {
label: qr.label,
mes: (chainMes && qr.mes ? `${chainMes} | ` : '') + qr.mes,
children: [],
};
qr.contextMenu?.forEach(ctxItem => {
let chain = ctxItem.chain;
let subName = ctxItem.preset;
const sub = presets.find(it => it.name == subName);
if (sub) {
// prevent circular references
if (hierarchy.indexOf(sub.name) == -1) {
const nextHierarchy = [...hierarchy, sub.name];
const nextLabelHierarchy = [...labelHierarchy, tree.label];
tree.children.push(new MenuHeader(sub.name));
sub.quickReplySlots.forEach(subQr => {
const subInfo = buildContextMenu(subQr, chain ? tree.mes : null, nextHierarchy, nextLabelHierarchy);
tree.children.push(new MenuItem(
subInfo.label,
subInfo.mes,
(evt) => {
evt.stopPropagation();
performQuickReply(subInfo.mes.replace(/%%parent(-\d+)?%%/g, (_, index) => {
return nextLabelHierarchy.slice(parseInt(index ?? '-1'))[0];
}));
},
subInfo.children,
));
});
}
}
});
return tree;
}
async function doQuickReplyBarPopout() {
//shared elements
const newQuickRepliesDiv = `<div id="quickReplies"></div>`
const popoutButtonClone = $("#quickReplyPopoutButton")
if ($("#quickReplyBarPopout").length === 0) {
console.debug('did not see popout yet, creating')
const template = $('#zoomed_avatar_template').html();
const controlBarHtml = `<div class="panelControlBar flex-container">
<div id="quickReplyBarPopoutheader" class="fa-solid fa-grip drag-grabber hoverglow"></div>
<div id="quickReplyBarPopoutClose" class="fa-solid fa-circle-xmark hoverglow"></div>
</div>`
const newElement = $(template);
let quickRepliesClone = $('#quickReplies').html()
newElement.attr('id', 'quickReplyBarPopout')
.removeClass('zoomed_avatar')
.addClass('draggable scrollY')
.empty()
.append(controlBarHtml)
.append(newQuickRepliesDiv)
//empty original bar
$("#quickReplyBar").empty()
//add clone in popout
$('body').append(newElement);
$("#quickReplies").append(quickRepliesClone).css('margin-top', '1em')
$('.quickReplyButton').on('click', function () {
let index = $(this).data('index');
sendQuickReply(index);
});
$('.quickReplyButton > .ctx-expander').on('click', function (evt) {
evt.stopPropagation();
let index = $(this.closest('.quickReplyButton')).data('index');
const qr = extension_settings.quickReply.quickReplySlots[index];
if (qr.contextMenu?.length) {
evt.preventDefault();
const tree = buildContextMenu(qr);
const menu = new ContextMenu(tree.children);
menu.show(evt);
}
})
$('.quickReplyButton').on('contextmenu', function (evt) {
let index = $(this).data('index');
const qr = extension_settings.quickReply.quickReplySlots[index];
if (qr.contextMenu?.length) {
evt.preventDefault();
const tree = buildContextMenu(qr);
const menu = new ContextMenu(tree.children);
menu.show(evt);
}
});
loadMovingUIState();
$("#quickReplyBarPopout").fadeIn(250)
dragElement(newElement)
$('#quickReplyBarPopoutClose').off('click').on('click', function () {
console.debug('saw existing popout, removing')
let quickRepliesClone = $('#quickReplies').html()
$("#quickReplyBar").append(newQuickRepliesDiv)
$("#quickReplies").prepend(quickRepliesClone)
$("#quickReplyBar").append(popoutButtonClone).fadeIn(250)
$("#quickReplyBarPopout").fadeOut(250, () => { $("#quickReplyBarPopout").remove() });
$('.quickReplyButton').on('click', function () {
let index = $(this).data('index');
sendQuickReply(index);
});
$('.quickReplyButton > .ctx-expander').on('click', function (evt) {
evt.stopPropagation();
let index = $(this.closest('.quickReplyButton')).data('index');
const qr = extension_settings.quickReply.quickReplySlots[index];
if (qr.contextMenu?.length) {
evt.preventDefault();
const tree = buildContextMenu(qr);
const menu = new ContextMenu(tree.children);
menu.show(evt);
}
})
$('.quickReplyButton').on('contextmenu', function (evt) {
let index = $(this).data('index');
const qr = extension_settings.quickReply.quickReplySlots[index];
if (qr.contextMenu?.length) {
evt.preventDefault();
const tree = buildContextMenu(qr);
const menu = new ContextMenu(tree.children);
menu.show(evt);
}
});
$("#quickReplyPopoutButton").off('click').on('click', doQuickReplyBarPopout)
})
}
}
function addQuickReplyBar() {
$('#quickReplyBar').remove();
let quickReplyButtonHtml = '';
var targetContainer;
if ($("#quickReplyBarPopout").length !== 0) {
targetContainer = 'popout'
} else {
targetContainer = 'bar'
$("#quickReplyBar").remove();
}
for (let i = 0; i < extension_settings.quickReply.numberOfSlots; i++) {
let quickReplyMes = extension_settings.quickReply.quickReplySlots[i]?.mes || '';
let quickReplyLabel = extension_settings.quickReply.quickReplySlots[i]?.label || '';
quickReplyButtonHtml += `<div title="${quickReplyMes}" class="quickReplyButton" data-index="${i}" id="quickReply${i + 1}">${quickReplyLabel}</div>`;
const qr = extension_settings.quickReply.quickReplySlots[i];
const quickReplyMes = qr?.mes || '';
const quickReplyLabel = qr?.label || '';
const hidden = qr?.hidden ?? false;
let expander = '';
if (extension_settings.quickReply.quickReplySlots[i]?.contextMenu?.length) {
expander = '<span class="ctx-expander" title="Open context menu">⋮</span>';
}
quickReplyButtonHtml += `<div title="${quickReplyMes}" class="quickReplyButton ${hidden ? 'displayNone' : ''}" data-index="${i}" id="quickReply${i + 1}">${quickReplyLabel}${expander}</div>`;
}
const quickReplyBarFullHtml = `
@ -180,15 +445,43 @@ function addQuickReplyBar() {
<div id="quickReplies">
${quickReplyButtonHtml}
</div>
<div id="quickReplyPopoutButton" class="fa-solid fa-window-restore menu_button"></div>
</div>
`;
console.log(targetContainer)
if (targetContainer === 'bar') {
$('#send_form').prepend(quickReplyBarFullHtml);
} else {
$("#quickReplies").empty().append(quickReplyButtonHtml)
}
$('#send_form').prepend(quickReplyBarFullHtml);
$('.quickReplyButton').on('click', function () {
let index = $(this).data('index');
sendQuickReply(index);
});
$("#quickReplyPopoutButton").off('click').on('click', doQuickReplyBarPopout)
$('.quickReplyButton > .ctx-expander').on('click', function (evt) {
evt.stopPropagation();
let index = $(this.closest('.quickReplyButton')).data('index');
const qr = extension_settings.quickReply.quickReplySlots[index];
if (qr.contextMenu?.length) {
evt.preventDefault();
const tree = buildContextMenu(qr);
const menu = new ContextMenu(tree.children);
menu.show(evt);
}
})
$('.quickReplyButton').on('contextmenu', function (evt) {
let index = $(this).data('index');
const qr = extension_settings.quickReply.quickReplySlots[index];
if (qr.contextMenu?.length) {
evt.preventDefault();
const tree = buildContextMenu(qr);
const menu = new ContextMenu(tree.children);
menu.show(evt);
}
});
}
async function moduleWorker() {
@ -212,7 +505,52 @@ async function saveQuickReplyPreset() {
quickReplyEnabled: extension_settings.quickReply.quickReplyEnabled,
quickReplySlots: extension_settings.quickReply.quickReplySlots,
numberOfSlots: extension_settings.quickReply.numberOfSlots,
selectedPreset: name
AutoInputInject: extension_settings.quickReply.AutoInputInject,
selectedPreset: name,
}
const response = await fetch('/savequickreply', {
method: 'POST',
headers: getRequestHeaders(),
body: JSON.stringify(quickReplyPreset)
});
if (response.ok) {
const quickReplyPresetIndex = presets.findIndex(x => x.name == name);
if (quickReplyPresetIndex == -1) {
presets.push(quickReplyPreset);
const option = document.createElement('option');
option.selected = true;
option.value = name;
option.innerText = name;
$('#quickReplyPresets').append(option);
}
else {
presets[quickReplyPresetIndex] = quickReplyPreset;
$(`#quickReplyPresets option[value="${name}"]`).prop('selected', true);
}
saveSettingsDebounced();
} else {
toastr.warning('Failed to save Quick Reply Preset.')
}
}
//just a copy of save function with the name hardcoded to currently selected preset
async function updateQuickReplyPreset() {
const name = $("#quickReplyPresets").val()
if (!name) {
return;
}
const quickReplyPreset = {
name: name,
quickReplyEnabled: extension_settings.quickReply.quickReplyEnabled,
quickReplySlots: extension_settings.quickReply.quickReplySlots,
numberOfSlots: extension_settings.quickReply.numberOfSlots,
AutoInputInject: extension_settings.quickReply.AutoInputInject,
selectedPreset: name,
}
const response = await fetch('/savequickreply', {
@ -285,10 +623,13 @@ function generateQuickReplyElements() {
let quickReplyHtml = '';
for (let i = 1; i <= extension_settings.quickReply.numberOfSlots; i++) {
let itemNumber = i + 1
quickReplyHtml += `
<div class="flex-container alignitemsflexstart">
<div class="flex-container alignitemscenter" data-order="${i}">
<span class="drag-handle ui-sortable-handle"></span>
<input class="text_pole wide30p" id="quickReply${i}Label" placeholder="(Button label)">
<textarea id="quickReply${i}Mes" placeholder="(Custom message or /command)" class="text_pole widthUnset flex1" rows="2"></textarea>
<span class="menu_button menu_button_icon" id="quickReply${i}CtxButton" title="Additional options: context menu, auto-execution"></span>
<textarea id="quickReply${i}Mes" placeholder="(Custom message or /command)" class="text_pole widthUnset flex1 autoSetHeight" rows="2"></textarea>
</div>
`;
}
@ -298,6 +639,8 @@ function generateQuickReplyElements() {
for (let i = 1; i <= extension_settings.quickReply.numberOfSlots; i++) {
$(`#quickReply${i}Mes`).on('input', function () { onQuickReplyInput(i); });
$(`#quickReply${i}Label`).on('input', function () { onQuickReplyLabelInput(i); });
$(`#quickReply${i}CtxButton`).on('click', function () { onQuickReplyCtxButtonClick(i); });
$(`#quickReplyContainer > [data-order="${i}"]`).attr('data-contextMenu', JSON.stringify(extension_settings.quickReply.quickReplySlots[i - 1]?.contextMenu ?? []));
}
$('.quickReplySettings .inline-drawer-toggle').off('click').on('click', function () {
@ -346,6 +689,120 @@ async function doQR(_, text) {
whichQR.trigger('click')
}
function saveQROrder() {
//update html-level order data to match new sort
let i = 1
$('#quickReplyContainer').children().each(function () {
$(this).attr('data-order', i)
$(this).find('input').attr('id', `quickReply${i}Label`)
$(this).find('textarea').attr('id', `quickReply${i}Mes`)
i++
});
//rebuild the extension_Settings array based on new order
i = 1
$('#quickReplyContainer').children().each(function () {
onQuickReplyContextMenuChange(i)
onQuickReplyLabelInput(i)
onQuickReplyInput(i)
i++
});
}
let onMessageSentExecuting = false;
let onMessageReceivedExecuting = false;
let onChatChangedExecuting = false;
/**
* Executes quick replies on message received.
* @param {number} index New message index
* @returns {Promise<void>}
*/
async function onMessageReceived(index) {
if (!extension_settings.quickReply.quickReplyEnabled) return;
if (onMessageReceivedExecuting) return;
try {
onMessageReceivedExecuting = true;
for (let i = 0; i < extension_settings.quickReply.numberOfSlots; i++) {
const qr = extension_settings.quickReply.quickReplySlots[i];
if (qr?.autoExecute_botMessage) {
const message = getContext().chat[index];
if (message?.mes && message?.mes !== '...') {
await sendQuickReply(i);
}
}
}
} finally {
onMessageReceivedExecuting = false;
}
}
/**
* Executes quick replies on message sent.
* @param {number} index New message index
* @returns {Promise<void>}
*/
async function onMessageSent(index) {
if (!extension_settings.quickReply.quickReplyEnabled) return;
if (onMessageSentExecuting) return;
try {
onMessageSentExecuting = true;
for (let i = 0; i < extension_settings.quickReply.numberOfSlots; i++) {
const qr = extension_settings.quickReply.quickReplySlots[i];
if (qr?.autoExecute_userMessage) {
const message = getContext().chat[index];
if (message?.mes && message?.mes !== '...') {
await sendQuickReply(i);
}
}
}
} finally {
onMessageSentExecuting = false;
}
}
/**
* Executes quick replies on chat changed.
* @param {string} chatId New chat id
* @returns {Promise<void>}
*/
async function onChatChanged(chatId) {
if (!extension_settings.quickReply.quickReplyEnabled) return;
if (onChatChangedExecuting) return;
try {
onChatChangedExecuting = true;
for (let i = 0; i < extension_settings.quickReply.numberOfSlots; i++) {
const qr = extension_settings.quickReply.quickReplySlots[i];
if (qr?.autoExecute_chatLoad && chatId) {
await sendQuickReply(i);
}
}
} finally {
onChatChangedExecuting = false;
}
}
/**
* Executes quick replies on app ready.
* @returns {Promise<void>}
*/
async function onAppReady() {
if (!extension_settings.quickReply.quickReplyEnabled) return;
for (let i = 0; i < extension_settings.quickReply.numberOfSlots; i++) {
const qr = extension_settings.quickReply.quickReplySlots[i];
if (qr?.autoExecute_appStartup) {
await sendQuickReply(i);
}
}
}
jQuery(async () => {
moduleWorker();
setInterval(moduleWorker, UPDATE_INTERVAL);
@ -380,7 +837,10 @@ jQuery(async () => {
</select>
<div id="quickReplyPresetSaveButton" class="menu_button menu_button_icon">
<div class="fa-solid fa-save"></div>
<span>Save</span>
<span>Save New</span>
</div>
<div id="quickReplyPresetUpdateButton" class="menu_button menu_button_icon">
<span>Update</span>
</div>
</div>
<label for="quickReplyNumberOfSlots">Number of slots:</label>
@ -407,6 +867,12 @@ jQuery(async () => {
$('#quickReplyEnabled').on('input', onQuickReplyEnabledInput);
$('#quickReplyNumberOfSlotsApply').on('click', onQuickReplyNumberOfSlotsInput);
$("#quickReplyPresetSaveButton").on('click', saveQuickReplyPreset);
$("#quickReplyPresetUpdateButton").on('click', updateQuickReplyPreset);
$('#quickReplyContainer').sortable({
delay: getSortableDelay(),
stop: saveQROrder,
});
$("#quickReplyPresets").on('change', async function () {
const quickReplyPresetSelected = $(this).find(':selected').val();
@ -417,6 +883,11 @@ jQuery(async () => {
await loadSettings('init');
addQuickReplyBar();
eventSource.on(event_types.MESSAGE_RECEIVED, onMessageReceived);
eventSource.on(event_types.MESSAGE_SENT, onMessageSent);
eventSource.on(event_types.CHAT_CHANGED, onChatChanged);
eventSource.on(event_types.APP_READY, onAppReady);
});
jQuery(() => {

View File

@ -0,0 +1,65 @@
import { MenuItem } from "./MenuItem.js";
export class ContextMenu {
/**@type {MenuItem[]}*/ itemList = [];
/**@type {Boolean}*/ isActive = false;
/**@type {HTMLElement}*/ root;
/**@type {HTMLElement}*/ menu;
constructor(/**@type {MenuItem[]}*/items) {
this.itemList = items;
items.forEach(item => {
item.onExpand = () => {
items.filter(it => it != item)
.forEach(it => it.collapse());
};
});
}
render() {
if (!this.root) {
const blocker = document.createElement('div'); {
this.root = blocker;
blocker.classList.add('ctx-blocker');
blocker.addEventListener('click', () => this.hide());
const menu = document.createElement('ul'); {
this.menu = menu;
menu.classList.add('list-group');
menu.classList.add('ctx-menu');
this.itemList.forEach(it => menu.append(it.render()));
blocker.append(menu);
}
}
}
return this.root;
}
show({ clientX, clientY }) {
if (this.isActive) return;
this.isActive = true;
this.render();
this.menu.style.bottom = `${window.innerHeight - clientY}px`;
this.menu.style.left = `${clientX}px`;
document.body.append(this.root);
}
hide() {
if (this.root) {
this.root.remove();
}
this.isActive = false;
}
toggle(/**@type {PointerEvent}*/evt) {
if (this.isActive) {
this.hide();
} else {
this.show(evt);
}
}
}

View File

@ -0,0 +1,20 @@
import { MenuItem } from "./MenuItem.js";
export class MenuHeader extends MenuItem {
constructor(/**@type {String}*/label) {
super(label, null, null);
}
render() {
if (!this.root) {
const item = document.createElement('li'); {
this.root = item;
item.classList.add('list-group-item');
item.classList.add('ctx-header');
item.append(this.label);
}
}
return this.root;
}
}

View File

@ -0,0 +1,76 @@
import { SubMenu } from "./SubMenu.js";
export class MenuItem {
/**@type {String}*/ label;
/**@type {Object}*/ value;
/**@type {Function}*/ callback;
/**@type {MenuItem[]}*/ childList = [];
/**@type {SubMenu}*/ subMenu;
/**@type {Boolean}*/ isForceExpanded = false;
/**@type {HTMLElement}*/ root;
/**@type {Function}*/ onExpand;
constructor(/**@type {String}*/label, /**@type {Object}*/value, /**@type {function}*/callback, /**@type {MenuItem[]}*/children = []) {
this.label = label;
this.value = value;
this.callback = callback;
this.childList = children;
}
render() {
if (!this.root) {
const item = document.createElement('li'); {
this.root = item;
item.classList.add('list-group-item');
item.classList.add('ctx-item');
item.title = this.value;
if (this.callback) {
item.addEventListener('click', (evt) => this.callback(evt, this));
}
item.append(this.label);
if (this.childList.length > 0) {
item.classList.add('ctx-has-children');
const sub = new SubMenu(this.childList);
this.subMenu = sub;
const trigger = document.createElement('div'); {
trigger.classList.add('ctx-expander');
trigger.textContent = '⋮';
trigger.addEventListener('click', (evt) => {
evt.stopPropagation();
this.toggle();
});
item.append(trigger);
}
item.addEventListener('mouseover', () => sub.show(item));
item.addEventListener('mouseleave', () => sub.hide());
}
}
}
return this.root;
}
expand() {
this.subMenu?.show(this.root);
if (this.onExpand) {
this.onExpand();
}
}
collapse() {
this.subMenu?.hide();
}
toggle() {
if (this.subMenu.isActive) {
this.expand();
} else {
this.collapse();
}
}
}

View File

@ -0,0 +1,64 @@
import { MenuItem } from "./MenuItem.js";
export class SubMenu {
/**@type {MenuItem[]}*/ itemList = [];
/**@type {Boolean}*/ isActive = false;
/**@type {HTMLElement}*/ root;
constructor(/**@type {MenuItem[]}*/items) {
this.itemList = items;
}
render() {
if (!this.root) {
const menu = document.createElement('ul'); {
this.root = menu;
menu.classList.add('list-group');
menu.classList.add('ctx-menu');
menu.classList.add('ctx-sub-menu');
this.itemList.forEach(it => menu.append(it.render()));
}
}
return this.root;
}
show(/**@type {HTMLElement}*/parent) {
if (this.isActive) return;
this.isActive = true;
this.render();
parent.append(this.root);
requestAnimationFrame(() => {
const rect = this.root.getBoundingClientRect();
console.log(window.innerHeight, rect);
if (rect.bottom > window.innerHeight - 5) {
this.root.style.top = `${window.innerHeight - 5 - rect.bottom}px`;
}
if (rect.right > window.innerWidth - 5) {
this.root.style.left = 'unset';
this.root.style.right = '100%';
}
});
}
hide() {
if (this.root) {
this.root.remove();
this.root.style.top = '';
this.root.style.left = '';
}
this.isActive = false;
}
toggle(/**@type {HTMLElement}*/parent) {
if (this.isActive) {
this.hide();
} else {
this.show(parent);
}
}
}

View File

@ -1,7 +1,9 @@
#quickReplyBar {
outline: none;
/*
padding: 5px 0;
border-bottom: 1px solid var(--SmartThemeBorderColor);
*/
margin: 0;
transition: 0.3s;
opacity: 0.7;
@ -12,7 +14,7 @@
display: none;
max-width: 100%;
overflow-x: auto;
order: 10;
order: 1;
}
#quickReplies {
@ -25,13 +27,20 @@
width: 100%;
}
#quickReplyPopoutButton {
position: absolute;
right: 5px;
top: 0px;
}
#quickReplies div {
color: var(--SmartThemeBodyColor);
background-color: var(--black50a);
border: 1px solid var(--SmartThemeBorderColor);
border-radius: 10px;
padding: 3px 5px;
width: min-content;
margin: 3px 0;
/* width: min-content; */
cursor: pointer;
transition: 0.3s;
display: flex;
@ -44,4 +53,61 @@
opacity: 1;
filter: brightness(1.2);
cursor: pointer;
}
}
.ctx-blocker {
/* backdrop-filter: blur(1px); */
/* background-color: rgba(0 0 0 / 10%); */
bottom: 0;
left: 0;
position: fixed;
right: 0;
top: 0;
z-index: 999;
}
.ctx-menu {
position: absolute;
overflow: visible;
}
.list-group .list-group-item.ctx-header {
font-weight: bold;
cursor: default;
}
.ctx-item+.ctx-header {
border-top: 1px solid;
}
.ctx-item {
position: relative;
}
.ctx-expander {
border-left: 1px solid;
margin-left: 1em;
text-align: center;
width: 2em;
}
.ctx-expander:hover {
font-weight: bold;
}
.ctx-sub-menu {
position: absolute;
top: 0;
left: 100%;
}
@media screen and (max-width: 1000px) {
.ctx-blocker {
position: absolute;
}
.list-group .list-group-item.ctx-item {
padding: 1em;
}
}

View File

@ -33,7 +33,7 @@
<small data-i18n="Replace With">Replace With</small>
</label>
<div>
<textarea
<textarea
class="regex_replace_string text_pole wide100p textarea_compact"
placeholder="Use {{match}} to include the matched text from the Find Regex"
rows="2"
@ -45,7 +45,7 @@
<small data-i18n="Trim Out">Trim Out</small>
</label>
<div>
<textarea
<textarea
class="regex_trim_strings text_pole wide100p textarea_compact"
placeholder="Globally trims any unwanted parts from a regex match before replacement. Separate each element by an enter."
rows="3"
@ -86,6 +86,10 @@
<input type="checkbox" name="only_format_display" />
<span data-i18n="Only Format Display">Only Format Display</span>
</label>
<label class="checkbox flex-container" title="Chat history won't change, only the prompt as the request is sent (on generation)">
<input type="checkbox" name="only_format_prompt"/>
<span data-i18n="Only Format Prompt (?)">Only Format Prompt (?)</span>
</label>
<label class="checkbox flex-container">
<input type="checkbox" name="run_on_edit" />
<span data-i18n="Run On Edit">Run On Edit</span>

View File

@ -24,12 +24,12 @@ function regexFromString(input) {
try {
// Parse input
var m = input.match(/(\/?)(.+)\1([a-z]*)/i);
// Invalid flags
if (m[3] && !/^(?!.*?(.).*?\1)[gmixXsuUAJ]+$/.test(m[3])) {
return RegExp(input);
}
// Create the regular expression
return new RegExp(m[2], m[3]);
} catch {
@ -38,19 +38,24 @@ function regexFromString(input) {
}
// Parent function to fetch a regexed version of a raw string
function getRegexedString(rawString, placement, { characterOverride, isMarkdown } = {}) {
function getRegexedString(rawString, placement, { characterOverride, isMarkdown, isPrompt } = {}) {
let finalString = rawString;
if (extension_settings.disabledExtensions.includes("regex") || !rawString || placement === undefined) {
return finalString;
}
extension_settings.regex.forEach((script) => {
if ((script.markdownOnly && !isMarkdown) || (!script.markdownOnly && isMarkdown)) {
return;
}
if (script.placement.includes(placement)) {
finalString = runRegexScript(script, finalString, { characterOverride });
if (
// Script applies to Markdown and input is Markdown
(script.markdownOnly && isMarkdown) ||
// Script applies to Generate and input is Generate
(script.promptOnly && isPrompt) ||
// Script applies to all cases when neither "only"s are true, but there's no need to do it when `isMarkdown`, the as source (chat history) should already be changed beforehand
(!script.markdownOnly && !script.promptOnly && !isMarkdown)
) {
if (script.placement.includes(placement)) {
finalString = runRegexScript(script, finalString, { characterOverride });
}
}
});
@ -91,7 +96,7 @@ function runRegexScript(regexScript, rawString, { characterOverride } = {}) {
const subReplaceString = substituteRegexParams(
regexScript.replaceString,
trimCapturedMatch ?? trimFencedMatch,
{
{
characterOverride,
replaceStrategy: regexScript.replaceStrategy ?? regex_replace_strategy.REPLACE
}

View File

@ -76,10 +76,27 @@ async function loadRegexScripts() {
const scriptHtml = scriptTemplate.clone();
scriptHtml.attr('id', uuidv4());
scriptHtml.find('.regex_script_name').text(script.scriptName);
scriptHtml.find('.edit_existing_regex').on('click', async function() {
scriptHtml.find('.disable_regex').prop("checked", script.disabled ?? false)
.on('input', function () {
script.disabled = !!$(this).prop("checked");
saveSettingsDebounced();
});
scriptHtml.find('.regex-toggle-on').on('click', function () {
scriptHtml.find('.disable_regex').prop("checked", true).trigger('input');
});
scriptHtml.find('.regex-toggle-off').on('click', function () {
scriptHtml.find('.disable_regex').prop("checked", false).trigger('input');
});
scriptHtml.find('.edit_existing_regex').on('click', async function () {
await onRegexEditorOpenClick(scriptHtml.attr("id"));
});
scriptHtml.find('.delete_regex').on('click', async function() {
scriptHtml.find('.delete_regex').on('click', async function () {
const confirm = await callPopup("Are you sure you want to delete this regex script?", "confirm");
if (!confirm) {
return;
}
await deleteRegexScript({ existingId: scriptHtml.attr("id") });
});
@ -113,6 +130,9 @@ async function onRegexEditorOpenClick(existingId) {
editorHtml
.find(`input[name="only_format_display"]`)
.prop("checked", existingScript.markdownOnly ?? false);
editorHtml
.find(`input[name="only_format_prompt"]`)
.prop("checked", existingScript.promptOnly ?? false);
editorHtml
.find(`input[name="run_on_edit"]`)
.prop("checked", existingScript.runOnEdit ?? false);
@ -154,7 +174,7 @@ async function onRegexEditorOpenClick(existingId) {
editorHtml
.find(`input[name="replace_position"]`)
.filter(":checked")
.map(function() { return parseInt($(this).val()) })
.map(function () { return parseInt($(this).val()) })
.get()
.filter((e) => e !== NaN) || [],
disabled:
@ -165,6 +185,10 @@ async function onRegexEditorOpenClick(existingId) {
editorHtml
.find(`input[name="only_format_display"]`)
.prop("checked"),
promptOnly:
editorHtml
.find(`input[name="only_format_prompt"]`)
.prop("checked"),
runOnEdit:
editorHtml
.find(`input[name="run_on_edit"]`)
@ -197,6 +221,7 @@ function migrateSettings() {
script.placement = script.placement.filter((e) => e !== regex_placement.MD_DISPLAY);
script.markdownOnly = true
script.promptOnly = true
performSave = true;
}
@ -231,7 +256,7 @@ jQuery(async () => {
const settingsHtml = await $.get("scripts/extensions/regex/dropdown.html");
$("#extensions_settings2").append(settingsHtml);
$("#open_regex_editor").on("click", function() {
$("#open_regex_editor").on("click", function () {
onRegexEditorOpenClick(false);
});

View File

@ -2,6 +2,11 @@
<span class="drag-handle menu-handle">&#9776;</span>
<div class="regex_script_name flexGrow overflow-hidden"></div>
<div class="flex-container flexnowrap">
<label class="checkbox flex-container" for="regex_disable">
<input type="checkbox" name="regex_disable" class="disable_regex" />
<span class="regex-toggle-on fa-solid fa-toggle-on" title="Disable script"></span>
<span class="regex-toggle-off fa-solid fa-toggle-off" title="Enable script"></span>
</label>
<div class="edit_existing_regex menu_button">
<i class="fa-solid fa-pencil"></i>
</div>

View File

@ -5,6 +5,10 @@
flex-direction: row;
}
.regex_settings .checkbox {
align-items: center;
}
.regex-script-container {
margin-top: 10px;
margin-bottom: 10px;
@ -17,4 +21,34 @@
padding: 0 5px;
margin-top: 1px;
margin-bottom: 1px;
}
}
input.disable_regex {
display: none !important;
}
.regex-toggle-off {
cursor: pointer;
opacity: 0.5;
filter: grayscale(0.5);
}
.regex-toggle-on {
cursor: pointer;
}
.disable_regex:checked ~ .regex-toggle-off {
display: block;
}
.disable_regex:checked ~ .regex-toggle-on {
display: none;
}
.disable_regex:not(:checked) ~ .regex-toggle-off {
display: none;
}
.disable_regex:not(:checked) ~ .regex-toggle-on {
display: block;
}

View File

@ -0,0 +1,46 @@
import { getRequestHeaders } from "../../script.js";
import { extension_settings } from "../extensions.js";
import { SECRET_KEYS, secret_state } from "../secrets.js";
import { createThumbnail } from "../utils.js";
/**
* Generates a caption for an image using a multimodal model.
* @param {string} base64Img Base64 encoded image
* @param {string} prompt Prompt to use for captioning
* @returns {Promise<string>} Generated caption
*/
export async function getMultimodalCaption(base64Img, prompt) {
if (extension_settings.caption.multimodal_api === 'openai' && !secret_state[SECRET_KEYS.OPENAI]) {
throw new Error('OpenAI API key is not set.');
}
if (extension_settings.caption.multimodal_api === 'openrouter' && !secret_state[SECRET_KEYS.OPENROUTER]) {
throw new Error('OpenRouter API key is not set.');
}
// OpenRouter has a payload limit of ~2MB
const base64Bytes = base64Img.length * 0.75;
const compressionLimit = 2 * 1024 * 1024;
if (extension_settings.caption.multimodal_api === 'openrouter' && base64Bytes > compressionLimit) {
const maxSide = 1024;
base64Img = await createThumbnail(base64Img, maxSide, maxSide, 'image/jpeg');
}
const apiResult = await fetch('/api/openai/caption-image', {
method: 'POST',
headers: getRequestHeaders(),
body: JSON.stringify({
image: base64Img,
prompt: prompt,
api: extension_settings.caption.multimodal_api || 'openai',
model: extension_settings.caption.multimodal_model || 'gpt-4-vision-preview',
}),
});
if (!apiResult.ok) {
throw new Error('Failed to caption image via OpenAI.');
}
const { caption } = await apiResult.json();
return caption;
}

View File

@ -0,0 +1,31 @@
<div id="sd_comfy_workflow_editor_template">
<div class="sd_comfy_workflow_editor">
<h3><strong>ComfyUI Workflow Editor: <span id="sd_comfy_workflow_editor_name"></span></strong></h3>
<div class="sd_comfy_workflow_editor_content">
<div class="flex-container flexFlowColumn sd_comfy_workflow_editor_workflow_container">
<label for="sd_comfy_workflow_editor_workflow">Workflow (JSON)</label>
<textarea id="sd_comfy_workflow_editor_workflow" class="text_pole wide100p textarea_compact flex1" placeholder="Put the ComfyUI's workflow (JSON) here and replace the variable settings with placeholders."></textarea>
</div>
<div class="sd_comfy_workflow_editor_placeholder_container">
<div>Placeholders</div>
<ul class="sd_comfy_workflow_editor_placeholder_list">
<li data-placeholder="prompt" class="sd_comfy_workflow_editor_not_found">"%prompt%"</li>
<li data-placeholder="negative_prompt" class="sd_comfy_workflow_editor_not_found">"%negative_prompt%"</li>
<li data-placeholder="model" class="sd_comfy_workflow_editor_not_found">"%model%"</li>
<li data-placeholder="vae" class="sd_comfy_workflow_editor_not_found">"%vae%"</li>
<li data-placeholder="sampler" class="sd_comfy_workflow_editor_not_found">"%sampler%"</li>
<li data-placeholder="scheduler" class="sd_comfy_workflow_editor_not_found">"%scheduler%"</li>
<li data-placeholder="steps" class="sd_comfy_workflow_editor_not_found">"%steps%"</li>
<li data-placeholder="scale" class="sd_comfy_workflow_editor_not_found">"%scale%"</li>
<li data-placeholder="width" class="sd_comfy_workflow_editor_not_found">"%width%"</li>
<li data-placeholder="height" class="sd_comfy_workflow_editor_not_found">"%height%"</li>
<li><hr></li>
<li data-placeholder="seed" class="sd_comfy_workflow_editor_not_found">
"%seed%"
<a href="javascript:;" class="notes-link"><span class="note-link-span" title="Will generate a new random seed in SillyTavern that is then used in the ComfyUI workflow.">?</span></a>
</li>
</ul>
</div>
</div>
</div>
</div>

File diff suppressed because it is too large Load Diff

View File

@ -1,10 +1,11 @@
{
"display_name": "Stable Diffusion",
"display_name": "Image Generation",
"loading_order": 10,
"requires": [],
"optional": [
"sd"
],
"generate_interceptor": "SD_ProcessTriggers",
"js": "index.js",
"css": "style.css",
"author": "Cohee#1207",

View File

@ -1,25 +1,31 @@
<div class="sd_settings">
<div class="inline-drawer">
<div class="inline-drawer-toggle inline-drawer-header">
<b>Stable Diffusion</b>
<b>
Image Generation
<a href="https://docs.sillytavern.app/extras/extensions/stable-diffusion/" class="notes-link" target="_blank">
<span class="note-link-span">?</span>
</a>
</b>
<div class="inline-drawer-icon fa-solid fa-circle-chevron-down down"></div>
</div>
<div class="inline-drawer-content">
<small><i>Use slash commands or the bottom Paintbrush button to generate images. Type <span class="monospace">/help</span> in chat for more details</i></small>
<br>
<label for="sd_refine_mode" class="checkbox_label" title="Allow to edit prompts manually before sending them to generation API">
<input id="sd_refine_mode" type="checkbox" />
Edit prompts before generation
</label>
<label for="sd_interactive_mode" class="checkbox_label" title="Automatically generate images when sending messages like 'send me a picture of cat'.">
<input id="sd_interactive_mode" type="checkbox" />
Interactive mode
</label>
<label for="sd_multimodal_captioning" class="checkbox_label" title="Use multimodal captioning to generate prompts for user and character portraits based on their avatars.">
<input id="sd_multimodal_captioning" type="checkbox" />
Use multimodal captioning for portraits
</label>
<label for="sd_expand" class="checkbox_label" title="Automatically extend prompts using text generation model">
<input id="sd_expand" type="checkbox" />
Auto-enhance prompts
</label>
<small>
This option uses an additional GPT-2 text generation model to add more details to the prompt generated by the main API.
Works best for SDXL image models. May not work well with other models, it is recommended to manually edit prompts in this case.
</small>
<label for="sd_source">Source</label>
<select id="sd_source">
<option value="extras">Extras API (local / remote)</option>
@ -27,6 +33,8 @@
<option value="auto">Stable Diffusion Web UI (AUTOMATIC1111)</option>
<option value="vlad">SD.Next (vladmandic)</option>
<option value="novel">NovelAI Diffusion</option>
<option value="openai">OpenAI (DALL-E)</option>
<option value="comfy">ComfyUI</option>
</select>
<div data-sd-source="auto">
<label for="sd_auto_url">SD Web UI URL</label>
@ -94,6 +102,47 @@
</div>
<i>Hint: Save an API key in the NovelAI API settings to use it here.</i>
</div>
<div data-sd-source="openai">
<small>These settings only apply to DALL-E 3</small>
<div class="flex-container">
<label for="sd_openai_style">Image Style</label>
<select id="sd_openai_style">
<option value="vivid">Vivid</option>
<option value="natural">Natural</option>
</select>
<label for="sd_openai_quality">Image Quality</label>
<select id="sd_openai_quality">
<option value="standard">Standard</option>
<option value="hd">HD</option>
</select>
</div>
</div>
<div data-sd-source="comfy">
<label for="sd_comfy_url">ComfyUI URL</label>
<div class="flex-container flexnowrap">
<input id="sd_comfy_url" type="text" class="text_pole" placeholder="Example: {{comfy_url}}" value="{{comfy_url}}" />
<div id="sd_comfy_validate" class="menu_button menu_button_icon">
<i class="fa-solid fa-check"></i>
<span data-i18n="Connect">
Connect
</span>
</div>
</div>
<p><i><b>Important:</b> The server must be accessible from the SillyTavern host machine.</i></p>
<label for="sd_comfy_workflow">ComfyUI Workflow</label>
<div class="flex-container flexnowrap">
<select id="sd_comfy_workflow" class="flex1 text_pole"></select>
<div id="sd_comfy_open_workflow_editor" class="menu_button menu_button_icon" title="Open workflow editor">
<i class="fa-solid fa-pen-to-square"></i>
</div>
<div id="sd_comfy_new_workflow" class="menu_button menu_button_icon" title="Create new workflow">
<i class="fa-solid fa-plus"></i>
</div>
<div id="sd_comfy_delete_workflow" class="menu_button menu_button_icon" title="Delete workflow">
<i class="fa-solid fa-trash-can"></i>
</div>
</div>
</div>
<label for="sd_scale">CFG Scale (<span id="sd_scale_value"></span>)</label>
<input id="sd_scale" type="range" min="{{scale_min}}" max="{{scale_max}}" step="{{scale_step}}" value="{{scale}}" />
<label for="sd_steps">Sampling steps (<span id="sd_steps_value"></span>)</label>
@ -102,10 +151,20 @@
<input id="sd_width" type="range" max="{{dimension_max}}" min="{{dimension_min}}" step="{{dimension_step}}" value="{{width}}" />
<label for="sd_height">Height (<span id="sd_height_value"></span>)</label>
<input id="sd_height" type="range" max="{{dimension_max}}" min="{{dimension_min}}" step="{{dimension_step}}" value="{{height}}" />
<label for="sd_model">Stable Diffusion model</label>
<label for="sd_model">Model</label>
<select id="sd_model"></select>
<label for="sd_sampler">Sampling method</label>
<select id="sd_sampler"></select>
<label for="sd_resolution">Resolution</label>
<select id="sd_resolution"><!-- Populated in JS --></select>
<div data-sd-source="comfy">
<label for="sd_scheduler">Scheduler</label>
<select id="sd_scheduler"></select>
</div>
<div data-sd-source="comfy">
<label for="sd_vae">VAE</label>
<select id="sd_vae"></select>
</div>
<div class="flex-container marginTop10 margin-bot-10px">
<label class="flex1 checkbox_label">
<input id="sd_restore_faces" type="checkbox" />
@ -153,7 +212,7 @@
</div>
<div class="inline-drawer">
<div class="inline-drawer-toggle inline-drawer-header">
<b>SD Prompt Templates</b>
<b>Image Prompt Templates</b>
<div class="inline-drawer-icon fa-solid fa-circle-chevron-down down"></div>
</div>
<div id="sd_prompt_templates" class="inline-drawer-content">

View File

@ -27,3 +27,58 @@
z-index: 30000;
backdrop-filter: blur(--SmartThemeBlurStrength);
}
#sd_comfy_open_workflow_editor {
display: flex;
flex-direction: row;
gap: 10px;
width: fit-content;
}
#sd_comfy_workflow_editor_template {
height: 100%;
}
.sd_comfy_workflow_editor {
display: flex;
flex-direction: column;
height: 100%;
}
.sd_comfy_workflow_editor_content {
display: flex;
flex: 1 1 auto;
flex-direction: row;
}
.sd_comfy_workflow_editor_workflow_container {
flex: 1 1 auto;
}
#sd_comfy_workflow_editor_workflow {
font-family: monospace;
}
.sd_comfy_workflow_editor_placeholder_container {
flex: 0 0 auto;
}
.sd_comfy_workflow_editor_placeholder_list {
font-size: x-small;
list-style: none;
margin: 5px 0;
padding: 3px 5px;
text-align: left;
}
.sd_comfy_workflow_editor_placeholder_list>li[data-placeholder]:before {
content: "✅ ";
}
.sd_comfy_workflow_editor_placeholder_list>li.sd_comfy_workflow_editor_not_found:before {
content: "❌ ";
}
.sd_comfy_workflow_editor_placeholder_list>li>.notes-link {
cursor: help;
}

View File

@ -1,33 +1,119 @@
import { callPopup, main_api } from "../../../script.js";
import { getContext } from "../../extensions.js";
import { registerSlashCommand } from "../../slash-commands.js";
import { getTokenCount, getTokenizerModel } from "../../tokenizers.js";
import { getFriendlyTokenizerName, getTextTokens, getTokenCount, tokenizers } from "../../tokenizers.js";
import { resetScrollHeight } from "../../utils.js";
function rgb2hex(rgb) {
rgb = rgb.match(/^rgba?[\s+]?\([\s+]?(\d+)[\s+]?,[\s+]?(\d+)[\s+]?,[\s+]?(\d+)[\s+]?/i);
return (rgb && rgb.length === 4) ? "#" +
("0" + parseInt(rgb[1], 10).toString(16)).slice(-2) +
("0" + parseInt(rgb[2], 10).toString(16)).slice(-2) +
("0" + parseInt(rgb[3], 10).toString(16)).slice(-2) : '';
}
$('button').click(function () {
var hex = rgb2hex($('input').val());
$('.result').html(hex);
});
async function doTokenCounter() {
const selectedTokenizer = main_api == 'openai'
? `tiktoken (${getTokenizerModel()})`
: $("#tokenizer").find(':selected').text();
const { tokenizerName, tokenizerId } = getFriendlyTokenizerName(main_api);
const html = `
<div class="wide100p">
<h3>Token Counter</h3>
<div class="justifyLeft">
<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: ${selectedTokenizer}</p>
<textarea id="token_counter_textarea" class="wide100p textarea_compact margin-bot-10px" rows="20"></textarea>
<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" disabled rows="1"></textarea>
</div>
</div>`;
const dialog = $(html);
dialog.find('#token_counter_textarea').on('input', () => {
const text = $('#token_counter_textarea').val();
const context = getContext();
const count = context.getTokenCount(text);
$('#token_counter_result').text(count);
const text = String($('#token_counter_textarea').val());
const ids = main_api == 'openai' ? getTextTokens(tokenizers.OPENAI, text) : getTextTokens(tokenizerId, text);
if (Array.isArray(ids) && ids.length > 0) {
$('#token_counter_ids').text(`[${ids.join(', ')}]`);
$('#token_counter_result').text(ids.length);
if (Object.hasOwnProperty.call(ids, 'chunks')) {
drawChunks(Object.getOwnPropertyDescriptor(ids, 'chunks').value, ids);
}
} else {
const context = getContext();
const count = context.getTokenCount(text);
$('#token_counter_ids').text('—');
$('#token_counter_result').text(count);
$('#tokenized_chunks_display').text('—');
}
resetScrollHeight($('#token_counter_textarea'));
resetScrollHeight($('#token_counter_ids'));
});
$('#dialogue_popup').addClass('wide_dialogue_popup');
callPopup(dialog, 'text');
callPopup(dialog, 'text', '', { wide: true, large: true });
}
/**
* Draws the tokenized chunks in the UI
* @param {string[]} chunks
* @param {number[]} ids
*/
function drawChunks(chunks, ids) {
const main_text_color = rgb2hex((getComputedStyle(document.documentElement).getPropertyValue('--SmartThemeBodyColor').trim()))
const italics_text_color = rgb2hex((getComputedStyle(document.documentElement).getPropertyValue('--SmartThemeEmColor').trim()))
const quote_text_color = rgb2hex((getComputedStyle(document.documentElement).getPropertyValue('--SmartThemeQuoteColor').trim()))
const blur_tint_color = rgb2hex((getComputedStyle(document.documentElement).getPropertyValue('--SmartThemeBlurTintColor').trim()))
const chat_tint_color = rgb2hex((getComputedStyle(document.documentElement).getPropertyValue('--SmartThemeChatTintColor').trim()))
const user_mes_blur_tint_color = rgb2hex((getComputedStyle(document.documentElement).getPropertyValue('--SmartThemeUserMesBlurTintColor').trim()))
const bot_mes_blur_tint_color = rgb2hex((getComputedStyle(document.documentElement).getPropertyValue('--SmartThemeBotMesBlurTintColor').trim()))
const shadow_color = rgb2hex((getComputedStyle(document.documentElement).getPropertyValue('--SmartThemeShadowColor').trim()))
const border_color = rgb2hex((getComputedStyle(document.documentElement).getPropertyValue('--SmartThemeBorderColor').trim()))
const pastelRainbow = [
//main_text_color,
//italics_text_color,
//quote_text_color,
'#FFB3BA',
'#FFDFBA',
'#FFFFBA',
'#BFFFBF',
'#BAE1FF',
'#FFBAF3',
];
$('#tokenized_chunks_display').empty();
for (let i = 0; i < chunks.length; i++) {
let chunk = chunks[i].replace(/▁/g, ' '); // This is a leading space in sentencepiece. More info: Lower one eighth block (U+2581)
// If <0xHEX>, decode it
if (/^<0x[0-9A-F]+>$/i.test(chunk)) {
const code = parseInt(chunk.substring(3, chunk.length - 1), 16);
chunk = String.fromCodePoint(code);
}
// If newline - insert a line break
if (chunk === '\n') {
$('#tokenized_chunks_display').append('<br>');
continue;
}
const color = pastelRainbow[i % pastelRainbow.length];
const chunkHtml = $(`<code style="background-color: ${color};">${chunk}</code>`);
chunkHtml.attr('title', ids[i]);
$('#tokenized_chunks_display').append(chunkHtml);
}
}
function doCount() {

View File

@ -0,0 +1,6 @@
#tokenized_chunks_display > code {
color: black;
text-shadow: none;
padding: 2px;
display: inline-block;
}

View File

@ -45,6 +45,8 @@ class ElevenLabsTtsProvider {
this.settings.stability = $('#elevenlabs_tts_stability').val()
this.settings.similarity_boost = $('#elevenlabs_tts_similarity_boost').val()
this.settings.model = $('#elevenlabs_tts_model').find(':selected').val()
$('#elevenlabs_tts_stability_output').text(this.settings.stability);
$('#elevenlabs_tts_similarity_boost_output').text(this.settings.similarity_boost);
saveTtsProviderSettings()
}
@ -79,6 +81,8 @@ class ElevenLabsTtsProvider {
$('#elevenlabs_tts_similarity_boost').on('input', this.onSettingsChange.bind(this))
$('#elevenlabs_tts_stability').on('input', this.onSettingsChange.bind(this))
$('#elevenlabs_tts_model').on('change', this.onSettingsChange.bind(this))
$('#elevenlabs_tts_stability_output').text(this.settings.stability);
$('#elevenlabs_tts_similarity_boost_output').text(this.settings.similarity_boost);
try {
await this.checkReady()

View File

@ -1,4 +1,4 @@
import { callPopup, cancelTtsPlay, eventSource, event_types, saveSettingsDebounced } from '../../../script.js'
import { callPopup, cancelTtsPlay, eventSource, event_types, name2, saveSettingsDebounced } from '../../../script.js'
import { ModuleWorkerWrapper, doExtrasFetch, extension_settings, getApiUrl, getContext, modules } from '../../extensions.js'
import { escapeRegex, getStringHash } from '../../utils.js'
import { EdgeTtsProvider } from './edge.js'
@ -8,6 +8,9 @@ import { CoquiTtsProvider } from './coqui.js'
import { SystemTtsProvider } from './system.js'
import { NovelTtsProvider } from './novel.js'
import { power_user } from '../../power-user.js'
import { registerSlashCommand } from '../../slash-commands.js'
import { OpenAITtsProvider } from './openai.js'
import {XTTSTtsProvider} from "./xtts.js"
export { talkingAnimation };
const UPDATE_INTERVAL = 1000
@ -68,14 +71,18 @@ export function getPreviewString(lang) {
let ttsProviders = {
ElevenLabs: ElevenLabsTtsProvider,
Silero: SileroTtsProvider,
XTTSv2: XTTSTtsProvider,
System: SystemTtsProvider,
Coqui: CoquiTtsProvider,
Edge: EdgeTtsProvider,
Novel: NovelTtsProvider,
OpenAI: OpenAITtsProvider,
}
let ttsProvider
let ttsProviderName
let ttsLastMessage = null;
async function onNarrateOneMessage() {
audioElement.src = '/sounds/silence.mp3';
const context = getContext();
@ -91,6 +98,36 @@ async function onNarrateOneMessage() {
moduleWorker();
}
async function onNarrateText(args, text) {
if (!text) {
return;
}
audioElement.src = '/sounds/silence.mp3';
// To load all characters in the voice map, set unrestricted to true
await initVoiceMap(true);
const baseName = args?.voice || name2;
const name = (baseName === 'SillyTavern System' ? DEFAULT_VOICE_MARKER : baseName) || DEFAULT_VOICE_MARKER;
const voiceMapEntry = voiceMap[name] === DEFAULT_VOICE_MARKER
? voiceMap[DEFAULT_VOICE_MARKER]
: voiceMap[name];
if (!voiceMapEntry || voiceMapEntry === DISABLED_VOICE_MARKER) {
toastr.info(`Specified voice for ${name} was not found. Check the TTS extension settings.`);
return;
}
resetTtsPlayback()
ttsJobQueue.push({ mes: text, name: name });
await moduleWorker();
// Return back to the chat voices
await initVoiceMap(false);
}
async function moduleWorker() {
// Primarily determining when to add new chat to the TTS queue
const enabled = $('#tts_enabled').is(':checked')
@ -122,30 +159,53 @@ async function moduleWorker() {
) {
currentMessageNumber = context.chat.length ? context.chat.length : 0
saveLastValues()
// Force to speak on the first message in the new chat
if (context.chat.length === 1) {
lastMessageHash = -1;
}
return
}
// take the count of messages
let lastMessageNumber = context.chat.length ? context.chat.length : 0
let lastMessageNumber = context.chat.length ? context.chat.length : 0;
// There's no new messages
let diff = lastMessageNumber - currentMessageNumber
let hashNew = getStringHash((chat.length && chat[chat.length - 1].mes) ?? '')
let diff = lastMessageNumber - currentMessageNumber;
let hashNew = getStringHash((chat.length && chat[chat.length - 1].mes) ?? '');
if (diff == 0 && hashNew === lastMessageHash) {
return
// if messages got deleted, diff will be < 0
if (diff < 0) {
// necessary actions will be taken by the onChatDeleted() handler
return;
}
const message = chat[chat.length - 1]
// if no new messages, or same message, or same message hash, do nothing
if (diff == 0 && hashNew === lastMessageHash) {
return;
}
// We're currently swiping or streaming. Don't generate voice
if (
!message ||
message.mes === '...' ||
message.mes === '' ||
(context.streamingProcessor && !context.streamingProcessor.isFinished)
) {
return
// If streaming, wait for streaming to finish before processing new messages
if (context.streamingProcessor && !context.streamingProcessor.isFinished) {
return;
}
// clone message object, as things go haywire if message object is altered below (it's passed by reference)
const message = structuredClone(chat[chat.length - 1]);
// if last message within current message, message got extended. only send diff to TTS.
if (ttsLastMessage !== null && message.mes.indexOf(ttsLastMessage) !== -1) {
let tmp = message.mes;
message.mes = message.mes.replace(ttsLastMessage, '');
ttsLastMessage = tmp;
} else {
ttsLastMessage = message.mes;
}
// We're currently swiping. Don't generate voice
if (!message || message.mes === '...' || message.mes === '') {
return;
}
// Don't generate if message doesn't have a display text
@ -246,6 +306,7 @@ window.debugTtsPlayback = debugTtsPlayback
//##################//
let audioElement = new Audio()
audioElement.id = 'tts_audio'
audioElement.autoplay = true
let audioJobQueue = []
@ -396,7 +457,7 @@ let currentTtsJob // Null if nothing is currently being processed
let currentMessageNumber = 0
function completeTtsJob() {
console.info(`Current TTS job for ${currentTtsJob.name} completed.`)
console.info(`Current TTS job for ${currentTtsJob?.name} completed.`)
currentTtsJob = null
}
@ -441,6 +502,14 @@ async function processTtsQueue() {
const partJoiner = (ttsProvider?.separator || ' ... ');
text = matches ? matches.join(partJoiner) : text;
}
if (typeof ttsProvider?.processText === 'function') {
text = await ttsProvider.processText(text);
}
// Collapse newlines and spaces into single space
text = text.replace(/\s+/g, ' ').trim();
console.log(`TTS: ${text}`)
const char = currentTtsJob.name
@ -628,12 +697,44 @@ export function saveTtsProviderSettings() {
async function onChatChanged() {
await resetTtsPlayback()
await initVoiceMap()
ttsLastMessage = null
}
function getCharacters(){
async function onChatDeleted() {
const context = getContext()
// update internal references to new last message
lastChatId = context.chatId
currentMessageNumber = context.chat.length ? context.chat.length : 0
// compare against lastMessageHash. If it's the same, we did not delete the last chat item, so no need to reset tts queue
let messageHash = getStringHash((context.chat.length && context.chat[context.chat.length - 1].mes) ?? '')
if (messageHash === lastMessageHash) {
return
}
lastMessageHash = messageHash
ttsLastMessage = (context.chat.length && context.chat[context.chat.length - 1].mes) ?? '';
// stop any tts playback since message might not exist anymore
await resetTtsPlayback()
}
/**
* Get characters in current chat
* @param {boolean} unrestricted - If true, will include all characters in voiceMapEntries, even if they are not in the current chat.
* @returns {string[]} - Array of character names
*/
function getCharacters(unrestricted) {
const context = getContext()
if (unrestricted) {
const names = context.characters.map(char => char.name);
names.unshift(DEFAULT_VOICE_MARKER);
return names;
}
let characters = []
if (context.groupId === null){
if (context.groupId === null) {
// Single char chat
characters.push(DEFAULT_VOICE_MARKER)
characters.push(context.name1)
@ -645,7 +746,7 @@ function getCharacters(){
const group = context.groups.find(group => context.groupId == group.id)
for (let member of group.members) {
// Remove suffix
if (member.endsWith('.png')){
if (member.endsWith('.png')) {
member = member.slice(0, -4)
}
characters.push(member)
@ -655,15 +756,15 @@ function getCharacters(){
}
function sanitizeId(input) {
// Remove any non-alphanumeric characters except underscore (_) and hyphen (-)
let sanitized = input.replace(/[^a-zA-Z0-9-_]/g, '');
// Remove any non-alphanumeric characters except underscore (_) and hyphen (-)
let sanitized = input.replace(/[^a-zA-Z0-9-_]/g, '');
// Ensure first character is always a letter
if (!/^[a-zA-Z]/.test(sanitized)) {
sanitized = 'element_' + sanitized;
}
// Ensure first character is always a letter
if (!/^[a-zA-Z]/.test(sanitized)) {
sanitized = 'element_' + sanitized;
}
return sanitized;
return sanitized;
}
function parseVoiceMap(voiceMapString) {
@ -685,13 +786,13 @@ function parseVoiceMap(voiceMapString) {
*/
function updateVoiceMap() {
const tempVoiceMap = {}
for (const voice of voiceMapEntries){
if (voice.voiceId === null){
for (const voice of voiceMapEntries) {
if (voice.voiceId === null) {
continue
}
tempVoiceMap[voice.name] = voice.voiceId
}
if (Object.keys(tempVoiceMap).length !== 0){
if (Object.keys(tempVoiceMap).length !== 0) {
voiceMap = tempVoiceMap
console.log(`Voicemap updated to ${JSON.stringify(voiceMap)}`)
}
@ -706,13 +807,13 @@ class VoiceMapEntry {
name
voiceId
selectElement
constructor (name, voiceId=DEFAULT_VOICE_MARKER) {
constructor(name, voiceId = DEFAULT_VOICE_MARKER) {
this.name = name
this.voiceId = voiceId
this.selectElement = null
}
addUI(voiceIds){
addUI(voiceIds) {
let sanitizedName = sanitizeId(this.name)
let defaultOption = this.name === DEFAULT_VOICE_MARKER ?
`<option>${DISABLED_VOICE_MARKER}</option>` :
@ -728,7 +829,7 @@ class VoiceMapEntry {
$('#tts_voicemap_block').append(template)
// Populate voice ID select list
for (const voiceId of voiceIds){
for (const voiceId of voiceIds) {
const option = document.createElement('option');
option.innerText = voiceId.name;
option.value = voiceId.name;
@ -748,12 +849,12 @@ class VoiceMapEntry {
/**
* Init voiceMapEntries for character select list.
*
* @param {boolean} unrestricted - If true, will include all characters in voiceMapEntries, even if they are not in the current chat.
*/
export async function initVoiceMap(){
export async function initVoiceMap(unrestricted = false) {
// Gate initialization if not enabled or TTS Provider not ready. Prevents error popups.
const enabled = $('#tts_enabled').is(':checked')
if (!enabled){
if (!enabled) {
return
}
@ -771,18 +872,18 @@ export async function initVoiceMap(){
// Clear existing voiceMap state
$('#tts_voicemap_block').empty()
voiceMapEntries = []
// Get characters in current chat
const characters = getCharacters()
const characters = getCharacters(unrestricted);
// Get saved voicemap from provider settings, handling new and old representations
let voiceMapFromSettings = {}
if ("voiceMap" in extension_settings.tts[ttsProviderName]) {
// Handle previous representation
if (typeof extension_settings.tts[ttsProviderName].voiceMap === "string"){
if (typeof extension_settings.tts[ttsProviderName].voiceMap === "string") {
voiceMapFromSettings = parseVoiceMap(extension_settings.tts[ttsProviderName].voiceMap)
// Handle new representation
} else if (typeof extension_settings.tts[ttsProviderName].voiceMap === "object"){
// Handle new representation
} else if (typeof extension_settings.tts[ttsProviderName].voiceMap === "object") {
voiceMapFromSettings = extension_settings.tts[ttsProviderName].voiceMap
}
}
@ -797,13 +898,13 @@ export async function initVoiceMap(){
}
// Build UI using VoiceMapEntry objects
for (const character of characters){
if (character === "SillyTavern System"){
for (const character of characters) {
if (character === "SillyTavern System") {
continue
}
// Check provider settings for voiceIds
let voiceId
if (character in voiceMapFromSettings){
if (character in voiceMapFromSettings) {
voiceId = voiceMapFromSettings[character]
} else if (character === DEFAULT_VOICE_MARKER) {
voiceId = DISABLED_VOICE_MARKER
@ -897,5 +998,8 @@ $(document).ready(function () {
setInterval(wrapper.update.bind(wrapper), UPDATE_INTERVAL) // Init depends on all the things
eventSource.on(event_types.MESSAGE_SWIPED, resetTtsPlayback);
eventSource.on(event_types.CHAT_CHANGED, onChatChanged)
eventSource.on(event_types.MESSAGE_DELETED, onChatDeleted);
eventSource.on(event_types.GROUP_UPDATED, onChatChanged)
registerSlashCommand('speak', onNarrateText, ['narrate', 'tts'], `<span class="monospace">(text)</span> narrate any text using currently selected character's voice. Use voice="Character Name" argument to set other voice from the voice map, example: <tt>/speak voice="Donald Duck" Quack!</tt>`, true, true);
document.body.appendChild(audioElement);
})

View File

@ -19,6 +19,17 @@ class NovelTtsProvider {
customVoices: []
}
/**
* Perform any text processing before passing to TTS engine.
* @param {string} text Input text
* @returns {string} Processed text
*/
processText(text) {
// Novel reads tilde as a word. Replace with full stop
text = text.replace(/~/g, '.');
return text;
}
get settingsHtml() {
let html = `
<div class="novel_tts_hints">

View File

@ -0,0 +1,148 @@
import { getRequestHeaders } from "../../../script.js"
import { saveTtsProviderSettings } from "./index.js";
export { OpenAITtsProvider }
class OpenAITtsProvider {
static voices = [
{ name: 'Alloy', voice_id: 'alloy', lang: 'en-US', preview_url: 'https://cdn.openai.com/API/docs/audio/alloy.wav' },
{ name: 'Echo', voice_id: 'echo', lang: 'en-US', preview_url: 'https://cdn.openai.com/API/docs/audio/echo.wav' },
{ name: 'Fable', voice_id: 'fable', lang: 'en-US', preview_url: 'https://cdn.openai.com/API/docs/audio/fable.wav' },
{ name: 'Onyx', voice_id: 'onyx', lang: 'en-US', preview_url: 'https://cdn.openai.com/API/docs/audio/onyx.wav' },
{ name: 'Nova', voice_id: 'nova', lang: 'en-US', preview_url: 'https://cdn.openai.com/API/docs/audio/nova.wav' },
{ name: 'Shimmer', voice_id: 'shimmer', lang: 'en-US', preview_url: 'https://cdn.openai.com/API/docs/audio/shimmer.wav' },
];
settings
voices = []
separator = ' . '
audioElement = document.createElement('audio')
defaultSettings = {
voiceMap: {},
customVoices: [],
model: 'tts-1',
speed: 1,
}
get settingsHtml() {
let html = `
<div>Use OpenAI's TTS engine.</div>
<small>Hint: Save an API key in the OpenAI API settings to use it here.</small>
<div>
<label for="openai-tts-model">Model:</label>
<select id="openai-tts-model">
<optgroup label="Latest">
<option value="tts-1">tts-1</option>
<option value="tts-1-hd">tts-1-hd</option>
</optgroup>
<optgroup label="Snapshots">
<option value="tts-1-1106">tts-1-1106</option>
<option value="tts-1-hd-1106">tts-1-hd-1106</option>
</optgroup>
<select>
</div>
<div>
<label for="openai-tts-speed">Speed: <span id="openai-tts-speed-output"></span></label>
<input type="range" id="openai-tts-speed" value="1" min="0.25" max="4" step="0.25">
</div>`;
return html;
}
async loadSettings(settings) {
// Populate Provider UI given input settings
if (Object.keys(settings).length == 0) {
console.info("Using default TTS Provider settings")
}
// Only accept keys defined in defaultSettings
this.settings = this.defaultSettings;
for (const key in settings) {
if (key in this.settings) {
this.settings[key] = settings[key];
} else {
throw `Invalid setting passed to TTS Provider: ${key}`;
}
}
$('#openai-tts-model').val(this.settings.model);
$('#openai-tts-model').on('change', () => {
this.onSettingsChange();
});
$('#openai-tts-speed').val(this.settings.speed);
$('#openai-tts-speed').on('input', () => {
this.onSettingsChange();
});
$('#openai-tts-speed-output').text(this.settings.speed);
await this.checkReady();
console.debug("OpenAI TTS: Settings loaded");
}
onSettingsChange() {
// Update dynamically
this.settings.model = String($('#openai-tts-model').find(':selected').val());
this.settings.speed = Number($('#openai-tts-speed').val());
$('#openai-tts-speed-output').text(this.settings.speed);
saveTtsProviderSettings();
}
async checkReady() {
await this.fetchTtsVoiceObjects();
}
async onRefreshClick() {
return;
}
async getVoice(voiceName) {
if (!voiceName) {
throw `TTS Voice name not provided`
}
const voice = OpenAITtsProvider.voices.find(voice => voice.voice_id === voiceName || voice.name === voiceName);
if (!voice) {
throw `TTS Voice not found: ${voiceName}`
}
return voice;
}
async generateTts(text, voiceId) {
const response = await this.fetchTtsGeneration(text, voiceId)
return response
}
async fetchTtsVoiceObjects() {
return OpenAITtsProvider.voices;
}
async previewTtsVoice(_) {
return;
}
async fetchTtsGeneration(inputText, voiceId) {
console.info(`Generating new TTS for voice_id ${voiceId}`)
const response = await fetch(`/api/openai/generate-voice`, {
method: 'POST',
headers: getRequestHeaders(),
body: JSON.stringify({
"text": inputText,
"voice": voiceId,
"model": this.settings.model,
"speed": this.settings.speed,
}),
});
if (!response.ok) {
toastr.error(response.statusText, 'TTS Generation Failed');
throw new Error(`HTTP ${response.status}: ${await response.text()}`);
}
return response;
}
}

View File

@ -1,8 +1,8 @@
# Provider Requirements.
# Provider Requirements.
Because I don't know how, or if you can, and/or maybe I am just too lazy to implement interfaces in JS, here's the requirements of a provider that the extension needs to operate.
### class YourTtsProvider
#### Required
#### Required
Exported for use in extension index.js, and added to providers list in index.js
1. generateTts(text, voiceId)
2. fetchTtsVoiceObjects()
@ -13,8 +13,9 @@ Exported for use in extension index.js, and added to providers list in index.js
7. settingsHtml field
#### Optional
1. previewTtsVoice()
1. previewTtsVoice()
2. separator field
3. processText(text)
# Requirement Descriptions
### generateTts(text, voiceId)
@ -49,14 +50,14 @@ Return without error to let TTS extension know that the provider is ready.
Return an error to block the main TTS extension for initializing the provider and UI. The error will be put in the TTS extension UI directly.
### loadSettings(settingsObject)
Required.
Required.
Handle the input settings from the TTS extension on provider load.
Put code in here to load your provider settings.
### settings field
Required, used for storing any provider state that needs to be saved.
Anything stored in this field is automatically persisted under extension_settings[providerName] by the main extension in `saveTtsProviderSettings()`, as well as loaded when the provider is selected in `loadTtsProvider(provider)`.
TTS extension doesn't expect any specific contents.
TTS extension doesn't expect any specific contents.
### settingsHtml field
Required, injected into the TTS extension UI. Besides adding it, not relied on by TTS extension directly.
@ -68,4 +69,8 @@ Function to handle playing previews of voice samples if no direct preview_url is
### separator field
Optional.
Used when narrate quoted text is enabled.
Defines the string of characters used to introduce separation between between the groups of extracted quoted text sent to the provider. The provider will use this to introduce pauses by default using `...`
Defines the string of characters used to introduce separation between between the groups of extracted quoted text sent to the provider. The provider will use this to introduce pauses by default using `...`
### processText(text)
Optional.
A function applied to the input text before passing it to the TTS generator. Can be async.

View File

@ -146,8 +146,8 @@ class SystemTtsProvider {
$('#system_tts_pitch').val(this.settings.pitch || this.defaultSettings.pitch);
// Trigger updates
$('#system_tts_rate').on("input", () =>{this.onSettingsChange()})
$('#system_tts_rate').on("input", () => {this.onSettingsChange()})
$('#system_tts_rate').on("input", () => { this.onSettingsChange() })
$('#system_tts_rate').on("input", () => { this.onSettingsChange() })
$('#system_tts_pitch_output').text(this.settings.pitch);
$('#system_tts_rate_output').text(this.settings.rate);
@ -155,7 +155,7 @@ class SystemTtsProvider {
}
// Perform a simple readiness check by trying to fetch voiceIds
async checkReady(){
async checkReady() {
await this.fetchTtsVoiceObjects()
}
@ -171,10 +171,16 @@ class SystemTtsProvider {
return [];
}
return speechSynthesis
.getVoices()
.sort((a, b) => a.lang.localeCompare(b.lang) || a.name.localeCompare(b.name))
.map(x => ({ name: x.name, voice_id: x.voiceURI, preview_url: false, lang: x.lang }));
return new Promise((resolve) => {
setTimeout(() => {
const voices = speechSynthesis
.getVoices()
.sort((a, b) => a.lang.localeCompare(b.lang) || a.name.localeCompare(b.name))
.map(x => ({ name: x.name, voice_id: x.voiceURI, preview_url: false, lang: x.lang }));
resolve(voices);
}, 1);
});
}
previewTtsVoice(voiceId) {

View File

@ -0,0 +1,207 @@
import { doExtrasFetch, getApiUrl, modules } from "../../extensions.js"
import { saveTtsProviderSettings } from "./index.js"
export { XTTSTtsProvider }
class XTTSTtsProvider {
//########//
// Config //
//########//
settings
ready = false
voices = []
separator = '. '
/**
* Perform any text processing before passing to TTS engine.
* @param {string} text Input text
* @returns {string} Processed text
*/
processText(text) {
// Replace fancy ellipsis with "..."
text = text.replace(/…/g, '...');
// Remove quotes
text = text.replace(/["“”‘’]/g, '');
// Replace multiple "." with single "."
text = text.replace(/\.+/g, '.');
return text;
}
languageLabels = {
"Arabic": "ar",
"Brazilian Portuguese": "pt",
"Chinese": "zh-cn",
"Czech": "cs",
"Dutch": "nl",
"English": "en",
"French": "fr",
"German": "de",
"Italian": "it",
"Polish": "pl",
"Russian": "ru",
"Spanish": "es",
"Turkish": "tr",
"Japanese": "ja",
"Korean": "ko",
"Hungarian": "hu",
"Hindi": "hi",
}
defaultSettings = {
provider_endpoint: "http://localhost:8020",
language: "en",
voiceMap: {}
}
get settingsHtml() {
let html = `
<label for="xtts_api_language">Language</label>
<select id="xtts_api_language">`;
for (let language in this.languageLabels) {
if (this.languageLabels[language] == this.settings?.language) {
html += `<option value="${this.languageLabels[language]}" selected="selected">${language}</option>`;
continue
}
html += `<option value="${this.languageLabels[language]}">${language}</option>`;
}
html += `
</select>
<label for="xtts_tts_endpoint">Provider Endpoint:</label>
<input id="xtts_tts_endpoint" type="text" class="text_pole" maxlength="250" value="${this.defaultSettings.provider_endpoint}"/>
`;
html += `
<span>
<span>Use <a target="_blank" href="https://github.com/daswer123/xtts-api-server">XTTSv2 TTS Server</a>.</span>
`;
return html;
}
onSettingsChange() {
// Used when provider settings are updated from UI
this.settings.provider_endpoint = $('#xtts_tts_endpoint').val()
this.settings.language = $('#xtts_api_language').val()
saveTtsProviderSettings()
}
async loadSettings(settings) {
// Pupulate Provider UI given input settings
if (Object.keys(settings).length == 0) {
console.info("Using default TTS Provider settings")
}
// Only accept keys defined in defaultSettings
this.settings = this.defaultSettings
for (const key in settings) {
if (key in this.settings) {
this.settings[key] = settings[key]
} else {
throw `Invalid setting passed to TTS Provider: ${key}`
}
}
const apiCheckInterval = setInterval(() => {
// Use Extras API if TTS support is enabled
if (modules.includes('tts') || modules.includes('xtts-tts')) {
const baseUrl = new URL(getApiUrl());
baseUrl.pathname = '/api/tts';
this.settings.provider_endpoint = baseUrl.toString();
$('#xtts_tts_endpoint').val(this.settings.provider_endpoint);
clearInterval(apiCheckInterval);
}
}, 2000);
$('#xtts_tts_endpoint').val(this.settings.provider_endpoint)
$('#xtts_tts_endpoint').on("input", () => { this.onSettingsChange() })
$('#xtts_api_language').val(this.settings.language)
$('#xtts_api_language').on("change", () => { this.onSettingsChange() })
await this.checkReady()
console.debug("XTTS: Settings loaded")
}
// Perform a simple readiness check by trying to fetch voiceIds
async checkReady() {
const response = await this.fetchTtsVoiceObjects()
}
async onRefreshClick() {
return
}
//#################//
// TTS Interfaces //
//#################//
async getVoice(voiceName) {
if (this.voices.length == 0) {
this.voices = await this.fetchTtsVoiceObjects()
}
const match = this.voices.filter(
XTTSVoice => XTTSVoice.name == voiceName
)[0]
if (!match) {
throw `TTS Voice name ${voiceName} not found`
}
return match
}
async generateTts(text, voiceId) {
const response = await this.fetchTtsGeneration(text, voiceId)
return response
}
//###########//
// API CALLS //
//###########//
async fetchTtsVoiceObjects() {
const response = await doExtrasFetch(`${this.settings.provider_endpoint}/speakers`)
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${await response.json()}`)
}
const responseJson = await response.json()
return responseJson
}
async fetchTtsGeneration(inputText, voiceId) {
console.info(`Generating new TTS for voice_id ${voiceId}`)
const response = await doExtrasFetch(
`${this.settings.provider_endpoint}/tts_to_audio/`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Cache-Control': 'no-cache' // Added this line to disable caching of file so new files are always played - Rolyat 7/7/23
},
body: JSON.stringify({
"text": inputText,
"speaker_wav": voiceId,
"language": this.settings.language
})
}
)
if (!response.ok) {
toastr.error(response.statusText, 'TTS Generation Failed');
throw new Error(`HTTP ${response.status}: ${await response.text()}`);
}
return response
}
// Interface not used by XTTS TTS
async fetchTtsFromHistory(history_item_id) {
return Promise.resolve(history_item_id);
}
}

View File

@ -19,10 +19,10 @@
</select>
<div id="vectors_advanced_settings" data-newbie-hidden>
<label for="vectors_template">
Insertion template:
Insertion Template
</label>
<textarea id="vectors_template" class="text_pole textarea_compact autoSetHeight" rows="2" placeholder="Use {{text}} macro to specify the position of retrieved text."></textarea>
<label for="vectors_position">Injection position:</label>
<label for="vectors_position">Injection Position</label>
<div class="radio_group">
<label>
<input type="radio" name="vectors_position" value="2" />

View File

@ -1,4 +1,4 @@
import { fuzzySearchCharacters, fuzzySearchGroups, fuzzySearchWorldInfo, power_user } from "./power-user.js";
import { fuzzySearchCharacters, fuzzySearchGroups, fuzzySearchTags, fuzzySearchWorldInfo, power_user } from "./power-user.js";
import { tag_map } from "./tags.js";
/**
@ -69,6 +69,20 @@ export class FilterHelper {
return data.filter(entity => fuzzySearchResults.includes(entity.uid));
}
/**
* Checks if the given entity is tagged with the given tag ID.
* @param {object} entity Searchable entity
* @param {string} tagId Tag ID to check
* @returns {boolean} Whether the entity is tagged with the given tag ID
*/
isElementTagged(entity, tagId) {
const isCharacter = entity.type === 'character';
const lookupValue = isCharacter ? entity.item.avatar : String(entity.id);
const isTagged = Array.isArray(tag_map[lookupValue]) && tag_map[lookupValue].includes(tagId);
return isTagged;
}
/**
* Applies a tag filter to the data.
* @param {any[]} data The data to filter.
@ -82,19 +96,12 @@ export class FilterHelper {
return data;
}
function isElementTagged(entity, tagId) {
const isCharacter = entity.type === 'character';
const lookupValue = isCharacter ? entity.item.avatar : String(entity.id);
const isTagged = Array.isArray(tag_map[lookupValue]) && tag_map[lookupValue].includes(tagId);
return isTagged;
}
function getIsTagged(entity) {
const tagFlags = selected.map(tagId => isElementTagged(entity, tagId));
const getIsTagged = (entity) => {
const tagFlags = selected.map(tagId => this.isElementTagged(entity, tagId));
const trueFlags = tagFlags.filter(x => x);
const isTagged = TAG_LOGIC_AND ? tagFlags.length === trueFlags.length : trueFlags.length > 0;
const excludedTagFlags = excluded.map(tagId => isElementTagged(entity, tagId));
const excludedTagFlags = excluded.map(tagId => this.isElementTagged(entity, tagId));
const isExcluded = excludedTagFlags.includes(true);
if (isExcluded) {
@ -148,16 +155,20 @@ export class FilterHelper {
const searchValue = this.filterData[FILTER_TYPES.SEARCH].trim().toLowerCase();
const fuzzySearchCharactersResults = power_user.fuzzy_search ? fuzzySearchCharacters(searchValue) : [];
const fuzzySearchGroupsResults = power_user.fuzzy_search ? fuzzySearchGroups(searchValue) : [];
const fuzzySearchTagsResult = power_user.fuzzy_search ? fuzzySearchTags(searchValue) : [];
function getIsValidSearch(entity) {
const isGroup = entity.type === 'group';
const isCharacter = entity.type === 'character';
const isTag = entity.type === 'tag';
if (power_user.fuzzy_search) {
if (isCharacter) {
return fuzzySearchCharactersResults.includes(parseInt(entity.id));
} else if (isGroup) {
return fuzzySearchGroupsResults.includes(String(entity.id));
} else if (isTag) {
return fuzzySearchTagsResult.includes(String(entity.id));
} else {
return false;
}

View File

@ -68,6 +68,7 @@ import {
setExternalAbortController,
baseChatReplace,
depth_prompt_depth_default,
loadItemizedPrompts,
} from "../script.js";
import { appendTagToList, createTagMapFromList, getTagsList, applyTagsOnCharacterSelect, tag_map, printTagFilters } from './tags.js';
import { FILTER_TYPES, FilterHelper } from './filters.js';
@ -75,6 +76,7 @@ import { FILTER_TYPES, FilterHelper } from './filters.js';
export {
selected_group,
is_group_automode_enabled,
hideMutedSprites,
is_group_generating,
group_generation_id,
groups,
@ -91,6 +93,7 @@ export {
let is_group_generating = false; // Group generation flag
let is_group_automode_enabled = false;
let hideMutedSprites = true;
let groups = [];
let selected_group = null;
let group_generation_id = null;
@ -168,6 +171,8 @@ export async function getGroupChat(groupId) {
const chat_id = group.chat_id;
const data = await loadGroupChat(chat_id);
await loadItemizedPrompts(getCurrentChatId());
if (Array.isArray(data) && data.length) {
data[0].is_group = true;
for (let key of data) {
@ -197,7 +202,70 @@ export async function getGroupChat(groupId) {
updateChatMetadata(metadata, true);
}
eventSource.emit(event_types.CHAT_CHANGED, getCurrentChatId());
await eventSource.emit(event_types.CHAT_CHANGED, getCurrentChatId());
}
/**
* Finds the character ID for a group member.
* @param {string} arg 1-based member index or character name
* @returns {number} 0-based character ID
*/
export function findGroupMemberId(arg) {
arg = arg?.trim();
if (!arg) {
console.warn('WARN: No argument provided for findGroupMemberId');
return;
}
const group = groups.find(x => x.id == selected_group);
if (!group || !Array.isArray(group.members)) {
console.warn('WARN: No group found for selected group ID');
return;
}
// Index is 1-based
const index = parseInt(arg) - 1;
const searchByName = isNaN(index);
if (searchByName) {
const memberNames = group.members.map(x => ({ name: characters.find(y => y.avatar === x)?.name, index: characters.findIndex(y => y.avatar === x) }));
const fuse = new Fuse(memberNames, { keys: ['name'] });
const result = fuse.search(arg);
if (!result.length) {
console.warn(`WARN: No group member found with name ${arg}`);
return;
}
const chid = result[0].item.index;
if (chid === -1) {
console.warn(`WARN: No character found for group member ${arg}`);
return;
}
console.log(`Triggering group member ${chid} (${arg}) from search result`, result[0]);
return chid;
} else {
const memberAvatar = group.members[index];
if (memberAvatar === undefined) {
console.warn(`WARN: No group member found at index ${index}`);
return;
}
const chid = characters.findIndex(x => x.avatar === memberAvatar);
if (chid === -1) {
console.warn(`WARN: No character found for group member ${memberAvatar} at index ${index}`);
return;
}
console.log(`Triggering group member ${memberAvatar} at index ${index}`);
return chid;
}
}
/**
@ -253,7 +321,7 @@ export function getGroupDepthPrompts(groupId, characterId) {
* Combines group members info a single string. Only for groups with generation mode set to APPEND.
* @param {string} groupId Group ID
* @param {number} characterId Current Character ID
* @returns {{description: string, personality: string, scenario: string, mesExample: string}} Group character cards combined
* @returns {{description: string, personality: string, scenario: string, mesExamples: string}} Group character cards combined
*/
export function getGroupCharacterCards(groupId, characterId) {
console.debug('getGroupCharacterCards entered for group: ', groupId);
@ -268,7 +336,7 @@ export function getGroupCharacterCards(groupId, characterId) {
let descriptions = [];
let personalities = [];
let scenarios = [];
let mesExamples = [];
let mesExamplesArray = [];
for (const member of group.members) {
const index = characters.findIndex(x => x.avatar === member);
@ -287,15 +355,15 @@ export function getGroupCharacterCards(groupId, characterId) {
descriptions.push(baseChatReplace(character.description.trim(), name1, character.name));
personalities.push(baseChatReplace(character.personality.trim(), name1, character.name));
scenarios.push(baseChatReplace(character.scenario.trim(), name1, character.name));
mesExamples.push(baseChatReplace(character.mes_example.trim(), name1, character.name));
mesExamplesArray.push(baseChatReplace(character.mes_example.trim(), name1, character.name));
}
const description = descriptions.join('\n');
const personality = personalities.join('\n');
const scenario = scenarioOverride?.trim() || scenarios.join('\n');
const mesExample = mesExamples.join('\n');
const mesExamples = mesExamplesArray.join('\n');
return { description, personality, scenario, mesExample };
return { description, personality, scenario, mesExamples };
}
function getFirstCharacterMessage(character) {
@ -913,10 +981,10 @@ async function deleteGroup(id) {
}
if (response.ok) {
await clearChat();
selected_group = null;
delete tag_map[id];
resetChatState();
clearChat();
await printMessages();
await getCharacters();
@ -1106,7 +1174,7 @@ function printGroupCandidates() {
function printGroupMembers() {
const storageKey = 'GroupMembers_PerPage';
$(".rm_group_members_pagination").each(function() {
$(".rm_group_members_pagination").each(function () {
$(this).pagination({
dataSource: getGroupCharacters({ doFilter: false, onlyMembers: true }),
pageRange: 1,
@ -1192,6 +1260,15 @@ async function onGroupSelfResponsesClick() {
}
}
async function onHideMutedSpritesClick(value) {
if (openGroupId) {
let _thisGroup = groups.find((x) => x.id == openGroupId);
_thisGroup.hideMutedSprites = value;
console.log(`_thisGroup.hideMutedSprites = ${_thisGroup.hideMutedSprites}`)
await editGroup(openGroupId, false, false);
}
}
function select_group_chats(groupId, skipAnimation) {
openGroupId = groupId;
newGroupMembers = [];
@ -1221,6 +1298,7 @@ function select_group_chats(groupId, skipAnimation) {
const groupHasMembers = !!$("#rm_group_members").children().length;
$("#rm_group_submit").prop("disabled", !groupHasMembers);
$("#rm_group_allow_self_responses").prop("checked", group && group.allow_self_responses);
$("#rm_group_hidemutedsprites").prop("checked", group && group.hideMutedSprites);
// bottom buttons
if (openGroupId) {
@ -1385,12 +1463,12 @@ export async function openGroupById(groupId) {
if (!is_send_press && !is_group_generating) {
if (selected_group !== groupId) {
await clearChat();
cancelTtsPlay();
selected_group = groupId;
setCharacterId(undefined);
setCharacterName('');
setEditedMessageId(undefined);
clearChat();
updateChatMetadata({}, true);
chat.length = 0;
await getGroupChat(groupId);
@ -1451,6 +1529,7 @@ async function createGroup() {
members: members,
avatar_url: isValidImageUrl(avatar_url) ? avatar_url : default_avatar,
allow_self_responses: allowSelfResponses,
hideMutedSprites: hideMutedSprites,
activation_strategy: activationStrategy,
generation_mode: generationMode,
disabled_members: [],
@ -1484,7 +1563,7 @@ export async function createNewGroupChat(groupId) {
group.past_metadata = {};
}
clearChat();
await clearChat();
chat.length = 0;
if (oldChatName) {
group.past_metadata[oldChatName] = Object.assign({}, chat_metadata);
@ -1537,7 +1616,7 @@ export async function openGroupChat(groupId, chatId) {
return;
}
clearChat();
await clearChat();
chat.length = 0;
const previousChat = group.chat_id;
group.past_metadata[previousChat] = Object.assign({}, chat_metadata);
@ -1718,6 +1797,12 @@ jQuery(() => {
is_group_automode_enabled = value;
eventSource.once(event_types.GENERATION_STOPPED, stopAutoModeGeneration);
});
$("#rm_group_hidemutedsprites").on("input", function () {
const value = $(this).prop("checked");
hideMutedSprites = value;
onHideMutedSpritesClick(value);
});
$("#send_textarea").on("keyup", onSendTextareaInput);
$("#groupCurrentMemberPopoutButton").on('click', doCurMemberListPopout);
$("#rm_group_chat_name").on("input", onGroupNameInput)

View File

@ -31,8 +31,8 @@ let horde_settings = {
trusted_workers_only: false,
};
const MAX_RETRIES = 240;
const CHECK_INTERVAL = 5000;
const MAX_RETRIES = 480;
const CHECK_INTERVAL = 2500;
const MIN_LENGTH = 16;
const getRequestArgs = () => ({
method: "GET",
@ -152,7 +152,7 @@ async function generateHorde(prompt, params, signal, reportProgress) {
for (let retryNumber = 0; retryNumber < MAX_RETRIES; retryNumber++) {
if (signal.aborted) {
await fetch(`https://horde.koboldai.net/api/v2/generate/text/status/${task_id}`, {
fetch(`https://horde.koboldai.net/api/v2/generate/text/status/${task_id}`, {
method: 'DELETE',
headers: {
"Client-Agent": CLIENT_VERSION,

View File

@ -105,7 +105,7 @@ function addLanguagesToDropdown() {
}
}
jQuery(async () => {
export function initLocales() {
waitUntilCondition(() => !!localeData);
applyLocale();
addLanguagesToDropdown();
@ -124,4 +124,4 @@ jQuery(async () => {
registerDebugFunction('getMissingTranslations', 'Get missing translations', 'Detects missing localization data and dumps the data into the browser console.', getMissingTranslations);
registerDebugFunction('applyLocale', 'Apply locale', 'Reapplies the currently selected locale to the page.', applyLocale);
});
};

View File

@ -358,6 +358,31 @@ function selectMatchingContextTemplate(name) {
}
}
/**
* Replaces instruct mode macros in the given input string.
* @param {string} input Input string.
* @returns {string} String with macros replaced.
*/
export function replaceInstructMacros(input) {
if (!input) {
return '';
}
input = input.replace(/{{instructSystem}}/gi, power_user.instruct.enabled ? power_user.instruct.system_prompt : '');
input = input.replace(/{{instructSystemPrefix}}/gi, power_user.instruct.enabled ? power_user.instruct.system_sequence_prefix : '');
input = input.replace(/{{instructSystemSuffix}}/gi, power_user.instruct.enabled ? power_user.instruct.system_sequence_suffix : '');
input = input.replace(/{{instructInput}}/gi, power_user.instruct.enabled ? power_user.instruct.input_sequence : '');
input = input.replace(/{{instructOutput}}/gi, power_user.instruct.enabled ? power_user.instruct.output_sequence : '');
input = input.replace(/{{instructFirstOutput}}/gi, power_user.instruct.enabled ? (power_user.instruct.first_output_sequence || power_user.instruct.output_sequence) : '');
input = input.replace(/{{instructLastOutput}}/gi, power_user.instruct.enabled ? (power_user.instruct.last_output_sequence || power_user.instruct.output_sequence) : '');
input = input.replace(/{{instructSeparator}}/gi, power_user.instruct.enabled ? power_user.instruct.separator_sequence : '');
input = input.replace(/{{instructStop}}/gi, power_user.instruct.enabled ? power_user.instruct.stop_sequence : '');
input = input.replace(/{{exampleSeparator}}/gi, power_user.context.example_separator);
input = input.replace(/{{chatStart}}/gi, power_user.context.chat_start);
return input;
}
jQuery(() => {
$('#instruct_set_default').on('click', function () {
if (power_user.instruct.preset === power_user.default_instruct) {

View File

@ -15,6 +15,7 @@ export const kai_settings = {
rep_pen: 1,
rep_pen_range: 0,
top_p: 1,
min_p: 0,
top_a: 1,
top_k: 0,
typical: 1,
@ -30,6 +31,11 @@ export const kai_settings = {
seed: -1,
};
/**
* Stable version of KoboldAI has a nasty payload validation.
* It will reject any payload that has a key that is not in the whitelist.
* @typedef {Object.<string, boolean>} kai_flags
*/
export const kai_flags = {
can_use_tokenization: false,
can_use_stop_sequence: false,
@ -37,6 +43,7 @@ export const kai_flags = {
can_use_default_badwordsids: false,
can_use_mirostat: false,
can_use_grammar: false,
can_use_min_p: false,
};
const defaultValues = Object.freeze(structuredClone(kai_settings));
@ -47,6 +54,7 @@ const MIN_STREAMING_KCPPVERSION = '1.30';
const MIN_TOKENIZATION_KCPPVERSION = '1.41';
const MIN_MIROSTAT_KCPPVERSION = '1.35';
const MIN_GRAMMAR_KCPPVERSION = '1.44';
const MIN_MIN_P_KCPPVERSION = '1.48';
const KOBOLDCPP_ORDER = [6, 0, 1, 3, 4, 2, 5];
export function formatKoboldUrl(value) {
@ -97,6 +105,7 @@ export function loadKoboldSettings(preset) {
*/
export function getKoboldGenerationData(finalPrompt, settings, maxLength, maxContextLength, isHorde, type) {
const isImpersonate = type === 'impersonate';
const isContinue = type === 'continue';
const sampler_order = kai_settings.sampler_order || settings.sampler_order;
let generate_data = {
@ -113,6 +122,7 @@ export function getKoboldGenerationData(finalPrompt, settings, maxLength, maxCon
top_a: kai_settings.top_a,
top_k: kai_settings.top_k,
top_p: kai_settings.top_p,
min_p: (kai_flags.can_use_min_p || isHorde) ? kai_settings.min_p : undefined,
typical: kai_settings.typical,
s1: sampler_order[0],
s2: sampler_order[1],
@ -123,14 +133,14 @@ export function getKoboldGenerationData(finalPrompt, settings, maxLength, maxCon
s7: sampler_order[6],
use_world_info: false,
singleline: false,
stop_sequence: (kai_flags.can_use_stop_sequence || isHorde) ? getStoppingStrings(isImpersonate) : undefined,
stop_sequence: (kai_flags.can_use_stop_sequence || isHorde) ? getStoppingStrings(isImpersonate, isContinue) : undefined,
streaming: kai_settings.streaming_kobold && kai_flags.can_use_streaming && type !== 'quiet',
can_abort: kai_flags.can_use_streaming,
mirostat: kai_flags.can_use_mirostat ? kai_settings.mirostat : undefined,
mirostat_tau: kai_flags.can_use_mirostat ? kai_settings.mirostat_tau : undefined,
mirostat_eta: kai_flags.can_use_mirostat ? kai_settings.mirostat_eta : undefined,
use_default_badwordsids: kai_flags.can_use_default_badwordsids ? kai_settings.use_default_badwordsids : undefined,
grammar: kai_flags.can_use_grammar ? substituteParams(kai_settings.grammar) : undefined,
mirostat: (kai_flags.can_use_mirostat || isHorde) ? kai_settings.mirostat : undefined,
mirostat_tau: (kai_flags.can_use_mirostat || isHorde) ? kai_settings.mirostat_tau : undefined,
mirostat_eta: (kai_flags.can_use_mirostat || isHorde) ? kai_settings.mirostat_eta : undefined,
use_default_badwordsids: (kai_flags.can_use_default_badwordsids || isHorde) ? kai_settings.use_default_badwordsids : undefined,
grammar: (kai_flags.can_use_grammar || isHorde) ? substituteParams(kai_settings.grammar) : undefined,
sampler_seed: kai_settings.seed >= 0 ? kai_settings.seed : undefined,
};
return generate_data;
@ -207,6 +217,13 @@ const sliders = [
format: (val) => val,
setValue: (val) => { kai_settings.top_p = Number(val); },
},
{
name: "min_p",
sliderId: "#min_p",
counterId: "#min_p_counter",
format: (val) => val,
setValue: (val) => { kai_settings.min_p = Number(val); },
},
{
name: "top_a",
sliderId: "#top_a",
@ -223,8 +240,8 @@ const sliders = [
},
{
name: "typical",
sliderId: "#typical",
counterId: "#typical_counter",
sliderId: "#typical_p",
counterId: "#typical_p_counter",
format: (val) => val,
setValue: (val) => { kai_settings.typical = Number(val); },
},
@ -293,6 +310,7 @@ export function setKoboldFlags(version, koboldVersion) {
kai_flags.can_use_default_badwordsids = canUseDefaultBadwordIds(version);
kai_flags.can_use_mirostat = canUseMirostat(koboldVersion);
kai_flags.can_use_grammar = canUseGrammar(koboldVersion);
kai_flags.can_use_min_p = canUseMinP(koboldVersion);
}
/**
@ -357,6 +375,17 @@ function canUseGrammar(koboldVersion) {
} else return false;
}
/**
* Determines if the Kobold min_p can be used with the given version.
* @param {{result:string, version:string;}} koboldVersion KoboldAI version object.
* @returns {boolean} True if the Kobold min_p can be used, false otherwise.
*/
function canUseMinP(koboldVersion) {
if (koboldVersion && koboldVersion.result == 'KoboldCpp') {
return (koboldVersion.version || '0.0').localeCompare(MIN_MIN_P_KCPPVERSION, undefined, { numeric: true, sensitivity: 'base' }) > -1;
} else return false;
}
/**
* Sorts the sampler items by the given order.
* @param {any[]} orderArray Sampler order array.

28
public/scripts/loader.js Normal file
View File

@ -0,0 +1,28 @@
const ELEMENT_ID = 'loader';
export function showLoader() {
const container = $('<div></div>').attr('id', ELEMENT_ID);
const loader = $('<div></div>').attr('id', 'load-spinner').addClass('fa-solid fa-gear fa-spin fa-3x')
container.append(loader);
$('body').append(container);
}
export function hideLoader() {
//Sets up a 2-step animation. Spinner blurs/fades out, and then the loader shadow does the same.
$(`#load-spinner`).on("transitionend webkitTransitionEnd oTransitionEnd MSTransitionEnd", function () {
//console.log('FADING BLUR SCREEN')
$(`#${ELEMENT_ID}`)
.animate({ opacity: 0 }, 300, function () {
//console.log('REMOVING LOADER')
$(`#${ELEMENT_ID}`).remove()
})
})
//console.log('BLURRING SPINNER')
$(`#load-spinner`)
.css({
'filter': 'blur(15px)',
'opacity': '0',
})
}

View File

@ -1,51 +1,34 @@
import { api_server_textgenerationwebui, getRequestHeaders, setGenerationParamsFromPreset } from "../script.js";
import { setGenerationParamsFromPreset } from "../script.js";
import { getDeviceInfo } from "./RossAscends-mods.js";
import { textgenerationwebui_settings } from "./textgen-settings.js";
let models = [];
/**
* @param {string} modelId
*/
export function getMancerModelURL(modelId) {
return `https://neuro.mancer.tech/webui/${modelId}/api`;
}
export async function loadMancerModels(data) {
if (!Array.isArray(data)) {
console.error('Invalid Mancer models data', data);
return;
}
export async function loadMancerModels() {
try {
const response = await fetch('/api/mancer/models', {
method: 'POST',
headers: getRequestHeaders(),
});
models = data;
if (!response.ok) {
return;
}
const data = await response.json();
models = data;
$('#mancer_model').empty();
for (const model of data) {
const option = document.createElement('option');
option.value = model.id;
option.text = model.name;
option.selected = api_server_textgenerationwebui === getMancerModelURL(model.id);
$('#mancer_model').append(option);
}
} catch {
console.warn('Failed to load Mancer models');
$('#mancer_model').empty();
for (const model of data) {
const option = document.createElement('option');
option.value = model.id;
option.text = model.name;
option.selected = model.id === textgenerationwebui_settings.mancer_model;
$('#mancer_model').append(option);
}
}
function onMancerModelSelect() {
const modelId = String($('#mancer_model').val());
const url = getMancerModelURL(modelId);
$('#mancer_api_url_text').val(url);
textgenerationwebui_settings.mancer_model = modelId;
$('#api_button_textgenerationwebui').trigger('click');
const context = models.find(x => x.id === modelId)?.context;
setGenerationParamsFromPreset({ max_length: context });
const limits = models.find(x => x.id === modelId)?.limits;
setGenerationParamsFromPreset({ max_length: limits.context, genamt: limits.completion });
}
function getMancerModelTemplate(option) {
@ -55,10 +38,13 @@ function getMancerModelTemplate(option) {
return option.text;
}
const creditsPerPrompt = (model.limits?.context - model.limits?.completion) * model.pricing?.prompt;
const creditsPerCompletion = model.limits?.completion * model.pricing?.completion;
const creditsTotal = Math.round(creditsPerPrompt + creditsPerCompletion).toFixed(0);
return $((`
<div class="flex-container flexFlowColumn">
<div><strong>${DOMPurify.sanitize(model.name)}</strong> | <span>${model.context} ctx</span></div>
<small>${DOMPurify.sanitize(model.description)}</small>
<div><strong>${DOMPurify.sanitize(model.name)}</strong> | <span>${model.limits?.context} ctx</span> / <span>${model.limits?.completion} res</span> | <small>Credits per request (max): ${creditsTotal}</small></div>
</div>
`));
}

View File

@ -1,9 +1,11 @@
import {
abortStatusCheck,
getRequestHeaders,
getStoppingStrings,
novelai_setting_names,
saveSettingsDebounced,
setGenerationParamsFromPreset
setGenerationParamsFromPreset,
substituteParams,
} from "../script.js";
import { getCfgPrompt } from "./cfg-scale.js";
import { MAX_CONTEXT_DEFAULT } from "./power-user.js";
@ -91,6 +93,7 @@ export async function loadNovelSubscriptionData() {
const result = await fetch('/api/novelai/status', {
method: 'POST',
headers: getRequestHeaders(),
signal: abortStatusCheck.signal,
});
if (result.ok) {
@ -184,9 +187,9 @@ function loadNovelSettingsUi(ui_settings) {
$("#rep_pen_slope_novel").val(ui_settings.repetition_penalty_slope);
$("#rep_pen_slope_counter_novel").val(Number(`${ui_settings.repetition_penalty_slope}`).toFixed(2));
$("#rep_pen_freq_novel").val(ui_settings.repetition_penalty_frequency);
$("#rep_pen_freq_counter_novel").val(Number(ui_settings.repetition_penalty_frequency).toFixed(2));
$("#rep_pen_freq_counter_novel").val(Number(ui_settings.repetition_penalty_frequency).toFixed(3));
$("#rep_pen_presence_novel").val(ui_settings.repetition_penalty_presence);
$("#rep_pen_presence_counter_novel").val(Number(ui_settings.repetition_penalty_presence).toFixed(2));
$("#rep_pen_presence_counter_novel").val(Number(ui_settings.repetition_penalty_presence).toFixed(3));
$("#tail_free_sampling_novel").val(ui_settings.tail_free_sampling);
$("#tail_free_sampling_counter_novel").val(Number(ui_settings.tail_free_sampling).toFixed(3));
$("#top_k_novel").val(ui_settings.top_k);
@ -194,9 +197,9 @@ function loadNovelSettingsUi(ui_settings) {
$("#top_p_novel").val(ui_settings.top_p);
$("#top_p_counter_novel").val(Number(ui_settings.top_p).toFixed(3));
$("#top_a_novel").val(ui_settings.top_a);
$("#top_a_counter_novel").val(Number(ui_settings.top_a).toFixed(2));
$("#top_a_counter_novel").val(Number(ui_settings.top_a).toFixed(3));
$("#typical_p_novel").val(ui_settings.typical_p);
$("#typical_p_counter_novel").val(Number(ui_settings.typical_p).toFixed(2));
$("#typical_p_counter_novel").val(Number(ui_settings.typical_p).toFixed(3));
$("#cfg_scale_novel").val(ui_settings.cfg_scale);
$("#cfg_scale_counter_novel").val(Number(ui_settings.cfg_scale).toFixed(2));
$("#phrase_rep_pen_novel").val(ui_settings.phrase_rep_pen || "off");
@ -245,13 +248,13 @@ const sliders = [
sliderId: "#rep_pen_freq_novel",
counterId: "#rep_pen_freq_counter_novel",
format: (val) => Number(val).toFixed(2),
setValue: (val) => { nai_settings.repetition_penalty_frequency = Number(val).toFixed(2); },
setValue: (val) => { nai_settings.repetition_penalty_frequency = Number(val).toFixed(3); },
},
{
sliderId: "#rep_pen_presence_novel",
counterId: "#rep_pen_presence_counter_novel",
format: (val) => `${val}`,
setValue: (val) => { nai_settings.repetition_penalty_presence = Number(val).toFixed(2); },
setValue: (val) => { nai_settings.repetition_penalty_presence = Number(val).toFixed(3); },
},
{
sliderId: "#tail_free_sampling_novel",
@ -275,13 +278,13 @@ const sliders = [
sliderId: "#top_a_novel",
counterId: "#top_a_counter_novel",
format: (val) => Number(val).toFixed(2),
setValue: (val) => { nai_settings.top_a = Number(val).toFixed(2); },
setValue: (val) => { nai_settings.top_a = Number(val).toFixed(3); },
},
{
sliderId: "#typical_p_novel",
counterId: "#typical_p_counter_novel",
format: (val) => Number(val).toFixed(2),
setValue: (val) => { nai_settings.typical_p = Number(val).toFixed(2); },
format: (val) => Number(val).toFixed(3),
setValue: (val) => { nai_settings.typical_p = Number(val).toFixed(3); },
},
{
sliderId: "#mirostat_tau_novel",
@ -406,7 +409,7 @@ function getBadWordPermutations(text) {
return result.filter(onlyUnique);
}
export function getNovelGenerationData(finalPrompt, this_settings, this_amount_gen, isImpersonate, cfgValues) {
export function getNovelGenerationData(finalPrompt, settings, maxLength, isImpersonate, isContinue, cfgValues) {
if (cfgValues && cfgValues.guidanceScale && cfgValues.guidanceScale?.value !== 1) {
cfgValues.negativePrompt = (getCfgPrompt(cfgValues.guidanceScale, true))?.value;
}
@ -416,7 +419,7 @@ export function getNovelGenerationData(finalPrompt, this_settings, this_amount_g
const tokenizerType = kayra ? tokenizers.NERD2 : (clio ? tokenizers.NERD : tokenizers.NONE);
const stopSequences = (tokenizerType !== tokenizers.NONE)
? getStoppingStrings(isImpersonate)
? getStoppingStrings(isImpersonate, isContinue)
.map(t => getTextTokens(tokenizerType, t))
: undefined;
@ -437,7 +440,7 @@ export function getNovelGenerationData(finalPrompt, this_settings, this_amount_g
"model": nai_settings.model_novel,
"use_string": true,
"temperature": Number(nai_settings.temperature),
"max_length": this_amount_gen < maximum_output_length ? this_amount_gen : maximum_output_length,
"max_length": maxLength < maximum_output_length ? maxLength : maximum_output_length,
"min_length": Number(nai_settings.min_length),
"tail_free_sampling": Number(nai_settings.tail_free_sampling),
"repetition_penalty": Number(nai_settings.repetition_penalty),
@ -452,7 +455,7 @@ export function getNovelGenerationData(finalPrompt, this_settings, this_amount_g
"mirostat_lr": Number(nai_settings.mirostat_lr),
"mirostat_tau": Number(nai_settings.mirostat_tau),
"cfg_scale": cfgValues?.guidanceScale?.value ?? Number(nai_settings.cfg_scale),
"cfg_uc": cfgValues?.negativePrompt ?? nai_settings.cfg_uc ?? "",
"cfg_uc": cfgValues?.negativePrompt ?? substituteParams(nai_settings.cfg_uc) ?? "",
"phrase_rep_pen": nai_settings.phrase_rep_pen,
"stop_sequences": stopSequences,
"bad_words_ids": badWordIds,
@ -461,7 +464,7 @@ export function getNovelGenerationData(finalPrompt, this_settings, this_amount_g
"use_cache": false,
"return_full_text": false,
"prefix": prefix,
"order": nai_settings.order || this_settings.order || default_order,
"order": nai_settings.order || settings.order || default_order,
};
}
@ -757,9 +760,9 @@ jQuery(function () {
// Update the selected preset to something appropriate
const default_preset = default_presets[nai_settings.model_novel];
$(`#settings_perset_novel`).val(novelai_setting_names[default_preset]);
$(`#settings_perset_novel option[value=${novelai_setting_names[default_preset]}]`).attr("selected", "true")
$(`#settings_perset_novel`).trigger("change");
$(`#settings_preset_novel`).val(novelai_setting_names[default_preset]);
$(`#settings_preset_novel option[value=${novelai_setting_names[default_preset]}]`).attr("selected", "true")
$(`#settings_preset_novel`).trigger("change");
});
$("#nai_prefix").on('change', function () {

File diff suppressed because it is too large Load Diff

View File

@ -1,11 +1,24 @@
/**
* This is a placeholder file for all the Persona Management code. Will be refactored into a separate file soon.
*/
import { callPopup, characters, chat_metadata, default_avatar, eventSource, event_types, getRequestHeaders, getThumbnailUrl, getUserAvatars, name1, saveMetadata, saveSettingsDebounced, setUserName, this_chid, user_avatar } from "../script.js";
import {
callPopup,
characters,
chat_metadata,
default_avatar,
eventSource,
event_types,
getRequestHeaders,
getThumbnailUrl,
getUserAvatars,
name1,
saveMetadata,
saveSettingsDebounced,
setUserName,
this_chid,
user_avatar,
} from "../script.js";
import { getContext } from "./extensions.js";
import { persona_description_positions, power_user } from "./power-user.js";
import { getTokenCount } from "./tokenizers.js";
import { debounce, delay } from "./utils.js";
import { debounce, delay, download, parseJsonFile } from "./utils.js";
/**
* Uploads an avatar file to the server
@ -38,20 +51,70 @@ async function uploadUserAvatar(url, name) {
});
}
async function createDummyPersona() {
await uploadUserAvatar(default_avatar);
/**
* Prompts the user to create a persona for the uploaded avatar.
* @param {string} avatarId User avatar id
* @returns {Promise} Promise that resolves when the persona is set
*/
export async function createPersona(avatarId) {
const personaName = await callPopup('<h3>Enter a name for this persona:</h3>Cancel if you\'re just uploading an avatar.', 'input', '');
if (!personaName) {
console.debug('User cancelled creating a persona');
return;
}
await delay(500);
const personaDescription = await callPopup('<h3>Enter a description for this persona:</h3>You can always add or change it later.', 'input', '', { rows: 4 });
initPersona(avatarId, personaName, personaDescription);
if (power_user.persona_show_notifications) {
toastr.success(`You can now pick ${personaName} as a persona in the Persona Management menu.`, 'Persona Created');
}
}
async function convertCharacterToPersona() {
const avatarUrl = characters[this_chid]?.avatar;
async function createDummyPersona() {
const personaName = await callPopup('<h3>Enter a name for this persona:</h3>', 'input', '');
if (!personaName) {
console.debug('User cancelled creating dummy persona');
return;
}
// Date + name (only ASCII) to make it unique
const avatarId = `${Date.now()}-${personaName.replace(/[^a-zA-Z0-9]/g, '')}.png`;
initPersona(avatarId, personaName, '');
await uploadUserAvatar(default_avatar, avatarId);
}
/**
* Initializes a persona for the given avatar id.
* @param {string} avatarId User avatar id
* @param {string} personaName Name for the persona
* @param {string} personaDescription Optional description for the persona
* @returns {void}
*/
export function initPersona(avatarId, personaName, personaDescription) {
power_user.personas[avatarId] = personaName;
power_user.persona_descriptions[avatarId] = {
description: personaDescription || '',
position: persona_description_positions.IN_PROMPT,
};
saveSettingsDebounced();
}
export async function convertCharacterToPersona(characterId = null) {
if (null === characterId) characterId = this_chid;
const avatarUrl = characters[characterId]?.avatar;
if (!avatarUrl) {
console.log("No avatar found for this character");
return;
}
const name = characters[this_chid]?.name;
let description = characters[this_chid]?.description;
const name = characters[characterId]?.name;
let description = characters[characterId]?.description;
const overwriteName = `${name} (Persona).png`;
if (overwriteName in power_user.personas) {
@ -208,6 +271,12 @@ export function selectCurrentPersona() {
}
setPersonaDescription();
// force firstMes {{user}} update on persona switch
const context = getContext();
if (context.characterId >= 0 && !context.groupId && context.chat.length === 1) {
$("#firstmessage_textarea").trigger('input')
}
}
}
@ -440,6 +509,96 @@ function setChatLockedPersona() {
updateUserLockIcon();
}
function onBackupPersonas() {
const timestamp = new Date().toISOString().split('T')[0].replace(/-/g, '');
const filename = `personas_${timestamp}.json`;
const data = JSON.stringify({
"personas": power_user.personas,
"persona_descriptions": power_user.persona_descriptions,
"default_persona": power_user.default_persona,
}, null, 2);
const blob = new Blob([data], { type: 'application/json' });
download(blob, filename, 'application/json');
}
async function onPersonasRestoreInput(e) {
const file = e.target.files[0];
if (!file) {
console.debug('No file selected');
return;
}
const data = await parseJsonFile(file);
if (!data) {
toastr.warning('Invalid file selected', 'Persona Management');
console.debug('Invalid file selected');
return;
}
if (!data.personas || !data.persona_descriptions || typeof data.personas !== 'object' || typeof data.persona_descriptions !== 'object') {
toastr.warning('Invalid file format', 'Persona Management');
console.debug('Invalid file selected');
return;
}
const avatarsList = await getUserAvatars();
const warnings = [];
// Merge personas with existing ones
for (const [key, value] of Object.entries(data.personas)) {
if (key in power_user.personas) {
warnings.push(`Persona "${key}" (${value}) already exists, skipping`);
continue;
}
power_user.personas[key] = value;
// If the avatar is missing, upload it
if (!avatarsList.includes(key)) {
warnings.push(`Persona image "${key}" (${value}) is missing, uploading default avatar`);
await uploadUserAvatar(default_avatar, key);
}
}
// Merge persona descriptions with existing ones
for (const [key, value] of Object.entries(data.persona_descriptions)) {
if (key in power_user.persona_descriptions) {
warnings.push(`Persona description for "${key}" (${power_user.personas[key]}) already exists, skipping`);
continue;
}
if (!power_user.personas[key]) {
warnings.push(`Persona for "${key}" does not exist, skipping`);
continue;
}
power_user.persona_descriptions[key] = value;
}
if (data.default_persona) {
if (data.default_persona in power_user.personas) {
power_user.default_persona = data.default_persona;
} else {
warnings.push(`Default persona "${data.default_persona}" does not exist, skipping`);
}
}
if (warnings.length) {
toastr.success('Personas restored with warnings. Check console for details.');
console.warn(`PERSONA RESTORE REPORT\n====================\n${warnings.join('\n')}`);
} else {
toastr.success('Personas restored successfully.');
}
await getUserAvatars();
setPersonaDescription();
saveSettingsDebounced();
$('#personas_restore_input').val('');
}
export function initPersonas() {
$(document).on('click', '.bind_user_name', bindUserNameToPersona);
$(document).on('click', '.set_default_persona', setDefaultPersona);
@ -448,6 +607,9 @@ export function initPersonas() {
$("#create_dummy_persona").on('click', createDummyPersona);
$('#persona_description').on('input', onPersonaDescriptionInput);
$('#persona_description_position').on('input', onPersonaDescriptionPositionInput);
$('#personas_backup').on('click', onBackupPersonas);
$('#personas_restore').on('click', () => $('#personas_restore_input').trigger('click'));
$('#personas_restore_input').on('change', onPersonasRestoreInput);
eventSource.on("charManagementDropdown", (target) => {
if (target === 'convert_to_persona') {

View File

@ -15,6 +15,9 @@ import {
setCharacterId,
setEditedMessageId,
renderTemplate,
chat,
getFirstDisplayedMessageId,
showMoreMessages,
} from "../script.js";
import { isMobile, initMovingUI, favsToHotswap } from "./RossAscends-mods.js";
import {
@ -28,9 +31,10 @@ import {
} from "./instruct-mode.js";
import { registerSlashCommand } from "./slash-commands.js";
import { tags } from "./tags.js";
import { tokenizers } from "./tokenizers.js";
import { countOccurrences, debounce, delay, isOdd, resetScrollHeight, sortMoments, timestampToMoment } from "./utils.js";
import { countOccurrences, debounce, delay, isOdd, resetScrollHeight, sortMoments, stringToRange, timestampToMoment } from "./utils.js";
export {
loadPowerUserSettings,
@ -45,7 +49,7 @@ export {
};
export const MAX_CONTEXT_DEFAULT = 8192;
const MAX_CONTEXT_UNLOCKED = 65536;
const MAX_CONTEXT_UNLOCKED = 200 * 1000;
const unlockedMaxContextStep = 256;
const maxContextMin = 512;
const maxContextStep = 64;
@ -62,6 +66,7 @@ export const ui_mode = {
const avatar_styles = {
ROUND: 0,
RECTANGULAR: 1,
SQUARE: 2,
}
export const chat_styles = {
@ -103,6 +108,7 @@ let power_user = {
target_length: 400,
},
markdown_escape_strings: '',
chat_truncation: 100,
ui_mode: ui_mode.POWER,
fast_ui_mode: true,
@ -162,6 +168,8 @@ let power_user = {
max_context_unlocked: false,
message_token_count_enabled: false,
expand_message_actions: false,
enableZenSliders: false,
enableLabMode: false,
prefer_character_prompt: true,
prefer_character_jailbreak: true,
quick_continue: false,
@ -213,6 +221,9 @@ let power_user = {
fuzzy_search: false,
encode_tags: false,
servers: [],
bogus_folders: false,
aux_field: 'character_version',
restore_user_input: true,
};
let themes = [];
@ -251,6 +262,8 @@ const storage_keys = {
mesIDDisplay_enabled: 'mesIDDisplayEnabled',
message_token_count_enabled: 'MessageTokenCountEnabled',
expand_message_actions: 'ExpandMessageActions',
enableZenSliders: 'enableZenSliders',
enableLabMode: 'enableLabMode',
};
const contextControls = [
@ -419,6 +432,352 @@ function switchMessageActions() {
$('.extraMesButtons, .extraMesButtonsHint').removeAttr('style');
}
var originalSliderValues = []
async function switchLabMode() {
/* if (power_user.enableZenSliders && power_user.enableLabMode) {
toastr.warning("Can't start Lab Mode while Zen Sliders are active")
return
//$("#enableZenSliders").trigger('click')
}
*/
await delay(100)
const value = localStorage.getItem(storage_keys.enableLabMode);
power_user.enableLabMode = value === null ? false : value == "true";
$("body").toggleClass("enableLabMode", power_user.enableLabMode);
$("#enableLabMode").prop("checked", power_user.enableLabMode);
if (power_user.enableLabMode) {
//save all original slider values into an array
$("#advanced-ai-config-block input").each(function () {
let id = $(this).attr('id')
let min = $(this).attr('min')
let max = $(this).attr('max')
let step = $(this).attr('step')
originalSliderValues.push({ id, min, max, step });
})
//console.log(originalSliderValues)
//remove limits on all inputs and hide sliders
$("#advanced-ai-config-block input")
.attr('min', '-99999')
.attr('max', '99999')
.attr('step', '0.001')
$("#labModeWarning").removeClass('displayNone')
//$("#advanced-ai-config-block input[type='range']").hide()
} else {
//re apply the original sliders values to each input
originalSliderValues.forEach(function (slider) {
$("#" + slider.id)
.attr('min', slider.min)
.attr('max', slider.max)
.attr('step', slider.step)
.trigger('input')
});
$("#advanced-ai-config-block input[type='range']").show()
$("#labModeWarning").addClass('displayNone')
}
}
async function switchZenSliders() {
await delay(100)
const value = localStorage.getItem(storage_keys.enableZenSliders);
power_user.enableZenSliders = value === null ? false : value == "true";
$("body").toggleClass("enableZenSliders", power_user.enableZenSliders);
$("#enableZenSliders").prop("checked", power_user.enableZenSliders);
if (power_user.enableZenSliders) {
$("#clickSlidersTips").hide()
$("#pro-settings-block input[type='number']").hide();
//hide number inputs that are not 'seed' inputs
$(`#textgenerationwebui_api-settings :input[type='number']:not([id^='seed']),
#kobold_api-settings :input[type='number']:not([id^='seed'])`).hide()
//hide original sliders
//exclude max context because its creation is handled by switchMaxContext()
$(`#textgenerationwebui_api-settings input[type='range'],
#kobold_api-settings input[type='range'],
#pro-settings-block input[type='range']:not(#max_context)`)
.hide()
.each(function () {
//make a zen slider for each original slider
CreateZenSliders($(this))
})
//this is for when zensliders is toggled after pageload
switchMaxContextSize()
} else {
$("#clickSlidersTips").show()
revertOriginalSliders();
}
function revertOriginalSliders() {
$(`#pro-settings-block input[type='number']`).show();
$(`#textgenerationwebui_api-settings input[type='number'],
#kobold_api-settings input[type='number']`).show();
$(`#textgenerationwebui_api-settings input[type='range'],
#kobold_api-settings input[type='range'],
#pro-settings-block input[type='range']`).each(function () {
$(this).show();
});
$('div[id$="_zenslider"]').remove();
}
}
async function CreateZenSliders(elmnt) {
//await delay(100)
var originalSlider = elmnt;
var sliderID = originalSlider.attr('id')
var sliderMin = Number(originalSlider.attr('min'))
var sliderMax = Number(originalSlider.attr('max'))
var sliderValue = originalSlider.val();
var sliderRange = sliderMax - sliderMin
var numSteps = 10
var decimals = 2
var offVal
if (sliderID == 'amount_gen') {
decimals = 0
var steps = [16, 50, 100, 150, 200, 256, 300, 400, 512, 1024];
sliderMin = 0
sliderMax = steps.length - 1
stepScale = 1;
numSteps = 10
sliderValue = steps.indexOf(Number(sliderValue))
if (sliderValue === -1) { sliderValue = 4 } // default to '200' if origSlider has value we can't use
}
//customize decimals
if (sliderID == 'max_context' ||
sliderID == 'mirostat_mode_textgenerationwebui' ||
sliderID == 'mirostat_tau_textgenerationwebui' ||
sliderID == 'top_k_textgenerationwebui' ||
sliderID == 'num_beams_textgenerationwebui' ||
sliderID == 'no_repeat_ngram_size_textgenerationwebui' ||
sliderID == 'min_length_textgenerationwebui' ||
sliderID == 'top_k' ||
sliderID == 'mirostat_mode_kobold' ||
sliderID == 'rep_pen_range') {
decimals = 0
}
if (sliderID == 'eta_cutoff_textgenerationwebui' ||
sliderID == 'epsilon_cutoff_textgenerationwebui') {
numSteps = 50
decimals = 1
}
//customize steps
if (sliderID == 'mirostat_mode_textgenerationwebui' ||
sliderID == 'mirostat_mode_kobold') {
numSteps = 2
}
if (sliderID == 'encoder_rep_pen_textgenerationwebui') {
numSteps = 14
}
if (sliderID == 'max_context') {
numSteps = 15
}
if (sliderID == 'rep_pen_range_textgenerationwebui') {
numSteps = 16
}
if (sliderID == 'mirostat_tau_textgenerationwebui' ||
sliderID == 'top_k_textgenerationwebui' ||
sliderID == 'num_beams_textgenerationwebui' ||
sliderID == 'no_repeat_ngram_size_textgenerationwebui' ||
sliderID == 'epsilon_cutoff_textgenerationwebui' ||
sliderID == 'tfs_textgenerationwebui' ||
sliderID == 'min_p_textgenerationwebui' ||
sliderID == 'temp_textgenerationwebui' ||
sliderID == 'temp') {
numSteps = 20
}
if (sliderID == 'mirostat_eta_textgenerationwebui' ||
sliderID == 'penalty_alpha_textgenerationwebui' ||
sliderID == 'length_penalty_textgenerationwebui') {
numSteps = 50
}
//customize off values
if (sliderID == 'presence_pen_textgenerationwebui' ||
sliderID == 'freq_pen_textgenerationwebui' ||
sliderID == 'mirostat_mode_textgenerationwebui' ||
sliderID == 'mirostat_mode_kobold' ||
sliderID == 'mirostat_tau_textgenerationwebui' ||
sliderID == 'mirostat_tau_kobold' ||
sliderID == 'mirostat_eta_textgenerationwebui' ||
sliderID == 'mirostat_eta_kobold' ||
sliderID == 'min_p_textgenerationwebui' ||
sliderID == 'min_p' ||
sliderID == 'no_repeat_ngram_size_textgenerationwebui' ||
sliderID == 'penalty_alpha_textgenerationwebui' ||
sliderID == 'length_penalty_textgenerationwebui' ||
sliderID == 'epsilon_cutoff_textgenerationwebui' ||
sliderID == 'rep_pen_range_textgenerationwebui' ||
sliderID == 'rep_pen_range' ||
sliderID == 'eta_cutoff_textgenerationwebui' ||
sliderID == 'top_a_textgenerationwebui' ||
sliderID == 'top_a' ||
sliderID == 'top_k_textgenerationwebui' ||
sliderID == 'top_k' ||
sliderID == 'rep_pen_slope' ||
sliderID == 'min_length_textgenerationwebui') {
offVal = 0
}
if (sliderID == 'rep_pen_textgenerationwebui' ||
sliderID == 'rep_pen' ||
sliderID == 'tfs_textgenerationwebui' ||
sliderID == 'tfs' ||
sliderID == 'top_p_textgenerationwebui' ||
sliderID == 'top_p' ||
sliderID == 'num_beams_textgenerationwebui' ||
sliderID == 'typical_p_textgenerationwebui' ||
sliderID == 'typical_p' ||
sliderID == 'encoder_rep_pen_textgenerationwebui' ||
sliderID == 'temp_textgenerationwebui' ||
sliderID == 'temp' ||
sliderID == 'guidance_scale_textgenerationwebui' ||
sliderID == 'guidance_scale') {
offVal = 1
}
if (sliderID == 'guidance_scale_textgenerationwebui') {
numSteps = 78
}
//customize amt gen steps
if (sliderID !== 'amount_gen') {
var stepScale = sliderRange / numSteps
}
var newSlider = $("<div>")
.attr('id', `${sliderID}_zenslider`)
.css("width", "100%")
.insertBefore(originalSlider);
newSlider.slider({
value: sliderValue,
step: stepScale,
min: sliderMin,
max: sliderMax,
create: function () {
var handle = $(this).find(".ui-slider-handle");
//handling creaetion of amt_gen
if (newSlider.attr('id') == 'amount_gen_zenslider') {
var handleText = steps[sliderValue]
var stepNumber = sliderValue
var leftMargin = ((stepNumber) / numSteps) * 50 * -1
handle.text(handleText)
.css('margin-left', `${leftMargin}px`)
//console.log(`initial value:${handleText}, stepNum:${stepNumber}, numSteps:${numSteps}, left-margin:${leftMargin}`)
} else {
//handling creation for all other sliders
var numVal = Number(sliderValue).toFixed(decimals)
offVal = Number(offVal).toFixed(decimals)
//console.log(`${sliderID}: offVal ${offVal}`)
if (numVal === offVal) {
handle.text('Off').css('color', 'rgba(128,128,128,0.5');
} else {
handle.text(numVal).css('color', '');
}
var stepNumber = ((sliderValue - sliderMin) / stepScale)
var leftMargin = (stepNumber / numSteps) * 50 * -1
var isManualInput = false
var valueBeforeManualInput
handle.css('margin-left', `${leftMargin}px`)
.attr('contenteditable', 'true')
.on('click', function () {
//this just selects all the text in the handle so user can overwrite easily
//needed because JQUery UI uses left/right arrow keys as well as home/end to move the slider..
valueBeforeManualInput = newSlider.val()
console.log(valueBeforeManualInput)
let handleElement = handle.get(0);
let range = document.createRange();
range.selectNodeContents(handleElement);
let selection = window.getSelection();
selection.removeAllRanges();
selection.addRange(range);
})
.on('keyup', function () {
valueBeforeManualInput = newSlider.val()
console.log(valueBeforeManualInput)
isManualInput = true
})
//trigger slider changes when user clicks away
.on('mouseup blur', function () {
let manualInput = parseFloat(handle.text()).toFixed(decimals)
if (isManualInput) {
//disallow manual inputs outside acceptable range
if (manualInput >= sliderMin && manualInput <= sliderMax) {
//if value is ok, assign to slider and update handle text and position
newSlider.val(manualInput)
handleSlideEvent.call(newSlider, null, { value: parseFloat(manualInput) }, 'manual');
valueBeforeManualInput = manualInput
} else {
//if value not ok, warn and reset to last known valid value
toastr.warning(`Invalid value. Must be between ${sliderMin} and ${sliderMax}`)
console.log(valueBeforeManualInput)
newSlider.val(valueBeforeManualInput)
handle.text(valueBeforeManualInput)
}
}
isManualInput = false
})
console.debug(sliderID, sliderValue, handleText, stepNumber, stepScale)
}
},
slide: handleSlideEvent
});
function handleSlideEvent(event, ui, type) {
var handle = $(this).find(".ui-slider-handle");
var numVal = Number(ui.value).toFixed(decimals);
offVal = Number(offVal).toFixed(decimals);
//console.log(numVal, sliderMin, sliderMax, numVal > sliderMax, numVal < sliderMin)
if (numVal > sliderMax) {
//console.log('clamping numVal to sliderMax')
numVal = sliderMax
}
if (numVal < sliderMin) {
//console.log('clamping numVal to sliderMin')
numVal = sliderMin
}
var sliderValRange = sliderMax - sliderMin
var stepNumber = ((ui.value - sliderMin) / stepScale).toFixed(0);
var handleText = (ui.value);
var leftMargin = (stepNumber / numSteps) * 50 * -1;
var percentOfMax = Number((ui.value / sliderMax)) //what % our value is of the max
var perStepPercent = 1 / numSteps //how far in % each step should be on the slider
var leftPos = newSlider.width() * (stepNumber * perStepPercent) //how big of a left margin to give the slider for manual inputs
/* console.log(`
numVal: ${numVal},
sliderMax: ${sliderMax}
sliderMin: ${sliderMin}
sliderValRange: ${sliderValRange}
stepScale: ${stepScale}
Step: ${stepNumber} of ${numSteps}
offVal: ${offVal}
initial value: ${handleText}
left-margin: ${leftMargin}
width: ${newSlider.width()}
percent of max: ${percentOfMax}
left: ${leftPos}`) */
//special handling for response length slider, pulls text aliases for step values from an array
if (newSlider.attr('id') == 'amount_gen_zenslider') {
handleText = steps[stepNumber]
handle.text(handleText);
newSlider.val(stepNumber)
}
//everything else uses the flat slider value
else {
//show 'off' if disabled value is set
if (numVal === offVal) { handle.text('Off').css('color', 'rgba(128,128,128,0.5'); }
else { handle.text(ui.value.toFixed(decimals)).css('color', ''); }
newSlider.val(handleText)
}
//for manually typed-in values we must adjust left position because JQUI doesn't do it for us
//if (type === 'manual') {
handle.css('left', leftPos)
//}
//adjust a negative left margin to avoid overflowing right side of slider body
handle.css('margin-left', `${leftMargin}px`);
originalSlider.val(handleText);
originalSlider.trigger('input');
originalSlider.trigger('change');
}
originalSlider.data("newSlider", newSlider);
await delay(1)
originalSlider.hide();
};
function switchUiMode() {
const fastUi = localStorage.getItem(storage_keys.fast_ui_mode);
power_user.fast_ui_mode = fastUi === null ? true : fastUi == "true";
@ -499,6 +858,7 @@ function noShadows() {
function applyAvatarStyle() {
power_user.avatar_style = Number(localStorage.getItem(storage_keys.avatar_style) ?? avatar_styles.ROUND);
$("body").toggleClass("big-avatars", power_user.avatar_style === avatar_styles.RECTANGULAR);
$("body").toggleClass("square-avatars", power_user.avatar_style === avatar_styles.SQUARE);
$("#avatar_style").val(power_user.avatar_style).prop("selected", true);
//$(`input[name="avatar_style"][value="${power_user.avatar_style}"]`).prop("checked", true);
@ -560,6 +920,11 @@ function applyChatWidth(type) {
async function applyThemeColor(type) {
if (type === 'main') {
document.documentElement.style.setProperty('--SmartThemeBodyColor', power_user.main_text_color);
const color = power_user.main_text_color.split('(')[1].split(')')[0].split(',');
document.documentElement.style.setProperty('--SmartThemeCheckboxBgColorR', color[0]);
document.documentElement.style.setProperty('--SmartThemeCheckboxBgColorG', color[1]);
document.documentElement.style.setProperty('--SmartThemeCheckboxBgColorB', color[2]);
document.documentElement.style.setProperty('--SmartThemeCheckboxBgColorA', color[3]);
}
if (type === 'italics') {
document.documentElement.style.setProperty('--SmartThemeEmColor', power_user.italics_text_color);
@ -780,13 +1145,34 @@ async function applyTheme(name) {
switchMessageActions();
}
},
{
key: 'enableZenSliders',
action: async () => {
localStorage.setItem(storage_keys.enableZenSliders, Boolean(power_user.enableZenSliders));
switchMessageActions();
}
},
{
key: 'enableLabMode',
action: async () => {
localStorage.setItem(storage_keys.enableLabMode, Boolean(power_user.enableLabMode));
switchMessageActions();
}
},
{
key: 'hotswap_enabled',
action: async () => {
localStorage.setItem(storage_keys.hotswap_enabled, Boolean(power_user.hotswap_enabled));
switchHotswap();
}
}
},
{
key: 'bogus_folders',
action: async () => {
$('#bogus_folders').prop('checked', power_user.bogus_folders);
await printCharacters(true);
},
},
];
for (const { key, selector, type, action } of themeProperties) {
@ -894,6 +1280,8 @@ function loadPowerUserSettings(settings, data) {
const timestamps = localStorage.getItem(storage_keys.timestamps_enabled);
const mesIDDisplay = localStorage.getItem(storage_keys.mesIDDisplay_enabled);
const expandMessageActions = localStorage.getItem(storage_keys.expand_message_actions);
const enableZenSliders = localStorage.getItem(storage_keys.enableZenSliders);
const enableLabMode = localStorage.getItem(storage_keys.enableLabMode);
power_user.fast_ui_mode = fastUi === null ? true : fastUi == "true";
power_user.movingUI = movingUI === null ? false : movingUI == "true";
power_user.noShadows = noShadows === null ? false : noShadows == "true";
@ -902,6 +1290,8 @@ function loadPowerUserSettings(settings, data) {
power_user.timestamps_enabled = timestamps === null ? true : timestamps == "true";
power_user.mesIDDisplay_enabled = mesIDDisplay === null ? true : mesIDDisplay == "true";
power_user.expand_message_actions = expandMessageActions === null ? true : expandMessageActions == "true";
power_user.enableZenSliders = enableZenSliders === null ? false : enableZenSliders == "true";
power_user.enableLabMode = enableLabMode === null ? false : enableLabMode == "true";
power_user.avatar_style = Number(localStorage.getItem(storage_keys.avatar_style) ?? avatar_styles.ROUND);
//power_user.chat_display = Number(localStorage.getItem(storage_keys.chat_display) ?? chat_styles.DEFAULT);
power_user.chat_width = Number(localStorage.getItem(storage_keys.chat_width) ?? 50);
@ -947,6 +1337,7 @@ function loadPowerUserSettings(settings, data) {
$("#console_log_prompts").prop("checked", power_user.console_log_prompts);
$('#auto_fix_generated_markdown').prop("checked", power_user.auto_fix_generated_markdown);
$('#auto_scroll_chat_to_bottom').prop("checked", power_user.auto_scroll_chat_to_bottom);
$('#bogus_folders').prop("checked", power_user.bogus_folders);
$(`#tokenizer option[value="${power_user.tokenizer}"]`).attr('selected', true);
$(`#send_on_enter option[value=${power_user.send_on_enter}]`).attr("selected", true);
$("#import_card_tags").prop("checked", power_user.import_card_tags);
@ -983,10 +1374,17 @@ function loadPowerUserSettings(settings, data) {
$("#mesIDDisplayEnabled").prop("checked", power_user.mesIDDisplay_enabled);
$("#prefer_character_prompt").prop("checked", power_user.prefer_character_prompt);
$("#prefer_character_jailbreak").prop("checked", power_user.prefer_character_jailbreak);
$("#enableZenSliders").prop('checked', power_user.enableZenSliders).trigger('input');
$("#enableLabMode").prop('checked', power_user.enableLabMode).trigger('input');
$(`input[name="avatar_style"][value="${power_user.avatar_style}"]`).prop("checked", true);
$(`#chat_display option[value=${power_user.chat_display}]`).attr("selected", true).trigger('change');
$('#chat_width_slider').val(power_user.chat_width);
$("#token_padding").val(power_user.token_padding);
$("#aux_field").val(power_user.aux_field);
$("#restore_user_input").prop("checked", power_user.restore_user_input);
$("#chat_truncation").val(power_user.chat_truncation);
$('#chat_truncation_counter').val(power_user.chat_truncation);
$("#font_scale").val(power_user.font_scale);
$("#font_scale_counter").val(power_user.font_scale);
@ -1087,16 +1485,17 @@ function loadMaxContextUnlocked() {
}
function switchMaxContextSize() {
const elements = [$('#max_context'), $('#rep_pen_range'), $('#rep_pen_range_textgenerationwebui')];
const elements = [$('#max_context'), $('#max_context_counter'), $('#rep_pen_range'), $('#rep_pen_range_textgenerationwebui')];
const maxValue = power_user.max_context_unlocked ? MAX_CONTEXT_UNLOCKED : MAX_CONTEXT_DEFAULT;
const minValue = power_user.max_context_unlocked ? maxContextMin : maxContextMin;
const steps = power_user.max_context_unlocked ? unlockedMaxContextStep : maxContextStep;
for (const element of elements) {
const id = element.attr('id');
element.attr('max', maxValue);
element.attr('step', steps);
if (element.attr('id') == 'max_context') {
if (typeof id === 'string' && id?.indexOf('max_context') !== -1) {
element.attr('min', minValue);
}
const value = Number(element.val());
@ -1105,6 +1504,10 @@ function switchMaxContextSize() {
element.val(maxValue).trigger('input');
}
}
if (power_user.enableZenSliders) {
$("#max_context_zenslider").remove()
CreateZenSliders($("#max_context"))
}
}
// Fetch a compiled object of all preset settings
@ -1279,6 +1682,22 @@ export function fuzzySearchWorldInfo(data, searchValue) {
return results.map(x => x.item?.uid);
}
export function fuzzySearchTags(searchValue) {
const fuse = new Fuse(tags, {
keys: [
{ name: 'name', weight: 1 },
],
includeScore: true,
ignoreLocation: true,
threshold: 0.2
});
const results = fuse.search(searchValue);
console.debug('Tags fuzzy search results for ' + searchValue, results);
const ids = results.map(x => String(x.item?.id)).filter(x => x);
return ids;
}
export function fuzzySearchGroups(searchValue) {
const fuse = new Fuse(groups, {
keys: [
@ -1365,7 +1784,17 @@ function sortEntitiesList(entities) {
return;
}
entities.sort((a, b) => sortFunc(a.item, b.item));
entities.sort((a, b) => {
if (a.type === 'tag' && b.type !== 'tag') {
return -1;
}
if (a.type !== 'tag' && b.type === 'tag') {
return 1;
}
return sortFunc(a.item, b.item);
});
}
async function saveTheme() {
@ -1402,11 +1831,11 @@ async function saveTheme() {
mesIDDisplay_enabled: power_user.mesIDDisplay_enabled,
message_token_count_enabled: power_user.message_token_count_enabled,
expand_message_actions: power_user.expand_message_actions,
enableZenSliders: power_user.enableZenSliders,
enableLabMode: power_user.enableLabMode,
hotswap_enabled: power_user.hotswap_enabled,
custom_css: power_user.custom_css,
bogus_folders: power_user.bogus_folders,
};
const response = await fetch('/savetheme', {
@ -1554,40 +1983,80 @@ function doNewChat() {
}, 1);
}
function doRandomChat() {
async function doRandomChat() {
resetSelectedGroup();
setCharacterId(Math.floor(Math.random() * characters.length).toString());
setTimeout(() => {
reloadCurrentChat();
}, 1);
const characterId = Math.floor(Math.random() * characters.length).toString();
setCharacterId(characterId);
await delay(1);
await reloadCurrentChat();
return characters[characterId]?.name;
}
/**
* Loads the chat until the given message ID is displayed.
* @param {number} mesId
* @returns JQuery<HTMLElement>
*/
async function loadUntilMesId(mesId) {
let target;
while (getFirstDisplayedMessageId() > mesId && getFirstDisplayedMessageId() !== 0) {
showMoreMessages();
await delay(1);
target = $("#chat").find(`.mes[mesid=${mesId}]`);
if (target.length) {
break;
}
}
if (!target.length) {
toastr.error(`Could not find message with ID: ${mesId}`)
return target;
}
return target;
}
async function doMesCut(_, text) {
console.debug(`was asked to cut message id #${text}`)
const range = stringToRange(text, 0, chat.length - 1);
//reject invalid args or no args
if (text && isNaN(text) || !text) {
toastr.error(`Must enter a single number only, non-number characters disallowed.`)
if (!range) {
toastr.warning(`Must provide a Message ID or a range to cut.`)
return
}
let mesIDToCut = Number(text).toFixed(0)
let mesToCut = $("#chat").find(`.mes[mesid=${mesIDToCut}]`)
let totalMesToCut = (range.end - range.start) + 1;
let mesIDToCut = range.start;
if (!mesToCut.length) {
toastr.error(`Could not find message with ID: ${mesIDToCut}`)
return
for (let i = 0; i < totalMesToCut; i++) {
let done = false;
let mesToCut = $("#chat").find(`.mes[mesid=${mesIDToCut}]`)
if (!mesToCut.length) {
mesToCut = await loadUntilMesId(mesIDToCut);
if (!mesToCut || !mesToCut.length) {
return;
}
}
setEditedMessageId(mesIDToCut);
eventSource.once(event_types.MESSAGE_DELETED, () => {
done = true;
});
mesToCut.find('.mes_edit_delete').trigger('click', { fromSlashCommand: true });
while (!done) {
await delay(1);
}
}
setEditedMessageId(mesIDToCut);
mesToCut.find('.mes_edit_delete').trigger('click', { fromSlashCommand: true });
}
async function doDelMode(_, text) {
//first enter delmode
$("#option_delete_mes").trigger('click')
$("#option_delete_mes").trigger('click', { fromSlashCommand: true });
//reject invalid args
if (text && isNaN(text)) {
@ -1602,15 +2071,24 @@ async function doDelMode(_, text) {
await delay(300) //same as above, need event signal for 'entered del mode'
console.debug('parsing msgs to del')
let numMesToDel = Number(text);
let lastMesID = Number($('.last_mes').attr('mesid'));
let lastMesID = Number($('#chat .mes').last().attr('mesid'));
let oldestMesIDToDel = lastMesID - numMesToDel + 1;
//disallow targeting first message
if (oldestMesIDToDel <= 0) {
oldestMesIDToDel = 1
if (oldestMesIDToDel < 0) {
toastr.warning(`Cannot delete more than ${chat.length} messages.`)
return;
}
let oldestMesToDel = $('#chat').find(`.mes[mesid=${oldestMesIDToDel}]`)
if (!oldestMesIDToDel) {
oldestMesToDel = await loadUntilMesId(oldestMesIDToDel);
if (!oldestMesToDel || !oldestMesToDel.length) {
return;
}
}
let oldestDelMesCheckbox = $(oldestMesToDel).find('.del_checkbox');
let newLastMesID = oldestMesIDToDel - 1;
console.debug(`DelMesReport -- numMesToDel: ${numMesToDel}, lastMesID: ${lastMesID}, oldestMesIDToDel:${oldestMesIDToDel}, newLastMesID: ${newLastMesID}`)
@ -1856,6 +2334,71 @@ function setAvgBG() {
}
async function setThemeCallback(_, text) {
const fuse = new Fuse(themes, {
keys: [
{ name: 'name', weight: 1 },
],
});
const results = fuse.search(text);
console.debug('Theme fuzzy search results for ' + text, results);
const theme = results[0]?.item;
if (!theme) {
toastr.warning(`Could not find theme with name: ${text}`);
return;
}
power_user.theme = theme.name;
applyTheme(theme.name);
$("#themes").val(theme.name);
saveSettingsDebounced();
}
async function setmovingUIPreset(_, text) {
const fuse = new Fuse(movingUIPresets, {
keys: [
{ name: 'name', weight: 1 },
],
});
const results = fuse.search(text);
console.debug('movingUI preset fuzzy search results for ' + text, results);
const preset = results[0]?.item;
if (!preset) {
toastr.warning(`Could not find preset with name: ${text}`);
return;
}
power_user.movingUIPreset = preset.name;
applyMovingUIPreset(preset.name);
$("#movingUIPresets").val(preset.name);
saveSettingsDebounced();
}
const EPHEMERAL_STOPPING_STRINGS = [];
/**
* Adds a stopping string to the list of stopping strings that are only used for the next generation.
* @param {string} value The stopping string to add
*/
export function addEphemeralStoppingString(value) {
if (!EPHEMERAL_STOPPING_STRINGS.includes(value)) {
console.debug('Adding ephemeral stopping string:', value);
EPHEMERAL_STOPPING_STRINGS.push(value);
}
}
export function flushEphemeralStoppingStrings() {
if (EPHEMERAL_STOPPING_STRINGS.length === 0) {
return;
}
console.debug('Flushing ephemeral stopping strings:', EPHEMERAL_STOPPING_STRINGS);
EPHEMERAL_STOPPING_STRINGS.splice(0, EPHEMERAL_STOPPING_STRINGS.length);
}
/**
* Gets the custom stopping strings from the power user settings.
@ -1863,39 +2406,47 @@ function setAvgBG() {
* @returns {string[]} An array of custom stopping strings
*/
export function getCustomStoppingStrings(limit = undefined) {
try {
// If there's no custom stopping strings, return an empty array
if (!power_user.custom_stopping_strings) {
function getPermanent() {
try {
// If there's no custom stopping strings, return an empty array
if (!power_user.custom_stopping_strings) {
return [];
}
// Parse the JSON string
let strings = JSON.parse(power_user.custom_stopping_strings);
// Make sure it's an array
if (!Array.isArray(strings)) {
return [];
}
// Make sure all the elements are strings and non-empty.
strings = strings.filter(s => typeof s === 'string' && s.length > 0);
// Substitute params if necessary
if (power_user.custom_stopping_strings_macro) {
strings = strings.map(x => substituteParams(x));
}
return strings;
} catch (error) {
// If there's an error, return an empty array
console.warn('Error parsing custom stopping strings:', error);
return [];
}
// Parse the JSON string
let strings = JSON.parse(power_user.custom_stopping_strings);
// Make sure it's an array
if (!Array.isArray(strings)) {
return [];
}
// Make sure all the elements are strings and non-empty.
strings = strings.filter(s => typeof s === 'string' && s.length > 0);
// Substitute params if necessary
if (power_user.custom_stopping_strings_macro) {
strings = strings.map(x => substituteParams(x));
}
// Apply the limit. If limit is 0, return all strings.
if (limit > 0) {
strings = strings.slice(0, limit);
}
return strings;
} catch (error) {
// If there's an error, return an empty array
console.warn('Error parsing custom stopping strings:', error);
return [];
}
const permanent = getPermanent();
const ephemeral = EPHEMERAL_STOPPING_STRINGS;
const strings = [...permanent, ...ephemeral];
// Apply the limit. If limit is 0, return all strings.
if (limit > 0) {
return strings.slice(0, limit);
}
return strings;
}
$(document).ready(() => {
@ -2073,6 +2624,12 @@ $(document).ready(() => {
setHotswapsDebounced();
});
$('#chat_truncation').on('input', function () {
power_user.chat_truncation = Number($('#chat_truncation').val());
$('#chat_truncation_counter').val(power_user.chat_truncation);
saveSettingsDebounced();
});
$(`input[name="font_scale"]`).on('input', async function (e) {
power_user.font_scale = Number(e.target.value);
$("#font_scale_counter").val(power_user.font_scale);
@ -2346,6 +2903,35 @@ $(document).ready(() => {
switchMessageActions();
});
$("#enableZenSliders").on("input", function () {
const value = !!$(this).prop('checked');
if (power_user.enableLabMode === true && value === true) {
//disallow zenSliders while Lab Mode is active
toastr.warning('Disable Mad Lab Mode before enabling Zen Sliders')
$(this).prop('checked', false).trigger('input');
return
}
power_user.enableZenSliders = value;
localStorage.setItem(storage_keys.enableZenSliders, Boolean(power_user.enableZenSliders));
saveSettingsDebounced();
switchZenSliders();
});
$("#enableLabMode").on("input", function () {
const value = !!$(this).prop('checked');
if (power_user.enableZenSliders === true && value === true) {
//disallow Lab Mode if ZenSliders are active
toastr.warning('Disable Zen Sliders before enabling Mad Lab Mode')
$(this).prop('checked', false).trigger('input');;
return
}
power_user.enableLabMode = value;
localStorage.setItem(storage_keys.enableLabMode, Boolean(power_user.enableLabMode));
saveSettingsDebounced();
switchLabMode();
});
$("#mesIDDisplayEnabled").on("input", function () {
const value = !!$(this).prop('checked');
power_user.mesIDDisplay_enabled = value;
@ -2456,6 +3042,25 @@ $(document).ready(() => {
switchSimpleMode();
});
$('#bogus_folders').on('input', function () {
const value = !!$(this).prop('checked');
power_user.bogus_folders = value;
saveSettingsDebounced();
printCharacters(true);
});
$('#aux_field').on('change', function () {
const value = $(this).find(':selected').val();
power_user.aux_field = String(value);
saveSettingsDebounced();
printCharacters(false);
});
$('#restore_user_input').on('input', function () {
power_user.restore_user_input = !!$(this).prop('checked');
saveSettingsDebounced();
});
$(document).on('click', '#debug_table [data-debug-function]', function () {
const functionId = $(this).data('debug-function');
const functionRecord = debug_functions.find(f => f.functionId === functionId);
@ -2478,8 +3083,10 @@ $(document).ready(() => {
registerSlashCommand('vn', toggleWaifu, [], ' swaps Visual Novel Mode On/Off', false, true);
registerSlashCommand('newchat', doNewChat, [], ' start a new chat with current character', true, true);
registerSlashCommand('random', doRandomChat, [], ' start a new chat with a random character', true, true);
registerSlashCommand('delmode', doDelMode, ['del'], '<span class="monospace">(optional number)</span> enter message deletion mode, and auto-deletes N messages if numeric argument is provided', true, true);
registerSlashCommand('cut', doMesCut, [], '<span class="monospace">(number)</span> cuts the specified message from the chat', true, true);
registerSlashCommand('delmode', doDelMode, ['del'], '<span class="monospace">(optional number)</span> enter message deletion mode, and auto-deletes last N messages if numeric argument is provided', true, true);
registerSlashCommand('cut', doMesCut, [], '<span class="monospace">(number or range)</span> cuts the specified message or continuous chunk from the chat, e.g. <tt>/cut 0-10</tt>. Ranges are inclusive!', true, true);
registerSlashCommand('resetpanels', doResetPanels, ['resetui'], ' resets UI panels to original state.', true, true);
registerSlashCommand('bgcol', setAvgBG, [], ' WIP test of auto-bg avg coloring', true, true);
registerSlashCommand('theme', setThemeCallback, [], '<span class="monospace">(name)</span> sets a UI theme by name', true, true);
registerSlashCommand('movingui', setmovingUIPreset, [], '<span class="monospace">(name)</span> activates a movingUI preset by name', true, true);
});

View File

@ -263,6 +263,7 @@ class PresetManager {
'streaming_kobold',
"enabled",
'seed',
'mancer_model',
];
const settings = Object.assign({}, getSettingsByApiId(this.apiId));

View File

@ -4,6 +4,7 @@ export const SECRET_KEYS = {
HORDE: 'api_key_horde',
MANCER: 'api_key_mancer',
APHRODITE: 'api_key_aphrodite',
TABBY: 'api_key_tabby',
OPENAI: 'api_key_openai',
NOVEL: 'api_key_novel',
CLAUDE: 'api_key_claude',
@ -12,6 +13,7 @@ export const SECRET_KEYS = {
AI21: 'api_key_ai21',
SCALE_COOKIE: 'scale_cookie',
PALM: 'api_key_palm',
SERPAPI: 'api_key_serpapi',
}
const INPUT_MAP = {
@ -26,6 +28,7 @@ const INPUT_MAP = {
[SECRET_KEYS.SCALE_COOKIE]: '#scale_cookie',
[SECRET_KEYS.PALM]: '#api_key_palm',
[SECRET_KEYS.APHRODITE]: '#api_key_aphrodite',
[SECRET_KEYS.TABBY]: '#api_key_tabby'
}
async function clearSecret() {
@ -52,7 +55,7 @@ async function viewSecrets() {
});
if (response.status == 403) {
callPopup('<h3>Forbidden</h3><p>To view your API keys here, set the value of allowKeysExposure to true in config.conf file and restart the SillyTavern server.</p>', 'text');
callPopup('<h3>Forbidden</h3><p>To view your API keys here, set the value of allowKeysExposure to true in config.yaml file and restart the SillyTavern server.</p>', 'text');
return;
}
@ -113,6 +116,23 @@ export async function readSecretState() {
}
}
export async function findSecret(key) {
try {
const response = await fetch('/api/secrets/find', {
method: 'POST',
headers: getRequestHeaders(),
body: JSON.stringify({ key }),
});
if (response.ok) {
const data = await response.json();
return data.value
}
} catch {
console.error('Could not find secret value: ', key);
}
}
function authorizeOpenRouter() {
const openRouterUrl = `https://openrouter.ai/auth?callback_url=${encodeURIComponent(location.origin)}`;
location.href = openRouterUrl;

File diff suppressed because it is too large Load Diff

View File

@ -6,11 +6,12 @@ import {
menu_type,
getCharacters,
entitiesFilter,
printCharacters,
} from "../script.js";
import { FILTER_TYPES, FilterHelper } from "./filters.js";
import { groupCandidatesFilter, selected_group } from "./group-chats.js";
import { uuidv4 } from "./utils.js";
import { groupCandidatesFilter, groups, selected_group } from "./group-chats.js";
import { download, onlyUnique, parseJsonFile, uuidv4 } from "./utils.js";
export {
tags,
@ -37,23 +38,22 @@ export const tag_filter_types = {
};
const ACTIONABLE_TAGS = {
FAV: { id: 1, name: 'Show only favorites', color: 'rgba(255, 255, 0, 0.5)', action: applyFavFilter, icon: 'fa-solid fa-star', class: 'filterByFavorites' },
GROUP: { id: 0, name: 'Show only groups', color: 'rgba(100, 100, 100, 0.5)', action: filterByGroups, icon: 'fa-solid fa-users', class: 'filterByGroups' },
VIEW: { id: 2, name: 'Manage tags', color: 'rgba(150, 100, 100, 0.5)', action: onViewTagsListClick, icon: 'fa-solid fa-gear', class: 'manageTags' },
HINT: { id: 3, name: 'Show Tag List', color: 'rgba(150, 100, 100, 0.5)', action: onTagListHintClick, icon: 'fa-solid fa-tags', class: 'showTagList' },
}
const InListActionable = {
VIEW: { id: 2, name: 'Manage tags', color: 'rgba(150, 100, 100, 0.5)', action: onViewTagsListClick, icon: 'fa-solid fa-gear' },
}
const DEFAULT_TAGS = [
{ id: uuidv4(), name: "Plain Text" },
{ id: uuidv4(), name: "OpenAI" },
{ id: uuidv4(), name: "W++" },
{ id: uuidv4(), name: "Boostyle" },
{ id: uuidv4(), name: "PList" },
{ id: uuidv4(), name: "AliChat" },
{ id: uuidv4(), name: "Plain Text", create_date: Date.now() },
{ id: uuidv4(), name: "OpenAI", create_date: Date.now() },
{ id: uuidv4(), name: "W++", create_date: Date.now() },
{ id: uuidv4(), name: "Boostyle", create_date: Date.now() },
{ id: uuidv4(), name: "PList", create_date: Date.now() },
{ id: uuidv4(), name: "AliChat", create_date: Date.now() },
];
let tags = [];
@ -137,8 +137,12 @@ function getTagKey() {
return null;
}
function addTagToMap(tagId) {
const key = getTagKey();
export function getTagKeyForCharacter(characterId = null) {
return characters[characterId]?.avatar;
}
function addTagToMap(tagId, characterId = null) {
const key = getTagKey() ?? getTagKeyForCharacter(characterId);
if (!key) {
return;
@ -149,11 +153,12 @@ function addTagToMap(tagId) {
}
else {
tag_map[key].push(tagId);
tag_map[key] = tag_map[key].filter(onlyUnique);
}
}
function removeTagFromMap(tagId) {
const key = getTagKey();
function removeTagFromMap(tagId, characterId = null) {
const key = getTagKey() ?? getTagKeyForCharacter(characterId);
if (!key) {
return;
@ -197,7 +202,17 @@ function selectTag(event, ui, listSelector) {
// add tag to the UI and internal map
appendTagToList(listSelector, tag, { removable: true });
appendTagToList(getInlineListSelector(), tag, { removable: false });
addTagToMap(tag.id);
// Optional, check for multiple character ids being present.
const characterData = event.target.closest('#bulk_tags_div')?.dataset.characters;
const characterIds = characterData ? JSON.parse(characterData).characterIds : null;
if (characterIds) {
characterIds.forEach((characterId) => addTagToMap(tag.id, characterId));
} else {
addTagToMap(tag.id);
}
saveSettingsDebounced();
printTagFilters(tag_filter_types.character);
printTagFilters(tag_filter_types.group_member);
@ -217,7 +232,6 @@ function getExistingTags(new_tags) {
return existing_tags
}
async function importTags(imported_char) {
let imported_tags = imported_char.tags.filter(t => t !== "ROOT" && t !== "TAVERN");
let existingTags = await getExistingTags(imported_tags);
@ -257,13 +271,13 @@ async function importTags(imported_char) {
return false;
}
function createNewTag(tagName) {
const tag = {
id: uuidv4(),
name: tagName,
color: '',
color2: '',
create_date: Date.now(),
};
tags.push(tag);
return tag;
@ -306,9 +320,9 @@ function appendTagToList(listElement, tag, { removable, selectable, action, isGe
tagElement.on('click', () => action.bind(tagElement)(filter));
tagElement.addClass('actionable');
}
if (action && tag.id === 2) {
/*if (action && tag.id === 2) {
tagElement.addClass('innerActionable hidden');
}
}*/
$(listElement).append(tagElement);
}
@ -383,8 +397,19 @@ function onTagRemoveClick(event) {
event.stopPropagation();
const tag = $(this).closest(".tag");
const tagId = tag.attr("id");
// Optional, check for multiple character ids being present.
const characterData = event.target.closest('#bulk_tags_div')?.dataset.characters;
const characterIds = characterData ? JSON.parse(characterData).characterIds : null;
tag.remove();
removeTagFromMap(tagId);
if (characterIds) {
characterIds.forEach((characterId) => removeTagFromMap(tagId, characterId));
} else {
removeTagFromMap(tagId);
}
$(`${getInlineListSelector()} .tag[id="${tagId}"]`).remove();
printTagFilters(tag_filter_types.character);
@ -439,7 +464,7 @@ function applyTagsOnGroupSelect() {
}
}
function createTagInput(inputSelector, listSelector) {
export function createTagInput(inputSelector, listSelector) {
$(inputSelector)
.autocomplete({
source: (i, o) => findTag(i, o, listSelector),
@ -451,55 +476,186 @@ function createTagInput(inputSelector, listSelector) {
function onViewTagsListClick() {
$('#dialogue_popup').addClass('large_dialogue_popup');
const list = document.createElement('div');
const list = $(document.createElement('div'));
list.attr('id', 'tag_view_list');
const everything = Object.values(tag_map).flat();
$(list).append('<h3>Tags</h3><i>Click on the tag name to edit it.</i><br>');
$(list).append('<i>Click on color box to assign new color.</i><br><br>');
for (const tag of tags.slice().sort((a, b) => a?.name?.toLowerCase()?.localeCompare(b?.name?.toLowerCase()))) {
const count = everything.filter(x => x == tag.id).length;
const template = $('#tag_view_template .tag_view_item').clone();
template.attr('id', tag.id);
template.find('.tag_view_counter_value').text(count);
template.find('.tag_view_name').text(tag.name);
template.find('.tag_view_name').addClass('tag');
template.find('.tag_view_name').css('background-color', tag.color);
template.find('.tag_view_name').css('color', tag.color2);
const colorPickerId = tag.id + "-tag-color";
const colorPicker2Id = tag.id + "-tag-color2";
template.find('.tagColorPickerHolder').html(
`<toolcool-color-picker id="${colorPickerId}" color="${tag.color}" class="tag-color"></toolcool-color-picker>`
);
template.find('.tagColorPicker2Holder').html(
`<toolcool-color-picker id="${colorPicker2Id}" color="${tag.color2}" class="tag-color2"></toolcool-color-picker>`
);
template.find('.tag-color').attr('id', colorPickerId);
template.find('.tag-color2').attr('id', colorPicker2Id);
list.appendChild(template.get(0));
setTimeout(function () {
document.querySelector(`.tag-color[id="${colorPickerId}"`).addEventListener('change', (evt) => {
onTagColorize(evt);
});
}, 100);
setTimeout(function () {
document.querySelector(`.tag-color2[id="${colorPicker2Id}"`).addEventListener('change', (evt) => {
onTagColorize2(evt);
});
}, 100);
$(colorPickerId).color = tag.color;
$(colorPicker2Id).color = tag.color2;
$(list).append(`
<div class="title_restorable alignItemsBaseline">
<h3>Tag Management</h3>
<div class="flex-container alignItemsBaseline">
<div class="menu_button menu_button_icon tag_view_backup" title="Save your tags to a file">
<i class="fa-solid fa-file-export"></i>
<span data-i18n="Backup">Backup</span>
</div>
<div class="menu_button menu_button_icon tag_view_restore" title="Restore tags from a file">
<i class="fa-solid fa-file-import"></i>
<span data-i18n="Restore">Restore</span>
</div>
<div class="menu_button menu_button_icon tag_view_create" title="Create a new tag">
<i class="fa-solid fa-plus"></i>
<span data-i18n="Create">Create</span>
</div>
<input type="file" id="tag_view_restore_input" hidden accept=".json">
</div>
</div>
<div class="justifyLeft m-b-1">
<small>
Click on the tag name to edit it.<br>
Click on color box to assign new color.
</small>
</div>`);
const sortedTags = tags.slice().sort((a, b) => a?.name?.toLowerCase()?.localeCompare(b?.name?.toLowerCase()));
for (const tag of sortedTags) {
appendViewTagToList(list, tag, everything);
}
callPopup(list.outerHTML, 'text');
callPopup(list, 'text');
}
async function onTagRestoreFileSelect(e) {
const file = e.target.files[0];
if (!file) {
console.log('Tag restore: No file selected.');
return;
}
const data = await parseJsonFile(file);
if (!data) {
toastr.warning('Empty file data', 'Tag restore');
console.log('Tag restore: File data empty.');
return;
}
if (!data.tags || !data.tag_map || !Array.isArray(data.tags) || typeof data.tag_map !== 'object') {
toastr.warning('Invalid file format', 'Tag restore');
console.log('Tag restore: Invalid file format.');
return;
}
const warnings = [];
// Import tags
for (const tag of data.tags) {
if (!tag.id || !tag.name) {
warnings.push(`Tag object is invalid: ${JSON.stringify(tag)}.`);
continue;
}
if (tags.find(x => x.id === tag.id)) {
warnings.push(`Tag with id ${tag.id} already exists.`);
continue;
}
tags.push(tag);
}
// Import tag_map
for (const key of Object.keys(data.tag_map)) {
const tagIds = data.tag_map[key];
if (!Array.isArray(tagIds)) {
warnings.push(`Tag map for key ${key} is invalid: ${JSON.stringify(tagIds)}.`);
continue;
}
// Verify that the key points to a valid character or group.
const characterExists = characters.some(x => String(x.avatar) === String(key));
const groupExists = groups.some(x => String(x.id) === String(key));
if (!characterExists && !groupExists) {
warnings.push(`Tag map key ${key} does not exist.`);
continue;
}
// Get existing tag ids for this key or empty array.
const existingTagIds = tag_map[key] || [];
// Merge existing and new tag ids. Remove duplicates.
tag_map[key] = existingTagIds.concat(tagIds).filter(onlyUnique);
// Verify that all tags exist. Remove tags that don't exist.
tag_map[key] = tag_map[key].filter(x => tags.some(y => String(y.id) === String(x)));
}
if (warnings.length) {
toastr.success('Tags restored with warnings. Check console for details.');
console.warn(`TAG RESTORE REPORT\n====================\n${warnings.join('\n')}`);
} else {
toastr.success('Tags restored successfully.');
}
$('#tag_view_restore_input').val('');
saveSettingsDebounced();
printCharacters(true);
onViewTagsListClick();
}
function onBackupRestoreClick() {
$('#tag_view_restore_input')
.off('change')
.on('change', onTagRestoreFileSelect)
.trigger('click');
}
function onTagsBackupClick() {
const timestamp = new Date().toISOString().split('T')[0].replace(/-/g, '');
const filename = `tags_${timestamp}.json`;
const data = {
tags: tags,
tag_map: tag_map,
};
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
download(blob, filename, 'application/json');
}
function onTagCreateClick() {
const tag = createNewTag('New Tag');
appendViewTagToList($('#tag_view_list'), tag, []);
printCharacters(false);
saveSettingsDebounced();
}
function appendViewTagToList(list, tag, everything) {
const count = everything.filter(x => x == tag.id).length;
const template = $('#tag_view_template .tag_view_item').clone();
template.attr('id', tag.id);
template.find('.tag_view_counter_value').text(count);
template.find('.tag_view_name').text(tag.name);
template.find('.tag_view_name').addClass('tag');
template.find('.tag_view_name').css('background-color', tag.color);
template.find('.tag_view_name').css('color', tag.color2);
const colorPickerId = tag.id + "-tag-color";
const colorPicker2Id = tag.id + "-tag-color2";
template.find('.tagColorPickerHolder').html(
`<toolcool-color-picker id="${colorPickerId}" color="${tag.color}" class="tag-color"></toolcool-color-picker>`
);
template.find('.tagColorPicker2Holder').html(
`<toolcool-color-picker id="${colorPicker2Id}" color="${tag.color2}" class="tag-color2"></toolcool-color-picker>`
);
template.find('.tag-color').attr('id', colorPickerId);
template.find('.tag-color2').attr('id', colorPicker2Id);
list.append(template);
setTimeout(function () {
document.querySelector(`.tag-color[id="${colorPickerId}"`).addEventListener('change', (evt) => {
onTagColorize(evt);
});
}, 100);
setTimeout(function () {
document.querySelector(`.tag-color2[id="${colorPicker2Id}"`).addEventListener('change', (evt) => {
onTagColorize2(evt);
});
}, 100);
$(colorPickerId).color = tag.color;
$(colorPicker2Id).color = tag.color2;
}
function onTagDeleteClick() {
@ -515,6 +671,7 @@ function onTagDeleteClick() {
tags.splice(index, 1);
$(`.tag[id="${id}"]`).remove();
$(`.tag_view_item[id="${id}"]`).remove();
printCharacters(false);
saveSettingsDebounced();
}
@ -533,6 +690,7 @@ function onTagColorize(evt) {
const newColor = evt.detail.rgba;
$(evt.target).parent().parent().find('.tag_view_name').css('background-color', newColor);
$(`.tag[id="${id}"]`).css('background-color', newColor);
$(`.bogus_folder_select[tagid="${id}"] .avatar`).css('background-color', newColor);
const tag = tags.find(x => x.id === id);
tag.color = newColor;
console.debug(tag);
@ -545,6 +703,7 @@ function onTagColorize2(evt) {
const newColor = evt.detail.rgba;
$(evt.target).parent().parent().find('.tag_view_name').css('color', newColor);
$(`.tag[id="${id}"]`).css('color', newColor);
$(`.bogus_folder_select[tagid="${id}"] .avatar`).css('color', newColor);
const tag = tags.find(x => x.id === id);
tag.color2 = newColor;
console.debug(tag);
@ -558,7 +717,7 @@ function onTagListHintClick() {
$(this).siblings(".innerActionable").toggleClass('hidden');
}
$(document).ready(() => {
jQuery(() => {
createTagInput('#tagInput', '#tagList');
createTagInput('#groupTagInput', '#groupTagList');
@ -571,4 +730,7 @@ $(document).ready(() => {
$(document).on("click", ".tags_view", onViewTagsListClick);
$(document).on("click", ".tag_delete", onTagDeleteClick);
$(document).on("input", ".tag_view_name", onTagRenameInput);
$(document).on("click", ".tag_view_create", onTagCreateClick);
$(document).on("click", ".tag_view_backup", onTagsBackupClick);
$(document).on("click", ".tag_view_restore", onBackupRestoreClick);
});

View File

@ -8,7 +8,7 @@ Text formatting commands:
<pre><code> like this</code></pre>
<ul>
<li><tt>`text`</tt> - displays as <code>inline code</code></li>
<li><tt> text</tt> - displays as a blockquote (note the space after >)</li>
<li><tt>&gt; text</tt> - displays as a blockquote (note the space after &gt;)</li>
<blockquote>like this</blockquote>
<li><tt># text</tt> - displays as a large header (note the space)</li>
<h1>like this</h1>

View File

@ -1,18 +1,66 @@
System-wide Replacement Macros:
<div>
System-wide Replacement Macros (in order of evaluation):
</div>
<ul>
<li><tt>&lcub;&lcub;user&rcub;&rcub;</tt> - your current Persona username</li>
<li><tt>&lcub;&lcub;char&rcub;&rcub;</tt> - the Character's name</li>
<li><tt>&lcub;&lcub;input&rcub;&rcub;</tt> - the user input</li>
<li><tt>&lcub;&lcub;// (note)&rcub;&rcub;</tt> - you can leave a note here, and the macro will be replaced with blank content. Not visible for the AI.</li>
<li><tt>&lcub;&lcub;time&rcub;&rcub;</tt> - the current time</li>
<li><tt>&lcub;&lcub;date&rcub;&rcub;</tt> - the current date</li>
<li><tt>&lcub;&lcub;weekday&rcub;&rcub;</tt> - the current weekday</li>
<li><tt>&lcub;&lcub;isotime&rcub;&rcub;</tt> - the current ISO date (YYYY-MM-DD)</li>
<li><tt>&lcub;&lcub;isodate&rcub;&rcub;</tt> - the current ISO time (24-hour clock)</li>
<li><tt>&lcub;&lcub;datetimeformat &hellip;&rcub;&rcub;</tt> - the current date/time in the specified format, e. g. for German date/time: <tt>&lcub;&lcub;datetimeformat DD.MM.YYYY HH:mm&rcub;&rcub;</tt></li>
<li><tt>&lcub;&lcub;bias "text here"&rcub;&rcub;</tt> - sets a behavioral bias for the AI until the next user input. Quotes around the text are important.</li>
<li><tt>&lcub;&lcub;banned "text here"&rcub;&rcub;</tt> - dynamically add text in the quotes to banned words sequences, if Text Generation WebUI backend used. Do nothing for others backends. Can be used anywhere (Character description, WI, AN, etc.) Quotes around the text are important.</li>
<li><tt>&lcub;&lcub;idle_duration&rcub;&rcub;</tt> - the time since the last user message was sent</li>
<li><tt>&lcub;&lcub;random:(args)&rcub;&rcub;</tt> - returns a random item from the list. (ex: &lcub;&lcub;random:1,2,3,4&rcub;&rcub; will return 1 of the 4 numbers at random. Works with text lists too.</li>
<li><tt>&lcub;&lcub;roll:(formula)&rcub;&rcub;</tt> - rolls a dice. (ex: &lcub;&lcub;roll:1d6&rcub;&rcub; will roll a 6-sided dice and return a number between 1 and 6)</li>
<li><tt>&lcub;&lcub;pipe&rcub;&rcub;</tt> only for slash command batching. Replaced with the returned result of the previous command.</li>
<li><tt>&lcub;&lcub;newline&rcub;&rcub;</tt> just inserts a newline.</li>
<li><tt>&lcub;&lcub;original&rcub;&rcub;</tt> global prompts defined in API settings. Only valid in Advanced Definitions prompt overrides.</li>
<li><tt>&lcub;&lcub;input&rcub;&rcub;</tt> the user input</li>
<li><tt>&lcub;&lcub;charPrompt&rcub;&rcub;</tt> the Character's Main Prompt override</li>
<li><tt>&lcub;&lcub;charJailbreak&rcub;&rcub;</tt> the Character's Jailbreak Prompt override</li>
<li><tt>&lcub;&lcub;description&rcub;&rcub;</tt> the Character's Description</li>
<li><tt>&lcub;&lcub;personality&rcub;&rcub;</tt> the Character's Personality</li>
<li><tt>&lcub;&lcub;scenario&rcub;&rcub;</tt> the Character's Scenario</li>
<li><tt>&lcub;&lcub;persona&rcub;&rcub;</tt> your current Persona Description</li>
<li><tt>&lcub;&lcub;mesExamples&rcub;&rcub;</tt> the Character's Dialogue Examples</li>
<li><tt>&lcub;&lcub;user&rcub;&rcub;</tt> your current Persona username</li>
<li><tt>&lcub;&lcub;char&rcub;&rcub;</tt> the Character's name</li>
<li><tt>&lcub;&lcub;lastMessage&rcub;&rcub;</tt> - the text of the latest chat message.</li>
<li><tt>&lcub;&lcub;lastMessageId&rcub;&rcub;</tt> index # of the latest chat message. Useful for slash command batching.</li>
<li><tt>&lcub;&lcub;// (note)&rcub;&rcub;</tt> you can leave a note here, and the macro will be replaced with blank content. Not visible for the AI.</li>
<li><tt>&lcub;&lcub;time&rcub;&rcub;</tt> the current time</li>
<li><tt>&lcub;&lcub;date&rcub;&rcub;</tt> the current date</li>
<li><tt>&lcub;&lcub;weekday&rcub;&rcub;</tt> the current weekday</li>
<li><tt>&lcub;&lcub;isotime&rcub;&rcub;</tt> the current ISO time (24-hour clock)</li>
<li><tt>&lcub;&lcub;isodate&rcub;&rcub;</tt> the current ISO date (YYYY-MM-DD)</li>
<li><tt>&lcub;&lcub;datetimeformat &hellip;&rcub;&rcub;</tt> the current date/time in the specified format, e. g. for German date/time: <tt>&lcub;&lcub;datetimeformat DD.MM.YYYY HH:mm&rcub;&rcub;</tt></li>
<li><tt>&lcub;&lcub;time_UTC±#&rcub;&rcub;</tt> the current time in the specified UTC time zone offset, e.g. UTC-4 or UTC+2</li>
<li><tt>&lcub;&lcub;idle_duration&rcub;&rcub;</tt> the time since the last user message was sent</li>
<li><tt>&lcub;&lcub;bias "text here"&rcub;&rcub;</tt> sets a behavioral bias for the AI until the next user input. Quotes around the text are important.</li>
<li><tt>&lcub;&lcub;roll:(formula)&rcub;&rcub;</tt> rolls a dice. (ex: <tt>>&lcub;&lcub;roll:1d6&rcub;&rcub</tt> will roll a 6-sided dice and return a number between 1 and 6)</li>
<li><tt>&lcub;&lcub;random:(args)&rcub;&rcub;</tt> returns a random item from the list. (ex: <tt>&lcub;&lcub;random:1,2,3,4&rcub;&rcub;</tt> will return 1 of the 4 numbers at random. Works with text lists too.</li>
<li><tt>&lcub;&lcub;random::(arg1)::(arg2)&rcub;&rcub;</tt> alternative syntax for random that allows to use commas in the list items.</li>
<li><tt>&lcub;&lcub;banned "text here"&rcub;&rcub;</tt> dynamically add text in the quotes to banned words sequences, if Text Generation WebUI backend used. Do nothing for others backends. Can be used anywhere (Character description, WI, AN, etc.) Quotes around the text are important.</li>
</ul>
<div>
Instruct Mode and Context Template Macros:
</div>
<div>
<small>(enabled in the Advanced Formatting settings)</small>
</div>
<ul>
<li><tt>&lcub;&lcub;exampleSeparator&rcub;&rcub;</tt> context template example dialogues separator</li>
<li><tt>&lcub;&lcub;chatStart&rcub;&rcub;</tt> context template chat start line</li>
<li><tt>&lcub;&lcub;instructSystem&rcub;&rcub;</tt> instruct system prompt</li>
<li><tt>&lcub;&lcub;instructSystemPrefix&rcub;&rcub;</tt> instruct system prompt prefix sequence</li>
<li><tt>&lcub;&lcub;instructSystemSuffix&rcub;&rcub;</tt> instruct system prompt suffix sequence</li>
<li><tt>&lcub;&lcub;instructInput&rcub;&rcub;</tt> instruct user input sequence</li>
<li><tt>&lcub;&lcub;instructOutput&rcub;&rcub;</tt> instruct assistant output sequence</li>
<li><tt>&lcub;&lcub;instructFirstOutput&rcub;&rcub;</tt> instruct assistant first output sequence</li>
<li><tt>&lcub;&lcub;instructLastOutput&rcub;&rcub;</tt> instruct assistant last output sequence</li>
<li><tt>&lcub;&lcub;instructSeparator&rcub;&rcub;</tt> instruct turn separator sequence</li>
<li><tt>&lcub;&lcub;instructStop&rcub;&rcub;</tt> instruct stop sequence</li>
</ul>
<div>
Chat variables Macros:
</div>
<div><small>Local variables = unique to the current chat</small></div>
<div><small>Global variables = works in any chat for any character</small></div>
<ul>
<li><tt>&lcub;&lcub;getvar::name&rcub;&rcub;</tt> replaced with the value of the local variable "name"</li>
<li><tt>&lcub;&lcub;setvar::name::value&rcub;&rcub;</tt> replaced with empty string, sets the local variable "name" to "value"</li>
<li><tt>&lcub;&lcub;addvar::name::increment&rcub;&rcub;</tt> replaced with the result of addition numeric value of "increment" to the local variable "name"</li>
<li><tt>&lcub;&lcub;getglobalvar::name&rcub;&rcub;</tt> replaced with the value of the global variable "name"</li>
<li><tt>&lcub;&lcub;setglobalvar::name::value&rcub;&rcub;</tt> replaced with empty string, sets the global variable "name" to "value"</li>
<li><tt>&lcub;&lcub;addglobalvar::name::value&rcub;&rcub;</tt> replaced with the result of addition numeric value of "increment" to the global variable "name"</li>
</ul>

View File

@ -3,16 +3,19 @@ import {
getRequestHeaders,
getStoppingStrings,
max_context,
online_status,
saveSettingsDebounced,
setGenerationParamsFromPreset,
setOnlineStatus,
substituteParams,
} from "../script.js";
import { loadMancerModels } from "./mancer-settings.js";
import {
power_user,
registerDebugFunction,
} from "./power-user.js";
import { getTextTokens, tokenizers } from "./tokenizers.js";
import { delay, onlyUnique } from "./utils.js";
import { SENTENCEPIECE_TOKENIZERS, getTextTokens, tokenizers } from "./tokenizers.js";
import { getSortableDelay, onlyUnique } from "./utils.js";
export {
textgenerationwebui_settings,
@ -25,10 +28,20 @@ export const textgen_types = {
OOBA: 'ooba',
MANCER: 'mancer',
APHRODITE: 'aphrodite',
TABBY: 'tabby',
KOBOLDCPP: 'koboldcpp',
};
// Maybe let it be configurable in the future?
// (7 days later) The future has come.
const MANCER_SERVER_KEY = 'mancer_server';
const MANCER_SERVER_DEFAULT = 'https://neuro.mancer.tech';
export let MANCER_SERVER = localStorage.getItem(MANCER_SERVER_KEY) ?? MANCER_SERVER_DEFAULT;
const KOBOLDCPP_ORDER = [6, 0, 1, 3, 4, 2, 5];
const textgenerationwebui_settings = {
temp: 0.7,
temperature_last: true,
top_p: 0.5,
top_k: 40,
top_a: 0,
@ -36,6 +49,7 @@ const textgenerationwebui_settings = {
epsilon_cutoff: 0,
eta_cutoff: 0,
typical_p: 1,
min_p: 0,
rep_pen: 1.2,
rep_pen_range: 0,
no_repeat_ngram_size: 0,
@ -56,7 +70,6 @@ const textgenerationwebui_settings = {
ban_eos_token: false,
skip_special_tokens: true,
streaming: false,
streaming_url: 'ws://127.0.0.1:5005/api/v1/stream',
mirostat_mode: 0,
mirostat_tau: 5,
mirostat_eta: 0.1,
@ -64,7 +77,17 @@ const textgenerationwebui_settings = {
negative_prompt: '',
grammar_string: '',
banned_tokens: '',
//n_aphrodite: 1,
//best_of_aphrodite: 1,
ignore_eos_token_aphrodite: false,
spaces_between_special_tokens_aphrodite: true,
//logits_processors_aphrodite: [],
//log_probs_aphrodite: 0,
//prompt_log_probs_aphrodite: 0,
type: textgen_types.OOBA,
mancer_model: 'mytholite',
legacy_api: false,
sampler_order: KOBOLDCPP_ORDER,
};
export let textgenerationwebui_banned_in_macros = [];
@ -74,6 +97,7 @@ export let textgenerationwebui_preset_names = [];
const setting_names = [
"temp",
"temperature_last",
"rep_pen",
"rep_pen_range",
"no_repeat_ngram_size",
@ -84,6 +108,7 @@ const setting_names = [
"epsilon_cutoff",
"eta_cutoff",
"typical_p",
"min_p",
"penalty_alpha",
"num_beams",
"length_penalty",
@ -98,7 +123,6 @@ const setting_names = [
"ban_eos_token",
"skip_special_tokens",
"streaming",
"streaming_url",
"mirostat_mode",
"mirostat_tau",
"mirostat_eta",
@ -106,9 +130,18 @@ const setting_names = [
"negative_prompt",
"grammar_string",
"banned_tokens",
"legacy_api",
//'n_aphrodite',
//'best_of_aphrodite',
'ignore_eos_token_aphrodite',
'spaces_between_special_tokens_aphrodite',
//'logits_processors_aphrodite',
//'log_probs_aphrodite',
//'prompt_log_probs_aphrodite'
"sampler_order",
];
function selectPreset(name) {
async function selectPreset(name) {
const preset = textgenerationwebui_presets[textgenerationwebui_preset_names.indexOf(name)];
if (!preset) {
@ -124,17 +157,21 @@ function selectPreset(name) {
saveSettingsDebounced();
}
function formatTextGenURL(value, use_mancer) {
function formatTextGenURL(value) {
try {
// Mancer doesn't need any formatting (it's hardcoded)
if (isMancer()) {
return value;
}
const url = new URL(value);
if (!power_user.relaxed_api_urls) {
if (use_mancer) { // If Mancer is in use, only require the URL to *end* with `/api`.
if (!url.pathname.endsWith('/api')) {
return null;
}
} else {
url.pathname = '/api';
}
if (url.pathname === '/api' && !textgenerationwebui_settings.legacy_api) {
toastr.info(`Enable Legacy API or start Ooba with the OpenAI extension enabled.`, 'Legacy API URL detected. Generation may fail.', { preventDuplicates: true, timeOut: 10000, extendedTimeOut: 20000 });
url.pathname = '';
}
if (!power_user.relaxed_api_urls && textgenerationwebui_settings.legacy_api) {
url.pathname = '/api';
}
return url.toString();
} catch { } // Just using URL as a validation check
@ -153,6 +190,7 @@ function getCustomTokenBans() {
return '';
}
const tokenizer = SENTENCEPIECE_TOKENIZERS.includes(power_user.tokenizer) ? power_user.tokenizer : tokenizers.LLAMA;
const result = [];
const sequences = textgenerationwebui_settings.banned_tokens
.split('\n')
@ -184,7 +222,7 @@ function getCustomTokenBans() {
}
} else {
try {
const tokens = getTextTokens(tokenizers.LLAMA, line);
const tokens = getTextTokens(tokenizer, line);
result.push(...tokens);
} catch {
console.log(`Could not tokenize raw text: ${line}`);
@ -220,7 +258,27 @@ function loadTextGenSettings(data, settings) {
setSettingByName(i, value);
}
$('#textgen_type').val(textgenerationwebui_settings.type).trigger('change');
$('#textgen_type').val(textgenerationwebui_settings.type);
showTypeSpecificControls(textgenerationwebui_settings.type);
//this is needed because showTypeSpecificControls() does not handle NOT declarations
if (isAphrodite()) {
$('[data-forAphro=False]').each(function () {
$(this).hide()
})
} else {
$('[data-forAphro=False]').each(function () {
$(this).show()
})
}
registerDebugFunction('change-mancer-url', 'Change Mancer base URL', 'Change Mancer API server base URL', () => {
const result = prompt(`Enter Mancer base URL\nDefault: ${MANCER_SERVER_DEFAULT}`, MANCER_SERVER);
if (result) {
localStorage.setItem(MANCER_SERVER_KEY, result);
MANCER_SERVER = result;
}
});
}
export function isMancer() {
@ -231,41 +289,105 @@ export function isAphrodite() {
return textgenerationwebui_settings.type === textgen_types.APHRODITE;
}
export function isTabby() {
return textgenerationwebui_settings.type === textgen_types.TABBY;
}
export function isOoba() {
return textgenerationwebui_settings.type === textgen_types.OOBA;
}
export function isKoboldCpp() {
return textgenerationwebui_settings.type === textgen_types.KOBOLDCPP;
}
export function getTextGenUrlSourceId() {
switch (textgenerationwebui_settings.type) {
case textgen_types.MANCER:
return "#mancer_api_url_text";
case textgen_types.OOBA:
return "#textgenerationwebui_api_url_text";
case textgen_types.APHRODITE:
return "#aphrodite_api_url_text";
case textgen_types.TABBY:
return "#tabby_api_url_text";
case textgen_types.KOBOLDCPP:
return "#koboldcpp_api_url_text";
}
}
/**
* Sorts the sampler items by the given order.
* @param {any[]} orderArray Sampler order array.
*/
function sortItemsByOrder(orderArray) {
console.debug('Preset samplers order: ' + orderArray);
const $draggableItems = $("#koboldcpp_order");
for (let i = 0; i < orderArray.length; i++) {
const index = orderArray[i];
const $item = $draggableItems.find(`[data-id="${index}"]`).detach();
$draggableItems.append($item);
}
}
jQuery(function () {
$('#koboldcpp_order').sortable({
delay: getSortableDelay(),
stop: function () {
const order = [];
$('#koboldcpp_order').children().each(function () {
order.push($(this).data('id'));
});
textgenerationwebui_settings.sampler_order = order;
console.log('Samplers reordered:', textgenerationwebui_settings.sampler_order);
saveSettingsDebounced();
},
});
$('#koboldcpp_default_order').on('click', function () {
textgenerationwebui_settings.sampler_order = KOBOLDCPP_ORDER;
sortItemsByOrder(textgenerationwebui_settings.sampler_order);
saveSettingsDebounced();
});
$('#textgen_type').on('change', function () {
const type = String($(this).val());
textgenerationwebui_settings.type = type;
$('[data-tg-type]').each(function () {
const tgType = $(this).attr('data-tg-type');
if (tgType == type) {
$(this).show();
} else {
$(this).hide();
if (isAphrodite()) {
//this is needed because showTypeSpecificControls() does not handle NOT declarations
$('[data-forAphro=False]').each(function () {
$(this).hide()
})
$('#mirostat_mode_textgenerationwebui').attr('step', 2) //Aphro disallows mode 1
$("#do_sample_textgenerationwebui").prop('checked', true) //Aphro should always do sample; 'otherwise set temp to 0 to mimic no sample'
$("#ban_eos_token_textgenerationwebui").prop('checked', false) //Aphro should not ban EOS, just ignore it; 'add token '2' to ban list do to this'
//special handling for Aphrodite topK -1 disable state
$('#top_k_textgenerationwebui').attr('min', -1)
if ($('#top_k_textgenerationwebui').val() === '0' || textgenerationwebui_settings['top_k'] === 0) {
textgenerationwebui_settings['top_k'] = -1
$('#top_k_textgenerationwebui').val('-1').trigger('input')
}
} else {
//this is needed because showTypeSpecificControls() does not handle NOT declarations
$('[data-forAphro=False]').each(function () {
$(this).show()
})
$('#mirostat_mode_textgenerationwebui').attr('step', 1)
//undo special Aphrodite setup for topK
$('#top_k_textgenerationwebui').attr('min', 0)
if ($('#top_k_textgenerationwebui').val() === '-1' || textgenerationwebui_settings['top_k'] === -1) {
textgenerationwebui_settings['top_k'] = 0
$('#top_k_textgenerationwebui').val('0').trigger('input')
}
});
if (isMancer()) {
loadMancerModels();
}
saveSettingsDebounced();
showTypeSpecificControls(type);
setOnlineStatus('no_connection');
$('#main_api').trigger('change');
$('#api_button_textgenerationwebui').trigger('click');
saveSettingsDebounced();
});
$('#settings_preset_textgenerationwebui').on('change', function () {
@ -292,66 +414,74 @@ jQuery(function () {
const value = Number($(this).val());
$(`#${id}_counter_textgenerationwebui`).val(value);
textgenerationwebui_settings[id] = value;
//special handling for aphrodite using -1 as disabled instead of 0
if ($(this).attr('id') === 'top_k_textgenerationwebui' && isAphrodite() && value === 0) {
textgenerationwebui_settings[id] = -1
$(this).val(-1)
}
}
saveSettingsDebounced();
});
}
})
function setSettingByName(i, value, trigger) {
function showTypeSpecificControls(type) {
$('[data-tg-type]').each(function () {
const tgType = $(this).attr('data-tg-type');
if (tgType == type) {
$(this).show();
} else {
$(this).hide();
}
});
}
function setSettingByName(setting, value, trigger) {
if (value === null || value === undefined) {
return;
}
const isCheckbox = $(`#${i}_textgenerationwebui`).attr('type') == 'checkbox';
const isText = $(`#${i}_textgenerationwebui`).attr('type') == 'text' || $(`#${i}_textgenerationwebui`).is('textarea');
if ('sampler_order' === setting) {
value = Array.isArray(value) ? value : KOBOLDCPP_ORDER;
sortItemsByOrder(value);
textgenerationwebui_settings.sampler_order = value;
return;
}
const isCheckbox = $(`#${setting}_textgenerationwebui`).attr('type') == 'checkbox';
const isText = $(`#${setting}_textgenerationwebui`).attr('type') == 'text' || $(`#${setting}_textgenerationwebui`).is('textarea');
if (isCheckbox) {
const val = Boolean(value);
$(`#${i}_textgenerationwebui`).prop('checked', val);
$(`#${setting}_textgenerationwebui`).prop('checked', val);
}
else if (isText) {
$(`#${i}_textgenerationwebui`).val(value);
$(`#${setting}_textgenerationwebui`).val(value);
}
else {
const val = parseFloat(value);
$(`#${i}_textgenerationwebui`).val(val);
$(`#${i}_counter_textgenerationwebui`).val(val);
$(`#${setting}_textgenerationwebui`).val(val);
$(`#${setting}_counter_textgenerationwebui`).val(val);
if (power_user.enableZenSliders) {
let zenSlider = $(`#${setting}_textgenerationwebui_zenslider`).slider()
zenSlider.slider('option', 'value', val)
zenSlider.slider('option', 'slide')
.call(zenSlider, null, {
handle: $('.ui-slider-handle', zenSlider), value: val
});
}
}
if (trigger) {
$(`#${i}_textgenerationwebui`).trigger('input');
$(`#${setting}_textgenerationwebui`).trigger('input');
}
}
async function generateTextGenWithStreaming(generate_data, signal) {
let streamingUrl = textgenerationwebui_settings.streaming_url;
generate_data.stream = true;
if (isMancer()) {
streamingUrl = api_server_textgenerationwebui.replace("http", "ws") + "/v1/stream";
}
if (isAphrodite()) {
streamingUrl = api_server_textgenerationwebui;
}
if (isMancer() || isOoba()) {
try {
const parsedUrl = new URL(streamingUrl);
if (parsedUrl.protocol !== 'ws:' && parsedUrl.protocol !== 'wss:') {
throw new Error('Invalid protocol');
}
} catch {
toastr.error('Invalid URL for streaming. Make sure it starts with ws:// or wss://');
return async function* () { throw new Error('Invalid URL for streaming.'); }
}
}
const response = await fetch('/generate_textgenerationwebui', {
const response = await fetch('/api/textgenerationwebui/generate', {
headers: {
...getRequestHeaders(),
'X-Response-Streaming': String(true),
'X-Streaming-URL': streamingUrl,
},
body: JSON.stringify(generate_data),
method: 'POST',
@ -362,75 +492,111 @@ async function generateTextGenWithStreaming(generate_data, signal) {
const decoder = new TextDecoder();
const reader = response.body.getReader();
let getMessage = '';
let messageBuffer = "";
while (true) {
const { done, value } = await reader.read();
let response = decoder.decode(value);
// We don't want carriage returns in our messages
let response = decoder.decode(value).replace(/\r/g, "");
if (isAphrodite()) {
const events = response.split('\n\n');
tryParseStreamingError(response);
for (const event of events) {
if (event.length == 0) {
continue;
}
let eventList = [];
try {
const { results } = JSON.parse(event);
messageBuffer += response;
eventList = messageBuffer.split("\n\n");
// Last element will be an empty string or a leftover partial message
messageBuffer = eventList.pop();
if (Array.isArray(results) && results.length > 0) {
getMessage = results[0].text;
yield getMessage;
// unhang UI thread
await delay(1);
}
} catch {
// Ignore
}
for (let event of eventList) {
if (event.startsWith('event: completion')) {
event = event.split("\n")[1];
}
if (done) {
if (typeof event !== 'string' || !event.length)
continue;
if (!event.startsWith("data"))
continue;
if (event == "data: [DONE]") {
return;
}
} else {
getMessage += response;
if (done) {
return;
}
let data = JSON.parse(event.substring(6));
// the first and last messages are undefined, protect against that
getMessage += data?.choices[0]?.text || '';
yield getMessage;
}
if (done) {
return;
}
}
}
}
export function getTextGenGenerationData(finalPrompt, this_amount_gen, isImpersonate, cfgValues) {
return {
/**
* Parses errors in streaming responses and displays them in toastr.
* @param {string} response - Response from the server.
* @returns {void} Nothing.
*/
function tryParseStreamingError(response) {
let data = {};
try {
data = JSON.parse(response);
} catch {
// No JSON. Do nothing.
}
const message = data?.error?.message || data?.message;
if (message) {
toastr.error(message, 'API Error');
throw new Error(message);
}
}
function toIntArray(string) {
if (!string) {
return [];
}
return string.split(',').map(x => parseInt(x)).filter(x => !isNaN(x));
}
function getModel() {
if (isMancer()) {
return textgenerationwebui_settings.mancer_model;
}
if (isAphrodite()) {
return online_status;
}
return undefined;
}
export function getTextGenGenerationData(finalPrompt, maxTokens, isImpersonate, isContinue, cfgValues) {
let APIflags = {
'prompt': finalPrompt,
'max_new_tokens': this_amount_gen,
'do_sample': textgenerationwebui_settings.do_sample,
'model': getModel(),
'max_new_tokens': maxTokens,
'max_tokens': maxTokens,
'temperature': textgenerationwebui_settings.temp,
'top_p': textgenerationwebui_settings.top_p,
'typical_p': textgenerationwebui_settings.typical_p,
'min_p': textgenerationwebui_settings.min_p,
'repetition_penalty': textgenerationwebui_settings.rep_pen,
'repetition_penalty_range': textgenerationwebui_settings.rep_pen_range,
'encoder_repetition_penalty': textgenerationwebui_settings.encoder_rep_pen,
'frequency_penalty': textgenerationwebui_settings.freq_pen,
'presence_penalty': textgenerationwebui_settings.presence_pen,
'top_k': textgenerationwebui_settings.top_k,
'min_length': textgenerationwebui_settings.min_length,
'no_repeat_ngram_size': textgenerationwebui_settings.no_repeat_ngram_size,
'min_tokens': textgenerationwebui_settings.min_length,
'num_beams': textgenerationwebui_settings.num_beams,
'penalty_alpha': textgenerationwebui_settings.penalty_alpha,
'length_penalty': textgenerationwebui_settings.length_penalty,
'early_stopping': textgenerationwebui_settings.early_stopping,
'guidance_scale': cfgValues?.guidanceScale?.value ?? textgenerationwebui_settings.guidance_scale ?? 1,
'negative_prompt': cfgValues?.negativePrompt ?? textgenerationwebui_settings.negative_prompt ?? '',
'seed': textgenerationwebui_settings.seed,
'add_bos_token': textgenerationwebui_settings.add_bos_token,
'stopping_strings': getStoppingStrings(isImpersonate),
'stopping_strings': getStoppingStrings(isImpersonate, isContinue),
'stop': getStoppingStrings(isImpersonate, isContinue),
'truncation_length': max_context,
'ban_eos_token': textgenerationwebui_settings.ban_eos_token,
'skip_special_tokens': textgenerationwebui_settings.skip_special_tokens,
@ -441,9 +607,43 @@ export function getTextGenGenerationData(finalPrompt, this_amount_gen, isImperso
'mirostat_mode': textgenerationwebui_settings.mirostat_mode,
'mirostat_tau': textgenerationwebui_settings.mirostat_tau,
'mirostat_eta': textgenerationwebui_settings.mirostat_eta,
'grammar_string': textgenerationwebui_settings.grammar_string,
'custom_token_bans': getCustomTokenBans(),
'custom_token_bans': isAphrodite() ? toIntArray(getCustomTokenBans()) : getCustomTokenBans(),
'use_mancer': isMancer(),
'use_aphrodite': isAphrodite(),
'use_tabby': isTabby(),
'use_koboldcpp': isKoboldCpp(),
'use_ooba': isOoba(),
'api_server': isMancer() ? MANCER_SERVER : api_server_textgenerationwebui,
'legacy_api': textgenerationwebui_settings.legacy_api && !isMancer(),
'sampler_order': isKoboldCpp() ? textgenerationwebui_settings.sampler_order : undefined,
};
let aphroditeExclusionFlags = {
'repetition_penalty_range': textgenerationwebui_settings.rep_pen_range,
'encoder_repetition_penalty': textgenerationwebui_settings.encoder_rep_pen,
'no_repeat_ngram_size': textgenerationwebui_settings.no_repeat_ngram_size,
'penalty_alpha': textgenerationwebui_settings.penalty_alpha,
'temperature_last': textgenerationwebui_settings.temperature_last,
'do_sample': textgenerationwebui_settings.do_sample,
'seed': textgenerationwebui_settings.seed,
'guidance_scale': cfgValues?.guidanceScale?.value ?? textgenerationwebui_settings.guidance_scale ?? 1,
'negative_prompt': cfgValues?.negativePrompt ?? substituteParams(textgenerationwebui_settings.negative_prompt) ?? '',
'grammar_string': textgenerationwebui_settings.grammar_string,
}
let aphroditeFlags = {
//'n': textgenerationwebui_settings.n_aphrodite,
//'best_of': textgenerationwebui_settings.n_aphrodite, //n must always == best_of and vice versa
'ignore_eos': textgenerationwebui_settings.ignore_eos_token_aphrodite,
'spaces_between_special_tokens': textgenerationwebui_settings.spaces_between_special_tokens_aphrodite,
//'logits_processors': textgenerationwebui_settings.logits_processors_aphrodite,
//'logprobs': textgenerationwebui_settings.log_probs_aphrodite,
//'prompt_logprobs': textgenerationwebui_settings.prompt_log_probs_aphrodite,
}
if (isAphrodite()) {
APIflags = Object.assign(APIflags, aphroditeFlags);
} else {
APIflags = Object.assign(APIflags, aphroditeExclusionFlags);
}
return APIflags
}

View File

@ -1,9 +1,10 @@
import { characters, main_api, nai_settings, online_status, this_chid } from "../script.js";
import { characters, getAPIServerUrl, main_api, nai_settings, online_status, this_chid } from "../script.js";
import { power_user, registerDebugFunction } from "./power-user.js";
import { chat_completion_sources, oai_settings } from "./openai.js";
import { chat_completion_sources, model_list, oai_settings } from "./openai.js";
import { groups, selected_group } from "./group-chats.js";
import { getStringHash } from "./utils.js";
import { kai_flags } from "./kai-settings.js";
import { isKoboldCpp, isMancer, isTabby, textgenerationwebui_settings } from "./textgen-settings.js";
export const CHARACTERS_PER_TOKEN_RATIO = 3.35;
const TOKENIZER_WARNING_KEY = 'tokenizationWarningShown';
@ -11,17 +12,25 @@ const TOKENIZER_WARNING_KEY = 'tokenizationWarningShown';
export const tokenizers = {
NONE: 0,
GPT2: 1,
/**
* @deprecated Use GPT2 instead.
*/
LEGACY: 2,
OPENAI: 2,
LLAMA: 3,
NERD: 4,
NERD2: 5,
API: 6,
MISTRAL: 7,
YI: 8,
BEST_MATCH: 99,
};
export const SENTENCEPIECE_TOKENIZERS = [
tokenizers.LLAMA,
tokenizers.MISTRAL,
tokenizers.YI,
// uncomment when NovelAI releases Kayra and Clio weights, lol
//tokenizers.NERD,
//tokenizers.NERD2,
];
const objectStore = new localforage.createInstance({ name: "SillyTavern_ChatCompletions" });
let tokenCache = {};
@ -65,8 +74,47 @@ async function resetTokenCache() {
}
}
function getTokenizerBestMatch() {
if (main_api === 'novel') {
/**
* Gets the friendly name of the current tokenizer.
* @param {string} forApi API to get the tokenizer for. Defaults to the main API.
* @returns { { tokenizerName: string, tokenizerId: number } } Tokenizer info
*/
export function getFriendlyTokenizerName(forApi) {
if (!forApi) {
forApi = main_api;
}
const tokenizerOption = $("#tokenizer").find(':selected');
let tokenizerId = Number(tokenizerOption.val());
let tokenizerName = tokenizerOption.text();
if (forApi !== 'openai' && tokenizerId === tokenizers.BEST_MATCH) {
tokenizerId = getTokenizerBestMatch(forApi);
tokenizerName = $(`#tokenizer option[value="${tokenizerId}"]`).text();
}
tokenizerName = forApi == 'openai'
? getTokenizerModel()
: tokenizerName;
tokenizerId = forApi == 'openai'
? tokenizers.OPENAI
: tokenizerId;
return { tokenizerName, tokenizerId };
}
/**
* Gets the best tokenizer for the current API.
* @param {string} forApi API to get the tokenizer for. Defaults to the main API.
* @returns {number} Tokenizer type.
*/
export function getTokenizerBestMatch(forApi) {
if (!forApi) {
forApi = main_api;
}
if (forApi === 'novel') {
if (nai_settings.model_novel.includes('clio')) {
return tokenizers.NERD;
}
@ -74,7 +122,7 @@ function getTokenizerBestMatch() {
return tokenizers.NERD2;
}
}
if (main_api === 'kobold' || main_api === 'textgenerationwebui' || main_api === 'koboldhorde') {
if (forApi === 'kobold' || forApi === 'textgenerationwebui' || forApi === 'koboldhorde') {
// Try to use the API tokenizer if possible:
// - API must be connected
// - Kobold must pass a version check
@ -108,6 +156,10 @@ function callTokenizer(type, str, padding) {
return countTokensRemote('/api/tokenize/nerdstash', str, padding);
case tokenizers.NERD2:
return countTokensRemote('/api/tokenize/nerdstash_v2', str, padding);
case tokenizers.MISTRAL:
return countTokensRemote('/api/tokenize/mistral', str, padding);
case tokenizers.YI:
return countTokensRemote('/api/tokenize/yi', str, padding);
case tokenizers.API:
return countTokensRemote('/tokenize_via_api', str, padding);
default:
@ -140,7 +192,7 @@ export function getTokenCount(str, padding = undefined) {
}
if (tokenizerType === tokenizers.BEST_MATCH) {
tokenizerType = getTokenizerBestMatch();
tokenizerType = getTokenizerBestMatch(main_api);
}
if (padding === undefined) {
@ -187,6 +239,9 @@ export function getTokenizerModel() {
const gpt4Tokenizer = 'gpt-4';
const gpt2Tokenizer = 'gpt2';
const claudeTokenizer = 'claude';
const llamaTokenizer = 'llama';
const mistralTokenizer = 'mistral';
const yiTokenizer = 'yi';
// Assuming no one would use it for different models.. right?
if (oai_settings.chat_completion_source == chat_completion_sources.SCALE) {
@ -214,7 +269,18 @@ export function getTokenizerModel() {
// And for OpenRouter (if not a site model, then it's impossible to determine the tokenizer)
if (oai_settings.chat_completion_source == chat_completion_sources.OPENROUTER && oai_settings.openrouter_model) {
if (oai_settings.openrouter_model.includes('gpt-4')) {
const model = model_list.find(x => x.id === oai_settings.openrouter_model);
if (model?.architecture?.tokenizer === 'Llama2') {
return llamaTokenizer;
}
else if (model?.architecture?.tokenizer === 'Mistral') {
return mistralTokenizer;
}
else if (model?.architecture?.tokenizer === 'Yi') {
return yiTokenizer;
}
else if (oai_settings.openrouter_model.includes('gpt-4')) {
return gpt4Tokenizer;
}
else if (oai_settings.openrouter_model.includes('gpt-3.5-turbo-0301')) {
@ -313,6 +379,17 @@ function getTokenCacheObject() {
return tokenCache[String(chatId)];
}
function getRemoteTokenizationParams(str) {
return {
text: str,
api: main_api,
url: getAPIServerUrl(),
legacy_api: main_api === 'textgenerationwebui' && textgenerationwebui_settings.legacy_api && !isMancer(),
use_tabby: main_api === 'textgenerationwebui' && isTabby(),
use_koboldcpp: main_api === 'textgenerationwebui' && isKoboldCpp(),
};
}
/**
* Counts token using the remote server API.
* @param {string} endpoint API endpoint.
@ -327,7 +404,7 @@ function countTokensRemote(endpoint, str, padding) {
async: false,
type: 'POST',
url: endpoint,
data: JSON.stringify({ text: str }),
data: JSON.stringify(getRemoteTokenizationParams(str)),
dataType: "json",
contentType: "application/json",
success: function (data) {
@ -357,19 +434,29 @@ function countTokensRemote(endpoint, str, padding) {
* Calls the underlying tokenizer model to encode a string to tokens.
* @param {string} endpoint API endpoint.
* @param {string} str String to tokenize.
* @param {string} model Tokenizer model.
* @returns {number[]} Array of token ids.
*/
function getTextTokensRemote(endpoint, str) {
function getTextTokensRemote(endpoint, str, model = '') {
if (model) {
endpoint += `?model=${model}`;
}
let ids = [];
jQuery.ajax({
async: false,
type: 'POST',
url: endpoint,
data: JSON.stringify({ text: str }),
data: JSON.stringify(getRemoteTokenizationParams(str)),
dataType: "json",
contentType: "application/json",
success: function (data) {
ids = data.ids;
// Don't want to break reverse compatibility, so sprinkle in some of the JS magic
if (Array.isArray(data.chunks)) {
Object.defineProperty(ids, 'chunks', { value: data.chunks });
}
}
});
return ids;
@ -380,7 +467,11 @@ function getTextTokensRemote(endpoint, str) {
* @param {string} endpoint API endpoint.
* @param {number[]} ids Array of token ids
*/
function decodeTextTokensRemote(endpoint, ids) {
function decodeTextTokensRemote(endpoint, ids, model = '') {
if (model) {
endpoint += `?model=${model}`;
}
let text = '';
jQuery.ajax({
async: false,
@ -412,6 +503,15 @@ export function getTextTokens(tokenizerType, str) {
return getTextTokensRemote('/api/tokenize/nerdstash', str);
case tokenizers.NERD2:
return getTextTokensRemote('/api/tokenize/nerdstash_v2', str);
case tokenizers.MISTRAL:
return getTextTokensRemote('/api/tokenize/mistral', str);
case tokenizers.YI:
return getTextTokensRemote('/api/tokenize/yi', str);
case tokenizers.OPENAI:
const model = getTokenizerModel();
return getTextTokensRemote('/api/tokenize/openai-encode', str, model);
case tokenizers.API:
return getTextTokensRemote('/tokenize_via_api', str);
default:
console.warn("Calling getTextTokens with unsupported tokenizer type", tokenizerType);
return [];
@ -433,6 +533,13 @@ export function decodeTextTokens(tokenizerType, ids) {
return decodeTextTokensRemote('/api/decode/nerdstash', ids);
case tokenizers.NERD2:
return decodeTextTokensRemote('/api/decode/nerdstash_v2', ids);
case tokenizers.MISTRAL:
return decodeTextTokensRemote('/api/decode/mistral', ids);
case tokenizers.YI:
return decodeTextTokensRemote('/api/decode/yi', ids);
case tokenizers.OPENAI:
const model = getTokenizerModel();
return decodeTextTokensRemote('/api/decode/openai', ids, model);
default:
console.warn("Calling decodeTextTokens with unsupported tokenizer type", tokenizerType);
return '';

View File

@ -27,6 +27,33 @@ export function isValidUrl(value) {
}
}
/**
* Parses ranges like 10-20 or 10.
* Range is inclusive. Start must be less than end.
* Returns null if invalid.
* @param {string} input The input string.
* @param {number} min The minimum value.
* @param {number} max The maximum value.
* @returns {{ start: number, end: number }} The parsed range.
*/
export function stringToRange(input, min, max) {
let start, end;
if (input.includes('-')) {
const parts = input.split('-');
start = parts[0] ? parseInt(parts[0], 10) : NaN;
end = parts[1] ? parseInt(parts[1], 10) : NaN;
} else {
start = end = parseInt(input, 10);
}
if (isNaN(start) || isNaN(end) || start > end || start < min || end > max) {
return null;
}
return { start, end };
}
/**
* Determines if a value is unique in an array.
* @param {any} value Current value.
@ -485,6 +512,38 @@ export function trimToStartSentence(input) {
return input;
}
/**
* Format bytes as human-readable text.
*
* @param bytes Number of bytes.
* @param si True to use metric (SI) units, aka powers of 1000. False to use
* binary (IEC), aka powers of 1024.
* @param dp Number of decimal places to display.
*
* @return Formatted string.
*/
export function humanFileSize(bytes, si = false, dp = 1) {
const thresh = si ? 1000 : 1024;
if (Math.abs(bytes) < thresh) {
return bytes + ' B';
}
const units = si
? ['kB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']
: ['KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB'];
let u = -1;
const r = 10 ** dp;
do {
bytes /= thresh;
++u;
} while (Math.round(Math.abs(bytes) * r) / r >= thresh && u < units.length - 1);
return bytes.toFixed(dp) + ' ' + units[u];
}
/**
* Counts the number of occurrences of a character in a string.
* @param {string} string The string to count occurrences in.
@ -506,6 +565,24 @@ export function countOccurrences(string, character) {
return count;
}
/**
* Checks if a string is "true" value.
* @param {string} arg String to check
* @returns {boolean} True if the string is true, false otherwise.
*/
export function isTrueBoolean(arg) {
return ['on', 'true', '1'].includes(arg);
}
/**
* Checks if a string is "false" value.
* @param {string} arg String to check
* @returns {boolean} True if the string is false, false otherwise.
*/
export function isFalseBoolean(arg) {
return ['off', 'false', '0'].includes(arg);
}
/**
* Checks if a number is odd.
* @param {number} number The number to check.
@ -523,7 +600,7 @@ export function timestampToMoment(timestamp) {
return moment.invalid();
}
// Unix time (legacy TAI)
// Unix time (legacy TAI / tags)
if (typeof timestamp === 'number') {
return moment(timestamp);
}
@ -914,9 +991,10 @@ export function loadFileToDocument(url, type) {
* @param {string} dataUrl The data URL encoded data of the image.
* @param {number} maxWidth The maximum width of the thumbnail.
* @param {number} maxHeight The maximum height of the thumbnail.
* @param {string} [type='image/jpeg'] The type of the thumbnail.
* @returns {Promise<string>} A promise that resolves to the thumbnail data URL.
*/
export function createThumbnail(dataUrl, maxWidth, maxHeight) {
export function createThumbnail(dataUrl, maxWidth, maxHeight, type = 'image/jpeg') {
return new Promise((resolve, reject) => {
const img = new Image();
img.src = dataUrl;
@ -941,7 +1019,7 @@ export function createThumbnail(dataUrl, maxWidth, maxHeight) {
ctx.drawImage(img, 0, 0, thumbnailWidth, thumbnailHeight);
// Convert the canvas to a data URL and resolve the promise
const thumbnailDataUrl = canvas.toDataURL('image/jpeg');
const thumbnailDataUrl = canvas.toDataURL(type);
resolve(thumbnailDataUrl);
};

542
public/scripts/variables.js Normal file
View File

@ -0,0 +1,542 @@
import { chat_metadata, getCurrentChatId, saveSettingsDebounced, sendSystemMessage, system_message_types } from "../script.js";
import { extension_settings, saveMetadataDebounced } from "./extensions.js";
import { executeSlashCommands, registerSlashCommand } from "./slash-commands.js";
function getLocalVariable(name) {
if (!chat_metadata.variables) {
chat_metadata.variables = {};
}
const localVariable = chat_metadata?.variables[name];
return (localVariable === '' || isNaN(Number(localVariable))) ? (localVariable || '') : Number(localVariable);
}
function setLocalVariable(name, value) {
if (!chat_metadata.variables) {
chat_metadata.variables = {};
}
chat_metadata.variables[name] = value;
saveMetadataDebounced();
return value;
}
function getGlobalVariable(name) {
const globalVariable = extension_settings.variables.global[name];
return (globalVariable === '' || isNaN(Number(globalVariable))) ? (globalVariable || '') : Number(globalVariable);
}
function setGlobalVariable(name, value) {
extension_settings.variables.global[name] = value;
saveSettingsDebounced();
}
function addLocalVariable(name, value) {
const currentValue = getLocalVariable(name) || 0;
const increment = Number(value);
if (isNaN(increment) || isNaN(Number(currentValue))) {
const stringValue = String(currentValue || '') + value;
setLocalVariable(name, stringValue);
return stringValue;
}
const newValue = Number(currentValue) + increment;
if (isNaN(newValue)) {
return '';
}
setLocalVariable(name, newValue);
return newValue;
}
function addGlobalVariable(name, value) {
const currentValue = getGlobalVariable(name) || 0;
const increment = Number(value);
if (isNaN(increment) || isNaN(Number(currentValue))) {
const stringValue = String(currentValue || '') + value;
setGlobalVariable(name, stringValue);
return stringValue;
}
const newValue = Number(currentValue) + increment;
if (isNaN(newValue)) {
return '';
}
setGlobalVariable(name, newValue);
return newValue;
}
function incrementLocalVariable(name) {
return addLocalVariable(name, 1);
}
function incrementGlobalVariable(name) {
return addGlobalVariable(name, 1);
}
function decrementLocalVariable(name) {
return addLocalVariable(name, -1);
}
function decrementGlobalVariable(name) {
return addGlobalVariable(name, -1);
}
/**
* Resolves a variable name to its value or returns the string as is if the variable does not exist.
* @param {string} name Variable name
* @returns {string} Variable value or the string literal
*/
export function resolveVariable(name) {
if (existsLocalVariable(name)) {
return getLocalVariable(name);
}
if (existsGlobalVariable(name)) {
return getGlobalVariable(name);
}
return name;
}
export function replaceVariableMacros(input) {
const lines = input.split('\n');
for (let i = 0; i < lines.length; i++) {
let line = lines[i];
// Skip lines without macros
if (!line || !line.includes('{{')) {
continue;
}
// Replace {{getvar::name}} with the value of the variable name
line = line.replace(/{{getvar::([^}]+)}}/gi, (_, name) => {
name = name.trim();
return getLocalVariable(name);
});
// Replace {{setvar::name::value}} with empty string and set the variable name to value
line = line.replace(/{{setvar::([^:]+)::([^}]+)}}/gi, (_, name, value) => {
name = name.trim();
setLocalVariable(name, value);
return '';
});
// Replace {{addvar::name::value}} with empty string and add value to the variable value
line = line.replace(/{{addvar::([^:]+)::([^}]+)}}/gi, (_, name, value) => {
name = name.trim();
addLocalVariable(name, value);;
return '';
});
// Replace {{getglobalvar::name}} with the value of the global variable name
line = line.replace(/{{getglobalvar::([^}]+)}}/gi, (_, name) => {
name = name.trim();
return getGlobalVariable(name);
});
// Replace {{setglobalvar::name::value}} with empty string and set the global variable name to value
line = line.replace(/{{setglobalvar::([^:]+)::([^}]+)}}/gi, (_, name, value) => {
name = name.trim();
setGlobalVariable(name, value);
return '';
});
// Replace {{addglobalvar::name::value}} with empty string and add value to the global variable value
line = line.replace(/{{addglobalvar::([^:]+)::([^}]+)}}/gi, (_, name, value) => {
name = name.trim();
addGlobalVariable(name, value);
return '';
});
lines[i] = line;
}
return lines.join('\n');
}
function listVariablesCallback() {
if (!chat_metadata.variables) {
chat_metadata.variables = {};
}
const localVariables = Object.entries(chat_metadata.variables).map(([name, value]) => `${name}: ${value}`);
const globalVariables = Object.entries(extension_settings.variables.global).map(([name, value]) => `${name}: ${value}`);
const localVariablesString = localVariables.length > 0 ? localVariables.join('\n\n') : 'No local variables';
const globalVariablesString = globalVariables.length > 0 ? globalVariables.join('\n\n') : 'No global variables';
const chatName = getCurrentChatId();
const converter = new showdown.Converter();
const message = `### Local variables (${chatName}):\n${localVariablesString}\n\n### Global variables:\n${globalVariablesString}`;
const htmlMessage = DOMPurify.sanitize(converter.makeHtml(message));
sendSystemMessage(system_message_types.GENERIC, htmlMessage);
}
async function whileCallback(args, command) {
const MAX_LOOPS = 100;
const isGuardOff = ['off', 'false', '0'].includes(args.guard?.toLowerCase());
const iterations = isGuardOff ? Number.MAX_SAFE_INTEGER : MAX_LOOPS;
for (let i = 0; i < iterations; i++) {
const { a, b, rule } = parseBooleanOperands(args);
const result = evalBoolean(rule, a, b);
if (result && command) {
await executeSubCommands(command);
} else {
break;
}
}
return '';
}
async function ifCallback(args, command) {
const { a, b, rule } = parseBooleanOperands(args);
const result = evalBoolean(rule, a, b);
if (result && command) {
return await executeSubCommands(command);
} else if (!result && args.else && typeof args.else === 'string' && args.else !== '') {
return await executeSubCommands(args.else);
}
return '';
}
/**
* Checks if a local variable exists.
* @param {string} name Local variable name
* @returns {boolean} True if the local variable exists, false otherwise
*/
function existsLocalVariable(name) {
return chat_metadata.variables && chat_metadata.variables[name] !== undefined;
}
/**
* Checks if a global variable exists.
* @param {string} name Global variable name
* @returns {boolean} True if the global variable exists, false otherwise
*/
function existsGlobalVariable(name) {
return extension_settings.variables.global && extension_settings.variables.global[name] !== undefined;
}
/**
* Parses boolean operands from command arguments.
* @param {object} args Command arguments
* @returns {{a: string | number, b: string | number, rule: string}} Boolean operands
*/
function parseBooleanOperands(args) {
// Resolution order: numeric literal, local variable, global variable, string literal
/**
* @param {string} operand Boolean operand candidate
*/
function getOperand(operand) {
if (operand === undefined) {
return '';
}
const operandNumber = Number(operand);
if (!isNaN(operandNumber)) {
return operandNumber;
}
if (existsLocalVariable(operand)) {
const operandLocalVariable = getLocalVariable(operand);
return operandLocalVariable ?? '';
}
if (existsGlobalVariable(operand)) {
const operandGlobalVariable = getGlobalVariable(operand);
return operandGlobalVariable ?? '';
}
const stringLiteral = String(operand);
return stringLiteral || '';
}
const left = getOperand(args.a || args.left || args.first || args.x);
const right = getOperand(args.b || args.right || args.second || args.y);
const rule = args.rule;
return { a: left, b: right, rule };
}
/**
* Evaluates a boolean comparison rule.
* @param {string} rule Boolean comparison rule
* @param {string|number} a The left operand
* @param {string|number} b The right operand
* @returns {boolean} True if the rule yields true, false otherwise
*/
function evalBoolean(rule, a, b) {
if (!rule) {
toastr.warning('The rule must be specified for the boolean comparison.', 'Invalid command');
throw new Error('Invalid command.');
}
let result = false;
if (typeof a === 'string' && typeof b !== 'number') {
const aString = String(a).toLowerCase();
const bString = String(b).toLowerCase();
switch (rule) {
case 'in':
result = aString.includes(bString);
break;
case 'nin':
result = !aString.includes(bString);
break;
case 'eq':
result = aString === bString;
break;
case 'neq':
result = aString !== bString;
break;
default:
toastr.error('Unknown boolean comparison rule for type string.', 'Invalid /if command');
throw new Error('Invalid command.');
}
} else if (typeof a === 'number') {
const aNumber = Number(a);
const bNumber = Number(b);
switch (rule) {
case 'not':
result = !aNumber;
break;
case 'gt':
result = aNumber > bNumber;
break;
case 'gte':
result = aNumber >= bNumber;
break;
case 'lt':
result = aNumber < bNumber;
break;
case 'lte':
result = aNumber <= bNumber;
break;
case 'eq':
result = aNumber === bNumber;
break;
case 'neq':
result = aNumber !== bNumber;
break;
default:
toastr.error('Unknown boolean comparison rule for type number.', 'Invalid command');
throw new Error('Invalid command.');
}
}
return result;
}
/**
* Executes a slash command from a string (may be enclosed in quotes) and returns the result.
* @param {string} command Command to execute. May contain escaped macro and batch separators.
* @returns {Promise<string>} Pipe result
*/
async function executeSubCommands(command) {
if (command.startsWith('"')) {
command = command.slice(1);
}
if (command.endsWith('"')) {
command = command.slice(0, -1);
}
const unescape = true;
const result = await executeSlashCommands(command, unescape);
if (!result || typeof result !== 'object') {
return '';
}
return result?.pipe || '';
}
/**
* Deletes a local variable.
* @param {string} name Variable name to delete
* @returns {string} Empty string
*/
function deleteLocalVariable(name) {
if (!existsLocalVariable(name)) {
console.warn(`The local variable "${name}" does not exist.`);
return '';
}
delete chat_metadata.variables[name];
saveMetadataDebounced();
return '';
}
/**
* Deletes a global variable.
* @param {string} name Variable name to delete
* @returns {string} Empty string
*/
function deleteGlobalVariable(name) {
if (!existsGlobalVariable(name)) {
console.warn(`The global variable "${name}" does not exist.`);
return '';
}
delete extension_settings.variables.global[name];
saveSettingsDebounced();
return '';
}
/**
* Parses a series of numeric values from a string.
* @param {string} value A space-separated list of numeric values or variable names
* @returns {number[]} An array of numeric values
*/
function parseNumericSeries(value) {
if (typeof value === 'number') {
return [value];
}
const array = value
.split(' ')
.map(i => i.trim())
.filter(i => i !== '')
.map(i => isNaN(Number(i)) ? Number(resolveVariable(i)) : Number(i))
.filter(i => !isNaN(i));
return array;
}
function performOperation(value, operation, singleOperand = false) {
if (!value) {
return 0;
}
const array = parseNumericSeries(value);
if (array.length === 0) {
return 0;
}
const result = singleOperand ? operation(array[0]) : operation(array);
if (isNaN(result) || !isFinite(result)) {
return 0;
}
return result;
}
function addValuesCallback(value) {
return performOperation(value, (array) => array.reduce((a, b) => a + b, 0));
}
function mulValuesCallback(value) {
return performOperation(value, (array) => array.reduce((a, b) => a * b, 1));
}
function minValuesCallback(value) {
return performOperation(value, (array) => Math.min(...array));
}
function maxValuesCallback(value) {
return performOperation(value, (array) => Math.max(...array));
}
function subValuesCallback(value) {
return performOperation(value, (array) => array[0] - array[1]);
}
function divValuesCallback(value) {
return performOperation(value, (array) => {
if (array[1] === 0) {
console.warn('Division by zero.');
return 0;
}
return array[0] / array[1];
});
}
function modValuesCallback(value) {
return performOperation(value, (array) => {
if (array[1] === 0) {
console.warn('Division by zero.');
return 0;
}
return array[0] % array[1];
});
}
function powValuesCallback(value) {
return performOperation(value, (array) => Math.pow(array[0], array[1]));
}
function sinValuesCallback(value) {
return performOperation(value, Math.sin, true);
}
function cosValuesCallback(value) {
return performOperation(value, Math.cos, true);
}
function logValuesCallback(value) {
return performOperation(value, Math.log, true);
}
function roundValuesCallback(value) {
return performOperation(value, Math.round, true);
}
function absValuesCallback(value) {
return performOperation(value, Math.abs, true);
}
function sqrtValuesCallback(value) {
return performOperation(value, Math.sqrt, true);
}
export function registerVariableCommands() {
registerSlashCommand('listvar', listVariablesCallback, [], ' list registered chat variables', true, true);
registerSlashCommand('setvar', (args, value) => setLocalVariable(args.key || args.name, value), [], '<span class="monospace">key=varname (value)</span> set a local variable value and pass it down the pipe, e.g. <tt>/setvar key=color green</tt>', true, true);
registerSlashCommand('getvar', (_, value) => getLocalVariable(value), [], '<span class="monospace">(key)</span> get a local variable value and pass it down the pipe, e.g. <tt>/getvar height</tt>', true, true);
registerSlashCommand('addvar', (args, value) => addLocalVariable(args.key || args.name, value), [], '<span class="monospace">key=varname (increment)</span> add a value to a local variable and pass the result down the pipe, e.g. <tt>/addvar score 10</tt>', true, true);
registerSlashCommand('setglobalvar', (args, value) => setGlobalVariable(args.key || args.name, value), [], '<span class="monospace">key=varname (value)</span> set a global variable value and pass it down the pipe, e.g. <tt>/setglobalvar key=color green</tt>', true, true);
registerSlashCommand('getglobalvar', (_, value) => getGlobalVariable(value), [], '<span class="monospace">(key)</span> get a global variable value and pass it down the pipe, e.g. <tt>/getglobalvar height</tt>', true, true);
registerSlashCommand('addglobalvar', (args, value) => addGlobalVariable(args.key || args.name, value), [], '<span class="monospace">key=varname (increment)</span> add a value to a global variable and pass the result down the pipe, e.g. <tt>/addglobalvar score 10</tt>', true, true);
registerSlashCommand('incvar', (_, value) => incrementLocalVariable(value), [], '<span class="monospace">(key)</span> increment a local variable by 1 and pass the result down the pipe, e.g. <tt>/incvar score</tt>', true, true);
registerSlashCommand('decvar', (_, value) => decrementLocalVariable(value), [], '<span class="monospace">(key)</span> decrement a local variable by 1 and pass the result down the pipe, e.g. <tt>/decvar score</tt>', true, true);
registerSlashCommand('incglobalvar', (_, value) => incrementGlobalVariable(value), [], '<span class="monospace">(key)</span> increment a global variable by 1 and pass the result down the pipe, e.g. <tt>/incglobalvar score</tt>', true, true);
registerSlashCommand('decglobalvar', (_, value) => decrementGlobalVariable(value), [], '<span class="monospace">(key)</span> decrement a global variable by 1 and pass the result down the pipe, e.g. <tt>/decglobalvar score</tt>', true, true);
registerSlashCommand('if', ifCallback, [], '<span class="monospace">left=varname1 right=varname2 rule=comparison else="(alt.command)" "(command)"</span> compare the value of the left operand "a" with the value of the right operand "b", and if the condition yields true, then execute any valid slash command enclosed in quotes and pass the result of the command execution down the pipe. Numeric values and string literals for left and right operands supported. Available rules: gt => a > b, gte => a >= b, lt => a < b, lte => a <= b, eq => a == b, neq => a != b, not => !a, in (strings) => a includes b, nin (strings) => a not includes b, e.g. <tt>/if left=score right=10 rule=gte "/speak You win"</tt> triggers a /speak command if the value of "score" is greater or equals 10.', true, true);
registerSlashCommand('while', whileCallback, [], '<span class="monospace">left=varname1 right=varname2 rule=comparison "(command)"</span> compare the value of the left operand "a" with the value of the right operand "b", and if the condition yields true, then execute any valid slash command enclosed in quotes. Numeric values and string literals for left and right operands supported. Available rules: gt => a > b, gte => a >= b, lt => a < b, lte => a <= b, eq => a == b, neq => a != b, not => !a, in (strings) => a includes b, nin (strings) => a not includes b, e.g. <tt>/setvar key=i 0 | /while left=i right=10 rule=let "/addvar key=i 1"</tt> adds 1 to the value of "i" until it reaches 10. Loops are limited to 100 iterations by default, pass guard=off to disable.', true, true);
registerSlashCommand('flushvar', (_, value) => deleteLocalVariable(value), [], '<span class="monospace">(key)</span> delete a local variable, e.g. <tt>/flushvar score</tt>', true, true);
registerSlashCommand('flushglobalvar', (_, value) => deleteGlobalVariable(value), [], '<span class="monospace">(key)</span> delete a global variable, e.g. <tt>/flushglobalvar score</tt>', true, true);
registerSlashCommand('add', (_, value) => addValuesCallback(value), [], '<span class="monospace">(a b c d)</span> performs an addition of the set of values and passes the result down the pipe, can use variable names, e.g. <tt>/add 10 i 30 j</tt>', true, true);
registerSlashCommand('mul', (_, value) => mulValuesCallback(value), [], '<span class="monospace">(a b c d)</span> performs a multiplication of the set of values and passes the result down the pipe, can use variable names, e.g. <tt>/mul 10 i 30 j</tt>', true, true);
registerSlashCommand('max', (_, value) => maxValuesCallback(value), [], '<span class="monospace">(a b c d)</span> returns the maximum value of the set of values and passes the result down the pipe, can use variable names, e.g. <tt>/max 10 i 30 j</tt>', true, true);
registerSlashCommand('min', (_, value) => minValuesCallback(value), [], '<span class="monospace">(a b c d)</span> returns the minimum value of the set of values and passes the result down the pipe, can use variable names, e.g. <tt>/min 10 i 30 j</tt>', true, true);
registerSlashCommand('sub', (_, value) => subValuesCallback(value), [], '<span class="monospace">(a b)</span> performs a subtraction of two values and passes the result down the pipe, can use variable names, e.g. <tt>/sub i 5</tt>', true, true);
registerSlashCommand('div', (_, value) => divValuesCallback(value), [], '<span class="monospace">(a b)</span> performs a division of two values and passes the result down the pipe, can use variable names, e.g. <tt>/div 10 i</tt>', true, true);
registerSlashCommand('mod', (_, value) => modValuesCallback(value), [], '<span class="monospace">(a b)</span> performs a modulo operation of two values and passes the result down the pipe, can use variable names, e.g. <tt>/mod i 2</tt>', true, true);
registerSlashCommand('pow', (_, value) => powValuesCallback(value), [], '<span class="monospace">(a b)</span> performs a power operation of two values and passes the result down the pipe, can use variable names, e.g. <tt>/pow i 2</tt>', true, true);
registerSlashCommand('sin', (_, value) => sinValuesCallback(value), [], '<span class="monospace">(a)</span> performs a sine operation of a value and passes the result down the pipe, can use variable names, e.g. <tt>/sin i</tt>', true, true);
registerSlashCommand('cos', (_, value) => cosValuesCallback(value), [], '<span class="monospace">(a)</span> performs a cosine operation of a value and passes the result down the pipe, can use variable names, e.g. <tt>/cos i</tt>', true, true);
registerSlashCommand('log', (_, value) => logValuesCallback(value), [], '<span class="monospace">(a)</span> performs a logarithm operation of a value and passes the result down the pipe, can use variable names, e.g. <tt>/log i</tt>', true, true);
registerSlashCommand('abs', (_, value) => absValuesCallback(value), [], '<span class="monospace">(a)</span> performs an absolute value operation of a value and passes the result down the pipe, can use variable names, e.g. <tt>/abs i</tt>', true, true);
registerSlashCommand('sqrt', (_, value) => sqrtValuesCallback(value), [], '<span class="monospace">(a)</span> performs a square root operation of a value and passes the result down the pipe, can use variable names, e.g. <tt>/sqrt i</tt>', true, true);
registerSlashCommand('round', (_, value) => roundValuesCallback(value), [], '<span class="monospace">(a)</span> rounds a value and passes the result down the pipe, can use variable names, e.g. <tt>/round i</tt>', true, true);
}

View File

@ -7,11 +7,14 @@ import { getDeviceInfo } from "./RossAscends-mods.js";
import { FILTER_TYPES, FilterHelper } from "./filters.js";
import { getTokenCount } from "./tokenizers.js";
import { power_user } from "./power-user.js";
import { getTagKeyForCharacter } from "./tags.js";
export {
world_info,
world_info_budget,
world_info_depth,
world_info_min_activations,
world_info_min_activations_depth_max,
world_info_recursive,
world_info_overflow_alert,
world_info_case_sensitive,
@ -35,6 +38,9 @@ let world_info = {};
let selected_world_info = [];
let world_names;
let world_info_depth = 2;
let world_info_min_activations = 0; // if > 0, will continue seeking chat until minimum world infos are activated
let world_info_min_activations_depth_max = 0; // used when (world_info_min_activations > 0)
let world_info_budget = 25;
let world_info_recursive = false;
let world_info_overflow_alert = false;
@ -55,14 +61,14 @@ const worldInfoFilter = new FilterHelper(() => updateEditor());
const SORT_ORDER_KEY = 'world_info_sort_order';
const METADATA_KEY = 'world_info';
const InputWidthReference = $("#WIInputWidthReference");
const DEFAULT_DEPTH = 4;
export function getWorldInfoSettings() {
return {
world_info,
world_info_depth,
world_info_min_activations,
world_info_min_activations_depth_max,
world_info_budget,
world_info_recursive,
world_info_overflow_alert,
@ -102,6 +108,10 @@ async function getWorldInfoPrompt(chat2, maxContext) {
function setWorldInfoSettings(settings, data) {
if (settings.world_info_depth !== undefined)
world_info_depth = Number(settings.world_info_depth);
if (settings.world_info_min_activations !== undefined)
world_info_min_activations = Number(settings.world_info_min_activations);
if (settings.world_info_min_activations_depth_max !== undefined)
world_info_min_activations_depth_max = Number(settings.world_info_min_activations_depth_max);
if (settings.world_info_budget !== undefined)
world_info_budget = Number(settings.world_info_budget);
if (settings.world_info_recursive !== undefined)
@ -138,6 +148,12 @@ function setWorldInfoSettings(settings, data) {
$("#world_info_depth_counter").val(world_info_depth);
$("#world_info_depth").val(world_info_depth);
$("#world_info_min_activations_counter").val(world_info_min_activations);
$("#world_info_min_activations").val(world_info_min_activations);
$("#world_info_min_activations_depth_max_counter").val(world_info_min_activations_depth_max);
$("#world_info_min_activations_depth_max").val(world_info_min_activations_depth_max);
$("#world_info_budget_counter").val(world_info_budget);
$("#world_info_budget").val(world_info_budget);
@ -212,7 +228,7 @@ async function loadWorldInfoData(name) {
}
async function updateWorldInfoList() {
var result = await fetch("/getsettings", {
const result = await fetch("/getsettings", {
method: "POST",
headers: getRequestHeaders(),
body: JSON.stringify({}),
@ -254,7 +270,15 @@ function sortEntries(data) {
const sortRule = option.data('rule');
const orderSign = sortOrder === 'asc' ? 1 : -1;
if (sortRule === 'priority') {
if (sortRule === 'custom') {
// First by display index, then by order, then by uid
data.sort((a, b) => {
const aValue = a.displayIndex;
const bValue = b.displayIndex;
return (aValue - bValue || b.order - a.order || a.uid - b.uid);
});
} else if (sortRule === 'priority') {
// First constant, then normal, then disabled. Then sort by order
data.sort((a, b) => {
const aValue = a.constant ? 0 : a.disable ? 2 : 1;
@ -360,31 +384,36 @@ function displayWorldEntries(name, data, navigation = navigation_option.none) {
nextText: '>',
formatNavigator: PAGINATION_TEMPLATE,
showNavigator: true,
callback: function (page) {
callback: function (/** @type {object[]} */ page) {
$("#world_popup_entries_list").empty();
const keywordHeaders = `
<div id="WIEntryHeaderTitlesPC" class="flex-container wide100p spaceBetween justifyCenter textAlignCenter" style="padding:0 2.5em;">
<small class="flex1">
Title/Memo
</small>
<small style="width:${InputWidthReference.width() + 5 + 'px'}">
<small style="width: calc(3.5em + 5px)">
Status
</small>
<small style="width:${InputWidthReference.width() + 20 + 'px'}">
<small style="width: calc(3.5em + 20px)">
Position
</small>
<small style="width:${InputWidthReference.width() + 15 + 'px'}">
<small style="width: calc(3.5em + 15px)">
Depth
</small>
<small style="width:${InputWidthReference.width() + 15 + 'px'}">
<small style="width: calc(3.5em + 15px)">
Order
</small>
<small style="width:${InputWidthReference.width() + 15 + 'px'}">
<small style="width: calc(3.5em + 15px)">
Trigger %
</small>
</div>`
const blocks = page.map(entry => getWorldEntry(name, data, entry));
const blocks = page.map(entry => getWorldEntry(name, data, entry)).filter(x => x);
const isCustomOrder = $('#world_info_sort_order').find(':selected').data('rule') === 'custom';
if (!isCustomOrder) {
blocks.forEach(block => {
block.find('.drag-handle').remove();
});
}
$("#world_popup_entries_list").append(keywordHeaders);
$("#world_popup_entries_list").append(blocks);
},
@ -486,6 +515,8 @@ function displayWorldEntries(name, data, navigation = navigation_option.none) {
delay: getSortableDelay(),
handle: ".drag-handle",
stop: async function (event, ui) {
const firstEntryUid = $('#world_popup_entries_list .world_entry').first().data('uid');
const minDisplayIndex = data?.entries[firstEntryUid]?.displayIndex ?? 0;
$('#world_popup_entries_list .world_entry').each(function (index) {
const uid = $(this).data('uid');
@ -497,8 +528,8 @@ function displayWorldEntries(name, data, navigation = navigation_option.none) {
return;
}
item.displayIndex = index;
setOriginalDataValue(data, uid, 'extensions.display_index', index);
item.displayIndex = minDisplayIndex + index;
setOriginalDataValue(data, uid, 'extensions.display_index', item.displayIndex);
});
console.table(Object.keys(data.entries).map(uid => data.entries[uid]).map(x => ({ uid: x.uid, key: x.key.join(','), displayIndex: x.displayIndex })));
@ -545,6 +576,10 @@ function deleteOriginalDataValue(data, uid) {
}
function getWorldEntry(name, data, entry) {
if (!data.entries[entry.uid]) {
return;
}
const template = $("#entry_edit_template .world_entry").clone();
template.data("uid", entry.uid);
template.attr("uid", entry.uid);
@ -569,7 +604,7 @@ function getWorldEntry(name, data, entry) {
setOriginalDataValue(data, uid, "keys", data.entries[uid].key);
saveWorldInfo(name, data);
});
keyInput.val(entry.key.join(",")).trigger("input");
keyInput.val(entry.key.join(", ")).trigger("input");
//initScrollHeight(keyInput);
// logic AND/NOT
@ -606,7 +641,7 @@ function getWorldEntry(name, data, entry) {
const value = $(this).prop("checked");
characterFilterLabel.text(value ? "Exclude Character(s)" : "Filter to Character(s)");
if (data.entries[uid].characterFilter) {
if (!value && data.entries[uid].characterFilter.names.length === 0) {
if (!value && data.entries[uid].characterFilter.names.length === 0 && data.entries[uid].characterFilter.tags.length === 0) {
delete data.entries[uid].characterFilter;
} else {
data.entries[uid].characterFilter.isExclude = value
@ -617,7 +652,8 @@ function getWorldEntry(name, data, entry) {
{
characterFilter: {
isExclude: true,
names: []
names: [],
tags: [],
}
}
);
@ -639,13 +675,25 @@ function getWorldEntry(name, data, entry) {
closeOnSelect: false,
});
}
const characters = getContext().characters;
characters.forEach((character) => {
const option = document.createElement('option');
const name = character.avatar.replace(/\.[^/.]+$/, "") ?? character.name
option.innerText = name
option.selected = entry.characterFilter?.names.includes(name)
characterFilter.append(option)
const name = character.avatar.replace(/\.[^/.]+$/, "") ?? character.name;
option.innerText = name;
option.selected = entry.characterFilter?.names?.includes(name);
option.setAttribute('data-type', 'character');
characterFilter.append(option);
});
const tags = getContext().tags;
tags.forEach((tag) => {
const option = document.createElement('option');
option.innerText = `[Tag] ${tag.name}`;
option.selected = entry.characterFilter?.tags?.includes(tag.id);
option.value = tag.id;
option.setAttribute('data-type', 'tag');
characterFilter.append(option);
});
characterFilter.on('mousedown change', async function (e) {
@ -656,16 +704,19 @@ function getWorldEntry(name, data, entry) {
}
const uid = $(this).data("uid");
const value = $(this).val();
if ((!value || value?.length === 0) && !data.entries[uid].characterFilter?.isExclude) {
const selected = $(this).find(':selected');
if ((!selected || selected?.length === 0) && !data.entries[uid].characterFilter?.isExclude) {
delete data.entries[uid].characterFilter;
} else {
const names = selected.filter('[data-type="character"]').map((_, e) => e instanceof HTMLOptionElement && e.innerText).toArray();
const tags = selected.filter('[data-type="tag"]').map((_, e) => e instanceof HTMLOptionElement && e.value).toArray();
Object.assign(
data.entries[uid],
{
characterFilter: {
isExclude: data.entries[uid].characterFilter?.isExclude ?? false,
names: value
names: names,
tags: tags,
}
}
);
@ -690,7 +741,7 @@ function getWorldEntry(name, data, entry) {
saveWorldInfo(name, data);
});
keySecondaryInput.val(entry.keysecondary.join(",")).trigger("input");
keySecondaryInput.val(entry.keysecondary.join(", ")).trigger("input");
initScrollHeight(keySecondaryInput);
// comment
@ -819,7 +870,7 @@ function getWorldEntry(name, data, entry) {
saveWorldInfo(name, data);
});
orderInput.val(entry.order).trigger("input");
orderInput.width(InputWidthReference.width() + 15 + 'px')
orderInput.css('width', 'calc(3em + 15px)');
// probability
if (entry.probability === undefined) {
@ -840,7 +891,7 @@ function getWorldEntry(name, data, entry) {
saveWorldInfo(name, data);
});
depthInput.val(entry.depth ?? DEFAULT_DEPTH).trigger("input");
depthInput.width(InputWidthReference.width() + 15 + 'px');
depthInput.css('width', 'calc(3em + 15px)');
// Hide by default unless depth is specified
if (entry.position === world_info_position.atDepth) {
@ -868,7 +919,7 @@ function getWorldEntry(name, data, entry) {
saveWorldInfo(name, data);
});
probabilityInput.val(entry.probability).trigger("input");
probabilityInput.width(InputWidthReference.width() + 15 + 'px')
probabilityInput.css('width', 'calc(3em + 15px)');
// probability toggle
if (entry.useProbability === undefined) {
@ -1379,6 +1430,7 @@ async function checkWorldInfo(chat, maxContext) {
// Combine the chat
let textToScan = chat.slice(0, messagesToLookBack).join("");
let minActivationMsgIndex = messagesToLookBack; // tracks chat index to satisfy `world_info_min_activations`
// Add the depth or AN if enabled
// Put this code here since otherwise, the chat reference is modified
@ -1402,6 +1454,7 @@ async function checkWorldInfo(chat, maxContext) {
textToScan = transformString(textToScan);
let needsToScan = true;
let token_budget_overflowed = false;
let count = 0;
let allActivatedEntries = new Set();
let failedProbabilityChecks = new Set();
@ -1429,15 +1482,35 @@ async function checkWorldInfo(chat, maxContext) {
for (let entry of sortedEntries) {
// Check if this entry applies to the character or if it's excluded
if (entry.characterFilter && entry.characterFilter?.names.length > 0) {
if (entry.characterFilter && entry.characterFilter?.names?.length > 0) {
const nameIncluded = entry.characterFilter.names.includes(getCharaFilename());
const filtered = entry.characterFilter.isExclude ? nameIncluded : !nameIncluded
if (filtered) {
console.debug(`WI entry ${entry.uid} filtered out by character`);
continue;
}
}
if (entry.characterFilter && entry.characterFilter?.tags?.length > 0) {
const tagKey = getTagKeyForCharacter(this_chid);
if (tagKey) {
const tagMapEntry = context.tagMap[tagKey];
if (Array.isArray(tagMapEntry)) {
// If tag map intersects with the tag exclusion list, skip
const includesTag = tagMapEntry.some((tag) => entry.characterFilter.tags.includes(tag));
const filtered = entry.characterFilter.isExclude ? includesTag : !includesTag;
if (filtered) {
console.debug(`WI entry ${entry.uid} filtered out by tag`);
continue;
}
}
}
}
if (failedProbabilityChecks.has(entry)) {
continue;
}
@ -1453,6 +1526,9 @@ async function checkWorldInfo(chat, maxContext) {
}
if (Array.isArray(entry.key) && entry.key.length) { //check for keywords existing
// If selectiveLogic isn't found, assume it's AND, only do this once per entry
const selectiveLogic = entry.selectiveLogic ?? 0;
let notFlag = true;
primary: for (let key of entry.key) {
const substituted = substituteParams(key);
console.debug(`${entry.uid}: ${substituted}`)
@ -1468,10 +1544,6 @@ async function checkWorldInfo(chat, maxContext) {
secondary: for (let keysecondary of entry.keysecondary) {
const secondarySubstituted = substituteParams(keysecondary);
console.debug(`uid:${entry.uid}: filtering ${secondarySubstituted}`);
// If selectiveLogic isn't found, assume it's AND
const selectiveLogic = entry.selectiveLogic ?? 0;
//AND operator
if (selectiveLogic === 0) {
console.debug('saw AND logic, checking..')
@ -1486,11 +1558,8 @@ async function checkWorldInfo(chat, maxContext) {
console.debug(`uid ${entry.uid}: checking NOT logic for ${secondarySubstituted}`)
if (secondarySubstituted && matchKeys(textToScan, secondarySubstituted.trim())) {
console.debug(`uid ${entry.uid}: canceled; filtered out by ${secondarySubstituted}`)
notFlag = false;
break primary;
} else {
console.debug(`${entry.uid}: activated; passed NOT filter`)
activatedNow.add(entry);
break secondary;
}
}
}
@ -1502,6 +1571,11 @@ async function checkWorldInfo(chat, maxContext) {
}
} else { console.debug('no active entries for logic checks yet') }
}
//for a NOT all entries must be checked, a single match invalidates activation
if (selectiveLogic === 1 && notFlag) {
console.debug(`${entry.uid}: activated; passed NOT filter`)
activatedNow.add(entry);
}
}
}
@ -1531,6 +1605,7 @@ async function checkWorldInfo(chat, maxContext) {
toastr.warning(`World info budget reached after ${allActivatedEntries.size} entries.`, 'World Info');
}
needsToScan = false;
token_budget_overflowed = true;
break;
}
@ -1553,6 +1628,22 @@ async function checkWorldInfo(chat, maxContext) {
textToScan = (currentlyActivatedText + '\n' + textToScan);
allActivatedText = (currentlyActivatedText + '\n' + allActivatedText);
}
// world_info_min_activations
if (!needsToScan && !token_budget_overflowed) {
if (world_info_min_activations > 0 && (allActivatedEntries.size < world_info_min_activations)) {
let over_max = false
over_max = (
world_info_min_activations_depth_max > 0 &&
minActivationMsgIndex > world_info_min_activations_depth_max
) || (minActivationMsgIndex >= chat.length)
if (!over_max) {
needsToScan = true
textToScan = transformString(chat.slice(minActivationMsgIndex, minActivationMsgIndex + 1).join(""));
minActivationMsgIndex += 1
}
}
}
}
// Forward-sorted list of entries for joining
@ -1736,6 +1827,7 @@ function convertCharacterBook(characterBook) {
probability: entry.extensions?.probability ?? null,
useProbability: entry.extensions?.useProbability ?? false,
depth: entry.extensions?.depth ?? DEFAULT_DEPTH,
selectiveLogic: entry.extensions?.selectiveLogic ?? 0,
};
});
@ -1839,7 +1931,7 @@ export async function importEmbeddedWorldInfo(skipPopup = false) {
function onWorldInfoChange(_, text) {
if (_ !== '__notSlashCommand__') { // if it's a slash command
if (text !== undefined) { // and args are provided
if (text.trim() !== '') { // and args are provided
const slashInputSplitText = text.trim().toLowerCase().split(",");
slashInputSplitText.forEach((worldName) => {
@ -1856,7 +1948,7 @@ function onWorldInfoChange(_, text) {
} else { // if no args, unset all worlds
toastr.success('Deactivated all worlds');
selected_world_info = [];
$("#world_info").val("");
$("#world_info").val(null).trigger('change');
}
} else { //if it's a pointer selection
let tempWorldInfo = [];
@ -2026,7 +2118,7 @@ jQuery(() => {
$("#world_editor_select").on('change', async () => {
$("#world_info_search").val('');
worldInfoFilter.setFilterData(FILTER_TYPES.WORLD_INFO_SEARCH, '', true);
const selectedIndex = $("#world_editor_select").find(":selected").val();
const selectedIndex = String($("#world_editor_select").find(":selected").val());
if (selectedIndex === "") {
hideWorldEditor();
@ -2041,27 +2133,39 @@ jQuery(() => {
eventSource.emit(event_types.WORLDINFO_SETTINGS_UPDATED);
}
$(document).on("input", "#world_info_depth", function () {
$("#world_info_depth").on('input', function () {
world_info_depth = Number($(this).val());
$("#world_info_depth_counter").val($(this).val());
saveSettings();
});
$(document).on("input", "#world_info_budget", function () {
$("#world_info_min_activations").on('input', function () {
world_info_min_activations = Number($(this).val());
$("#world_info_min_activations_counter").val($(this).val());
saveSettings();
});
$("#world_info_min_activations_depth_max").on('input', function () {
world_info_min_activations_depth_max = Number($(this).val());
$("#world_info_min_activations_depth_max_counter").val($(this).val());
saveSettings();
});
$("#world_info_budget").on('input', function () {
world_info_budget = Number($(this).val());
$("#world_info_budget_counter").val($(this).val());
saveSettings();
});
$(document).on("input", "#world_info_recursive", function () {
$("#world_info_recursive").on('input', function () {
world_info_recursive = !!$(this).prop('checked');
saveSettings();
})
});
$('#world_info_case_sensitive').on('input', function () {
world_info_case_sensitive = !!$(this).prop('checked');
saveSettings();
})
});
$('#world_info_match_whole_words').on('input', function () {
world_info_match_whole_words = !!$(this).prop('checked');
@ -2115,11 +2219,9 @@ jQuery(() => {
updateEditor(navigation_option.previous);
});
$('#world_info_sort_order').on('change', function (e) {
if (e.target instanceof HTMLOptionElement) {
localStorage.setItem(SORT_ORDER_KEY, e.target.value);
}
$('#world_info_sort_order').on('change', function () {
const value = String($(this).find(":selected").val());
localStorage.setItem(SORT_ORDER_KEY, value);
updateEditor(navigation_option.none);
})

View File

@ -1,6 +1,9 @@
@charset "UTF-8";
@import url(css/promptmanager.css);
@import url(css/loader.css);
@import url(css/character-group-overlay.css);
@import url(css/file-form.css);
:root {
--doc-height: 100%;
@ -23,6 +26,8 @@
--grey10: rgb(25, 25, 25);
--grey30: rgb(75, 75, 75);
--grey50: rgb(125, 125, 125);
--grey5020a: rgba(125, 125, 125, 0.2);
--grey5050a: rgba(125, 125, 125, 0.5);
--grey70: rgb(175, 175, 175);
--grey75: rgb(190, 190, 190);
@ -53,6 +58,13 @@
--SmartThemeBlurStrength: calc(var(--blurStrength) * 1px);
--SmartThemeShadowColor: rgba(0, 0, 0, 0.5);
--SmartThemeBorderColor: rgba(0, 0, 0, 0.5);
--SmartThemeCheckboxBgColorR: 220;
--SmartThemeCheckboxBgColorG: 220;
--SmartThemeCheckboxBgColorB: 210;
--SmartThemeCheckboxTickColorValue: calc(((((var(--SmartThemeCheckboxBgColorR) * 299) + (var(--SmartThemeCheckboxBgColorG) * 587) + (var(--SmartThemeCheckboxBgColorB) * 114)) / 1000) - 128) * -1000);
--SmartThemeCheckboxTickColor: rgb(var(--SmartThemeCheckboxTickColorValue),
var(--SmartThemeCheckboxTickColorValue),
var(--SmartThemeCheckboxTickColorValue));
--sheldWidth: 50vw;
@ -70,8 +82,8 @@
color-scheme: only light;
/* Send form variables */
--bottomFormBlockPadding: calc(var(--mainFontSize) / 3);
--bottomFormIconSize: calc(var(--mainFontSize) * 2);
--bottomFormBlockPadding: calc(var(--mainFontSize) / 2.5);
--bottomFormIconSize: calc(var(--mainFontSize) * 1.9);
--bottomFormBlockSize: calc(var(--bottomFormIconSize) + var(--bottomFormBlockPadding));
/*Top Bar Scaling Variables*/
@ -114,6 +126,7 @@ body {
font-family: "Noto Sans", "Noto Color Emoji", sans-serif;
font-size: var(--mainFontSize);
color: var(--SmartThemeBodyColor);
overflow: hidden;
}
::-webkit-scrollbar {
@ -122,7 +135,7 @@ body {
}
.scrollY {
overflow-y: auto;
overflow-y: auto !important;
}
::-webkit-scrollbar-thumb {
@ -213,10 +226,15 @@ table.responsiveTable {
color: var(--white50a);
}
.mes[is_system="true"] .mes_text br {
.mes[is_system="true"][ch_name="SillyTavern System"] .mes_text br {
display: none;
}
.mes[is_system="true"] .avatar {
opacity: 0.9;
filter: grayscale(25%);
}
.mes_text table {
border-spacing: 0;
border-collapse: collapse;
@ -235,9 +253,7 @@ table.responsiveTable {
}
.mes_text li tt {
min-width: 80px;
display: inline-block;
text-align: right;
}
.mes_text br,
@ -261,6 +277,15 @@ table.responsiveTable {
color: var(--SmartThemeQuoteColor);
}
.mes_text font[color] em,
.mes_text font[color] i {
color: inherit;
}
.mes_text font[color] q {
color: inherit;
}
.mes_text rp {
display: block;
}
@ -303,10 +328,23 @@ table.responsiveTable {
.mes_translate,
.sd_message_gen,
.mes_ghost,
.mes_narrate {
display: none;
}
.mes[is_system="true"] .mes_hide {
display: none;
}
.mes[is_system="false"] .mes_unhide {
display: none;
}
.mes[is_system="true"] .mes_ghost {
display: flex;
}
small {
color: var(--grey70);
}
@ -377,12 +415,12 @@ hr {
#bg1 {
background-image: url('backgrounds/tavern day.jpg');
z-index: -2;
z-index: -3;
}
#bg_custom {
background-image: none;
z-index: -1;
z-index: -2;
}
/*TOPPER margin*/
@ -510,7 +548,7 @@ hr {
/* special case for desktop Safari to allow #sheld resizing */
@media only screen and (min-width: 1024px) and (-webkit-min-device-pixel-ratio: 2) and (pointer: fine) {
#form_sheld {
margin-bottom: 5px;
margin-bottom: 4px;
}
}
@ -530,10 +568,9 @@ hr {
background-color: var(--crimson70a) !important;
}
#send_but_sheld {
#nonQRFormItems {
padding: 0;
border: 0;
height: var(--bottomFormBlockSize);
position: relative;
background-position: center;
display: flex;
@ -541,10 +578,28 @@ hr {
column-gap: 5px;
font-size: var(--bottomFormIconSize);
overflow: hidden;
order: 1003;
order: 25;
width: 100%;
}
#send_but_sheld>div {
#leftSendForm,
#rightSendForm {
display: flex;
flex-wrap: wrap;
}
#leftSendForm {
order: 1;
padding-left: 2px;
}
#rightSendForm {
order: 3;
padding-right: 2px;
}
#send_form>#nonQRFormItems>div>div:not(.mes_stop) {
width: var(--bottomFormBlockSize);
height: var(--bottomFormBlockSize);
margin: 0;
@ -556,25 +611,26 @@ hr {
display: flex;
align-items: center;
justify-content: center;
transition: all 300ms;
}
#options_button:hover,
#send_but_sheld>div:hover {
#send_form>#nonQRFormItems>div>div:hover {
opacity: 1;
filter: brightness(1.2);
}
#send_but {
order: 99999;
order: 2;
}
#mes_continue {
order: 99998;
order: 1;
}
#send_but_sheld .mes_stop {
#send_form .mes_stop {
display: none;
order: 99997;
order: 2;
padding-right: 2px;
}
#options_button {
@ -593,7 +649,7 @@ hr {
transition: 0.3s;
display: flex;
align-items: center;
order: 1001;
order: 2;
}
.font-family-reset {
@ -611,6 +667,7 @@ hr {
backdrop-filter: blur(var(--SmartThemeBlurStrength));
box-shadow: 0 0 10px rgba(0, 0, 0, 0.5);
flex-flow: column;
border-radius: 10px;
}
.options-content,
@ -631,6 +688,7 @@ hr {
display: flex;
align-items: center;
justify-content: center;
pointer-events: none;
}
.options-content hr {
@ -641,7 +699,7 @@ hr {
}
#extensionsMenuButton {
order: 100;
order: 4;
padding: 1px;
}
@ -710,6 +768,10 @@ hr {
/*only affects bubblechat to make it sit nicely at the bottom*/
}
.last_mes .mes_text {
padding-right: 30px;
}
/* SWIPE RELATED STYLES*/
.swipe_right,
@ -860,14 +922,25 @@ hr {
box-shadow: 0 0 5px var(--black50a);
}
.bogus_folder_select .avatar,
.character_select .avatar {
flex: unset;
}
.bogus_folder_select .avatar {
justify-content: center;
background-color: var(--SmartThemeBlurTintColor);
color: var(--SmartThemeBodyColor);
outline-style: solid;
outline-width: 1px;
outline-color: var(--SmartThemeBorderColor);
}
.mes_block {
padding-top: 0;
padding-left: 10px;
width: 100%;
overflow-x: hidden;
}
.mes_text {
@ -892,7 +965,7 @@ textarea {
background-color: var(--black30a);
outline: none;
border: 1px solid var(--SmartThemeBorderColor);
border-radius: 7px;
border-radius: 5px;
color: var(--SmartThemeBodyColor);
font-size: var(--mainFontSize);
font-family: "Noto Sans", "Noto Color Emoji", sans-serif;
@ -924,12 +997,12 @@ select {
background-color: rgba(255, 0, 0, 0);
border: 0;
box-shadow: none;
padding-top: 6px;
padding: 6px;
font-family: "Noto Sans", "Noto Color Emoji", sans-serif;
margin: 0;
text-shadow: 0px 0px calc(var(--shadowWidth) * 1px) var(--SmartThemeShadowColor);
flex: 1;
order: 1002;
order: 3;
}
.text_pole::placeholder {
@ -945,6 +1018,7 @@ select {
@media screen and (max-width: 1000px) {
#form_create textarea {
flex-grow: 1;
min-height: 20svh;
}
}
@ -964,8 +1038,7 @@ select {
margin-bottom: 0;
}
#character_cross,
#select_chat_cross {
#character_cross {
position: absolute;
right: 5px;
top: 5px;
@ -982,7 +1055,7 @@ select {
background-color: var(--black30a);
color: var(--SmartThemeBodyColor);
border: 1px solid var(--SmartThemeBorderColor);
border-radius: 7px;
border-radius: 5px;
font-family: "Noto Sans", "Noto Color Emoji", sans-serif;
padding: 3px 5px;
width: 100%;
@ -1184,6 +1257,20 @@ input[type="file"] {
width: calc(100% - 85px);
}
#rm_print_characters_block .empty_block {
display: flex;
flex-direction: column;
gap: 10px;
flex-wrap: wrap;
text-align: center;
height: 100%;
width: 100%;
opacity: 0.5;
justify-content: center;
margin: 0 auto;
align-items: center;
}
#rm_print_characters_block {
overflow-y: auto;
flex-grow: 1;
@ -1292,7 +1379,7 @@ select {
padding: 3px 2px;
background-color: var(--black30a);
border: 1px solid var(--SmartThemeBorderColor);
border-radius: 7px;
border-radius: 5px;
margin-bottom: 5px;
height: min-content;
}
@ -1330,7 +1417,8 @@ select option:not(:checked) {
.menu_button.disabled {
filter: brightness(75%) grayscale(1);
cursor: not-allowed;
opacity: 0.5;
pointer-events: none;
}
.fav_on {
@ -1414,14 +1502,14 @@ select option:not(:checked) {
margin: 0;
height: fit-content;
padding: 5px;
border-radius: 7px;
border-radius: 5px;
aspect-ratio: 1 / 1;
}
#character_sort_order {
margin: 0;
flex: 1;
border-radius: 7px;
border-radius: 5px;
height: auto;
}
@ -1450,6 +1538,7 @@ input[type=search]:focus::-webkit-search-cancel-button {
pointer-events: all;
}
.bogus_folder_select,
.character_select {
display: flex;
flex-direction: row;
@ -1476,6 +1565,7 @@ input[type=search]:focus::-webkit-search-cancel-button {
font-style: italic;
}
.bogus_folder_select .avatar,
.character_select .avatar {
align-self: center;
}
@ -1503,15 +1593,12 @@ input[type=search]:focus::-webkit-search-cancel-button {
display: block;
}
.bogus_folder_select:hover,
.character_select:hover {
background-color: var(--white30a);
}
/*LEFT SIDE BG MENU*/
#logo_block {
z-index: 3001;
}
/* BG MENU */
#bg_menu {
cursor: pointer;
@ -1838,6 +1925,7 @@ grammarly-extension {
/* Focus */
#bulk_tag_popup,
#dialogue_popup {
width: 500px;
max-width: 90vw;
@ -1880,6 +1968,7 @@ grammarly-extension {
width: unset !important;
}
#bulk_tag_popup_holder,
#dialogue_popup_holder {
display: flex;
flex-direction: column;
@ -1900,6 +1989,7 @@ grammarly-extension {
gap: 20px;
}
#bulk_tag_popup_reset,
#dialogue_popup_ok {
background-color: var(--crimson70a);
cursor: pointer;
@ -1910,6 +2000,7 @@ grammarly-extension {
width: 100%;
}
#bulk_tag_popup_cancel,
#dialogue_popup_cancel {
cursor: pointer;
}
@ -1940,7 +2031,7 @@ grammarly-extension {
color: var(--SmartThemeBodyColor);
background-color: var(--black50a);
border: 1px solid var(--SmartThemeBorderColor);
border-radius: 7px;
border-radius: 5px;
padding: 3px 5px;
width: min-content;
cursor: pointer;
@ -1953,8 +2044,7 @@ grammarly-extension {
}
.avatar_div .menu_button,
.form_create_bottom_buttons_block .menu_button,
#select_chat_import .menu_button {
.form_create_bottom_buttons_block .menu_button {
font-weight: bold;
padding: 5px;
margin: 0;
@ -2033,7 +2123,7 @@ grammarly-extension {
flex-grow: 1;
}
.prompt_order>div:hover {
.prompt_order:not(.ui-sortable-disabled)>div:hover {
background-color: var(--SmartThemeBorderColor);
}
@ -2048,6 +2138,11 @@ grammarly-extension {
filter: grayscale(0.5);
}
.ui-sortable-disabled,
.prompt_order.ui-sortable-disabled>div {
cursor: not-allowed;
}
.prompt_order .toggle_button {
padding-right: 0;
}
@ -2060,11 +2155,7 @@ grammarly-extension {
content: '☐';
}
/* ------ online status indicators and texts. 2 = kobold AI, 3 = Novel AI ----------*/
#online_status2,
#online_status3,
#online_status_horde,
.online_status4 {
.online_status {
opacity: 0.8;
margin-top: 2px;
margin-bottom: 15px;
@ -2073,21 +2164,19 @@ grammarly-extension {
gap: 5px;
}
#online_status_indicator2,
#online_status_indicator3,
#online_status_indicator_horde,
.online_status_indicator4 {
border-radius: 7px;
.online_status_indicator.success {
background-color: green;
}
.online_status_indicator {
border-radius: 100%;
width: 14px;
height: 14px;
background-color: red;
display: inline-block;
}
#online_status_text2,
#online_status_text3,
#online_status_text_horde,
.online_status_text4 {
.online_status_text {
margin-left: 4px;
display: inline-block;
}
@ -2115,15 +2204,6 @@ grammarly-extension {
gap: 5px;
}
/* STLYES FOR THE CHAT MESSAGE DELETION CHECKBOXES */
/* ------------------------------------------------*/
.del_checkbox {
display: none;
opacity: 0.7;
margin-top: 12px;
margin-right: 12px;
}
/* Override toastr default styles */
body #toast-container {
@ -2142,47 +2222,58 @@ body #toast-container>div {
display: block;
}
input[type='checkbox']:not(#nav-toggle):not(#rm_button_panel_pin) {
input[type='checkbox']:not(#nav-toggle):not(#rm_button_panel_pin):not(#lm_button_panel_pin):not(#WI_panel_pin) {
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
outline: none;
outline: 1px solid var(--grey5020a);
position: relative;
width: var(--mainFontSize);
height: var(--mainFontSize);
overflow: hidden;
border-radius: 3px;
background-color: white;
box-shadow: inset 0 0 3px 0 var(--black70a);
border: 1px solid var(--SmartThemeBorderColor);
background-color: var(--SmartThemeBodyColor);
box-shadow: inset 0 0 2px 0 var(--SmartThemeShadowColor);
cursor: pointer;
transform: translateY(-0.075em);
flex-shrink: 0;
place-content: center;
filter: brightness(1.2);
}
input[type='checkbox']:not(#nav-toggle):not(#rm_button_panel_pin):not(#lm_button_panel_pin)::after {
content: '';
color: var(--white100);
position: absolute;
top: 1px;
right: 1px;
bottom: 1px;
left: 1px;
background-color: var(--transparent);
background-size: contain;
background-position: center center;
background-repeat: no-repeat;
border-radius: 2px;
-webkit-transform: scale(0);
input[type='checkbox']:not(#nav-toggle):not(#rm_button_panel_pin):not(#lm_button_panel_pin):not(#WI_panel_pin):not(.del_checkbox) {
display: grid;
}
input[type="checkbox"]:not(#nav-toggle):not(#rm_button_panel_pin):not(#lm_button_panel_pin):not(#WI_panel_pin)::before {
content: "";
width: 0.65em;
height: 0.65em;
transform: scale(0);
-webkit-transition: 0.25s ease-in-out;
transition: 0.25s ease-in-out;
background-image: url("data:image/svg+xml;base64,PCEtLSBHZW5lcmF0ZWQgYnkgSWNvTW9vbi5pbyAtLT4KPHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgd2lkdGg9IjQ0OCIgaGVpZ2h0PSI0NDgiIHZpZXdCb3g9IjAgMCA0NDggNDQ4Ij4KPHRpdGxlPjwvdGl0bGU+CjxnIGlkPSJpY29tb29uLWlnbm9yZSI+CjwvZz4KPHBhdGggZD0iTTQxNy43NSAxNDEuNWMwIDYuMjUtMi41IDEyLjUtNyAxN2wtMjE1IDIxNWMtNC41IDQuNS0xMC43NSA3LTE3IDdzLTEyLjUtMi41LTE3LTdsLTEyNC41LTEyNC41Yy00LjUtNC41LTctMTAuNzUtNy0xN3MyLjUtMTIuNSA3LTE3bDM0LTM0YzQuNS00LjUgMTAuNzUtNyAxNy03czEyLjUgMi41IDE3IDdsNzMuNSA3My43NSAxNjQtMTY0LjI1YzQuNS00LjUgMTAuNzUtNyAxNy03czEyLjUgMi41IDE3IDdsMzQgMzRjNC41IDQuNSA3IDEwLjc1IDcgMTd6Ij48L3BhdGg+Cjwvc3ZnPgo=");
transition: 120ms transform ease-in-out;
box-shadow: inset 1em 1em var(--SmartThemeCheckboxTickColor);
transform-origin: bottom left;
clip-path: polygon(14% 44%, 0 65%, 50% 100%, 100% 16%, 80% 0%, 43% 62%);
}
input[type='checkbox']:not(#nav-toggle):not(#rm_button_panel_pin):not(#lm_button_panel_pin):checked::after {
-webkit-transform: scale(1);
input[type="checkbox"]:not(#nav-toggle):not(#rm_button_panel_pin):not(#lm_button_panel_pin):not(#WI_panel_pin):checked::before {
transform: scale(1);
}
input[type="checkbox"]:not(#nav-toggle):not(#rm_button_panel_pin):not(#lm_button_panel_pin):not(#WI_panel_pin):disabled {
color: grey;
cursor: not-allowed;
}
.del_checkbox {
display: none;
opacity: 0.7;
margin-top: 12px;
margin-right: 12px;
}
#user_avatar_block {
display: flex;
grid-gap: 10px;
@ -2281,6 +2372,36 @@ input[type='checkbox']:not(#nav-toggle):not(#rm_button_panel_pin):not(#lm_button
width: 70px;
}
.neo-range-input {
display: block;
cursor: text;
background-color: var(--black30a);
border: 1px solid var(--SmartThemeBorderColor);
border-radius: 0 0 5px 5px;
padding: 2px;
padding-left: 1em;
padding-top: 5px;
text-align: center;
width: 100%;
}
.neo-range-slider {
-webkit-appearance: none !important;
appearance: none !important;
margin: 0 !important;
margin-top: 7px !important;
padding: 0 !important;
width: 100% !important;
height: 5px !important;
background: var(--white50a) !important;
border-radius: 7px 7px 0 0 !important;
background-size: 70% 100% !important;
background-repeat: no-repeat !important;
box-shadow: inset 0 0 2px var(--black50a) !important;
cursor: ew-resize !important;
z-index: 1;
}
.range-block-range {
margin: 0;
flex: 5;
@ -2317,22 +2438,23 @@ input[type="range"]::-webkit-slider-thumb {
.note-link-span {
color: var(--SmartThemeQuoteColor);
border: 1px solid var(--SmartThemeQuoteColor);
border-radius: 10px;
line-height: var(--mainFontSize);
font-size: var(--mainFontSize);
font-weight: 700;
width: calc(var(--mainFontSize) + 0.2rem);
height: calc(var(--mainFontSize) + 0.2rem);
display: inline-block;
opacity: 0.5;
margin: 0 5px;
text-align: center;
border-radius: 100%;
box-shadow: 0 0 3px black;
transition: all 250ms;
}
.note-link-span:hover {
.topRightInset {
position: absolute;
top: 6px;
right: 23px;
}
.note-link-span:hover,
.note-link-span-lrg:hover {
opacity: 1;
}
@ -2379,14 +2501,6 @@ input[type="range"]::-webkit-slider-thumb {
display: none;
}
.last_mes .mes_buttons {
right: -30px;
}
.last_mes .mes_block {
margin-right: 30px;
}
.mes_buttons .mes_edit,
.mes_buttons .mes_bookmark,
.mes_buttons .mes_create_bookmark,
@ -2478,7 +2592,7 @@ input[type="range"]::-webkit-slider-thumb {
display: flex;
flex-direction: row;
column-gap: 5px;
align-items: center;
align-items: baseline;
}
.auto_continue_settings_block {
@ -2654,30 +2768,15 @@ h5 {
margin: 0;
}
#select_chat_import {
display: grid;
grid-template-columns: min-content auto;
align-items: center;
grid-gap: 10px;
margin-bottom: 10px;
}
.select_chat_block_wrapper {
display: grid;
grid-template-columns: auto min-content;
align-items: center;
grid-gap: 10px;
cursor: pointer;
}
.select_chat_block {
border-radius: 10px;
margin-top: 10px;
border-radius: 5px;
margin-top: 5px;
border: 1px solid var(--SmartThemeBorderColor);
padding: 10px;
display: grid;
grid-template-columns: min-content auto;
grid-template-rows: auto auto;
grid-gap: 10px;
padding: 5px 7px;
}
.select_chat_block:hover {
@ -2692,12 +2791,6 @@ h5 {
grid-row: span 2;
}
#select_chat_name_wrapper {
display: flex;
gap: 5px;
flex-wrap: wrap;
}
.select_chat_block_filename_item {
opacity: 0.5;
width: fit-content;
@ -2717,16 +2810,6 @@ h5 {
font-size: calc(var(--mainFontSize) - .25rem);
}
#select_chat_cross {
position: absolute;
right: 15px;
top: 15px;
width: 20px;
height: 20px;
cursor: pointer;
opacity: 0.6;
}
.PastChat_cross {
width: 15px;
height: 15px;
@ -2778,17 +2861,62 @@ body .ui-front {
z-index: 10000;
}
body .ui-slider-handle {
background-color: var(--SmartThemeBlurTintColor);
border: 1px solid var(--SmartThemeBorderColor) !important;
border-radius: 5px;
outline: 1px solid var(--grey5020a);
box-shadow: 0 0 3px var(--black50a);
text-shadow: 0px 0px calc(var(--shadowWidth) * 1px) var(--SmartThemeShadowColor);
width: 50px !important;
padding: 0 5px;
text-align: center;
margin-left: 0;
opacity: 1 !important;
transition: filter 200ms;
filter: brightness(1.2);
}
.ui-slider-handle.ui-state-default {
color: var(--SmartThemeBodyColor);
background: var(--SmartThemeBlurTintColor);
}
.ui-slider-handle:focus {
outline: none;
}
.ui-slider-handle.ui-state-hover {
color: var(--SmartThemeBodyColor);
background: var(--SmartThemeBlurTintColor);
filter: brightness(1.2)
}
.ui-slider-handle.ui-state-active {
color: var(--SmartThemeBodyColor);
background: var(--SmartThemeBlurTintColor);
filter: brightness(1.5);
border-color: var(--SmartThemeBorderColor) !important;
}
body .ui-widget-content {
background-color: var(--SmartThemeBlurTintColor);
border: 1px solid var(--SmartThemeBorderColor) !important;
border-radius: 10px;
box-shadow: 0 0 5px black;
box-shadow: 0 0 3px var(--black50a);
text-shadow: 0px 0px calc(var(--shadowWidth) * 1px) var(--SmartThemeShadowColor);
backdrop-filter: blur(calc(var(--SmartThemeBlurStrength)*2));
color: var(--SmartThemeBodyColor);
}
body .ui-widget-content .ui-state-active {
.ui-slider {
margin: 5px 0;
outline: 1px solid var(--grey5050a);
border-radius: 5px !important;
}
body .ui-widget-content .ui-state-active:not(.ui-slider-handle) {
margin: unset !important;
}
@ -2804,7 +2932,7 @@ body .ui-widget-content li {
align-items: center;
cursor: pointer;
opacity: 0.5;
transition: all 200ms;
transition: opacity 200ms;
}
body .ui-widget-content li:hover {
@ -3081,7 +3209,8 @@ a {
#extensions_settings .inline-drawer-toggle.inline-drawer-header,
#extensions_settings2 .inline-drawer-toggle.inline-drawer-header,
#user-settings-block h4 {
#user-settings-block h4,
.standoutHeader {
background-image: linear-gradient(348deg, var(--white30a)2%, var(--grey30a)10%, var(--black70a)95%, var(--SmartThemeQuoteColor)100%);
margin-bottom: 5px;
border-radius: 10px;
@ -3389,14 +3518,6 @@ a {
align-items: center;
}
.prompt_overridden,
.jailbreak_overridden {
color: var(--SmartThemeQuoteColor);
font-weight: bold;
font-style: italic;
font-size: 0.8em;
}
.openai_restorable .right_menu_button img {
height: 20px;
}
@ -3432,9 +3553,6 @@ a {
flex-wrap: wrap;
}
#max_context_unlocked_warning {
flex-basis: 100%;
}
#max_context_unlocked:not(:checked)+div {
display: none;
@ -3470,6 +3588,7 @@ a {
aspect-ratio: 2 / 3;
padding: 0;
border: 0;
background-color: transparent;
}
.zoomed_avatar img {
@ -3539,8 +3658,8 @@ a {
.icon-svg {
fill: currentColor;
/* Takes on the color of the surrounding text */
width: 16px;
height: 16px;
width: auto;
height: 14px;
vertical-align: middle;
/* To align with adjacent text */
}
@ -3621,27 +3740,15 @@ a {
#show_more_messages {
text-align: center;
margin: 10px 0;
margin: 10px auto;
font-weight: 500;
text-decoration: underline;
order: -1;
cursor: pointer;
}
#select_chat_search {
background-color: transparent;
border: none;
outline: none;
color: var(--SmartThemeBodyColor);
display: inline-block;
/* Change display to inline-block */
vertical-align: middle;
/* Align to middle if there's a height discrepancy */
width: 200px;
font-size: 16px;
z-index: 10;
margin-left: 10px;
/* Give some space between the button and search box */
padding: 0.5em 1em;
background-color: var(--SmartThemeBlurTintColor);
width: fit-content;
border-radius: 10px;
outline: 1px solid var(--SmartThemeBorderColor);
}
.draggable img {
@ -3665,4 +3772,4 @@ a {
height: 100vh;
z-index: 9999;
}
}
}

833
server.js

File diff suppressed because it is too large Load Diff

View File

@ -897,6 +897,8 @@ export interface ModelGenerationInputKobold {
top_k?: number;
/** Top-p sampling value. */
top_p?: number;
/** Min-p sampling value. */
min_p?: number;
/** Typical sampling value. */
typical?: number;
/** Array of integers representing the sampler order to be used */

View File

@ -37,6 +37,25 @@ function checkAssetFileName(inputFilename) {
return path.normalize(inputFilename).replace(/^(\.\.(\/|\\|$))+/, '');;
}
// Recursive function to get files
function getFiles(dir, files = []) {
// Get an array of all files and directories in the passed directory using fs.readdirSync
const fileList = fs.readdirSync(dir);
// Create the full path of the file/directory by concatenating the passed directory and file/directory name
for (const file of fileList) {
const name = `${dir}/${file}`;
// Check if the current file/directory is a directory using fs.statSync
if (fs.statSync(name).isDirectory()) {
// If it is a directory, recursively call the getFiles function with the directory path and the files array
getFiles(name, files);
} else {
// If it is a file, push the full path to the files array
files.push(name);
}
}
return files;
}
/**
* Registers the endpoints for the asset management.
* @param {import('express').Express} app Express app
@ -70,16 +89,14 @@ function registerEndpoints(app, jsonParser) {
// Live2d assets
if (folder == "live2d") {
output[folder] = [];
const live2d_folders = fs.readdirSync(path.join(folderPath, folder));
for (let model_folder of live2d_folders) {
const live2d_model_path = path.join(folderPath, folder, model_folder);
if (fs.statSync(live2d_model_path).isDirectory()) {
for (let file of fs.readdirSync(live2d_model_path)) {
if (file.includes("model")) {
//console.debug("Asset live2d model found:",file)
output[folder].push([`${model_folder}`, path.join("assets", folder, model_folder, file)]);
}
}
const live2d_folder = path.normalize(path.join(folderPath, folder));
const files = getFiles(live2d_folder);
//console.debug("FILE FOUND:",files)
for (let file of files) {
file = path.normalize(file.replace('public' + path.sep, ''));
if (file.includes("model") && file.endsWith(".json")) {
//console.debug("Asset live2d model found:",file)
output[folder].push(path.normalize(path.join(file)));
}
}
continue;
@ -256,8 +273,8 @@ function registerEndpoints(app, jsonParser) {
if (fs.statSync(live2dModelPath).isDirectory()) {
for (let file of fs.readdirSync(live2dModelPath)) {
//console.debug("Character live2d model found:", file)
if (file.includes("model"))
output.push([`${modelFolder}`, path.join("characters", name, category, modelFolder, file)]);
if (file.includes("model") && file.endsWith(".json"))
output.push(path.join("characters", name, category, modelFolder, file));
}
}
}

Some files were not shown because too many files have changed in this diff Show More