Merge remote-tracking branch 'upstream/staging' into Gabraham/Markdown-Single-Space-Indent

This commit is contained in:
Gabraham 2024-05-22 04:03:33 -04:00
commit a9c3a808ac
152 changed files with 17317 additions and 1922 deletions

View File

@ -44,6 +44,7 @@ module.exports = {
toastr: 'readonly',
Readability: 'readonly',
isProbablyReaderable: 'readonly',
ePub: 'readonly',
},
},
],

40
.github/readme.md vendored
View File

@ -196,6 +196,43 @@ For MacOS / Linux all of these will be done in a Terminal.
5. Start the install launcher with: `chmod +x install.sh && ./install.sh` and choose what you wanna install
6. After installation start the launcher with: `chmod +x launcher.sh && ./launcher.sh`
## 🐋 Installing via Docker
These instructions assume you have installed Docker, are able to access your command line for the installation of containers, and familiar with their general operation.
### Building the image yourself
We have a comprehensive guide on using SillyTavern in Docker [here](http://docs.sillytavern.app/installation/docker/) which covers installations on Windows, macOS and Linux! Give it a read if you wish to build the image yourself.
### Using the GitHub Container Registry (easiest)
You will need two mandatory directory mappings and a port mapping to allow SillyTavern to function. In the command, replace your selections in the following places:
#### Container Variables
##### Volume Mappings
- [config] - The directory where SillyTavern configuration files will be stored on your host machine
- [data] - The directory where SillyTavern user data (including characters) will be stored on your host machine
- [plugins] - (optional) The directory where SillyTavern server plugins will be stored on your host machine
##### Port Mappings
- [PublicPort] - The port to expose the traffic on. This is mandatory, as you will be accessing the instance from outside of its virtual machine container. DO NOT expose this to the internet without implementing a separate service for security.
##### Additional Settings
- [TimeZone] - The timezone your instance should use. This is useful for making logs match your local time for easier troubleshooting. Use your TZ Identifier. (https://en.wikipedia.org/wiki/List_of_tz_database_time_zones)
- [DockerNet] - The docker network that the container should be created with a connection to. If you don't know what it is, see the [official Docker documentation](https://docs.docker.com/reference/cli/docker/network/).
- [version] - On the right-hand side of this GitHub page, you'll see "Packages". Select the "sillytavern" package and you'll see the image versions. The image tag "latest" will keep you up-to-date with the current release. You can also utilize "staging" and "release" tags that point to the nightly images of the respective branches, but this may not be appropriate, if you are utilizing extensions that could be broken, and may need time to update.
#### Install command
1. Open your Command Line
2. Run the following command `docker create --name='sillytavern' --net='[DockerNet]' -e TZ="[TimeZone]" -p '8000:8000/tcp' -v '[plugins]':'/home/node/app/plugins':'rw' -v '[config]':'/home/node/app/config':'rw' -v '[data]':'/home/node/app/data':'rw' 'ghcr.io/sillytavern/sillytavern:[version]' `
> Note that 8000 is a default listening port. Don't forget to use an appropriate port if you change it in the config.
## 📱 Mobile - Installing via termux
> \[!NOTE]
@ -326,7 +363,7 @@ but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.**
* TAI Base by Humi: Unknown license
* TAI Base by Humi: MIT
* Cohee's modifications and derived code: AGPL v3
* RossAscends' additions: AGPL v3
* Portions of CncAnon's TavernAITurbo mod: Unknown license
@ -347,6 +384,7 @@ GNU Affero General Public License for more details.**
* Korean translation by @doloroushyeonse
* k_euler_a support for Horde by <https://github.com/Teashrock>
* Chinese translation by [@XXpE3](https://github.com/XXpE3), 中文 ISSUES 可以联系 @XXpE3
* Docker guide by [@mrguymiah](https://github.com/mrguymiah) and [@Bronya-Rand](https://github.com/Bronya-Rand)
<!-- LINK GROUP -->
[back-to-top]: https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square

View File

@ -30,7 +30,7 @@ jobs:
run: |
echo "IMAGE_NAME=${REPO,,}" >> ${GITHUB_ENV}
# Using the following workaround because currently GitHub Actions
# Using the following workaround because currently GitHub Actions
# does not support logical AND/OR operations on triggers
# It's currently not possible to have `branches` under the `schedule` trigger
- name: Checkout the release branch (on release)
@ -65,7 +65,10 @@ jobs:
id: metadata
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: ${{ env.BRANCH_NAME }}
# Release version tag if the workflow is triggered by a release
# Branch name tag if the workflow is triggered by a push
tags: |
${{ github.event_name == 'release' && github.ref_name || env.BRANCH_NAME }}
# Login into package repository as the person who created the release
- name: Log in to the Container registry
@ -92,5 +95,6 @@ jobs:
- name: Docker tag latest and push
if: ${{ github.event_name == 'release' }}
run: |
docker tag $IMAGE_NAME:${{ github.ref_name }} $IMAGE_NAME:latest
docker push $IMAGE_NAME:latest
docker pull ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.ref_name }}
docker tag ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.ref_name }} ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
docker push ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest

1
.gitignore vendored
View File

@ -47,3 +47,4 @@ access.log
public/css/user.css
/plugins/
/data
/default/scaffold

Binary file not shown.

Before

Width:  |  Height:  |  Size: 338 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 598 KiB

View File

@ -107,14 +107,6 @@
"filename": "default_Seraphina.png",
"type": "character"
},
{
"filename": "default_CodingSensei.png",
"type": "character"
},
{
"filename": "default_FluxTheCat.png",
"type": "character"
},
{
"filename": "Seraphina",
"type": "sprites"

View File

@ -231,6 +231,7 @@
"api_url_scale": "",
"show_external_models": false,
"assistant_prefill": "",
"assistant_impersonation": "",
"human_sysprompt_message": "Let's get started. Please generate your response based on the information and instructions provided above.",
"use_ai21_tokenizer": false,
"use_google_tokenizer": false,

View File

@ -387,14 +387,8 @@
}
],
"tag_map": {
"default_FluxTheCat.png": [
"1345561466591"
],
"default_Seraphina.png": [
"1345561466591"
],
"default_CodingSensei.png": [
"1345561466591"
]
},
"nai_settings": {
@ -630,6 +624,7 @@
"show_external_models": false,
"proxy_password": "",
"assistant_prefill": "",
"assistant_impersonation": "",
"use_ai21_tokenizer": false
}
}

View File

@ -0,0 +1,26 @@
# Content Scaffolding
Content files in this folder will be copied for all users (old and new) on the server startup.
1. You **must** create an `index.json` file in `/default/scaffold` for it to work. The syntax is the same as for default content.
2. All file paths should be relative to `/default/scaffold`, the use of subdirectories is allowed.
3. Scaffolded files are copied first, so they override any of the default files (presets/settings/etc.) that have the same file name.
## Example
```json
[
{
"filename": "themes/Midnight.json",
"type": "theme"
},
{
"filename": "backgrounds/city.png",
"type": "background"
},
{
"filename": "characters/Charlie.png",
"type": "character"
}
]
```

View File

@ -10,4 +10,5 @@ services:
volumes:
- "./config:/home/node/app/config"
- "./data:/home/node/app/data"
- "./plugins:/home/node/app/plugins"
restart: unless-stopped

65
package-lock.json generated
View File

@ -1,18 +1,17 @@
{
"name": "sillytavern",
"version": "1.12.0-preview",
"version": "1.12.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "sillytavern",
"version": "1.12.0-preview",
"version": "1.12.0",
"hasInstallScript": true,
"license": "AGPL-3.0",
"dependencies": {
"@agnai/sentencepiece-js": "^1.1.1",
"@agnai/web-tokenizers": "^0.1.3",
"@dqbd/tiktoken": "^1.0.13",
"@zeldafan0225/ai_horde": "^4.0.1",
"archiver": "^7.0.1",
"bing-translate-api": "^2.9.1",
@ -26,7 +25,6 @@
"express": "^4.19.2",
"form-data": "^4.0.0",
"google-translate-api-browser": "^3.0.1",
"gpt3-tokenizer": "^1.1.5",
"he": "^1.2.0",
"helmet": "^7.1.0",
"ip-matching": "^2.1.2",
@ -46,6 +44,7 @@
"sanitize-filename": "^1.6.3",
"sillytavern-transformers": "^2.14.6",
"simple-git": "^3.19.1",
"tiktoken": "^1.0.15",
"vectra": "^0.2.2",
"wavefile": "^11.0.0",
"write-file-atomic": "^5.0.1",
@ -61,6 +60,9 @@
"@types/jquery": "^3.5.29",
"eslint": "^8.55.0",
"jquery": "^3.6.4"
},
"engines": {
"node": ">= 18"
}
},
"node_modules/@aashutoshrathi/word-wrap": {
@ -82,10 +84,6 @@
"version": "0.1.3",
"license": "Apache-2.0"
},
"node_modules/@dqbd/tiktoken": {
"version": "1.0.13",
"license": "MIT"
},
"node_modules/@eslint-community/eslint-utils": {
"version": "4.4.0",
"dev": true,
@ -1214,10 +1212,6 @@
"version": "1.1.1",
"license": "MIT"
},
"node_modules/array-keyed-map": {
"version": "2.1.3",
"license": "ISC"
},
"node_modules/async": {
"version": "3.2.5",
"resolved": "https://registry.npmjs.org/async/-/async-3.2.5.tgz",
@ -2739,16 +2733,6 @@
"version": "1.1.4",
"license": "MIT"
},
"node_modules/gpt3-tokenizer": {
"version": "1.1.5",
"license": "MIT",
"dependencies": {
"array-keyed-map": "^2.1.3"
},
"engines": {
"node": ">=12"
}
},
"node_modules/graceful-fs": {
"version": "4.2.11",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
@ -3889,7 +3873,6 @@
},
"node_modules/punycode": {
"version": "2.3.1",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6"
@ -4403,6 +4386,11 @@
"dev": true,
"license": "MIT"
},
"node_modules/tiktoken": {
"version": "1.0.15",
"resolved": "https://registry.npmjs.org/tiktoken/-/tiktoken-1.0.15.tgz",
"integrity": "sha512-sCsrq/vMWUSEW29CJLNmPvWxlVp7yh2tlkAjpJltIKqp5CKf98ZNpdeHRmAlPVFlGEbswDc6SmI8vz64W/qErw=="
},
"node_modules/timm": {
"version": "1.7.1",
"license": "MIT"
@ -4434,8 +4422,15 @@
}
},
"node_modules/tr46": {
"version": "0.0.3",
"license": "MIT"
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/tr46/-/tr46-5.0.0.tgz",
"integrity": "sha512-tk2G5R2KRwBd+ZN0zaEXpmzdKyOYksXwywulIX95MBODjSzMIuQnQ3m8JxgbhnL1LeVo7lqQKsYa1O3Htl7K5g==",
"dependencies": {
"punycode": "^2.3.1"
},
"engines": {
"node": ">=18"
}
},
"node_modules/truncate-utf8-bytes": {
"version": "1.0.2",
@ -4584,19 +4579,27 @@
}
},
"node_modules/webidl-conversions": {
"version": "3.0.1",
"license": "BSD-2-Clause"
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz",
"integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==",
"engines": {
"node": ">=12"
}
},
"node_modules/whatwg-fetch": {
"version": "3.6.18",
"license": "MIT"
},
"node_modules/whatwg-url": {
"version": "5.0.0",
"license": "MIT",
"version": "14.0.0",
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.0.0.tgz",
"integrity": "sha512-1lfMEm2IEr7RIV+f4lUNPOqfFL+pO+Xw3fJSqmjX9AbXcXcYOkCe1P6+9VBZB6n94af16NfZf+sSk0JCBZC9aw==",
"dependencies": {
"tr46": "~0.0.3",
"webidl-conversions": "^3.0.0"
"tr46": "^5.0.0",
"webidl-conversions": "^7.0.0"
},
"engines": {
"node": ">=18"
}
},
"node_modules/which": {

View File

@ -2,7 +2,6 @@
"dependencies": {
"@agnai/sentencepiece-js": "^1.1.1",
"@agnai/web-tokenizers": "^0.1.3",
"@dqbd/tiktoken": "^1.0.13",
"@zeldafan0225/ai_horde": "^4.0.1",
"archiver": "^7.0.1",
"bing-translate-api": "^2.9.1",
@ -16,7 +15,6 @@
"express": "^4.19.2",
"form-data": "^4.0.0",
"google-translate-api-browser": "^3.0.1",
"gpt3-tokenizer": "^1.1.5",
"he": "^1.2.0",
"helmet": "^7.1.0",
"ip-matching": "^2.1.2",
@ -36,6 +34,7 @@
"sanitize-filename": "^1.6.3",
"sillytavern-transformers": "^2.14.6",
"simple-git": "^3.19.1",
"tiktoken": "^1.0.15",
"vectra": "^0.2.2",
"wavefile": "^11.0.0",
"write-file-atomic": "^5.0.1",
@ -44,6 +43,9 @@
"yargs": "^17.7.1",
"yauzl": "^2.10.0"
},
"engines": {
"node": ">= 18"
},
"overrides": {
"parse-bmfont-xml": {
"xml2js": "^0.5.0"
@ -59,6 +61,9 @@
},
"@zeldafan0225/ai_horde": {
"esbuild": "npm:dry-uninstall"
},
"node-fetch": {
"whatwg-url": "^14.0.0"
}
},
"name": "sillytavern",
@ -68,13 +73,15 @@
"type": "git",
"url": "https://github.com/SillyTavern/SillyTavern.git"
},
"version": "1.12.0-preview",
"version": "1.12.0",
"scripts": {
"start": "node server.js",
"start-multi": "node server.js --disableCsrf",
"start:no-csrf": "node server.js --disableCsrf",
"postinstall": "node post-install.js",
"lint": "eslint \"src/**/*.js\" \"public/**/*.js\" ./*.js",
"lint-fix": "eslint \"src/**/*.js\" \"public/**/*.js\" ./*.js --fix"
"lint:fix": "eslint \"src/**/*.js\" \"public/**/*.js\" ./*.js --fix",
"plugins:update": "node plugins update",
"plugins:install": "node plugins install"
},
"bin": {
"sillytavern": "./server.js"

75
plugins.js Normal file
View File

@ -0,0 +1,75 @@
// Plugin manager script.
// Usage: node plugins.js update
// More operations coming soon.
const { default: git } = require('simple-git');
const fs = require('fs');
const path = require('path');
const { color } = require('./src/util');
const pluginsPath = './plugins';
const command = process.argv[2];
if (command === 'update') {
console.log(color.magenta('Updating all plugins'));
updatePlugins();
}
if (command === 'install') {
const pluginName = process.argv[3];
console.log('Installing a new plugin', color.green(pluginName));
installPlugin(pluginName);
}
async function updatePlugins() {
const directories = fs.readdirSync(pluginsPath)
.filter(file => !file.startsWith('.'))
.filter(file => fs.statSync(path.join(pluginsPath, file)).isDirectory());
console.log(`Found ${color.cyan(directories.length)} directories in ./plugins`);
for (const directory of directories) {
try {
console.log(`Updating plugin ${color.green(directory)}...`);
const pluginPath = path.join(pluginsPath, directory);
const pluginRepo = git(pluginPath);
await pluginRepo.fetch();
const commitHash = await pluginRepo.revparse(['HEAD']);
const trackingBranch = await pluginRepo.revparse(['--abbrev-ref', '@{u}']);
const log = await pluginRepo.log({
from: commitHash,
to: trackingBranch,
});
if (log.total === 0) {
console.log(`Plugin ${color.blue(directory)} is already up to date`);
continue;
}
await pluginRepo.pull();
const latestCommit = await pluginRepo.revparse(['HEAD']);
console.log(`Plugin ${color.green(directory)} updated to commit ${color.cyan(latestCommit)}`);
} catch (error) {
console.error(color.red(`Failed to update plugin ${directory}: ${error.message}`));
}
}
console.log(color.magenta('All plugins updated!'));
}
async function installPlugin(pluginName) {
try {
const pluginPath = path.join(pluginsPath, path.basename(pluginName, '.git'));
if (fs.existsSync(pluginPath)) {
return console.log(color.yellow(`Directory already exists at ${pluginPath}`));
}
await git().clone(pluginName, pluginPath, { '--depth': 1 });
console.log(`Plugin ${color.green(pluginName)} installed to ${color.cyan(pluginPath)}`);
}
catch (error) {
console.error(color.red(`Failed to install plugin ${pluginName}`), error);
}
}

View File

@ -171,3 +171,78 @@
.select2-results__option.select2-results__message::before {
display: none;
}
.select2-selection__choice__display {
/* Fix weird alignment on the left side */
margin-left: 1px;
}
/* Styling for choice remove icon */
span.select2.select2-container .select2-selection__choice__remove {
cursor: pointer;
transition: background-color 0.3s;
color: var(--SmartThemeBodyColor);
background-color: var(--black50a);
}
span.select2.select2-container .select2-selection__choice__remove:hover {
color: var(--SmartThemeBodyColor);
background-color: var(--white30a);
}
/* Custom class to support styling to show clickable choice options */
.select2_choice_clickable+span.select2-container .select2-selection__choice__display {
cursor: pointer;
}
.select2_choice_clickable_buttonstyle+span.select2-container .select2-selection__choice__display {
cursor: pointer;
transition: background-color 0.3s;
color: var(--SmartThemeBodyColor);
background-color: var(--black50a);
}
.select2_choice_clickable_buttonstyle+span.select2-container .select2-selection__choice__display:hover {
background-color: var(--white30a);
}
/* Custom class to support same line multi inputs of select2 controls */
.select2_multi_sameline+span.select2-container .select2-selection--multiple {
display: flex;
flex-wrap: wrap;
}.select2_multi_sameline+span.select2-container .select2-selection--multiple .select2-search--inline {
/* Allow search placeholder to take up all space if needed */
flex-grow: 1;
}
.select2_multi_sameline+span.select2-container .select2-selection--multiple .select2-selection__rendered {
/* Fix weird styling choice or huge margin around selected options */
margin-block-start: 2px;
margin-block-end: 2px;
}
.select2_multi_sameline+span.select2-container .select2-selection--multiple .select2-search__field {
/* Min height to reserve spacing */
min-height: calc(var(--mainFontSize) + 13px);
/* Min width to be clickable */
min-width: 4em;
align-content: center;
/* Fix search textarea alignment issue with UL elements */
margin-top: 0px;
height: unset;
/* Prevent height from jumping around when input is focused */
line-height: 1;
}
.select2_multi_sameline+span.select2-container .select2-selection--multiple .select2-selection__rendered {
/* Min height to reserve spacing */
min-height: calc(var(--mainFontSize) + 13px);
}
/* Make search bar invisible unless the select2 is active, to save space */
.select2_multi_sameline+span.select2-container .select2-selection--multiple .select2-search--inline {
height: 1px;
}
.select2_multi_sameline+span.select2-container.select2-container--focus .select2-selection--multiple .select2-search--inline {
height: unset;
}

View File

@ -360,6 +360,14 @@
flex: 2 !important;
}
.flex3 {
flex: 3;
}
.flex4 {
flex: 4;
}
.flexFlowColumn {
flex-flow: column;
}
@ -563,4 +571,4 @@ textarea:disabled {
height: 30px;
text-align: center;
padding: 5px;
}
}

View File

@ -103,7 +103,8 @@
}
#bulkTagsList,
#tagList .tag {
#tagList .tag,
#groupTagList .tag {
opacity: 0.6;
}
@ -193,7 +194,8 @@
filter: brightness(75%) saturate(0.6);
}
.tag_as_folder:hover {
.tag_as_folder:hover,
.tag_as_folder.flash {
filter: brightness(150%) saturate(0.6) !important;
}

View File

@ -76,6 +76,12 @@
.world_entry_form_control {
display: flex;
flex-direction: column;
position: relative;
}
.world_entry_form_control .keyprimarytextpole,
.world_entry_form_control .keysecondarytextpole {
padding-right: 25px;
}
.world_entry_thin_controls {
@ -101,7 +107,7 @@
height: auto;
margin-top: 0;
margin-bottom: 0;
min-height: calc(var(--mainFontSize) + 13px);
min-height: calc(var(--mainFontSize) + 14px);
}
.delete_entry_button {
@ -197,20 +203,57 @@
display: none;
}
#world_info+span.select2-container .select2-selection__choice__remove,
#world_info+span.select2-container .select2-selection__choice__display {
cursor: pointer;
transition: background-color 0.3s;
span.select2-container .select2-selection__choice__display:has(> .regex_item),
span.select2-container .select2-results__option:has(> .result_block .regex_item) {
background-color: #D27D2D30;
}
.regex_item .regex_icon {
background-color: var(--black30a);
color: var(--SmartThemeBodyColor);
background-color: var(--black50a);
border: 1px solid var(--SmartThemeBorderColor);
border-radius: 7px;
font-weight: bold;
font-size: calc(var(--mainFontSize) * 0.75);
padding: 0px 3px;
position: relative;
top: -1px;
margin-right: 3px;
}
#world_info+span.select2-container .select2-selection__choice__display {
/* Fix weird alignment on the left side */
margin-left: 1px;
.select2-results__option .regex_item .regex_icon {
margin-right: 6px;
}
#world_info+span.select2-container .select2-selection__choice__remove:hover,
#world_info+span.select2-container .select2-selection__choice__display:hover {
background-color: var(--white30a);
.select2-results__option .item_count {
margin-left: 10px;
float: right;
}
select.keyselect+span.select2-container .select2-selection--multiple {
padding-right: 30px;
}
.switch_input_type_icon {
cursor: pointer;
font-weight: bold;
height: 20px;
width: fit-content;
margin-right: 5px;
margin-top: calc(5px + var(--mainFontSize));
position: absolute;
right: 0;
padding: 1px;
background-color: transparent;
border: none;
font-size: 1em;
opacity: 0.5;
color: var(--SmartThemeBodyColor);
transition: opacity 0.3s;
}
.switch_input_type_icon:hover {
opacity: 1;
}

1360
public/global.d.ts vendored Normal file

File diff suppressed because it is too large Load Diff

48
public/img/groq.svg Normal file
View File

@ -0,0 +1,48 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="107.644"
height="156.436"
viewBox="0 0 107.644 156.436"
fill="none"
version="1.1"
id="svg9"
sodipodi:docname="groqcloud_dark_v2.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">
<sodipodi:namedview
id="namedview9"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="0.667"
inkscape:cx="499.25037"
inkscape:cy="56.971514"
inkscape:window-width="1312"
inkscape:window-height="449"
inkscape:window-x="0"
inkscape:window-y="38"
inkscape:window-maximized="0"
inkscape:current-layer="svg9" />
<defs
id="defs9">
<clipPath
id="clip0_872_2594">
<rect
width="1000"
height="200.345"
id="rect9"
x="0"
y="0" />
</clipPath>
</defs>
<path
d="M 54.0487,0.00281139 C 24.4736,-0.29748861 0.303066,23.497811 0.00281057,53.072911 -0.297445,82.648011 23.4978,106.89401 53.0729,107.11901 c 0.3003,0 0.6756,0 0.9758,0 H 71.6888 V 87.077011 H 54.0487 c -18.4656,0.225 -33.6285,-14.563 -33.8537,-33.1033 -0.2252,-18.4657 14.5624,-33.6286 33.1031,-33.8538 0.2252,0 0.5255,0 0.7506,0 18.4657,0 33.5536,15.0128 33.5536,33.4784 v 49.316699 c 0,18.316 -14.9377,33.254 -33.2533,33.479 -8.7825,0 -17.1145,-3.603 -23.2698,-9.834 l -14.187,14.187 c 9.8333,9.909 23.1947,15.539 37.1565,15.689 h 0.7507 c 29.1998,-0.451 52.6946,-24.096 52.8446,-53.296 V 52.247211 C 106.894,23.197511 83.1735,0.00281139 54.1238,0.00281139 Z"
id="path7" />
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@ -0,0 +1,40 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
version="1.0"
width="70pt"
height="70pt"
viewBox="0 0 70 70"
preserveAspectRatio="xMidYMid"
id="svg15"
sodipodi:docname="infermatic.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="defs15" />
<sodipodi:namedview
id="namedview15"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="pt"
inkscape:zoom="0.75112613"
inkscape:cx="306.2069"
inkscape:cy="50.590705"
inkscape:window-width="1312"
inkscape:window-height="449"
inkscape:window-x="0"
inkscape:window-y="38"
inkscape:window-maximized="0"
inkscape:current-layer="svg15" />
<path
id="path15"
d="m 1030,375 v -75 h 75 74 l 6,33 c 4,18 5,51 3,72 l -3,40 -77,3 -78,3 z m 547,619 c -4,-4 -7,-41 -7,-81 v -74 l 78,3 77,3 v 75 75 l -70,3 c -39,1 -74,0 -78,-4 z m -547,-74 v -79 l 133,-3 132,-3 3,-267 2,-268 h 215 215 v 75 75 h -135 -135 l -2,273 -3,272 -212,3 -213,2 z"
transform="matrix(0.1,0,0,-0.1,-103,99.999998)" />
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -9,6 +9,13 @@
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="darkreader-lock">
<meta name="robots" content="noindex, nofollow" />
<style>
/* Put critical CSS here. The rest should go in stylesheets. */
body {
background-color: rgb(36, 36, 37);
}
</style>
<link rel="preload" as="style" href="style.css">
<link rel="manifest" crossorigin="use-credentials" href="manifest.json">
<link href="webfonts/NotoSans/stylesheet.css" rel="stylesheet">
<link href="css/fontawesome.min.css" rel="stylesheet">
@ -109,7 +116,7 @@
</h4>
<div class="flex-container flexNoGap">
<select id="settings_preset_novel" class="flex1 text_pole" data-preset-manager-for="novel">
<option value="gui" data-i18n="default">Default</option>
<option value="gui" data-i18n="Default">Default</option>
</select>
<div class="flex-container marginLeft5 ">
<input type="file" hidden data-preset-manager-file="novel" accept=".json, .settings">
@ -127,7 +134,7 @@
<h4 class="margin0"><span data-i18n="openaipresets">Chat Completion Presets</span></h4>
<div class="flex-container flexNoGap">
<select id="settings_preset_openai" class="flex1 text_pole" data-preset-manager-for="openai">
<option value="gui" data-i18n="default">Default</option>
<option value="gui" data-i18n="Default">Default</option>
</select>
<div class="flex-container marginLeft5 ">
<input id="openai_preset_import_file" type="file" accept=".json,.settings" hidden />
@ -239,7 +246,7 @@
</div>
</div>
<div class="range-block">
<div class="range-block-title" data-i18n="temperature">
<div class="range-block-title" data-i18n="Temperature">
Temperature
</div>
<div class="range-block-range-and-counter">
@ -416,7 +423,7 @@
</span>
</div>
</div>
<div class="range-block" data-source="openai,claude,windowai,openrouter,ai21,scale,makersuite,mistralai,custom,cohere,perplexity">
<div class="range-block" data-source="openai,claude,windowai,openrouter,ai21,scale,makersuite,mistralai,custom,cohere,perplexity,groq">
<div class="range-block-title" data-i18n="Temperature">
Temperature
</div>
@ -429,7 +436,7 @@
</div>
</div>
</div>
<div data-newbie-hidden class="range-block" data-source="openai,openrouter,ai21,custom,cohere,perplexity">
<div data-newbie-hidden class="range-block" data-source="openai,openrouter,ai21,custom,cohere,perplexity,groq">
<div class="range-block-title" data-i18n="Frequency Penalty">
Frequency Penalty
</div>
@ -442,7 +449,7 @@
</div>
</div>
</div>
<div data-newbie-hidden class="range-block" data-source="openai,openrouter,ai21,custom,cohere,perplexity">
<div data-newbie-hidden class="range-block" data-source="openai,openrouter,ai21,custom,cohere,perplexity,groq">
<div class="range-block-title" data-i18n="Presence Penalty">
Presence Penalty
</div>
@ -481,7 +488,7 @@
</div>
</div>
</div>
<div data-newbie-hidden class="range-block" data-source="openai,claude,openrouter,ai21,scale,makersuite,mistralai,custom,cohere,perplexity">
<div data-newbie-hidden class="range-block" data-source="openai,claude,openrouter,ai21,scale,makersuite,mistralai,custom,cohere,perplexity,groq">
<div class="range-block-title" data-i18n="Top-p">
Top P
</div>
@ -717,7 +724,7 @@
</div>
</div>
</div>
<div data-newbie-hidden class="range-block" data-source="openai,openrouter,mistralai,custom,cohere">
<div data-newbie-hidden class="range-block" data-source="openai,openrouter,mistralai,custom,cohere,groq">
<div class="range-block-title justifyLeft" data-i18n="Seed">
Seed
</div>
@ -744,7 +751,7 @@
<div data-newbie-hidden class="alignitemscenter flex-container flexFlowColumn flexBasis30p flexGrow flexShrink gap0">
<small>
<span data-i18n="Top K">Top K</span>
<div class="fa-solid fa-circle-info opacity50p" title="Top K sets a maximum amount of top tokens that can be chosen from.&#13;E.g Top K is 20, this means only the 20 highest ranking tokens will be kept (regardless of their probabilities being diverse or limited).&#13;Set to 0 to disable."></div>
<div class="fa-solid fa-circle-info opacity50p" data-i18n="[title]Top K sets a maximum amount of top tokens that can be chosen from" title="Top K sets a maximum amount of top tokens that can be chosen from.&#13;E.g Top K is 20, this means only the 20 highest ranking tokens will be kept (regardless of their probabilities being diverse or limited).&#13;Set to 0 to disable."></div>
</small>
<input class="neo-range-slider" type="range" id="top_k" name="volume" min="0" max="100" step="1">
<input class="neo-range-input" type="number" min="0" max="100" step="1" data-for="top_k" id="top_k_counter">
@ -752,7 +759,7 @@
<div data-newbie-hidden class="alignitemscenter flex-container flexFlowColumn flexBasis30p flexGrow flexShrink gap0">
<small>
Top P
<div class="fa-solid fa-circle-info opacity50p" title="Top P (a.k.a. nucleus sampling) adds up all the top tokens required to add up to the target percentage.&#13;E.g If the Top 2 tokens are both 25%, and Top P is 0.50, only the Top 2 tokens are considered.&#13;Set to 1.0 to disable."></div>
<div class="fa-solid fa-circle-info opacity50p" data-i18n="[title]Top P (a.k.a. nucleus sampling)" title="Top P (a.k.a. nucleus sampling) adds up all the top tokens required to add up to the target percentage.&#13;E.g If the Top 2 tokens are both 25%, and Top P is 0.50, only the Top 2 tokens are considered.&#13;Set to 1.0 to disable."></div>
</small>
<input class="neo-range-slider" type="range" id="top_p" name="volume" min="0" max="1" step="0.01">
<input class="neo-range-input" type="number" min="0" max="1" step="0.01" data-for="top_p" id="top_p_counter">
@ -760,7 +767,7 @@
<div data-newbie-hidden class="alignitemscenter flex-container flexFlowColumn flexBasis30p flexGrow flexShrink gap0">
<small>
<span data-i18n="Typical P">Typical P</span>
<div class="fa-solid fa-circle-info opacity50p" title="Typical P Sampling prioritizes tokens based on their deviation from the average entropy of the set.&#13;It maintains tokens whose cumulative probability is close to a predefined threshold (e.g., 0.5), emphasizing those with average information content.&#13;Set to 1.0 to disable."></div>
<div class="fa-solid fa-circle-info opacity50p" data-i18n="[title]Typical P Sampling prioritizes tokens based on their deviation from the average entropy of the set" title="Typical P Sampling prioritizes tokens based on their deviation from the average entropy of the set.&#13;It maintains tokens whose cumulative probability is close to a predefined threshold (e.g., 0.5), emphasizing those with average information content.&#13;Set to 1.0 to disable."></div>
</small>
<input class="neo-range-slider" type="range" id="typical_p" name="volume" min="0" max="1" step="0.001">
<input class="neo-range-input" type="number" min="0" max="1" step="0.001" data-for="typical_p" id="typical_p_counter">
@ -768,7 +775,7 @@
<div class="alignitemscenter flex-container flexFlowColumn flexBasis30p flexGrow flexShrink gap0">
<small>
<span data-i18n="Min P">Min P</span>
<div class="fa-solid fa-circle-info opacity50p" title="Min P sets a base minimum probability.&#13;This is scaled according to the top token's probability.&#13;E.g If Top token is 80% probability, and Min P is 0.1, only tokens higher than 8% would be considered.&#13;Set to 0 to disable."></div>
<div class="fa-solid fa-circle-info opacity50p" data-i18n="[title]Min P sets a base minimum probability" title="Min P sets a base minimum probability.&#13;This is scaled according to the top token's probability.&#13;E.g If Top token is 80% probability, and Min P is 0.1, only tokens higher than 8% would be considered.&#13;Set to 0 to disable."></div>
</small>
<input class="neo-range-slider" type="range" id="min_p" name="volume" min="0" max="1" step="0.001">
<input class="neo-range-input" type="number" min="0" max="1" step="0.001" data-for="min_p" id="min_p_counter">
@ -776,7 +783,7 @@
<div data-newbie-hidden class="alignitemscenter flex-container flexFlowColumn flexBasis30p flexGrow flexShrink gap0">
<small>
<span data-i18n="Top A">Top A</span>
<div class="fa-solid fa-circle-info opacity50p" title="Top A sets a threshold for token selection based on the square of the highest token probability.&#13;E.g if the Top-A value is 0.2 and the top token's probability is 50%, tokens with probabilities below 5% (0.2 * 0.5^2) are excluded.&#13;Set to 0 to disable."></div>
<div class="fa-solid fa-circle-info opacity50p" data-i18n="[title]Top A sets a threshold for token selection based on the square of the highest token probability" title="Top A sets a threshold for token selection based on the square of the highest token probability.&#13;E.g if the Top-A value is 0.2 and the top token's probability is 50%, tokens with probabilities below 5% (0.2 * 0.5^2) are excluded.&#13;Set to 0 to disable."></div>
</small>
<input class="neo-range-slider" type="range" id="top_a" name="volume" min="0" max="1" step="0.001">
<input class="neo-range-input" type="number" min="0" max="1" step="0.001" data-for="top_a" id="top_a_counter">
@ -784,7 +791,7 @@
<div data-newbie-hidden class="alignitemscenter flex-container flexFlowColumn flexBasis30p flexGrow flexShrink gap0">
<small>
<span data-i18n="Tail Free Sampling">TFS</span>
<div class="fa-solid fa-circle-info opacity50p" title="Tail-Free Sampling (TFS) searches for a tail of low-probability tokens in the distribution,&#13;by analyzing the rate of change in token probabilities using derivatives. It retains tokens up to a threshold (e.g., 0.3) based on the normalized second derivative.&#13;The closer to 0, the more discarded tokens. Set to 1.0 to disable."></div>
<div class="fa-solid fa-circle-info opacity50p" data-i18n="[title]Tail-Free Sampling (TFS)" title="Tail-Free Sampling (TFS) searches for a tail of low-probability tokens in the distribution,&#13;by analyzing the rate of change in token probabilities using derivatives. It retains tokens up to a threshold (e.g., 0.3) based on the normalized second derivative.&#13;The closer to 0, the more discarded tokens. Set to 1.0 to disable."></div>
</small>
<input class="neo-range-slider" type="range" id="tfs" name="volume" min="0" max="1" step="0.001">
<input class="neo-range-input" type="number" min="0" max="1" step="0.001" data-for="tfs" id="tfs_counter">
@ -861,7 +868,7 @@
</small>
</a>
</h4>
<textarea id="grammar" rows="4" class="text_pole textarea_compact monospace" placeholder="Type in the desired custom grammar"></textarea>
<textarea id="grammar" rows="4" class="text_pole textarea_compact monospace" data-i18n="[placeholder]Type in the desired custom grammar" placeholder="Type in the desired custom grammar"></textarea>
</div>
<div data-newbie-hidden name="KoboldSamplerOrderBlock" class="range-block flexFlowColumn">
<hr class="wide100p">
@ -935,7 +942,7 @@
</div>
</div>
<div class="range-block">
<div class="range-block-title title_restorable">
<div id="logit_bias_novel" class="range-block-title title_restorable">
<span data-i18n="Logit Bias">Logit Bias</span>
<div id="novelai_logit_bias_new_entry" class="menu_button menu_button_icon">
<i class="fa-xs fa-solid fa-plus"></i>
@ -1124,6 +1131,11 @@
<div id="samplerResetButton" class="menu_button whitespacenowrap" data-i18n="Neutralize Samplers">Neutralize Samplers</div>
<div class="fa-solid fa-circle-info opacity50p" title="Set all samplers to their neutral/disabled state." data-i18n="[title]Set all samplers to their neutral/disabled state."></div>
</small>
<small class="flex-container alignitemscenter">
<div id="samplerSelectButton" class="menu_button whitespacenowrap" data-i18n="Sampler Select">Sampler Select</div>
<div class="fa-solid fa-circle-info opacity50p" title="Customize displayed samplers or add custom samplers." data-i18n="[title]Customize displayed samplers or add custom samplers."></div>
</small>
</div>
<div data-newbie-hidden data-tg-type="mancer, vllm, aphrodite" class="flex-container flexFlowColumn alignitemscenter flexBasis100p flexGrow flexShrink gap0">
<small data-i18n="Multiple swipes per generation">Multiple swipes per generation</small>
@ -1232,7 +1244,7 @@
<input class="neo-range-slider" type="range" id="no_repeat_ngram_size_textgenerationwebui" name="volume" min="0" max="20" step="1">
<input class="neo-range-input" type="number" min="0" max="20" step="1" data-for="no_repeat_ngram_size_textgenerationwebui" id="no_repeat_ngram_size_counter_textgenerationwebui">
</div>
<div data-newbie-hidden data-tg-type="mancer, ooba, dreamgen" class="alignitemscenter flex-container flexFlowColumn flexBasis30p flexGrow flexShrink gap0">
<div data-newbie-hidden data-tg-type="mancer, ooba, tabby, dreamgen" class="alignitemscenter flex-container flexFlowColumn flexBasis30p flexGrow flexShrink gap0">
<small data-i18n="Min Length">Min Length</small>
<input class="neo-range-slider" type="range" id="min_length_textgenerationwebui" name="volume" min="0" max="2000" step="1" />
<input class="neo-range-input" type="number" min="0" max="2000" step="1" data-for="min_length_textgenerationwebui" id="min_length_counter_textgenerationwebui">
@ -1247,27 +1259,26 @@
<label data-i18n="Smooth Sampling">Smooth Sampling</label>
<div class=" fa-solid fa-circle-info opacity50p " data-i18n="[title]Smooth Sampling" title="Allows you to use quadratic/cubic transformations to adjust the distribution. Lower Smoothing Factor values will be more creative, usually between 0.2-0.3 is the sweetspot (assuming the curve = 1). Higher Smoothing Curve values will make the curve steeper, which will punish low probability choices more aggressively. 1.0 curve is equivalent to only using Smoothing Factor."></div>
</h4>
<div class="flex-container flexFlowRow gap10px flexShrink">
<div data-tg-type="mancer, ooba, koboldcpp, aphrodite, tabby" class="flex-container flexFlowRow gap10px flexShrink">
<div data-newbie-hidden class="alignitemscenter flex-container flexFlowColumn flexBasis48p flexGrow flexShrink gap0">
<small data-i18n="Smoothing Factor">Smoothing Factor</small>
<input class="neo-range-slider" type="range" id="smoothing_factor_textgenerationwebui" name="volume" min="0" max="10" step="0.01" />
<input class="neo-range-input" type="number" min="0" max="10" step="0.01" data-for="smoothing_factor_textgenerationwebui" id="smoothing_factor_counter_textgenerationwebui">
</div>
<div data-newbie-hidden class="alignitemscenter flex-container flexFlowColumn flexBasis48p flexGrow flexShrink gap0">
<div data-tg-type="mancer, ooba, koboldcpp, aphrodite" data-newbie-hidden class="alignitemscenter flex-container flexFlowColumn flexBasis48p flexGrow flexShrink gap0">
<small data-i18n="Smoothing Curve">Smoothing Curve</small>
<input class="neo-range-slider" type="range" id="smoothing_curve_textgenerationwebui" name="volume" min="1" max="10" step="0.01" />
<input class="neo-range-input" type="number" min="1" max="10" step="0.01" data-for="smoothing_curve_textgenerationwebui" id="smoothing_curve_counter_textgenerationwebui">
</div>
</div>
</div>
<div data-newbie-hidden data-tg-type="ooba, mancer, koboldcpp, tabby, llamacpp, aphrodite" name="dynaTempBlock" class="wide100p">
<h4 class="wide100p textAlignCenter" data-i18n="DynaTemp">
<div class="flex-container alignitemscenter" style="justify-content: center;">
<div data-newbie-hidden data-tg-type="ooba, mancer, koboldcpp, tabby, llamacpp, aphrodite" id="dynatemp_block_ooba" class="wide100p">
<h4 class="wide100p textAlignCenter">
<div class="flex-container alignitemscenter justifyCenter">
<div class="checkbox_label" for="dynatemp_textgenerationwebui">
<input type="checkbox" id="dynatemp_textgenerationwebui" />
<small data-i18n="dynatemp"></small>
</div>
<span style="text-align: center;" data-i18n="Dynamic Temperature">Dynamic Temperature</span>
<span class="textAlignCenter" data-i18n="Dynamic Temperature">Dynamic Temperature</span>
<div class="fa-solid fa-circle-info opacity50p" data-i18n="[title]Scale Temperature dynamically per token, based on the variation of probabilities" title="Scale Temperature dynamically per token, based on the variation of probabilities."></div>
</div>
</h4>
@ -1289,7 +1300,7 @@
</div>
</div>
</div>
<div data-newbie-hidden name="miroStatBlock" class="wide100p">
<div data-newbie-hidden id="mirostat_block_ooba" class="wide100p">
<h4 class="wide100p textAlignCenter">
<label data-i18n="Mirostat (mode=1 is only for llama.cpp)">Mirostat</label>
<div class=" fa-solid fa-circle-info opacity50p " data-i18n="[title]Mirostat is a thermostat for output perplexity" title="Mirostat is a thermostat for output perplexity.&#13;Mirostat matches the output perplexity to that of the input, thus avoiding the repetition trap&#13;(where, as the autoregressive inference produces text, the perplexity of the output tends toward zero)&#13;and the confusion trap (where the perplexity diverges).&#13;For details, see the paper Mirostat: A Neural Text Decoding Algorithm that Directly Controls Perplexity by Basu et al. (2020).&#13;Mode chooses the Mirostat version. 0=disable, 1=Mirostat 1.0 (llama.cpp only), 2=Mirostat 2.0."></div>
@ -1318,7 +1329,7 @@
</div>
</div>
</div>
<div data-newbie-hidden data-tg-type="ooba" name="beamSearchBlock" class="wide100p">
<div data-newbie-hidden data-tg-type="ooba, vllm" name="beamSearchBlock" class="wide100p">
<h4 class="wide100p textAlignCenter">
<label>
<span data-i18n="Beam search">Beam Search</span>
@ -1370,7 +1381,7 @@
<div class="fa-solid fa-circle-info opacity50p " data-i18n="[title]Add the bos_token to the beginning of prompts. Disabling this can make the replies more creative" title="Add the bos_token to the beginning of prompts. Disabling this can make the replies more creative."></div>
</label>
</label>
<label data-tg-type="ooba, llamacpp" class="checkbox_label flexGrow flexShrink" for="ban_eos_token_textgenerationwebui">
<label data-tg-type="ooba, llamacpp, tabby" class="checkbox_label flexGrow flexShrink" for="ban_eos_token_textgenerationwebui">
<input type="checkbox" id="ban_eos_token_textgenerationwebui" />
<label>
<small data-i18n="Ban EOS Token">Ban EOS Token</small>
@ -1401,11 +1412,11 @@
</label>
</div>
</div>
<div data-newbie-hidden class="flex-container flexFlowColumn alignitemscenter flexBasis48p flexGrow flexShrink gap0">
<div data-tg-type="mancer, ooba, koboldcpp, vllm, aphrodite, llamacpp, ollama" data-newbie-hidden class="flex-container flexFlowColumn alignitemscenter flexBasis48p flexGrow flexShrink gap0">
<small data-i18n="Seed" class="textAlignCenter">Seed</small>
<input type="number" id="seed_textgenerationwebui" class="text_pole textAlignCenter" min="-1" value="-1" maxlength="100" />
</div>
<div data-newbie-hidden class="wide100p">
<div id="banned_tokens_block_ooba" data-newbie-hidden class="wide100p">
<hr data-newbie-hidden class="width100p">
<h4 class="range-block-title justifyCenter">
<span data-i18n="Banned Tokens">Banned Tokens</span>
@ -1416,7 +1427,7 @@
</div>
</div>
<div class="range-block wide100p">
<div class="range-block-title title_restorable">
<div id="logit_bias_textgenerationwebui" class="range-block-title title_restorable">
<span data-i18n="Logit Bias">Logit Bias</span>
<div id="textgen_logit_bias_new_entry" class="menu_button menu_button_icon">
<i class="fa-xs fa-solid fa-plus"></i>
@ -1430,7 +1441,7 @@
<div class="logit_bias_list"></div>
</div>
</div>
<div data-newbie-hidden data-tg-type="ooba, tabby" class="wide100p">
<div id="cfg_block_ooba" data-newbie-hidden data-tg-type="ooba, tabby" class="wide100p">
<hr class="width100p">
<h4 data-i18n="CFG" class="textAlignCenter">CFG
<div class="margin5 fa-solid fa-circle-info opacity50p " data-i18n="[title]Classifier Free Guidance. More helpful tip coming soon" title="Classifier Free Guidance. More helpful tip coming soon."></div>
@ -1452,7 +1463,7 @@
</div>
</div>
</div>
<div data-newbie-hidden id="json_schema_block" data-tg-type="tabby" class="wide100p">
<div data-newbie-hidden id="json_schema_block" data-tg-type="tabby, llamacpp" class="wide100p">
<hr class="wide100p">
<h4 class="wide100p textAlignCenter"><span data-i18n="JSON Schema">JSON Schema</span>
<a href="https://json-schema.org/learn/getting-started-step-by-step" target="_blank">
@ -1467,17 +1478,18 @@
<hr class="wide100p">
<h4 class="wide100p textAlignCenter">
<label>
<span data-i18n="GBNF Grammar">GBNF Grammar</span>
<span data-i18n="Grammar String">Grammar String</span>
<div class="margin5 fa-solid fa-circle-info opacity50p " data-i18n="[title]GNBF or ENBF, depends on the backend in use. If you're using this you should know which." title="GNBF or ENBF, depends on the backend in use. If you're using this you should know which."></div>
<a href="https://github.com/ggerganov/llama.cpp/blob/master/grammars/README.md" target="_blank">
<small>
<div class="fa-solid fa-circle-question note-link-span"></div>
<div class="fa-solid fa-up-right-from-square note-link-span"></div>
</small>
</a>
</label>
</h4>
<textarea id="grammar_string_textgenerationwebui" rows="4" class="text_pole textarea_compact monospace" data-i18n="[placeholder]Type in the desired custom grammar" placeholder="Type in the desired custom grammar"></textarea>
</div>
<div data-newbie-hidden data-tg-type="koboldcpp" class="range-block flexFlowColumn wide100p">
<div id="sampler_order_block" data-newbie-hidden data-tg-type="koboldcpp" class="range-block flexFlowColumn wide100p">
<hr class="wide100p">
<div class="range-block-title">
<span data-i18n="Samplers Order">Samplers Order</span>
@ -1593,7 +1605,7 @@
<label class="checkbox_label flexWrap alignItemsCenter" for="character_names_completion">
<input type="radio" id="character_names_completion" name="character_names" value="1">
<span data-i18n="Completion">Completion Object</span>
<i class="right_menu_button fa-solid fa-circle-exclamation" title="Restrictions apply: only Latin alphanumerics and underscores. Doesn't work for all sources, notably: Claude, MistralAI, Google."></i>
<i class="right_menu_button fa-solid fa-circle-exclamation" data-i18n="[title]Restrictions apply: only Latin alphanumerics and underscores. Doesn't work for all sources, notably: Claude, MistralAI, Google." title="Restrictions apply: only Latin alphanumerics and underscores. Doesn't work for all sources, notably: Claude, MistralAI, Google."></i>
<small class="flexBasis100p" data-i18n="Add character names to completion objects.">
Add character names to completion objects.
</small>
@ -1676,11 +1688,21 @@
<input id="openai_image_inlining" type="checkbox" />
<span data-i18n="Send inline images">Send inline images</span>
<div id="image_inlining_hint" class="flexBasis100p toggle-description justifyLeft">
Sends images in prompts if the model supports it (e.g. GPT-4V, Claude 3 or Llava 13B).
Use the <code><i class="fa-solid fa-paperclip"></i></code> action on any message or the
<code><i class="fa-solid fa-wand-magic-sparkles"></i></code> menu to attach an image file to the chat.
<span data-i18n="image_inlining_hint_1">Sends images in prompts if the model supports it (e.g. GPT-4V, Claude 3 or Llava 13B).
Use the</span> <code><i class="fa-solid fa-paperclip"></i></code> <span data-i18n="image_inlining_hint_2">action on any message or the</span>
<code><i class="fa-solid fa-wand-magic-sparkles"></i></code> <span data-i18n="image_inlining_hint_3">menu to attach an image file to the chat.</span>
</div>
</label>
<div class="flex-container flexFlowColumn wide100p textAlignCenter marginTop10" data-source="openai,custom">
<label for="openai_inline_image_quality">
Inline Image Quality
</label>
<select id="openai_inline_image_quality">
<option value="auto">Auto</option>
<option value="low">Low</option>
<option value="high">High</option>
</select>
</div>
</div>
<div class="range-block" data-source="ai21">
<label for="use_ai21_tokenizer" title="Use AI21 Tokenizer" class="checkbox_label widthFreeExpand">
@ -1701,13 +1723,14 @@
<div class="range-block" data-source="makersuite">
<label for="use_makersuite_sysprompt" class="checkbox_label widthFreeExpand">
<input id="use_makersuite_sysprompt" type="checkbox" />
<span data-i18n="Use system prompt (Gemini 1.5 pro+ only)">
Use system prompt (Gemini 1.5 pro+ only)
<span>
<span data-i18n="Use system prompt">Use system prompt</span><br>
<small data-i18n="(Gemini 1.5 Pro/Flash only)">(Gemini 1.5 Pro/Flash only)</small>
</span>
</label>
<div class="toggle-description justifyLeft marginBot5">
<span data-i18n="Merges all system messages up until the first message with a non system role, and sends them through google's system_instruction field instead of with the rest of the prompt contents.">
Merges all system messages up until the first message with a non system role, and sends them through google's system_instruction field instead of with the rest of the prompt contents.
<span data-i18n="Merges all system messages up until the first message with a non-system role, and sends them in a system_instruction field.">
Merges all system messages up until the first message with a non-system role, and sends them in a <code>system_instruction</code> field.
</span>
</div>
</div>
@ -1715,6 +1738,8 @@
<div class="wide100p">
<span id="claude_assistant_prefill_text" data-i18n="Assistant Prefill">Assistant Prefill</span>
<textarea id="claude_assistant_prefill" class="text_pole textarea_compact" name="assistant_prefill autoSetHeight" rows="3" maxlength="10000" data-i18n="[placeholder]Start Claude's answer with..." placeholder="Start Claude's answer with..."></textarea>
<span id="claude_assistant_impersonation_text" data-i18n="Assistant Impersonation Prefill">Assistant Impersonation Prefill</span>
<textarea id="claude_assistant_impersonation" class="text_pole textarea_compact" name="assistant_impersonation autoSetHeight" rows="3" maxlength="10000" data-i18n="[placeholder]Start Claude's answer with..." placeholder="Start Claude's answer with..."></textarea>
</div>
<label for="claude_use_sysprompt" class="checkbox_label widthFreeExpand">
<input id="claude_use_sysprompt" type="checkbox" />
@ -1739,7 +1764,7 @@
</div>
</div>
<div data-newbie-hidden class="range-block m-t-1" data-source="openai,openrouter,scale">
<div class="range-block-title openai_restorable" data-i18n="Logit Bias">
<div id="logit_bias_openai" class="range-block-title openai_restorable" data-i18n="Logit Bias">
Logit Bias
</div>
<div class="toggle-description justifyLeft" data-i18n="Helps to ban or reenforce the usage of certain words">
@ -1981,6 +2006,11 @@
</option>
</select>
</div>
<div>
<h4 data-i18n="Model Providers">Model Providers</h4>
<select id="openrouter_providers_text" class="openrouter_providers" multiple>
</select>
</div>
</div>
<div data-tg-type="infermaticai" class="flex-container flexFlowColumn">
<h4 data-i18n="InfermaticAI API Key">InfermaticAI API Key</h4>
@ -2258,6 +2288,7 @@
<option value="ai21">AI21</option>
<option value="claude">Claude</option>
<option value="cohere">Cohere</option>
<option value="groq">Groq</option>
<option value="makersuite">Google MakerSuite</option>
<option value="mistralai">MistralAI</option>
<option value="openrouter">OpenRouter</option>
@ -2391,6 +2422,10 @@
<option value="gpt-4-32k-0613">gpt-4-32k-0613 (2023)</option>
<option value="gpt-4-32k-0314">gpt-4-32k-0314 (2023)</option>
</optgroup>
<optgroup label="GPT-4o">
<option value="gpt-4o">gpt-4o</option>
<option value="gpt-4o-2024-05-13">gpt-4o-2024-05-13</option>
</optgroup>
<optgroup label="GPT-4 Turbo">
<option value="gpt-4-turbo">gpt-4-turbo</option>
<option value="gpt-4-turbo-2024-04-09">gpt-4-turbo-2024-04-09</option>
@ -2494,6 +2529,11 @@
<option data-i18n="Connect to the API">-- Connect to the API --</option>
</select>
</div>
<div>
<h4 data-i18n="Model Providers">Model Providers</h4>
<select id="openrouter_providers_chat" class="openrouter_providers" multiple>
</select>
</div>
<div class="marginTopBot5">
<div class="inline-drawer wide100p">
<div class="inline-drawer-toggle inline-drawer-header">
@ -2615,6 +2655,8 @@
<h4 data-i18n="Google Model">Google Model</h4>
<select id="model_google_select">
<optgroup label="Latest">
<!-- Doesn't work without "latest". Maybe my key is scuffed? -->
<option value="gemini-1.5-flash-latest">Gemini 1.5 Flash</option>
<!-- Points to 1.0, no default 1.5 endpoint -->
<option value="gemini-pro">Gemini Pro</option>
<option value="gemini-pro-vision">Gemini Pro Vision</option>
@ -2646,11 +2688,13 @@
<optgroup label="Latest">
<option value="open-mistral-7b">open-mistral-7b</option>
<option value="open-mixtral-8x7b">open-mixtral-8x7b</option>
<option value="open-mixtral-8x22b">open-mixtral-8x22b</option>
<option value="mistral-small-latest">mistral-small-latest</option>
<option value="mistral-medium-latest">mistral-medium-latest</option>
<option value="mistral-large-latest">mistral-large-latest</option>
</optgroup>
<optgroup label="Sub-versions">
<option value="open-mixtral-8x22b-2404">open-mixtral-8x22b-2404</option>
<option value="mistral-tiny-2312">mistral-tiny-2312</option>
<option value="mistral-small-2312">mistral-small-2312</option>
<option value="mistral-small-2402">mistral-small-2402</option>
@ -2660,6 +2704,23 @@
</select>
</div>
</form>
<div id="groq_form" data-source="groq">
<h4 data-i18n="Groq API Key">Groq API Key</h4>
<div class="flex-container">
<input id="api_key_groq" name="api_key_groq" class="text_pole flex1" maxlength="500" value="" type="text" autocomplete="off">
<div title="Clear your API key" data-i18n="[title]Clear your API key" class="menu_button fa-solid fa-circle-xmark clear-api-key" data-key="api_key_groq"></div>
</div>
<div data-for="api_key_groq" class="neutral_warning">
For privacy reasons, your API key will be hidden after you reload the page.
</div>
<h4 data-i18n="Groq Model">Groq Model</h4>
<select id="model_groq_select">
<option value="llama3-8b-8192">llama3-8b-8192</option>
<option value="llama3-70b-8192">llama3-70b-8192</option>
<option value="mixtral-8x7b-32768">mixtral-8x7b-32768</option>
<option value="gemma-7b-it">gemma-7b-it</option>
</select>
</div>
<div id="perplexity_form" data-source="perplexity">
<h4 data-i18n="Perplexity API Key">Perplexity API Key</h4>
<div class="flex-container">
@ -2672,6 +2733,10 @@
<h4 data-i18n="Perplexity Model">Perplexity Model</h4>
<select id="model_perplexity_select">
<optgroup label="Perplexity Models">
<option value="llama-3-sonar-small-32k-chat">llama-3-sonar-small-32k-chat</option>
<option value="llama-3-sonar-small-32k-online">llama-3-sonar-small-32k-online</option>
<option value="llama-3-sonar-large-32k-chat">llama-3-sonar-large-32k-chat</option>
<option value="llama-3-sonar-large-32k-online">llama-3-sonar-large-32k-online</option>
<option value="sonar-small-chat">sonar-small-chat</option>
<option value="sonar-small-online">sonar-small-online</option>
<option value="sonar-medium-chat">sonar-medium-chat</option>
@ -3142,11 +3207,13 @@
<option value="0">None / Estimated</option>
<option value="1">GPT-2</option>
<!-- Option #2 was a legacy GPT-2/3 tokenizer -->
<option value="3">LLaMA</option>
<option value="3">Llama 1/2</option>
<option value="12">Llama 3</option>
<option value="4">NerdStash (NovelAI Clio)</option>
<option value="5">NerdStash v2 (NovelAI Kayra)</option>
<option value="7">Mistral</option>
<option value="8">Yi</option>
<option value="11">Claude 1/2</option>
<option value="6">API (WebUI / koboldcpp)</option>
</select>
</div>
@ -3266,7 +3333,7 @@
<span data-i18n="Active World(s) for all chats"><small>Active World(s) for all chats</small></span>
</div>
<div class="range-block-range">
<select id="world_info" multiple>
<select id="world_info" class="select2_multi_sameline" multiple>
<option value="" data-i18n="-- World Info not found --">-- World Info not found -- </option>
</select>
</div>
@ -3382,8 +3449,14 @@
</label>
<label title="If the entry key consists of only one word, it would not be matched as part of other words" data-i18n="[title]If the entry key consists of only one word, it would not be matched as part of other words" class="checkbox_label flex1">
<input id="world_info_match_whole_words" type="checkbox" />
<small data-i18n="Match whole words" class="whitespacenowrap flex1">
Match whole words
<small data-i18n="Match Whole Words" class="whitespacenowrap flex1">
Match Whole Words
</small>
</label>
<label title="Only the entries with the most number of key matches will be selected for Inclusion Group filtering" data-i18n="[title]Only the entries with the most number of key matches will be selected for Inclusion Group filtering" class="checkbox_label flex1">
<input id="world_info_use_group_scoring" type="checkbox" />
<small data-i18n="Use Group Scoring" class="whitespacenowrap flex1">
Use Group Scoring
</small>
</label>
<label title="Alert if your world info is greater than the allocated budget." data-i18n="[title]Alert if your world info is greater than the allocated budget." class="checkbox_label flex1">
@ -3530,7 +3603,7 @@
<div class="flex-container">
<span data-i18n="Chat Style:">Chat Style:</span><br>
<select id="chat_display" class="widthNatural flex1 margin0">
<option value="0" data-i18n="Default">Flat</span>
<option value="0" data-i18n="Flat">Flat</span>
<option value="1" data-i18n="Bubbles">Bubbles</option>
<option value="2" data-i18n="Document">Document</option>
</select>
@ -3800,7 +3873,7 @@
Character Handling
</h4>
<div title="If set in the advanced character definitions, this field will be displayed in the characters list." data-i18n="[title]If set in the advanced character definitions, this field will be displayed in the characters list.">
<label for="aux_field" data-i18n="Char List Subheader"><small>Char List Subheader</small></label>
<label for="aux_field"><small data-i18n="Char List Subheader">Char List Subheader</small></label>
<select id="aux_field">
<option data-i18n="Character Version" value="character_version">Character Version</option>
<option data-i18n="Created by" value="creator">Created by</option>
@ -3836,9 +3909,9 @@
</label>
</div>
<h4><span data-i18n="Miscellaneous">Miscellaneous</span></h4>
<div>
<div name="MiscellaneousToggles">
<h4><span data-i18n="Miscellaneous">Miscellaneous</span></h4>
<div data-newbie-hidden class="flex-container">
<div id="reload_chat" class="menu_button whitespacenowrap" data-i18n="[title]Reload and redraw the currently open chat" title="Reload and redraw the currently open chat.">
<small data-i18n="Reload Chat">Reload Chat</small>
@ -3905,7 +3978,7 @@
</div>
</label>
<div id="movingui-preset-save-button" title="Save changes to a new MovingUI preset file." data-i18n="[title]Save movingUI changes to a new file" class="menu_button margin0 fa-solid fa-save"></div>
<div data-newbie-hidden id="movingUIreset" title="Reset MovingUI panel sizes/locations." class="menu_button fa-solid fa-recycle margin0" data-i18n="Reset Panels"></div>
<div data-newbie-hidden id="movingUIreset" title="Reset MovingUI panel sizes/locations." class="menu_button fa-solid fa-recycle margin0" data-i18n="[title]Reset MovingUI panel sizes/locations."></div>
</div>
</div>
<div data-newbie-hidden id="CustomCSS-block" class="flex-container flexFlowColumn">
@ -3913,8 +3986,8 @@
<span data-i18n="Custom CSS">Custom CSS</span>
<i class="editor_maximize fa-solid fa-maximize right_menu_button" data-for="customCSS" title="Expand the editor"></i>
</h4>
<div class="flex-container flexnowrap alignitemscenter">
<textarea id="customCSS" class="text_pole margin0 margin-r5 textarea_compact monospace"></textarea>
<div id="CustomCSS-textAreaBlock" class="flex-container flexnowrap alignitemscenter">
<textarea id="customCSS" class="text_pole margin0 margin-r5 textarea_compact monospace" rows="8"></textarea>
</div>
</div>
</div>
@ -3931,8 +4004,8 @@
<div class="fa-solid fa-circle-info opacity50p" data-i18n="[title]The number of chat history messages to load before pagination." title="The number of chat history messages to load before pagination."></div>
</small>
<input class="neo-range-slider" type="range" id="chat_truncation" name="chat_truncation" min="0" max="1000" step="25">
<input class="neo-range-input" type="number" min="0" max="1000" step="25" data-for="chat_truncation" id="chat_truncation_counter">
<input class="neo-range-slider" type="range" id="chat_truncation" name="chat_truncation" min="0" max="1000" step="5">
<input class="neo-range-input" type="number" min="0" max="1000" step="5" data-for="chat_truncation" id="chat_truncation_counter">
<small data-i18n="(0 = All)">(0 = All)</small>
</div>
@ -4015,7 +4088,7 @@
<small class="fa-solid fa-circle-question note-link-small"></small>
</a>
</label>
<label class="checkbox_label" for="forbid_external_media" title="Disalow embedded media from other domains in chat messages." data-i18n="[title]Disalow embedded media from other domains in chat messages">
<label class="checkbox_label" for="forbid_external_media" title="Disallow embedded media from other domains in chat messages." data-i18n="[title]Disallow embedded media from other domains in chat messages">
<input id="forbid_external_media" type="checkbox" />
<small data-i18n="Forbid External Media">Forbid External Media</small>
</label>
@ -4064,7 +4137,82 @@
</div>
</div>
</div>
<div name="AutoCompleteToggle">
<h4 data-i18n="AutoComplete Settings">AutoComplete Settings</h4>
<label data-newbie-hidden class="checkbox_label" for="stscript_autocomplete_autoHide">
<input id="stscript_autocomplete_autoHide" type="checkbox" />
<small data-i18n="Automatically hide details">
Automatically hide details
</small>
</label>
<div class="flex-container">
<div class="flex1" title="Determines how entries are found for autocomplete." data-i18n="[title]Determines how entries are found for autocomplete.">
<label for="stscript_matching" data-i18n="Autocomplete Matching"><small>Matching</small></label>
<select id="stscript_matching">
<option data-i18n="Starts with" value="strict">Starts with</option>
<option data-i18n="Includes" value="includes">Includes</option>
<option data-i18n="Fuzzy" value="fuzzy">Fuzzy</option>
</select>
</div>
<div class="flex1" title="Sets the style of the autocomplete." data-i18n="[title]Sets the style of the autocomplete.">
<label for="stscript_autocomplete_style" data-i18n="Autocomplete Style"><small>Style</small></label>
<div class="flex-container flexFlowRow alignItemsBaseline">
<select id="stscript_autocomplete_style">
<option data-i18n="Follow Theme" value="theme">Follow Theme</option>
<option data-i18n="Dark" value="dark">Dark</option>
<option data-i18n="Light" value="light">Light</option>
</select>
<!-- <div class="menu_button fa-solid fa-pen-to-square" title="Customize colors"></div> -->
</div>
</div>
</div>
<div class="flex-container flexFlowColumn gap0" title="Sets the font size of the autocomplete." data-i18n="[title]Sets the font size of the autocomplete.">
<label for="stscript_autocomplete_font_scale"><small>Font Scale</small></label>
<input class="neo-range-slider" type="range" id="stscript_autocomplete_font_scale" min="0.5" max="2" step="0.01">
<input class="neo-range-input" type="number" min="0.5" max="2" step="0.01" data-for="stscript_autocomplete_font_scale" id="stscript_autocomplete_font_scale_counter">
</div>
<div title="Sets the width of the autocomplete." data-i18n="[title]Sets the width of the autocomplete.">
<label for="stscript_autocomplete_width" data-i18n="Autocomplete Width"><small>Width</small></label>
<div class="doubleRangeContainer">
<div class="doubleRangeInputContainer">
<input type="range" id="stscript_autocomplete_width_left" min="0" max="2" step="1">
<datalist id="stscript_autocomplete_width_left_values">
<option value="0" label="input" title="chat input box"></option>
<option value="1" label="chat" title="entire chat width"></option>
<option value="2" label="full" title="full window width"></option>
</datalist>
</div>
<div class="doubleRangeInputContainer">
<input type="range" id="stscript_autocomplete_width_right" min="0" max="2" step="1">
<datalist id="stscript_autocomplete_width_right_values">
<option value="0" label="input" title="chat input box"></option>
<option value="1" label="chat" title="entire chat width"></option>
<option value="2" label="full" title="full window width"></option>
</datalist>
</div>
</div>
</div>
</div>
<div name="STscriptToggles">
<h4 data-i18n="STscript Settings">STscript Settings</h4>
<div title="Sets default flags for the STscript parser." data-i18n="[title]Sets default flags for the STscript parser.">
<label data-i18n="Parser Flags"><small>Parser Flags</small></label>
<label class="checkbox_label" title="Switch to stricter escaping, allowing all delimiting characters to be escaped with a backslash, and backslashes to be escaped as well." data-i18n="[title]Switch to stricter escaping, allowing all dellimiting characters to be escaped with a backslash, and backslashes to be escaped as well.">
<input id="stscript_parser_flag_strict_escaping" type="checkbox" />
<span data-i18n="STRICT_ESCAPING"><small>STRICT_ESCAPING</small></span>
<a href="https://docs.sillytavern.app/usage/st-script/#strict-escaping" target="_blank" class="notes-link">
<span class="fa-solid fa-circle-question note-link-span"></span>
</a>
</label>
<label class="checkbox_label" title="Replace all {{getvar::}} and {{getglobalvar::}} macros with scoped variables to avoid double macro substitution." data-i18n="[title]Replace all {{getvar::}} and {{getglobalvar::}} macros with scoped variables to avoid double macro substitution.">
<input id="stscript_parser_flag_replace_getvar" type="checkbox" />
<span data-i18n="REPLACE_GETVAR"><small>REPLACE_GETVAR</small></span>
<a href="https://docs.sillytavern.app/usage/st-script/#replace-variable-macros" target="_blank" class="notes-link">
<span class="fa-solid fa-circle-question note-link-span"></span>
</a>
</label>
</div>
</div>
</div>
</div>
</div>
@ -4233,7 +4381,22 @@
<option value="0" data-i18n="In Story String / Prompt Manager">In Story String / Prompt Manager</option>
<option value="2" data-i18n="Top of Author's Note">Top of Author's Note</option>
<option value="3" data-i18n="Bottom of Author's Note">Bottom of Author's Note</option>
<option value="4" data-i18n="In-chat @ Depth">In-chat @ Depth</option>
</select>
<div id="persona_depth_position_settings" class="flex-container">
<div class="flex1">
<label for="persona_depth_value" data-i18n="Depth:">Depth:</label>
<input id="persona_depth_value" class="text_pole" type="number" min="0" max="999" step="1">
</div>
<div class="flex1">
<label for="persona_depth_role" data-i18n="Role:">Role:</label>
<select id="persona_depth_role" class="text_pole">
<option data-i18n="System" value="0">System</option>
<option data-i18n="User" value="1">User</option>
<option data-i18n="Assistant" value="2">Assistant</option>
</select>
</div>
</div>
</div>
</div>
<div class="range-block">
@ -4354,6 +4517,9 @@
<option id="replace_update" data-i18n="Replace / Update">
Replace / Update
</option>
<option id="import_tags" data-i18n="Import Tags">
Import Tags
</option>
<!--<option id="dupe_button">
Duplicate
</option>
@ -4625,7 +4791,7 @@
</div>
</div>
<!-- various fullscreen popups -->
<template id="shadow_popup_template">
<template id="shadow_popup_template" data-i18n="[popup_text_save]popup_text_save;[popup_text_yes]popup_text_yes;[popup_text_no]popup_text_no;[popup_text_cancel]popup_text_cancel;[popup_text_import]popup_text_import" popup_text_save="Save" popup_text_yes="Yes" popup_text_no="No" popup_text_cancel="Cancel" popup_text_import="Import"> <!-- localization data holder for popups -->
<div class="shadow_popup">
<div class="dialogue_popup">
<div class="dialogue_popup_holder">
@ -4864,11 +5030,11 @@
</div>
<div id="background_template" class="template_element">
<div class="bg_example flex-container" bgfile="" class="bg_example_img" title="">
<div title="Copy to system backgrounds" class="bg_button bg_example_copy fa-solid fa-file-arrow-up"></div>
<div title="Rename background" class="bg_button bg_example_edit fa-solid fa-pencil"></div>
<div title="Lock" class="bg_button bg_example_lock fa-solid fa-lock"></div>
<div title="Unlock" class="bg_button bg_example_unlock fa-solid fa-lock-open"></div>
<div title="Delete background" class="bg_button bg_example_cross fa-solid fa-circle-xmark"></div>
<div title="Copy to system backgrounds" data-i18n="[title]Copy to system backgrounds" class="bg_button bg_example_copy fa-solid fa-file-arrow-up"></div>
<div title="Rename background" data-i18n="[title]Rename background" class="bg_button bg_example_edit fa-solid fa-pencil"></div>
<div title="Lock" data-i18n="[title]Lock" class="bg_button bg_example_lock fa-solid fa-lock"></div>
<div title="Unlock" data-i18n="[title]Unlock" class="bg_button bg_example_unlock fa-solid fa-lock-open"></div>
<div title="Delete background" data-i18n="[title]Delete background" class="bg_button bg_example_cross fa-solid fa-circle-xmark"></div>
<div class="BGSampleTitle">
</div>
</div>
@ -5007,13 +5173,19 @@
<div class="WIEnteryHeaderControls flex-container">
<div name="PositionBlock" class="world_entry_form_control world_entry_form_radios wi-enter-footer-text">
<label for="position" class="WIEntryHeaderTitleMobile" data-i18n="Position:">Position:</label>
<select name="position" class="text_pole widthNatural margin0" data-i18n="[title]T_Position" title="↑Char: Before Character Definitions&#13;↓Char: After Character Definitions&#13;↑AN: Before Author's Note&#13;↓AN: After Author's Note&#13;@D ⚙️: at Depth (System)&#13;@D 👤: at Depth (User)&#13;@D 🤖: at Depth (Assistant)">
<select name="position" class="text_pole widthNatural margin0" data-i18n="[title]T_Position" title="↑Char: Before Character Definitions&#13;↓Char: After Character Definitions&#13;↑EM: Before Example Messages&#13;↓EM: After Example Messages&#13;↑AN: Before Author's Note&#13;↓AN: After Author's Note&#13;@D ⚙️: at Depth (System)&#13;@D 👤: at Depth (User)&#13;@D 🤖: at Depth (Assistant)">
<option value="0" data-role="" data-i18n="Before Char Defs">
↑Char
</option>
<option value="1" data-role="" data-i18n="After Char Defs">
↓Char
</option>
<option value="5" data-role="" data-i18n="Before EM">
↑EM
</option>
<option value="6" data-role="" data-i18n="After EM">
↓EM
</option>
<option value="2" data-role="" data-i18n="Before AN">
↑AN
</option>
@ -5059,7 +5231,11 @@
</span>
</small>
<small class="textAlignCenter" data-i18n="Primary Keywords">Primary Keywords</small>
<textarea class="text_pole keyprimarytextpole" name="key" rows="1" data-i18n="[placeholder]Comma separated (required)" placeholder="Comma separated (required)" maxlength="2000"></textarea>
<select class="keyprimaryselect keyselect select2_multi_sameline" name="key" data-i18n="[placeholder]Keywords or Regexes" placeholder="Keywords or Regexes" multiple="multiple"></select>
<textarea class="text_pole keyprimarytextpole mobile" name="key" rows="1" data-i18n="[placeholder]Comma separated list" placeholder="Comma separated list" maxlength="2000" style="display: none;"></textarea>
<button type="button" class="switch_input_type_icon" tabindex="-1" title="Switch to plaintext mode" data-icon-on="✨" data-icon-off="⌨️" data-tooltip-on="Switch to fancy mode" data-tooltip-off="Switch to plaintext mode">
⌨️
</button>
</div>
<div class="world_entry_form_control">
<small class="textAlignCenter" data-i18n="Logic">Logic</small>
@ -5077,9 +5253,11 @@
</span>
</small>
<small class="textAlignCenter" data-i18n="Optional Filter">Optional Filter</small>
<div class="flex-container flexFlowRow alignitemscenter">
<textarea class="text_pole keysecondarytextpole" name="keysecondary" rows="1" data-i18n="[placeholder]Comma separated (ignored if empty)" placeholder="Comma separated list" maxlength="2000"></textarea>
</div>
<select class="keysecondaryselect keyselect select2_multi_sameline" name="keysecondary" data-i18n="[placeholder]Keywords or Regexes (ignored if empty)" placeholder="Keywords or Regexes (ignored if empty)" multiple="multiple"></select>
<textarea class="text_pole keysecondarytextpole mobile" name="keysecondary" rows="1" data-i18n="[placeholder]Comma separated list (ignored if empty)" placeholder="Comma separated list (ignored if empty)" maxlength="2000" style="display: none;"></textarea>
<button type="button" class="switch_input_type_icon" tabindex="-1" title="Switch to plaintext mode" data-icon-on="✨" data-icon-off="⌨️" data-tooltip-on="Switch to fancy mode" data-tooltip-off="Switch to plaintext mode">
⌨️
</button>
</div>
</div>
<div name="perEntryOverridesBlock" class="flex-container wide100p alignitemscenter">
@ -5103,6 +5281,14 @@
<option value="false" data-i18n="No">No</option>
</select>
</div>
<div class="world_entry_form_control flex1">
<small class="textAlignCenter" data-i18n="Use Group Scoring">Use Group Scoring</small>
<select name="useGroupScoring" class="text_pole widthNatural margin0">
<option value="null" data-i18n="Use global setting">Use global setting</option>
<option value="true" data-i18n="Yes">Yes</option>
<option value="false" data-i18n="No">No</option>
</select>
</div>
<div class="world_entry_form_control flex1" title="Can be used to automatically activate Quick Replies" data-i18n="[title]Can be used to automatically activate Quick Replies">
<small class="textAlignCenter" data-i18n="Automation ID">Automation ID</small>
<input class="text_pole margin0" name="automationId" type="text" placeholder="( None )" data-i18n="[placeholder]( None )">
@ -5133,6 +5319,12 @@
Prevent further recursion (this entry will not activate others)
</span>
</label>
<label class="checkbox flex-container alignitemscenter flexNoGap">
<input type="checkbox" name="delay_until_recursion" />
<span data-i18n="Delay until recursion (this entry can only be activated on recursive checking)">
Delay until recursion (this entry can only be activated on recursive checking)
</span>
</label>
</div>
</span>
</small>
@ -5149,7 +5341,7 @@
</div>
</div>
<div class="flex-container wide100p flexGap10">
<div class="flex1 flex-container flexFlowColumn flexNoGap">
<div class="flex4 flex-container flexFlowColumn flexNoGap">
<div class="flex-container justifySpaceBetween">
<small for="characterFilter" data-i18n="Filter to Character(s)">
Filter to Character(s)
@ -5162,26 +5354,25 @@
</label>
</div>
<div class="range-block-range">
<select name="characterFilter" multiple>
<select name="characterFilter" class="select2_multi_sameline" multiple>
<option value="">
<span data-i18n="-- Characters not found --">-- Characters not found --</span>
</option>
</select>
</div>
</div>
<div class="flex1 flex-container flexFlowColumn flexNoGap">
<div class="flex3 flex-container flexFlowColumn flexNoGap">
<div class="flex-container justifySpaceBetween">
<small for="group" data-i18n="Inclusion Group">
Inclusion Group
<a href="https://docs.sillytavern.app/usage/core-concepts/worldinfo/#inclusion-group" class="notes-link" target="_blank"
title="Inclusion Groups ensure only one entry from a group is activated at a time, if multiple are triggered.&#13;&#13;Documentation: World Info - Inclusion Group" data-i18n="[title]Inclusion Groups ensure only one entry from a group is activated at a time, if multiple are triggered.&#13;&#13;Documentation: World Info - Inclusion Group">
<a href="https://docs.sillytavern.app/usage/core-concepts/worldinfo/#inclusion-group" class="notes-link" target="_blank" title="Inclusion Groups ensure only one entry from a group is activated at a time, if multiple are triggered.&#13;Supports multiple comma-separated groups.&#13;&#13;Documentation: World Info - Inclusion Group" data-i18n="[title]Inclusion Groups ensure only one entry from a group is activated at a time, if multiple are triggered.&#13;&#13;Documentation: World Info - Inclusion Group">
<span class="fa-solid fa-circle-question note-link-span"></span>
</a>
</small>
<label class="checkbox_label flexNoGap margin-r5" for="groupOverride">
<input type="checkbox" name="groupOverride" />
<span>
<small data-i18n="Prioritize Inclusion" title="Prioritize this entry: When checked, this entry is prioritized out of all selections.&#13;If multiple are prioritized, the one with the highest 'Order' is chosen.&#13;" data-i18n="[title]Prioritize this entry: When checked, this entry is prioritized out of all selections.&#13;If multiple are prioritized, the one with the highest 'Order' is chosen.">
<small data-i18n="Prioritize Inclusion" title="Prioritize this entry: When checked, this entry is prioritized out of all selections.&#13;If multiple are prioritized, the one with the highest 'Order' is chosen.&#13;" data-i18n="[title]Prioritize this entry: When checked, this entry is prioritized out of all selections.&#13;If multiple are prioritized, the one with the highest 'Order' is chosen.">
Prioritize Inclusion
<div class="fa-solid fa-circle-info opacity50p"></div>
</small>
@ -5192,6 +5383,16 @@
<input type="text" class="text_pole margin0" name="group" rows="1" data-i18n="[placeholder]Only one entry with the same label will be activated" placeholder="Only one entry with the same label will be activated">
</div>
</div>
<div class="flex1 flex-container flexFlowColumn flexNoGap" data-i18n="[title]A relative likelihood of entry activation within the group" title="A relative likelihood of entry activation within the group">
<div class="flex-container justifySpaceBetween marginBot5">
<small for="groupWeight" data-i18n="Group Weight">
Group Weight
</small>
</div>
<div class="range-block-range">
<input type="number" class="text_pole margin0" name="groupWeight" rows="1" placeholder="100" min="1" max="999999">
</div>
</div>
</div>
<div name="WIEntryBottomControls" class="flex-container flex1 justifySpaceBetween world_entry_form_horizontal">
<div class="flex-container flexFlowColumn flexNoGap wi-enter-footer-text ">
@ -5274,45 +5475,45 @@
</div>
</div>
<div id="completion_prompt_manager_popup_edit">
<h3>Edit</h3>
<h3 data-i18n="prompt_manager_edit">Edit</h3>
<div class="completion_prompt_manager_popup_entry">
<form class="completion_prompt_manager_popup_entry_form">
<div class="flex-container gap10px">
<div class="completion_prompt_manager_popup_entry_form_control flex1">
<label for="completion_prompt_manager_popup_entry_form_name">
<span>Name</span>
<span data-i18n="prompt_manager_name">Name</span>
</label>
<div class="text_muted">A name for this prompt.</div>
<div class="text_muted" data-i18n="A name for this prompt.">A name for this prompt.</div>
<input id="completion_prompt_manager_popup_entry_form_name" class="text_pole" type="text" name="name" />
</div>
<div class="completion_prompt_manager_popup_entry_form_control flex1">
<label for="completion_prompt_manager_popup_entry_form_role">
<span>Role</span>
<span data-i18n="Role">Role</span>
</label>
<div class="text_muted">To whom this message will be attributed.</div>
<div class="text_muted" data-i18n="To whom this message will be attributed.">To whom this message will be attributed.</div>
<select id="completion_prompt_manager_popup_entry_form_role" class="text_pole" name="role">
<option value="system">System</option>
<option value="user">User</option>
<option value="assistant">AI Assistant</option>
<option data-i18n="System" value="system">System</option>
<option data-i18n="User" value="user">User</option>
<option data-i18n="AI Assistant" value="assistant">AI Assistant</option>
</select>
</div>
</div>
<div class="flex-container gap10px">
<div class="completion_prompt_manager_popup_entry_form_control flex1">
<label for="completion_prompt_manager_popup_entry_form_injection_position">
<span>Position</span>
<span data-i18n="prompt_manager_position">Position</span>
</label>
<div class="text_muted">Injection position. Next to other prompts (relative) or in-chat (absolute).</div>
<div class="text_muted" data-i18n="Injection position. Next to other prompts (relative) or in-chat (absolute).">Injection position. Next to other prompts (relative) or in-chat (absolute).</div>
<select id="completion_prompt_manager_popup_entry_form_injection_position" class="text_pole" name="injection_position">
<option value="0">Relative</option>
<option value="1">Absolute</option>
<option data-i18n="prompt_manager_relative" value="0">Relative</option>
<option data-i18n="prompt_manager_absolute" value="1">Absolute</option>
</select>
</div>
<div id="completion_prompt_manager_depth_block" class="completion_prompt_manager_popup_entry_form_control flex1">
<label for="completion_prompt_manager_popup_entry_form_injection_depth">
<span>Depth</span>
<span data-i18n="prompt_manager_depth">Depth</span>
</label>
<div class="text_muted">Injection depth. 0 = after the last message, 1 = before the last message, etc.</div>
<div class="text_muted" data-i18n="Injection depth. 0 = after the last message, 1 = before the last message, etc.">Injection depth. 0 = after the last message, 1 = before the last message, etc.</div>
<input id="completion_prompt_manager_popup_entry_form_injection_depth" class="text_pole" type="number" name="injection_depth" min="0" max="999" value="4" />
</div>
</div>
@ -5320,14 +5521,14 @@
<div class="flex-container alignItemsCenter">
<div class="flex1">
<label for="completion_prompt_manager_popup_entry_form_prompt">
<span>Prompt</span>
<span data-i18n="Prompt">Prompt</span>
</label>
<div class="text_muted">The prompt to be sent.</div>
<div class="text_muted" data-i18n="The prompt to be sent.">The prompt to be sent.</div>
</div>
<div id="completion_prompt_manager_forbid_overrides_block">
<label class="checkbox_label" for="completion_prompt_manager_popup_entry_form_forbid_overrides" title="This prompt cannot be overridden by character cards, even if overrides are preferred.">
<label class="checkbox_label" for="completion_prompt_manager_popup_entry_form_forbid_overrides" data-i18n="[title]This prompt cannot be overridden by character cards, even if overrides are preferred." title="This prompt cannot be overridden by character cards, even if overrides are preferred.">
<input type="checkbox" id="completion_prompt_manager_popup_entry_form_forbid_overrides" name="forbid_overrides" />
<span>Forbid Overrides</span>
<span data-i18n="prompt_manager_forbid_overrides">Forbid Overrides</span>
</label>
</div>
</div>
@ -5436,11 +5637,23 @@
<li><span data-i18n="welcome_message_part_4">Type</span> <code>/help</code> <span data-i18n="welcome_message_part_5">in chat for commands and macros.</span></li>
<li><span data-i18n="welcome_message_part_6">Join the</span> <a href="https://discord.gg/sillytavern" data-i18n="Discord server" target="_blank">Discord server</a> <span data-i18n="welcome_message_part_7">for info and announcements.</span></li>
</ul>
<div id="onboarding-UI-language-block" class="flex-container alignItemsBaseline">
<span data-i18n="UI Language">Language:</span>
<select id="onboarding_ui_language_select" class="flex1 margin0">
<option value="en">English</option>
</select>
</div>
<b data-i18n="SillyTavern is aimed at advanced users.">
SillyTavern is aimed at advanced users.
</b>
<div data-i18n="If you're new to this, enable the simplified UI mode below.">
If you're new to this, enable the simplified UI mode below.
<div>
<span data-i18n="If you're new to this, enable the simplified UI mode below.">
If you're new to this, enable the simplified UI mode below.
</span>
<br>
<span data-i18n="Change it later in the 'User Settings' panel.">
Change it later in the 'User Settings' panel.
</span>
</div>
<label class="checkbox_label">
<input type="checkbox" name="enable_simple_mode" />
@ -5448,6 +5661,24 @@
Enable simple UI mode
</span>
</label>
<div class="textAlignCenter">
<h3 data-i18n="Looking for AI characters?">
Looking for AI characters?
</h3>
<span>
<span class="menu_button menu_button_icon external_import_button">
<i class="fa-solid fa-cloud-arrow-down"></i>
<span data-i18n="onboarding_import">Import</span>
</span>
<span data-i18n="from supported sources or view">
from supported sources or view
</span>
<span class="open_characters_library menu_button menu_button_icon">
<i class="fa-solid fa-image-portrait"></i>
<span data-i18n="Sample characters">Sample characters</span>
</span>
</span>
</div>
<h3 data-i18n="Your Persona">
Your Persona
</h3>
@ -5455,6 +5686,7 @@
<span data-i18n="Before you get started, you must select a persona name.">
Before you get started, you must select a persona name.
</span>
<br>
<span data-i18n="welcome_message_part_8">This can be changed at any time via the</span> <code><i class="fa-solid fa-face-smile"></i></code> <span data-i18n="welcome_message_part_9">icon.</span>
</div>
<h4 data-i18n="Persona Name:">Persona Name:</h4>
@ -5972,6 +6204,15 @@
</div>
<textarea id="send_textarea" name="text" data-i18n="[no_connection_text]Not connected to API!;[connected_text]Type a message, or /? for help" placeholder="Not connected to API!" no_connection_text="Not connected to API!" connected_text="Type a message, or /? for help"></textarea>
<div id="rightSendForm" class="alignContentCenter">
<div id="stscript_continue" title="Continue script execution" class="stscript_btn stscript_continue" data-i18n="[title]Continue script execution">
<i class="fa-solid fa-play"></i>
</div>
<div id="stscript_pause" title="Pause script execution" class="stscript_btn stscript_pause" data-i18n="[title]Pause script execution">
<i class="fa-solid fa-pause"></i>
</div>
<div id="stscript_stop" title="Abort script execution" class="stscript_btn stscript_stop" data-i18n="[title]Abort script execution">
<i class="fa-solid fa-stop"></i>
</div>
<div id="mes_stop" title="Abort request" class="mes_stop" data-i18n="[title]Abort request">
<i class="fa-solid fa-circle-stop"></i>
</div>
@ -6055,11 +6296,11 @@
</div>
<div id="zoomed_avatar_template" class="template_element">
<div class="zoomed_avatar">
<div class="panelControlBar flex-container">
<div class="fa-fw fa-solid fa-grip drag-grabber"></div>
<div class="fa-fw fa-solid fa-circle-xmark dragClose" id="closeZoom"></div>
</div>
<div class="zoomed_avatar_container">
<div class="panelControlBar flex-container">
<div class="fa-fw fa-solid fa-grip drag-grabber"></div>
<div class="fa-fw fa-solid fa-circle-xmark dragClose" id="closeZoom"></div>
</div>
<img class="zoomed_avatar_img" src="" data-izoomify-url="" data-izoomify-magnify="1.8" data-izoomify-duration="300" alt="">
</div>
</div>
@ -6135,31 +6376,11 @@
<script type="module" src="lib/structured-clone/monkey-patch.js"></script>
<script type="module" src="lib/swiped-events.js"></script>
<script type="module" src="lib/eventemitter.js"></script>
<script type="module" src="scripts/power-user.js"></script>
<script type="module" src="scripts/i18n.js"></script>
<script type="module" src="script.js"></script>
<script type="module" src="scripts/world-info.js"></script>
<script type="module" src="scripts/group-chats.js"></script>
<script type="module" src="scripts/kai-settings.js"></script>
<script type="module" src="scripts/textgen-settings.js"></script>
<script type="module" src="scripts/textgen-models.js"></script>
<script type="module" src="scripts/bookmarks.js"></script>
<script type="module" src="scripts/horde.js"></script>
<script type="module" src="scripts/RossAscends-mods.js"></script>
<script type="module" src="scripts/slash-commands.js"></script>
<script type="module" src="scripts/tags.js"></script>
<script type="module" src="scripts/secrets.js"></script>
<script type="module" src="scripts/extensions.js"></script>
<script type="module" src="scripts/authors-note.js"></script>
<script type="module" src="scripts/preset-manager.js"></script>
<script type="module" src="scripts/filters.js"></script>
<script type="module" src="scripts/personas.js"></script>
<script type="module" src="scripts/server-history.js"></script>
<script type="module" src="scripts/setting-search.js"></script>
<script type="module" src="scripts/bulk-edit.js"></script>
<script type="module" src="scripts/cfg-scale.js"></script>
<script type="module" src="scripts/chats.js"></script>
<script type="module" src="scripts/user.js"></script>
<script type="module" src="scripts/setting-search.js"></script>
<script type="module" src="scripts/server-history.js"></script>
<script type="module" src="script.js"></script>
<script>
// Configure toast library:
toastr.options.escapeHtml = true; // Prevent raw HTML inserts

View File

@ -42,6 +42,46 @@ EventEmitter.prototype.on = function (event, listener) {
this.events[event].push(listener);
};
/**
* Makes the listener the last to be called when the event is emitted
* @param {string} event Event name
* @param {function} listener Event listener
*/
EventEmitter.prototype.makeLast = function (event, listener) {
if (typeof this.events[event] !== 'object') {
this.events[event] = [];
}
const events = this.events[event];
const idx = events.indexOf(listener);
if (idx > -1) {
events.splice(idx, 1);
}
events.push(listener);
}
/**
* Makes the listener the first to be called when the event is emitted
* @param {string} event Event name
* @param {function} listener Event listener
*/
EventEmitter.prototype.makeFirst = function (event, listener) {
if (typeof this.events[event] !== 'object') {
this.events[event] = [];
}
const events = this.events[event];
const idx = events.indexOf(listener);
if (idx > -1) {
events.splice(idx, 1);
}
events.unshift(listener);
}
EventEmitter.prototype.removeListener = function (event, listener) {
var idx;

View File

@ -3,7 +3,6 @@
"kobldpresets": "الإعدادات المسبقة لـ Kobold",
"guikoboldaisettings": "إعدادات واجهة KoboldAI",
"novelaipreserts": "الإعدادات المسبقة لـ NovelAI",
"default": "افتراضي",
"openaipresets": "الإعدادات المسبقة لـ OpenAI",
"text gen webio(ooba) presets": "الإعدادات المسبقة لـ WebUI(ooba)",
"response legth(tokens)": "طول الاستجابة (بعدد الاحرف او الرموز)",
@ -62,7 +61,7 @@
"Temperature": "درجة الحرارة",
"Frequency Penalty": "عقوبة التكرار",
"Presence Penalty": "عقوبة الوجود",
"Top-p": "أعلى p",
"Top-p": "أعلى p",
"Display bot response text chunks as they are generated": "عرض النصوص لجظة بلحظة",
"Top A": "أعلى A",
"Typical Sampling": "عينة نموذجية",
@ -101,7 +100,7 @@
"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",
"Tell the AI that NSFW is allowed.": "قل للذكاء الاصطناعي أنه يُسمح بـ NSFW",
"NSFW Prioritized": "الأولوية للمحتوى غير مناسب للعمل",
"NSFW prompt text goes first in the prompt to emphasize its effect.": "النص الغير مناسب للعمل يأتي أولاً في التعليمات لتأكيد تأثيره.",
"Streaming": "البث المباشر ل",
@ -141,7 +140,7 @@
"Influences bot behavior in its responses": "يؤثر على سلوك الروبوت في ردوده.",
"Connect": "الاتصال",
"Test Message": "رسالة اختبار",
"API": "واجهة برمجة التطبيقات (API)",
"API": "واجهة برمجة التطبيقات (API)",
"KoboldAI": "KoboldAI",
"Use Horde": "استخدام Horde",
"API url": "رابط API",
@ -206,7 +205,7 @@
"Scale API Key": "مفتاح API لـ Scale",
"Alt Method": "طريقة بديلة",
"AI21 API Key": "مفتاح API لـ AI21",
"AI21 Model": "نموذج AI21",
"AI21 Model": "نموذج AI21",
"View API Usage Metrics": "عرض مقاييس استخدام واجهة برمجة التطبيقات",
"Show External models (provided by API)": "عرض النماذج الخارجية (المقدمة من قبل واجهة برمجة التطبيقات)",
"Bot": "روبوت:",
@ -495,7 +494,6 @@
"Global Lore First": "سرد العالم أولاً",
"Recursive Scan": "فحص متكرر",
"Case Sensitive": "حساس لحالة الأحرف",
"Match whole words": "تطابق الكلمات الكاملة",
"Alert On Overflow": "تنبيه عند التجاوز",
"World/Lore Editor": "محرر العالم/السرد",
"--- None ---": "--- لا شيء ---",
@ -915,7 +913,7 @@
"Use the appropriate tokenizer for Google models via their API. Slower prompt processing, but offers much more accurate token counting.": "استخدم المحلل النحوي المناسب لنماذج Google عبر واجهة برمجة التطبيقات الخاصة بهم. معالجة الإشارات الأولية بطيئة، ولكنها تقدم عداد رمز دقيق جدًا.",
"Load koboldcpp order": "تحميل أمر koboldcpp",
"Use Google Tokenizer": "استخدم محلل النحوي من Google"
}
}

View File

@ -3,7 +3,6 @@
"kobldpresets": "Kobold-Einstellungen von vorher",
"guikoboldaisettings": "KoboldAI-Einstellungen für das Menü",
"novelaipreserts": "NovelAI-Einstellungen von früher",
"default": "Normal",
"openaipresets": "OpenAI-Einstellungen von vorher",
"text gen webio(ooba) presets": "WebUI(ooba)-Einstellungen für Texterstellung",
"response legth(tokens)": "Länge der Antwort (Tokens)",
@ -494,7 +493,6 @@
"Global Lore First": "Globale Lore zuerst",
"Recursive Scan": "Rekursive Suche",
"Case Sensitive": "Groß-/Kleinschreibung beachten",
"Match whole words": "Ganze Wörter abgleichen",
"Alert On Overflow": "Warnung bei Überlauf",
"World/Lore Editor": "Welt-/Lore-Editor",
"--- None ---": "--- Keine ---",
@ -917,5 +915,5 @@
}
}

View File

@ -3,7 +3,6 @@
"kobldpresets": "Preajustes de Kobold",
"guikoboldaisettings": "Ajustes de interfaz de KoboldAI",
"novelaipreserts": "Preajustes de NovelAI",
"default": "Predeterminado",
"openaipresets": "Preajustes de OpenAI",
"text gen webio(ooba) presets": "Preajustes de Text Gen WebUI(ooba)",
"response legth(tokens)": "Longitud de respuesta (tokens)",
@ -494,7 +493,6 @@
"Global Lore First": "Historia Global Primero",
"Recursive Scan": "Escaneo Recursiva",
"Case Sensitive": "Sensible a mayúsculas y minúsculas",
"Match whole words": "Coincidir palabras completas",
"Alert On Overflow": "Alerta en Desbordamiento",
"World/Lore Editor": "Editor de Mundo/Historia",
"--- None ---": "--- Ninguno ---",
@ -891,6 +889,7 @@
"Chat API": " API de chat",
"and pick a character": "y elige un personaje",
"in the chat bar": "en la barra de chat",
"You can browse a list of bundled characters in the Download Extensions & Assets menu within": "Puedes explorar una lista de personajes incluidos en el menú de Download Extensions & Assets dentro de ",
"Confused or lost?": "¿Confundido o perdido?",
"click these icons!": "¡Haz clic en estos iconos!",
"SillyTavern Documentation Site": "Sitio de documentación de SillyTavern",
@ -914,7 +913,4 @@
"Use the appropriate tokenizer for Google models via their API. Slower prompt processing, but offers much more accurate token counting.": "Usa el tokenizador apropiado para los modelos de Google a través de su API. Procesamiento de indicaciones más lento, pero ofrece un recuento de tokens mucho más preciso.",
"Load koboldcpp order": "Cargar orden de koboldcpp",
"Use Google Tokenizer": "Usar Tokenizador de Google"
}

View File

@ -3,7 +3,6 @@
"kobldpresets": "Préréglages de Kobold",
"guikoboldaisettings": "Paramètres de l'interface utilisateur de KoboldAI",
"novelaipreserts": "Préréglages de NovelAI",
"default": "Par défaut",
"openaipresets": "Préréglages d'OpenAI",
"text gen webio(ooba) presets": "Préréglages de WebUI(ooba)",
"response legth(tokens)": "Longueur de la réponse (en tokens)",
@ -205,7 +204,7 @@
"Scale API Key": "Clé API Scale",
"Alt Method": "Méthode alternative",
"AI21 API Key": "Clé API AI21",
"AI21 Model": "Modèle AI21",
"AI21 Model": "Modèle AI21",
"View API Usage Metrics": "Afficher les mesures d'utilisation de l'API",
"Show External models (provided by API)": "Afficher les modèles externes (fournis par l'API)",
"Bot": "Bot",
@ -494,7 +493,6 @@
"Global Lore First": "Lore global d'abord",
"Recursive Scan": "Analyse récursive",
"Case Sensitive": "Sensible à la casse",
"Match whole words": "Correspondre aux mots entiers",
"Alert On Overflow": "Alerte en cas de dépassement",
"World/Lore Editor": "Éditeur de monde/lore",
"--- None ---": "--- Aucun ---",
@ -914,5 +912,5 @@
"Use the appropriate tokenizer for Google models via their API. Slower prompt processing, but offers much more accurate token counting.": "Utilisez le tokenizer approprié pour les modèles Google via leur API. Traitement des invitations plus lent, mais offre un décompte de jetons beaucoup plus précis.",
"Load koboldcpp order": "Charger l'ordre koboldcpp",
"Use Google Tokenizer": "Utiliser le tokenizer Google"
}
}

View File

@ -3,7 +3,6 @@
"kobldpresets": "Fyrir stillingar Kobold",
"guikoboldaisettings": "Stillingar fyrir KoboldAI viðmót",
"novelaipreserts": "Fyrir stillingar NovelAI",
"default": "Sjálfgefið",
"openaipresets": "Fyrir stillingar OpenAI",
"text gen webio(ooba) presets": "Fyrir stillingar WebUI(ooba) textagerðar",
"response legth(tokens)": "Lengd svars (í táknum eða stöfum)",
@ -62,7 +61,7 @@
"Temperature": "Hitastig",
"Frequency Penalty": "Tíðnarefning",
"Presence Penalty": "Tilkoma refning",
"Top-p": "Topp-p",
"Top-p": "Topp-p",
"Display bot response text chunks as they are generated": "Birta bætir svarborðstextabrot þegar þau eru búnar til",
"Top A": "Topp A",
"Typical Sampling": "Venjuleg úrtaka",
@ -495,7 +494,6 @@
"Global Lore First": "Fyrst heimsfræði",
"Recursive Scan": "Endurkvæm skoðun",
"Case Sensitive": "Skilgreiningarfræðilegt",
"Match whole words": "Nákvæm samræmi",
"Alert On Overflow": "Viðvörun um flæði",
"World/Lore Editor": "Heims-/fræðiritari",
"--- None ---": "--- Engin ---",
@ -915,5 +913,5 @@
"Use the appropriate tokenizer for Google models via their API. Slower prompt processing, but offers much more accurate token counting.": "Notaðu rétta tokenizer fyrir Google módel með þeirra API. Hægri umhvörf fyrir hvöttavinnslu, en býður upp á miklu nákvæmari talningu á táknunum.",
"Load koboldcpp order": "Hlaðið inn færslu af koboldcpp",
"Use Google Tokenizer": "Notaðu Google Tokenizer"
}
}

View File

@ -3,7 +3,6 @@
"kobldpresets": "Preimpostazioni Kobold",
"guikoboldaisettings": "Impostazioni dell'interfaccia KoboldAI",
"novelaipreserts": "Preimpostazioni NovelAI",
"default": "Predefinito",
"openaipresets": "Preimpostazioni OpenAI",
"text gen webio(ooba) presets": "Preimpostazioni WebUI(ooba) per la generazione di testo",
"response legth(tokens)": "Lunghezza della risposta (token)",
@ -495,7 +494,6 @@
"Global Lore First": "Lore Globale Prima",
"Recursive Scan": "Scansione Ricorsiva",
"Case Sensitive": "Sensibile alle Maiuscole",
"Match whole words": "Corrispondi a parole intere",
"Alert On Overflow": "Avviso Su Overflow",
"World/Lore Editor": "Editor di Mondo/Lore",
"--- None ---": "--- Nessuno ---",
@ -917,5 +915,5 @@
"Use Google Tokenizer": "Usa il Tokenizer di Google"
}
}

View File

@ -3,7 +3,6 @@
"kobldpresets": "Koboldのプリセット",
"guikoboldaisettings": "KoboldAIのGUI設定",
"novelaipreserts": "NovelAIのプリセット",
"default": "デフォルト",
"openaipresets": "OpenAIのプリセット",
"text gen webio(ooba) presets": "WebUI(ooba)のプリセット",
"response legth(tokens)": "応答の長さ(トークン数)",
@ -140,7 +139,7 @@
"Influences bot behavior in its responses": "返信でボットの動作に影響を与えます",
"Connect": "接続",
"Test Message": "テストメッセージ",
"API": "API",
"API": "API",
"KoboldAI": "KoboldAI",
"Use Horde": "ホードを使用",
"API url": "API URL",
@ -494,7 +493,6 @@
"Global Lore First": "グローバルロアを最初に表示",
"Recursive Scan": "再帰的スキャン",
"Case Sensitive": "大文字と小文字を区別する",
"Match whole words": "完全な単語の一致",
"Alert On Overflow": "オーバーフロー時に警告",
"World/Lore Editor": "ワールド/ロアの編集",
"--- None ---": "--- なし ---",
@ -914,5 +912,5 @@
"Use the appropriate tokenizer for Google models via their API. Slower prompt processing, but offers much more accurate token counting.": "Googleモデル用の適切なトークナイザーを使用します。 API経由で。 処理が遅くなりますが、トークンの数え上げがはるかに正確になります。",
"Load koboldcpp order": "koboldcppオーダーを読み込む",
"Use Google Tokenizer": "Googleトークナイザーを使用"
}
}

View File

@ -3,7 +3,6 @@
"kobldpresets": "코볼드 사전 설정",
"guikoboldaisettings": "KoboldAI 인터페이스 설정",
"novelaipreserts": "NovelAI 사전 설정",
"default": "기본값",
"openaipresets": "OpenAI 사전 설정",
"text gen webio(ooba) presets": "텍스트 생성 WebUI(ooba) 사전 설정",
"response legth(tokens)": "응답 길이 (토큰)",
@ -425,7 +424,7 @@
"Start new chat": "새로운 채팅 시작",
"View past chats": "과거 채팅 보기",
"Delete messages": "메시지 삭제",
"Impersonate": "사칭",
"Impersonate": "대신 말하기",
"Regenerate": "재생성",
"PNG": "PNG",
"JSON": "JSON",
@ -495,7 +494,6 @@
"Global Lore First": "글로벌 로어 우선",
"Recursive Scan": "재귀 스캔",
"Case Sensitive": "대소문자 구분",
"Match whole words": "전체 단어 일치",
"Alert On Overflow": "오버플로우 알림",
"World/Lore Editor": "월드/로어 편집기",
"--- None ---": "--- 없음 ---",
@ -914,7 +912,30 @@
"Learn how to contribute your idle GPU cycles to the Horde": "여유로운 GPU 주기를 호드에 기여하는 방법 배우기",
"Use the appropriate tokenizer for Google models via their API. Slower prompt processing, but offers much more accurate token counting.": "Google 모델용 적절한 토크나이저를 사용하여 API를 통해 제공됩니다. 더 느린 프롬프트 처리지만 훨씬 정확한 토큰 계산을 제공합니다.",
"Load koboldcpp order": "코볼드 CPP 순서로 로드",
"Use Google Tokenizer": "Google 토크나이저 사용"
"Use Google Tokenizer": "구글 토크나이저 사용",
"Hide Chat Avatars": "채팅 아바타 숨기기",
"Hide avatars in chat messages.": "채팅 메시지에서 아바타 숨김.",
"Avatar Hover Magnification": "아바타 마우스오버 시 확대",
"Enable magnification for zoomed avatar display.": "마우스 오버 시 아바타가 커지도록 설정하세요.",
"AutoComplete Settings": "자동 완성 설정",
"Autocomplete Matching": "자동 완성 매칭",
"Starts with": "시작하는 단어로",
"Autocomplete Style": "자동 완성 스타일",
"Includes": "포함하는",
"Fuzzy": "퍼지 매칭",
"Follow Theme": "테마 적용",
"Dark": "다크 모드",
"Sets the font size of the autocomplete.": "자동 완성 글꼴 크기 설정",
"Autocomplete Width": "자동 완성 너비 조절",
"Parser Flags": "파서 플래그 설정",
"Sets default flags for the STscript parser.": "STscript 파서 기본 플래그 설정",
"Switch to stricter escaping, allowing all delimiting characters to be escaped with a backslash, and backslashes to be escaped as well.": "모든 구분자를 백슬래시로 이스케이핑하고, 백슬래시 자체도 이스케이프할 수 있도록 엄격한 방식으로 전환합니다.",
"STscript Settings": "STscript 설정",
"Smooth Streaming": "부드러운 스트리밍",
"Experimental feature. May not work for all backends.": "실험적인 기능으로, 모든 백엔드에서 작동이 보장되지는 않을 수 있습니다.",
"Char List Subheader": "문자 목록 하위 제목",
"Account": "계정",
"Theme Colors": "테마 색상",
"# Messages to Load": "로딩할 메시지 수"
}
}

View File

@ -1,6 +1,7 @@
[
{ "lang": "ar-sa", "display": "عربي (Arabic)" },
{ "lang": "zh-cn", "display": "简体中文 (Chinese) (Simplified)" },
{ "lang": "zh-tw", "display": "繁體中文 (Chinese) (Taiwan)" },
{ "lang": "nl-nl", "display": "Nederlands (Dutch)" },
{ "lang": "de-de", "display": "Deutsch (German)" },
{ "lang": "fr-fr", "display": "Français (French)" },

View File

@ -3,7 +3,6 @@
"kobldpresets": "Kobold voorinstellingen",
"guikoboldaisettings": "KoboldAI-interface-instellingen",
"novelaipreserts": "NovelAI-voorinstellingen",
"default": "Standaard",
"openaipresets": "OpenAI-voorinstellingen",
"text gen webio(ooba) presets": "WebUI(ooba)-voorinstellingen voor tekstgeneratie",
"response legth(tokens)": "Reactielengte (tokens)",
@ -495,7 +494,6 @@
"Global Lore First": "Globale Lore Eerst",
"Recursive Scan": "Recursieve Scan",
"Case Sensitive": "Hoofdlettergevoelig",
"Match whole words": "Hele woorden matchen",
"Alert On Overflow": "Waarschuwing bij overloop",
"World/Lore Editor": "Wereld/Lore Editor",
"--- None ---": "--- Geen ---",
@ -917,5 +915,5 @@
"Use Google Tokenizer": "Google Tokenizer gebruiken"
}
}

View File

@ -3,7 +3,6 @@
"kobldpresets": "Configurações predefinidas do Kobold",
"guikoboldaisettings": "Configurações da interface do KoboldAI",
"novelaipreserts": "Configurações predefinidas do NovelAI",
"default": "Padrão",
"openaipresets": "Configurações predefinidas do OpenAI",
"text gen webio(ooba) presets": "Configurações predefinidas do WebUI(ooba) para geração de texto",
"response legth(tokens)": "Comprimento da resposta (tokens)",
@ -493,7 +492,6 @@
"Global Lore First": "Lore Global Primeiro",
"Recursive Scan": "Verificação Recursiva",
"Case Sensitive": "Sensível a Maiúsculas",
"Match whole words": "Corresponder palavras inteiras",
"Alert On Overflow": "Alerta em Overflow",
"World/Lore Editor": "Editor de Mundo/Lore",
"--- None ---": "--- Nenhum ---",
@ -915,5 +913,5 @@
"Use Google Tokenizer": "Usar Tokenizer do Google"
}
}

View File

@ -3,7 +3,6 @@
"kobldpresets": "Пресеты для Kobold",
"guikoboldaisettings": "Настройки из интерфейса KoboldAI",
"novelaipreserts": "Пресеты для NovelAI",
"default": "По умолчанию",
"openaipresets": "Пресеты для OpenAI",
"text gen webio(ooba) presets": "Пресеты для WebUI(ooba)",
"response legth(tokens)": "Ответ (в токенах)",
@ -33,7 +32,7 @@
"Variability parameter for Mirostat outputs": "Параметр изменчивости для выходных данных Mirostat.",
"Learning rate of Mirostat": "Скорость обучения Mirostat.",
"Strength of the Contrastive Search regularization term. Set to 0 to disable CS": "Сила условия регуляризации контрастивного поиска. Установите значение 0, чтобы отключить CS.",
"Temperature Last": "Temperature Last",
"Temperature Last": "Температура последней",
"Use the temperature sampler last": "Использовать Temperature сэмплер в последнюю очередь. Это почти всегда разумно.\nПри включении: сначала выборка набора правдоподобных токенов, затем применение Temperature для корректировки их относительных вероятностей (технически, логитов).\nПри отключении: сначала применение Temperature для корректировки относительных вероятностей ВСЕХ токенов, затем выборка правдоподобных токенов из этого.\nОтключение Temperature Last увеличивает вероятности в хвосте распределения, что увеличивает шансы получить несогласованный ответ.",
"LLaMA / Mistral / Yi models only": "Только для моделей LLaMA / Mistral / Yi. Перед этим обязательно выберите подходящий токенизатор.\nПоследовательности, которых не должно быть на выходе.\nОдна на строку. Текст или [идентификаторы токенов].\nМногие токены имеют пробел впереди. Используйте счетчик токенов, если не уверены.",
"Example: some text [42, 69, 1337]": "Пример:\nкакой-то текст\n[42, 69, 1337]",
@ -128,7 +127,7 @@
"Jailbreak prompt": "Джейлбрейк-промпт",
"Prompt that is used when the Jailbreak toggle is on": "Промпт, применяемый при включенном джейлбрейке.",
"Impersonation prompt": "Промпт для перевоплощения",
"Prompt that is used for Impersonation function": "Промпт, применяемый при генерации действий за пользователя",
"Prompt that is used for Impersonation function": "Промпт, применяемый при генерации действий от лица пользователя",
"Logit Bias": "Смещение логитов",
"Helps to ban or reenforce the usage of certain words": "Запрещает или поощряет использование определенных слов",
"View / Edit bias preset": "Просмотр / Редактирование пресета смещения",
@ -179,8 +178,8 @@
"Novel AI Model": "Модель NovelAI",
"If you are using:": "Если вы используете:",
"oobabooga/text-generation-webui": "",
"Make sure you run it with": "Убедитесь, что вы запустили его с",
"flag": "флажком",
"Make sure you run it with": "Обязательно запускайте его с флагом",
"flag": "",
"API key (optional)": "Ключ API (опционально)",
"Server url": "URL-адрес сервера",
"Custom model (optional)": "Пользовательская модель (опционально)",
@ -195,13 +194,13 @@
"Example: ws://127.0.0.1:5005/api/v1/stream": "Пример: ws://127.0.0.1:5005/api/v1/stream",
"Mancer API key": "Ключ от Mancer API",
"Example: https://neuro.mancer.tech/webui/MODEL/api": "Пример: https://neuro.mancer.tech/webui/MODEL/api",
"to get your OpenAI API key.": "для получения ключа от OpenAI API",
"to get your OpenAI API key.": "для получения ключа от API OpenAI",
"Window AI Model": "Модель Window AI",
"OpenAI Model": "Модель OpenAI",
"Claude API Key": "Ключ от Claude API",
"Get your key from": "Получите ключ в",
"Anthropic's developer console": "консоли разработчика Anthropic",
"Slack and Poe cookies will not work here, do not bother trying.": "Файлы cookie Slack и Poe здесь не подойдут, можете их не пробовать.",
"Slack and Poe cookies will not work here, do not bother trying.": "Cookie от Slack и Poe здесь не подойдут, можете их не пробовать.",
"Claude Model": "Модель Claude",
"Scale API Key": "Ключ от Scale API",
"Alt Method": "Альтернативный метод",
@ -228,7 +227,7 @@
"Disable example chats formatting": "Отключить форматирование примеров чата",
"Disable chat start formatting": "Отключить форматирование начала чата",
"Custom Chat Separator": "Кастомный разделитель для чата",
"Replace Macro in Custom Stopping Strings": "Заменить макрос в пользовательских стоп-строках",
"Replace Macro in Custom Stopping Strings": "Заменить макросы в пользовательских стоп-строках",
"Strip Example Messages from Prompt": "Удалить примеры сообщений из подсказки",
"Story String": "Строка истории",
"Example Separator": "Пример разделителя",
@ -276,7 +275,7 @@
"World Info": "Информация о мире",
"Scan Depth": "Глубина сканирования",
"Case-Sensitive": "С учетом регистра",
"Match Whole Words": "Только целые слова",
"Match Whole Words": "Только полное совпадение",
"Use global setting": "Использовать глобальную настройку",
"Yes": "Да",
"No": "Нет",
@ -495,7 +494,6 @@
"Global Lore First": "Сначала глобальный лор",
"Recursive Scan": "Рекурсивное сканирование",
"Case Sensitive": "Учитывать регистр",
"Match whole words": "Только полное совпадение",
"Alert On Overflow": "Оповещение о переполнении",
"World/Lore Editor": "Редактировать мир или лор",
"--- None ---": "--- Отсутствует ---",
@ -535,13 +533,13 @@
"Streaming FPS": "FPS для стриминга",
"Gestures": "Жесты",
"Message IDs": "ID сообщений",
"Prefer Character Card Prompt": "Предпочитать промпт из карточки персонажа",
"Prefer Character Card Jailbreak": "Предпочитать джейлбрейк из карточки керсонажа",
"Prefer Character Card Prompt": "Приоритет промпту из карточки персонажа",
"Prefer Character Card Jailbreak": "Приоритет джейлбрейку из карточки персонажа",
"Press Send to continue": "Кнопка отправки продолжает сообщение",
"Quick 'Continue' button": "Кнопка быстрого продолжения",
"Log prompts to console": "Выводить промпты в консоль",
"Never resize avatars": "Никогда не менять размер аватаров",
"Show avatar filenames": "Показывать названия файлов аватаров",
"Never resize avatars": "Не менять размер аватарок",
"Show avatar filenames": "Показывать названия файлов аватарок",
"Import Card Tags": "Импорт тегов карточки",
"Confirm message deletion": "Подтверждение удаления сообщений",
"Spoiler Free Mode": "Режим без спойлеров",
@ -562,7 +560,7 @@
"Reduce chat height, and put a static sprite behind the chat window": "Уменьшить высоту чата и поместить статичный спрайт за окном чата.",
"Always show the full list of the Message Actions context items for chat messages, instead of hiding them behind '...'": "Всегда показывать полный список действий с сообщением, а не прятать их за '...'.",
"Alternative UI for numeric sampling parameters with fewer steps": "Альтернативный пользовательский интерфейс для числовых параметров выборки с меньшим количеством шагов.",
"Entirely unrestrict all numeric sampling parameters": "Полностью разграничить все числовые параметры выборки.",
"Entirely unrestrict all numeric sampling parameters": "Снять ограничения со всех числовых сэмплеров.",
"Time the AI's message generation, and show the duration in the chat log": "Время генерации сообщений ИИ и его показ в журнале чата.",
"Show a timestamp for each message in the chat log": "Показывать временную метку для каждого сообщения в журнале чата.",
"Show an icon for the API that generated the message": "Показать значок API, сгенерировавшего сообщение.",
@ -834,7 +832,7 @@
"Sampler Priority": "Приоритет сэмплеров",
"Ooba only. Determines the order of samplers.": "Только Ooba. Определяет порядок сэмплеров.",
"Load default order": "Загрузить стандартный порядок",
"Max Tokens Second": "Максимальное количество токенов в секунду",
"Max Tokens Second": "Макс. кол-во токенов в секунду",
"CFG": "CFG",
"No items": "Нет элементов",
"Extras API key (optional)": "Ключ от Extras API (необязательно)",
@ -868,10 +866,10 @@
"Send names in the message objects. Helps the model to associate messages with characters.": "Отправить имена в объектах сообщений. Помогает модели ассоциировать сообщения с персонажами.",
"Continue prefill": "Префилл для продолжения",
"Continue sends the last message as assistant role instead of system message with instruction.": "Продолжение отправляет последнее сообщение в роли ассистента, вместо системного сообщения с инструкцией.",
"Squash system messages": "Склеивать сообщения системыы",
"Combines consecutive system messages into one (excluding example dialogues). May improve coherence for some models.": "Объединяет последовательные системные сообщения в одно (за исключением примеров диалогов). Может улучшить согласованность для некоторых моделей.",
"Send inline images": "Отправлять встроенные изображения",
"Assistant Prefill": "Префилл от ассистента",
"Squash system messages": "Склеивать сообщения системы",
"Combines consecutive system messages into one (excluding example dialogues). May improve coherence for some models.": "Объединяет последовательные системные сообщения в одно (за исключением примеров диалогов). У некоторых моделей может улучшить логичность ответов.",
"Send inline images": "Отправлять inline-картинки",
"Assistant Prefill": "Префилл для ассистента",
"Start Claude's answer with...": "Начать ответ Клода с...",
"Use system prompt (Claude 2.1+ only)": "Использовать системный промпт (только Claude 2.1+)",
"Send the system prompt for supported models. If disabled, the user message is added to the beginning of the prompt.": "Отправлять системный промпт для поддерживаемых моделей. Если отключено, сообщение пользователя добавляется в начало промпта.",
@ -880,18 +878,18 @@
"Insert prompt": "Вставить промпт",
"Delete prompt": "Удалить промпт",
"Import a prompt list": "Импортировать список промптов",
"Export this prompt list": "Экспортировать этот список промпт",
"Export this prompt list": "Экспортировать этот список промптов",
"Reset current character": "Сбросить текущего персонажа",
"New prompt": "Новый промпт",
"Tokens": "токенов",
"Want to update?": "Хотите обновиться?",
"How to start chatting?": "Как начать общение?",
"Click": "Нажмите",
"and select a": " и выберите",
"and select a": " и выберите ",
"Chat API": "API чата",
"and pick a character": "и выберите персонажа",
"in the chat bar": " в поле чата",
"Confused or lost?": "Запутались или потерялись?",
"Confused or lost?": "Не можете в чём-то разобраться?",
"click these icons!": "нажмите на эти значки!",
"SillyTavern Documentation Site": "Сайт документации SillyTavern",
"Extras Installation Guide": "Руководство по установке Extras",
@ -1095,5 +1093,185 @@
"Space": "Пробел",
"Newline": "Новая строка",
"Double Newline": "Две новые строки",
"The next chunk of the continued message will be appended using this as a separator.": "Используется в качестве разделителя между уже имеющимся сообщением и его новым отрывком, при генерации продолжения"
"The next chunk of the continued message will be appended using this as a separator.": "Используется в качестве разделителя между уже имеющимся сообщением и его новым отрывком, при генерации продолжения",
"Regex Editor": "Редактор рег. выражений",
"ext_regex_open_editor": "Открыть редактор",
"ext_regex_import_script": "Импорт скрипта",
"ext_regex_saved_scripts": "Сохранённые скрипты",
"ext_regex_desc": "Regex - это инструмент, позволяющий находить и изменять строки, используя регулярные выражения. Для более подробной информации нажмите ? рядом с заголовком.",
"Input": "Поле ввода",
"ext_regex_test_input_placeholder": "Введите текст...",
"Output": "Результат",
"ext_regex_output_placeholder": "Пусто",
"Script Name": "Название скрипта",
"Find Regex": "Рег. выражение для поиска",
"Replace With": "Замена",
"ext_regex_replace_string_placeholder": "Чтобы вставить всё вхождение рег. выражения, используйте {{match}}. Чтобы вставить группу символов, используйте $1, $2 и т.д.",
"Trim Out": "Усечение",
"ext_regex_trim_placeholder": "Удалить перед обработкой ненужные части текста. Каждый элемент с новой строки.",
"Slash Commands": "Слэш-команды",
"Min Depth": "Мин. глубина",
"ext_regex_min_depth_desc": "При форматировании затрагивать только те сообщения, которые находятся как минимум на глубине N. 0 = последнее сообщение, 1 = предпоследнее и т.д. Учитываются только видимые сообщения, т.е. не скрытые и не системные.",
"ext_regex_max_depth_desc": "При форматировании затрагивать только те сообщения, которые находятся на глубине не более N. 0 = последнее сообщение, 1 = предпоследнее и т.д. Учитываются только видимые сообщения, т.е. не скрытые и не системные.",
"ext_regex_min_depth_placeholder": "Неогранич.",
"ext_regex_other_options": "Другие опции",
"Only Format Display": "Только визуально",
"ext_regex_only_format_prompt_desc": "История чата не изменится, замена будет осуществляться только в промпте (при генерации)",
"Only Format Prompt (?)": "Только промпт",
"Run On Edit": "Выполнять при редактировании",
"Substitute Regex": "Заменить в рег. выражении",
"ext_regex_substitute_regex_desc": "Перед выполнением заменять {{макросы}} в рег. выражении",
"Test Mode": "Протестировать",
"ext_regex_affects": "Затрагивает",
"ext_regex_user_input": "Ваши сообщения",
"ext_regex_ai_output": "Ответы ИИ",
"ext_regex_disable_script": "Отключить скрипт",
"ext_regex_enable_script": "Включить скрипт",
"ext_regex_edit_script": "Редактировать",
"ext_regex_export_script": "Экспортировать",
"ext_regex_delete_script": "Удалить",
"ext_sum_with": "Для пересказа использовать:",
"ext_sum_main_api": "Основное API",
"ext_sum_current_summary": "Текущий пересказ:",
"ext_sum_restore_previous": "Восстановить предыдущий",
"ext_sum_memory_placeholder": "Сгенерированный пересказ будет здесь...",
"ext_sum_force_text": "Пересказать сейчас",
"ext_sum_force_tip": "Сгенерировать пересказ прямо сейчас.",
"Disable automatic summary updates. While paused, the summary remains as-is. You can still force an update by pressing the Summarize now button (which is only available with the Main API).": "Отключить авто-обновление пересказа. Пересказ всё время будет фиксированным. Однако останется возможность принудительно обновить пересказ через кнопку \"Пересказать сейчас\" (доступно только через Основное API)",
"ext_sum_pause": "Приостановить",
"Omit World Info and Author's Note from text to be summarized. Only has an effect when using the Main API. The Extras API always omits WI/AN.": "Исключать из пересказа Информацию о мире и Заметки автора. Работает только для Основного API. Extras API всегда их исключает.",
"ext_sum_no_wi_an": "Без мира и заметок",
"ext_sum_settings_tip": "Изменить промпт пересказа, место для инжекта и т.д.",
"ext_sum_settings": "Настройки пересказа",
"ext_sum_prompt_builder": "Алгоритм формирования промпта",
"ext_sum_prompt_builder_1_desc": "Расширение само составит промпт с учётом непересказанных сообщений. Во время генерации чат недоступен.",
"ext_sum_prompt_builder_1": "Прямой, блокирующий",
"ext_sum_prompt_builder_2_desc": "Расширение само составит промпт с учётом непересказанных сообщений. Во время генерации чат доступен. Может не поддерживаться некоторыми бэкендами.",
"ext_sum_prompt_builder_2": "Прямой, неблокирующий",
"ext_sum_prompt_builder_3_desc": "Расширение будет использовать стандартные основные настройки промпта, и добавит свой промпт в качестве последнего системного сообщения.",
"ext_sum_prompt_builder_3": "Классический, блокирующий",
"Summary Prompt": "Промпт для пересказа",
"ext_sum_restore_default_prompt_tip": "Восстановить стандартный промпт",
"ext_sum_prompt_placeholder": "Этот промпт будет отправлен ИИ при запросе на генерацию пересказа. Макрос {{words}} будет заменён на значение параметра \"Количество слов\".",
"ext_sum_target_length_1": "Целевая длина пересказа (слов):",
"ext_sum_target_length_2": "",
"ext_sum_target_length_3": "",
"ext_sum_api_response_length_1": "Длина ответа от API (токенов):",
"ext_sum_api_response_length_2": "",
"ext_sum_api_response_length_3": " ",
"ext_sum_0_default": "по умолчанию = 0",
"ext_sum_raw_max_msg": "[Прямое форматирование] Макс. сообщений в запросе",
"ext_sum_0_unlimited": "неограничено = 0",
"Update frequency": "Частота обновления",
"ext_sum_update_every_messages_1": "Интервал обновления (кол-во сообщений):",
"ext_sum_update_every_messages_2": "",
"ext_sum_pause": "Приостановить",
"ext_sum_update_every_words_1": "Интервал обновления (кол-во слов):",
"ext_sum_update_every_words_2": "",
"ext_sum_0_disable": "для отключения поставьте 0",
"ext_sum_auto_adjust_desc": "Попытаться автоматически рассчитать значение интервала, исходя из статистики чата",
"ext_sum_both_sliders": "Если оба ползунка отличны от нуля, то оба будут триггерить генерацию пересказа с соответствующей периодичностью.",
"ext_sum_injection_template": "Шаблон для инжекта",
"ext_sum_memory_template_placeholder": "Макрос {{summary}} будет заменён на содержимое пересказа",
"ext_sum_injection_position": "Куда инжектить",
"How many messages before the current end of the chat.": "Сколько сообщений от конца чата.",
"Official Documentation": "официальной документацией",
"Change it later in the 'User Settings' panel.": "Его можно будет выключить в меню \"Настройки пользователя\"",
"Looking for AI characters?": "Ищете ИИ-персонажей?",
"onboarding_import": "Импортируйте",
"from supported sources or view": "из источника или посмотрите",
"Sample characters": "Стандартных персонажей",
"popup_text_save": "Сохранить",
"popup_text_yes": "Да",
"popup_text_no": "Нет",
"popup_text_cancel": "Отмена",
"Enter the URL of the content to import": "Введите URL-адрес импортируемого контента",
"Supported sources:": "Поддерживаются следующие источники:",
"char_import_example": "Пример:",
"char_import_1": "Персонаж с Chub (прямая ссылка или ID)",
"char_import_2": "Лорбук с Chub (прямая ссылка или ID)",
"char_import_3": "Персонаж с JanitorAI (прямая ссылка или UUID)",
"char_import_4": "Персонаж с Pygmalion.chat (прямая ссылка или UUID)",
"char_import_5": "Персонаж с AICharacterCard.com (прямая ссылка или ID)",
"char_import_6": "Прямая ссылка на PNG-файл (чтобы узнать список разрешённых хостов, загляните в",
"char_import_7": ")",
"popup_text_import": "Импортировать",
"Grammar String": "Грамматика",
"GNBF or ENBF, depends on the backend in use. If you're using this you should know which.": "GNBF или ENBF, зависит от бэкенда. Если вы это используете, то, скорее всего, сами знаете, какой именно.",
"Account": "Аккаунт",
"Hi,": "Привет,",
"To enable multi-account features, restart the SillyTavern server with": "Чтобы активировать систему аккаунтов, перезапустите SillyTavern, выставив",
"set to true in the config.yaml file.": "в файле config.yaml в положение true.",
"Account Info": "Об аккаунте",
"Handle:": "Хэндл:",
"Role:": "Роль:",
"Created:": "Создан:",
"Password:": "Пароль:",
"This account is password protected.": "Аккаунт защищён паролем.",
"This account is not password protected.": "Аккаунт не защищён паролем.",
"Account Actions": "Действия",
"Settings Snapshots": "Снимки настроек",
"Manage your settings snapshots.": "Управление снимками настроек.",
"Download Backup": "Скачать бэкап",
"Download a complete backup of your user data.": "Скачать полный бэкап данных пользователя.",
"Danger Zone": "Опасно",
"Reset Settings": "Сбросить настройки",
"Reset your settings to factory defaults.": "Сбросить настройки до заводских.",
"Reset Everything": "Сбросить всё",
"Wipe all user data and reset your account to factory settings.": "Стереть все данные пользователя и сбросить аккаунт до заводских настроек.",
"Set your custom avatar.": "Установить аватарку",
"Remove your custom avatar.": "Сбросить аватарку",
"Make a Snapshot": "Сделать снимок",
"Avoid cropping and resizing imported character images. When off, crop/resize to 512x768": "Не менять размер картинок у импортируемых персонажей. При отключении все картинки будут приводиться к размеру 512х768",
"Char List Subheader": "Доп. заголовок в списке персонажей",
"# Messages to Load": "Сколько сообщений загружать",
"(0 = All)": "(0 = все)",
"Theme Colors": "Цвета темы",
"Specify colors for your theme.": "Настройте собственные цвета для вашей темы.",
"Update speed of streamed text.": "Скорость обновления текста при стриминге.",
"The number of chat history messages to load before pagination.": "Кол-во сообщений чата, загружаемых перед пагинацией.",
"Chat Width": "Ширина чата",
"Width of the main chat window in % of screen width": "Ширина окна с чатом, в % от ширины экрана",
"Blur strength on UI panels.": "Сила размытия на панелях UI.",
"Font size": "Размер шрифта",
"Strength of the text shadows": "Размер теней, отбрасываемых текстом",
"Total Tokens:": "Всего токенов:",
"prompt_manager_edit": "Редактирование",
"prompt_manager_tokens": "Токенов",
"prompt_manager_name": "Имя",
"A name for this prompt.": "Имя данного промпта.",
"To whom this message will be attributed.": "От чьего лица будет отправляться сообщение.",
"AI Assistant": "ИИ-ассистент",
"prompt_manager_position": "Точка инжекта",
"Injection position. Next to other prompts (relative) or in-chat (absolute).": "Как рассчитывать позицию для инжекта. Она может располагаться по отношению к другим промптам (относительная) либо по отношению к чату (абсолютная).",
"prompt_manager_relative": "Относительная",
"prompt_manager_absolute": "Абсолютная",
"prompt_manager_depth": "Глубина",
"Injection depth. 0 = after the last message, 1 = before the last message, etc.": "Глубина вставки. 0 = после последнего сообщения, 1 = перед последним сообщением, и т.д.",
"The prompt to be sent.": "Отправляемый ИИ промпт.",
"prompt_manager_forbid_overrides": "Запретить перезапись",
"This prompt cannot be overridden by character cards, even if overrides are preferred.": "Карточка персонажа не сможет перезаписать этот промпт, даже если настройки отдают приоритет именно ей.",
"image_inlining_hint_1": "Отправлять картинки как часть промпта, если позволяет модель (такой функционал поддерживают GPT-4V, Claude 3 или Llava 13B). Чтобы добавить в чат изображение, используйте на нужном сообщении действие",
"image_inlining_hint_2": ". Также это можно сделать через меню",
"image_inlining_hint_3": ".",
"Contest Winners": "Победители конкурса",
"Rename background": "Переименовать фон",
"Lock": "Закрепить",
"Unlock": "Открепить",
"Delete background": "Удалить фон",
"Export all": "Экспортировать всё",
"Export all your prompts to a file": "Экспортировать все промпты в виде файла",
"Don't add character names.": "Не добавлять имя персонажа",
"Add character names to completion objects.": "Добавлять имя персонажа как часть Completion object",
"Message Content": "Внутри сообщения",
"Prepend character names to message contents.": "Предварять сообщения именем персонажа.",
"Character Names Behavior": "Вставка имени персонажа",
"Restrictions apply: only Latin alphanumerics and underscores. Doesn't work for all sources, notably: Claude, MistralAI, Google.": "Только латинские буквы, цифры и знак подчёркивания. Работает не для всех бэкендов, в частности для Claude, MistralAI, Google.",
"and pick a character.": "и выберите персонажа.",
"Record a snapshot of your current settings.": "Сделать снимок текущих настроек.",
"Restore this snapshot": "Откатиться к этому снимку",
"To change your user avatar, use the buttons below or select a default persona in the Persona Management menu.": "Чтобы сменить аватарку, используйте кнопки ниже, либо выберите персону по умолчанию в меню управления персоной.",
"These characters are the winners of character design contests and have outstandable quality.": "Персонажи наивысшего качества, одержавшие победу в конкурсе персонажей.",
"Featured Characters": "Рекомендуемые персонажи",
"These characters are the finalists of character design contests and have remarkable quality.": "Персонажи отличного качества, финалисты конкурса персонажей."
}

View File

@ -3,7 +3,6 @@
"kobldpresets": "Налаштування Kobold",
"guikoboldaisettings": "З інтерфейсу KoboldAI",
"novelaipreserts": "Налаштування NovelAI",
"default": "За замовчуванням",
"openaipresets": "Налаштування OpenAI",
"text gen webio(ooba) presets": "Налаштування Text Completion",
"response legth(tokens)": "Відповідь (токени)",
@ -495,7 +494,6 @@
"Global Lore First": "Глобальна інформація першою",
"Recursive Scan": "Рекурсивне сканування",
"Case Sensitive": "Чутливість до регістру",
"Match whole words": "Відповідність цілим словам",
"Alert On Overflow": "Сповіщення при переповненні",
"World/Lore Editor": "Редактор світу/книги",
"--- None ---": "--- Нічого ---",

View File

@ -3,7 +3,6 @@
"kobldpresets": "Cài đặt trước Kobold",
"guikoboldaisettings": "Cài đặt giao diện KoboldAI",
"novelaipreserts": "Cài đặt trước NovelAI",
"default": "Mặc định",
"openaipresets": "Cài đặt trước OpenAI",
"text gen webio(ooba) presets": "Cài đặt trước WebUI(ooba) của máy tạo văn bản",
"response legth(tokens)": "Độ dài phản hồi (trong các token)",
@ -62,7 +61,7 @@
"Temperature": "Nhiệt độ",
"Frequency Penalty": "Phạt Tần số",
"Presence Penalty": "Phạt Sự hiện",
"Top-p": "Top-p",
"Top-p": "Top-p",
"Display bot response text chunks as they are generated": "Hiển thị các phần văn bản phản hồi của bot khi chúng được tạo ra",
"Top A": "Top A",
"Typical Sampling": "Mẫu Đại diện",
@ -141,7 +140,7 @@
"Influences bot behavior in its responses": "Ảnh hưởng đến hành vi của bot trong các phản hồi của nó",
"Connect": "Kết nối",
"Test Message": "Tin nhắn kiểm tra",
"API": "Giao diện lập trình ứng dụng (API)",
"API": "Giao diện lập trình ứng dụng (API)",
"KoboldAI": "KoboldAI",
"Use Horde": "Sử dụng Horde",
"API url": "URL API",
@ -206,7 +205,7 @@
"Scale API Key": "Khóa API của Scale",
"Alt Method": "Phương pháp thay thế",
"AI21 API Key": "Khóa API của AI21",
"AI21 Model": "Mô hình AI21",
"AI21 Model": "Mô hình AI21",
"View API Usage Metrics": "Xem số liệu sử dụng API",
"Show External models (provided by API)": "Hiển thị các mô hình bên ngoài (do API cung cấp)",
"Bot": "Bot:",
@ -495,7 +494,6 @@
"Global Lore First": "Sử liệu toàn cầu đầu tiên",
"Recursive Scan": "Quét đệ quy",
"Case Sensitive": "Phân biệt chữ hoa chữ thường",
"Match whole words": "Khớp toàn bộ từ",
"Alert On Overflow": "Cảnh báo khi tràn",
"World/Lore Editor": "Trình soạn thảo Thế giới/Sử liệu",
"--- None ---": "--- Không ---",
@ -915,5 +913,5 @@
"Use the appropriate tokenizer for Google models via their API. Slower prompt processing, but offers much more accurate token counting.": "Sử dụng bộ mã hóa phù hợp cho các mô hình của Google thông qua API của họ. Xử lý lời mời chậm hơn, nhưng cung cấp đếm token chính xác hơn nhiều.",
"Load koboldcpp order": "Tải đơn hàng koboldcpp",
"Use Google Tokenizer": "Sử dụng bộ mã hóa của Google"
}
}

View File

@ -3,7 +3,6 @@
"kobldpresets": "Kobold 预设",
"guikoboldaisettings": "KoboldAI 用户界面设置",
"novelaipreserts": "NovelAI 预设",
"default": "默认",
"openaipresets": "对话补全预设",
"text gen webio(ooba) presets": "WebUI(ooba) 预设",
"response legth(tokens)": "响应长度(以词符数计)",
@ -495,7 +494,6 @@
"Global Lore First": "全局世界书优先",
"Recursive Scan": "递归扫描",
"Case Sensitive": "区分大小写",
"Match whole words": "完整匹配单词",
"Alert On Overflow": "溢出警报",
"World/Lore Editor": "世界书编辑器",
"--- None ---": "--- 无 ---",

1269
public/locales/zh-tw.json Normal file

File diff suppressed because it is too large Load Diff

View File

@ -9,6 +9,13 @@
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="darkreader-lock">
<meta name="robots" content="noindex, nofollow" />
<style>
/* Put critical CSS here. The rest should go in stylesheets. */
body {
background-color: rgb(36, 36, 37);
}
</style>
<link rel="preload" as="style" href="style.css">
<link rel="apple-touch-icon" sizes="57x57" href="img/apple-icon-57x57.png" />
<link rel="apple-touch-icon" sizes="72x72" href="img/apple-icon-72x72.png" />
<link rel="apple-touch-icon" sizes="114x114" href="img/apple-icon-114x114.png" />
@ -20,8 +27,8 @@
<link rel="manifest" crossorigin="use-credentials" href="manifest.json">
<link href="webfonts/NotoSans/stylesheet.css" rel="stylesheet">
<!-- fontawesome webfonts-->
<link href="css/fontawesome.css" rel="stylesheet">
<link href="css/solid.css" rel="stylesheet">
<link href="css/fontawesome.min.css" rel="stylesheet">
<link href="css/solid.min.css" rel="stylesheet">
<link href="css/user.css" rel="stylesheet">
<script src="lib/jquery-3.5.1.min.js"></script>
<script src="scripts/login.js"></script>

File diff suppressed because it is too large Load Diff

View File

@ -378,6 +378,7 @@ function RA_autoconnect(PrevApi) {
|| (secret_state[SECRET_KEYS.MISTRALAI] && oai_settings.chat_completion_source == chat_completion_sources.MISTRALAI)
|| (secret_state[SECRET_KEYS.COHERE] && oai_settings.chat_completion_source == chat_completion_sources.COHERE)
|| (secret_state[SECRET_KEYS.PERPLEXITY] && oai_settings.chat_completion_source == chat_completion_sources.PERPLEXITY)
|| (secret_state[SECRET_KEYS.GROQ] && oai_settings.chat_completion_source == chat_completion_sources.GROQ)
|| (isValidUrl(oai_settings.custom_url) && oai_settings.chat_completion_source == chat_completion_sources.CUSTOM)
) {
$('#api_button_openai').trigger('click');
@ -423,7 +424,7 @@ function restoreUserInput() {
const userInput = LoadLocal('userInput');
if (userInput) {
$('#send_textarea').val(userInput).trigger('input');
$('#send_textarea').val(userInput)[0].dispatchEvent(new Event('input', { bubbles: true }));
}
}
@ -435,10 +436,8 @@ const saveUserInputDebounced = debounce(saveUserInput);
// Make the DIV element draggable:
// THIRD UPDATE, prevent resize window breaks and smartly handle saving
export function dragElement(elmnt) {
var hasBeenDraggedByUser = false;
var isHeaderBeingDragged = false;
var isMouseDown = false;
var pos1 = 0, pos2 = 0, pos3 = 0, pos4 = 0;
@ -449,16 +448,16 @@ export function dragElement(elmnt) {
var elmntName = elmnt.attr('id');
console.debug(`dragElement called for ${elmntName}`);
const elmntNameEscaped = $.escapeSelector(elmntName);
console.debug(`dragElement escaped name: ${elmntNameEscaped}`);
const elmntHeader = $(`#${elmntNameEscaped}header`);
if (elmntHeader.length) {
elmntHeader.off('mousedown').on('mousedown', (e) => {
hasBeenDraggedByUser = true;
elmntHeader.off('mousedown').on('mousedown', (e) => { //listener for drag handle repositioning
isHeaderBeingDragged = true;
isMouseDown = true;
observer.observe(elmnt.get(0), { attributes: true, attributeFilter: ['style'] });
dragMouseDown(e);
});
$(elmnt).off('mousedown').on('mousedown', () => {
$(elmnt).off('mousedown').on('mousedown', () => { //listener for resize
isMouseDown = true;
observer.observe(elmnt.get(0), { attributes: true, attributeFilter: ['style'] });
});
@ -466,20 +465,19 @@ export function dragElement(elmnt) {
const observer = new MutationObserver((mutations) => {
const target = mutations[0].target;
if (!$(target).is(':visible')
|| $(target).hasClass('resizing')
|| Number((String(target.height).replace('px', ''))) < 50
|| Number((String(target.width).replace('px', ''))) < 50
|| power_user.movingUI === false
|| isMobile()
if (!$(target).is(':visible') //abort if element is invisible
|| $(target).hasClass('resizing') //being auto-resized by other JS code
|| Number((String(target.height).replace('px', ''))) < 50 //too short
|| Number((String(target.width).replace('px', ''))) < 50 //too narrow
|| power_user.movingUI === false // if MUI is not turned on
|| isMobile() // if it's a mobile screen
) {
console.debug('aborting mutator');
return;
}
//console.debug(left + width, winWidth, hasBeenDraggedByUser, isMouseDown)
const style = getComputedStyle(target); //use computed values because not all CSS are set by default
height = target.offsetHeight;
width = target.offsetWidth;
const style = getComputedStyle(target);
height = parseInt(style.height)
width = parseInt(style.width)
top = parseInt(style.top);
left = parseInt(style.left);
right = parseInt(style.right);
@ -494,53 +492,53 @@ export function dragElement(elmnt) {
topBarFirstX = parseInt(topbarstyle.marginInline);
topBarLastY = parseInt(topbarstyle.height);
/*console.log(`
winWidth: ${winWidth}, winHeight: ${winHeight}
sheldWidth: ${sheldWidth}
X: ${$(elmnt).css('left')}
Y: ${$(elmnt).css('top')}
MaxX: ${maxX}, MaxY: ${maxY}
height: ${height}
width: ${width}
Topbar 1st X: ${topBarFirstX}
TopBar lastX: ${topBarLastX}
`);*/
//prepare an empty poweruser object for the item being altered if we don't have one already
if (!power_user.movingUIState[elmntName]) {
console.debug(`adding config property for ${elmntName}`);
power_user.movingUIState[elmntName] = {};
}
//only record position changes if caused by a user click-drag
if (hasBeenDraggedByUser && isMouseDown) {
power_user.movingUIState[elmntName].top = top;
power_user.movingUIState[elmntName].left = left;
power_user.movingUIState[elmntName].right = right;
power_user.movingUIState[elmntName].bottom = bottom;
power_user.movingUIState[elmntName].margin = 'unset';
}
//handle resizing
if (!hasBeenDraggedByUser && isMouseDown) {
console.debug('saw resize, NOT header drag');
if (!isHeaderBeingDragged && isMouseDown) { //if user is dragging the resize handle (not in header)
let imgHeight, imgWidth, imageAspectRatio;
let containerAspectRatio = height / width;
//prevent resizing offscreen
if (top + elmnt.height() >= winHeight) {
console.debug('resizing height to prevent offscreen');
elmnt.css('height', winHeight - top - 1 + 'px');
}
//force aspect ratio for zoomed avatars
if ($(elmnt).attr('id').startsWith('zoomFor_')) {
let zoomedAvatarImage = $(elmnt).find('.zoomed_avatar_img');
imgHeight = zoomedAvatarImage.height();
imgWidth = zoomedAvatarImage.width();
imageAspectRatio = imgHeight / imgWidth;
if (left + elmnt.width() >= winWidth) {
console.debug('resizing width to prevent offscreen');
elmnt.css('width', winWidth - left - 1 + 'px');
// Maintain aspect ratio
if (containerAspectRatio !== imageAspectRatio) {
elmnt.css('width', elmnt.width());
elmnt.css('height', elmnt.width() * imageAspectRatio);
}
// Prevent resizing offscreen
if (top + elmnt.height() >= winHeight) {
elmnt.css('height', winHeight - top - 1 + 'px');
elmnt.css('width', (winHeight - top - 1) / imageAspectRatio + 'px');
}
if (left + elmnt.width() >= winWidth) {
elmnt.css('width', winWidth - left - 1 + 'px');
elmnt.css('height', (winWidth - left - 1) * imageAspectRatio + 'px');
}
} else { //prevent divs that are not zoomedAvatars from resizing offscreen
if (top + elmnt.height() >= winHeight) {
elmnt.css('height', winHeight - top - 1 + 'px');
}
if (left + elmnt.width() >= winWidth) {
elmnt.css('width', winWidth - left - 1 + 'px');
}
}
//prevent resizing from top left into the top bar
if (top < topBarLastY && maxX >= topBarFirstX && left <= topBarFirstX
) {
console.debug('prevent topbar underlap resize');
if (top < topBarLastY && maxX >= topBarFirstX && left <= topBarFirstX) {
elmnt.css('width', width - 1 + 'px');
}
@ -549,11 +547,11 @@ export function dragElement(elmnt) {
elmnt.css('top', top);
//set a listener for mouseup to save new width/height
elmnt.off('mouseup').on('mouseup', () => {
console.debug(`Saving ${elmntName} Height/Width`);
$(window).off('mouseup').on('mouseup', () => {
console.log(`Saving ${elmntName} Height/Width`);
// check if the height or width actually changed
if (power_user.movingUIState[elmntName].width === width && power_user.movingUIState[elmntName].height === height) {
console.debug('no change detected, aborting save');
if (power_user.movingUIState[elmntName].width === elmnt.width() && power_user.movingUIState[elmntName].height === elmnt.height()) {
console.log('no change detected, aborting save');
return;
}
@ -561,12 +559,27 @@ export function dragElement(elmnt) {
power_user.movingUIState[elmntName].height = height;
eventSource.emit('resizeUI', elmntName);
saveSettingsDebounced();
imgHeight = null;
imgWidth = null;
height = null;
width = null;
containerAspectRatio = null;
imageAspectRatio = null;
$(window).off('mouseup');
});
}
//handle dragging hit detection
if (hasBeenDraggedByUser && isMouseDown) {
//prevent dragging offscreen
//only record position changes if header is being dragged
power_user.movingUIState[elmntName].top = top;
power_user.movingUIState[elmntName].left = left;
power_user.movingUIState[elmntName].right = right;
power_user.movingUIState[elmntName].bottom = bottom;
power_user.movingUIState[elmntName].margin = 'unset';
//handle dragging hit detection to prevent dragging offscreen
if (isHeaderBeingDragged && isMouseDown) {
if (top <= 0) {
elmnt.css('top', '0px');
} else if (maxY >= winHeight) {
@ -578,27 +591,14 @@ export function dragElement(elmnt) {
} else if (maxX >= winWidth) {
elmnt.css('left', winWidth - maxX + left - 1 + 'px');
}
//prevent underlap with topbar div
/*
if (top < topBarLastY
&& (maxX >= topBarFirstX && left <= topBarFirstX //elmnt is hitting topbar from left side
|| left <= topBarLastX && maxX >= topBarLastX //elmnt is hitting topbar from right side
|| left >= topBarFirstX && maxX <= topBarLastX) //elmnt hitting topbar in the middle
) {
console.debug('topbar hit')
elmnt.css('top', top + 1 + "px");
}
*/
}
// Check if the element header exists and set the listener on the grabber
// Check if the element header exists and set the reposition listener on the grabber in the header
if (elmntHeader.length) {
elmntHeader.off('mousedown').on('mousedown', (e) => {
console.debug('listener started from header');
dragMouseDown(e);
});
} else {
} else { //if no header, put the listener on the elmnt itself.
elmnt.off('mousedown').on('mousedown', dragMouseDown);
}
});
@ -606,7 +606,7 @@ export function dragElement(elmnt) {
function dragMouseDown(e) {
if (e) {
hasBeenDraggedByUser = true;
isHeaderBeingDragged = true;
e.preventDefault();
pos3 = e.clientX; //mouse X at click
pos4 = e.clientY; //mouse Y at click
@ -638,34 +638,20 @@ export function dragElement(elmnt) {
elmnt.css('margin', 'unset');
elmnt.css('left', (elmnt.offset().left - pos1) + 'px');
elmnt.css('top', (elmnt.offset().top - pos2) + 'px');
elmnt.css('right', ((winWidth - maxX) + 'px'));
elmnt.css('bottom', ((winHeight - maxY) + 'px'));
/* elmnt.css('right', ((winWidth - maxX) + 'px'));
elmnt.css('bottom', ((winHeight - maxY) + 'px')); */
// Height/Width here are for visuals only, and are not saved to settings
// required because some divs do hot have a set width/height..
// and will defaults to shrink to min value of 100px set in CSS file
// Height/Width here are for visuals only, and are not saved to settings.
// This is required because some divs do hot have a set width/height
// and will default to shrink to min value of 100px set in CSS file
elmnt.css('height', height);
elmnt.css('width', width);
/*
console.log(`
winWidth: ${winWidth}, winHeight: ${winHeight}
sheldWidth: ${sheldWidth}
X: ${$(elmnt).css('left')}
Y: ${$(elmnt).css('top')}
MaxX: ${maxX}, MaxY: ${maxY}
height: ${height}
width: ${width}
Topbar 1st X: ${topBarFirstX}
TopBar lastX: ${topBarLastX}
`);
*/
return;
}
function closeDragElement() {
console.debug('drag finished');
hasBeenDraggedByUser = false;
isHeaderBeingDragged = false;
isMouseDown = false;
$(document).off('mouseup', closeDragElement);
$(document).off('mousemove', elementDrag);
@ -675,6 +661,12 @@ export function dragElement(elmnt) {
observer.disconnect();
console.debug(`Saving ${elmntName} UI position`);
saveSettingsDebounced();
top = null;
left = null;
right = null;
bottom = null;
maxX = null;
maxY = null;
}
}
@ -701,12 +693,12 @@ const isFirefox = navigator.userAgent.toLowerCase().indexOf('firefox') > -1;
*/
function autoFitSendTextArea() {
const originalScrollBottom = chatBlock.scrollHeight - (chatBlock.scrollTop + chatBlock.offsetHeight);
if (sendTextArea.scrollHeight == sendTextArea.offsetHeight) {
if (Math.ceil(sendTextArea.scrollHeight + 3) >= Math.floor(sendTextArea.offsetHeight)) {
// Needs to be pulled dynamically because it is affected by font size changes
const sendTextAreaMinHeight = window.getComputedStyle(sendTextArea).getPropertyValue('min-height');
sendTextArea.style.height = sendTextAreaMinHeight;
}
sendTextArea.style.height = sendTextArea.scrollHeight + 0.3 + 'px';
sendTextArea.style.height = sendTextArea.scrollHeight + 3 + 'px';
if (!isFirefox) {
const newScrollTop = Math.round(chatBlock.scrollHeight - (chatBlock.offsetHeight + originalScrollBottom));
@ -1132,6 +1124,11 @@ export function initRossMods() {
return;
}
if ($('#dialogue_del_mes_cancel').is(':visible')) {
$('#dialogue_del_mes_cancel').trigger('click');
return;
}
if ($('.drawer-content')
.not('#WorldInfo')
.not('#left-nav-panel')

View File

@ -9,10 +9,12 @@ import {
} from '../script.js';
import { selected_group } from './group-chats.js';
import { extension_settings, getContext, saveMetadataDebounced } from './extensions.js';
import { registerSlashCommand } from './slash-commands.js';
import { getCharaFilename, debounce, delay } from './utils.js';
import { getTokenCountAsync } from './tokenizers.js';
import { debounce_timeout } from './constants.js';
import { SlashCommandParser } from './slash-commands/SlashCommandParser.js';
import { SlashCommand } from './slash-commands/SlashCommand.js';
import { ARGUMENT_TYPE, SlashCommandArgument } from './slash-commands/SlashCommandArgument.js';
export { MODULE_NAME as NOTE_MODULE_NAME };
const MODULE_NAME = '2_floating_prompt'; // <= Deliberate, for sorting lower than memory
@ -455,9 +457,59 @@ export function initAuthorsNote() {
});
$('#option_toggle_AN').on('click', onANMenuItemClick);
registerSlashCommand('note', setNoteTextCommand, [], '<span class=\'monospace\'>(text)</span> sets an author\'s note for the currently selected chat', true, true);
registerSlashCommand('depth', setNoteDepthCommand, [], '<span class=\'monospace\'>(number)</span> sets an author\'s note depth for in-chat positioning', true, true);
registerSlashCommand('freq', setNoteIntervalCommand, ['interval'], '<span class=\'monospace\'>(number)</span> sets an author\'s note insertion frequency', true, true);
registerSlashCommand('pos', setNotePositionCommand, ['position'], '(<span class=\'monospace\'>chat</span> or <span class=\'monospace\'>scenario</span>) sets an author\'s note position', true, true);
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'note',
callback: setNoteTextCommand,
unnamedArgumentList: [
new SlashCommandArgument(
'text', [ARGUMENT_TYPE.STRING], true,
),
],
helpString: `
<div>
Sets an author's note for the currently selected chat.
</div>
`,
}));
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'depth',
callback: setNoteDepthCommand,
unnamedArgumentList: [
new SlashCommandArgument(
'number', [ARGUMENT_TYPE.NUMBER], true,
),
],
helpString: `
<div>
Sets an author's note depth for in-chat positioning.
</div>
`,
}));
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'freq',
callback: setNoteIntervalCommand,
namedArgumentList: [],
unnamedArgumentList: [
new SlashCommandArgument(
'number', [ARGUMENT_TYPE.NUMBER], true,
),
],
helpString: `
<div>
Sets an author's note insertion frequency.
</div>
`,
}));
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'pos',
callback: setNotePositionCommand,
namedArgumentList: [],
unnamedArgumentList: [
new SlashCommandArgument(
'position', [ARGUMENT_TYPE.STRING], true, false, null, ['chat', 'scenario'],
),
],
helpString: `
<div>
Sets an author's note position.
</div>
`,
}));
eventSource.on(event_types.CHAT_CHANGED, onChatChanged);
}

View File

@ -0,0 +1,792 @@
import { power_user } from '../power-user.js';
import { debounce, escapeRegex } from '../utils.js';
import { AutoCompleteOption } from './AutoCompleteOption.js';
import { AutoCompleteFuzzyScore } from './AutoCompleteFuzzyScore.js';
import { BlankAutoCompleteOption } from './BlankAutoCompleteOption.js';
// eslint-disable-next-line no-unused-vars
import { AutoCompleteNameResult } from './AutoCompleteNameResult.js';
import { AutoCompleteSecondaryNameResult } from './AutoCompleteSecondaryNameResult.js';
/**@readonly*/
/**@enum {Number}*/
export const AUTOCOMPLETE_WIDTH = {
'INPUT': 0,
'CHAT': 1,
'FULL': 2,
};
export class AutoComplete {
/**@type {HTMLTextAreaElement}*/ textarea;
/**@type {boolean}*/ isFloating = false;
/**@type {()=>boolean}*/ checkIfActivate;
/**@type {(text:string, index:number) => Promise<AutoCompleteNameResult>}*/ getNameAt;
/**@type {boolean}*/ isActive = false;
/**@type {boolean}*/ isReplaceable = false;
/**@type {boolean}*/ isShowingDetails = false;
/**@type {boolean}*/ wasForced = false;
/**@type {boolean}*/ isForceHidden = false;
/**@type {boolean}*/ canBeAutoHidden = false;
/**@type {string}*/ text;
/**@type {AutoCompleteNameResult}*/ parserResult;
/**@type {AutoCompleteSecondaryNameResult}*/ secondaryParserResult;
get effectiveParserResult() { return this.secondaryParserResult ?? this.parserResult; }
/**@type {string}*/ name;
/**@type {boolean}*/ startQuote;
/**@type {boolean}*/ endQuote;
/**@type {number}*/ selectionStart;
/**@type {RegExp}*/ fuzzyRegex;
/**@type {AutoCompleteOption[]}*/ result = [];
/**@type {AutoCompleteOption}*/ selectedItem = null;
/**@type {HTMLElement}*/ clone;
/**@type {HTMLElement}*/ domWrap;
/**@type {HTMLElement}*/ dom;
/**@type {HTMLElement}*/ detailsWrap;
/**@type {HTMLElement}*/ detailsDom;
/**@type {function}*/ renderDebounced;
/**@type {function}*/ renderDetailsDebounced;
/**@type {function}*/ updatePositionDebounced;
/**@type {function}*/ updateDetailsPositionDebounced;
/**@type {function}*/ updateFloatingPositionDebounced;
get matchType() {
return power_user.stscript.matching ?? 'fuzzy';
}
get autoHide() {
return power_user.stscript.autocomplete.autoHide ?? false;
}
/**
* @param {HTMLTextAreaElement} textarea The textarea to receive autocomplete.
* @param {() => boolean} checkIfActivate Function should return true only if under the current conditions, autocomplete should display (e.g., for slash commands: autoComplete.text[0] == '/')
* @param {(text: string, index: number) => Promise<AutoCompleteNameResult>} getNameAt Function should return (unfiltered, matching against input is done in AutoComplete) information about name options at index in text.
* @param {boolean} isFloating Whether autocomplete should float at the keyboard cursor.
*/
constructor(textarea, checkIfActivate, getNameAt, isFloating = false) {
this.textarea = textarea;
this.checkIfActivate = checkIfActivate;
this.getNameAt = getNameAt;
this.isFloating = isFloating;
this.domWrap = document.createElement('div'); {
this.domWrap.classList.add('autoComplete-wrap');
if (isFloating) this.domWrap.classList.add('isFloating');
}
this.dom = document.createElement('ul'); {
this.dom.classList.add('autoComplete');
this.domWrap.append(this.dom);
}
this.detailsWrap = document.createElement('div'); {
this.detailsWrap.classList.add('autoComplete-detailsWrap');
if (isFloating) this.detailsWrap.classList.add('isFloating');
}
this.detailsDom = document.createElement('div'); {
this.detailsDom.classList.add('autoComplete-details');
this.detailsWrap.append(this.detailsDom);
}
this.renderDebounced = debounce(this.render.bind(this), 10);
this.renderDetailsDebounced = debounce(this.renderDetails.bind(this), 10);
this.updatePositionDebounced = debounce(this.updatePosition.bind(this), 10);
this.updateDetailsPositionDebounced = debounce(this.updateDetailsPosition.bind(this), 10);
this.updateFloatingPositionDebounced = debounce(this.updateFloatingPosition.bind(this), 10);
textarea.addEventListener('input', ()=>this.text != this.textarea.value && this.show(true, this.wasForced));
textarea.addEventListener('keydown', (evt)=>this.handleKeyDown(evt));
textarea.addEventListener('click', ()=>this.isActive ? this.show() : null);
textarea.addEventListener('selectionchange', ()=>this.show());
textarea.addEventListener('blur', ()=>this.hide());
if (isFloating) {
textarea.addEventListener('scroll', ()=>this.updateFloatingPositionDebounced());
}
window.addEventListener('resize', ()=>this.updatePositionDebounced());
}
/**
*
* @param {AutoCompleteOption} option
*/
makeItem(option) {
const li = option.renderItem();
// gotta listen to pointerdown (happens before textarea-blur)
li.addEventListener('pointerdown', (evt)=>{
evt.preventDefault();
this.selectedItem = this.result.find(it=>it.name == li.getAttribute('data-name'));
this.select();
});
return li;
}
/**
*
* @param {AutoCompleteOption} item
*/
updateName(item) {
const chars = Array.from(item.dom.querySelector('.name').children);
switch (this.matchType) {
case 'strict': {
chars.forEach((it, idx)=>{
if (idx + item.nameOffset < item.name.length) {
it.classList.add('matched');
} else {
it.classList.remove('matched');
}
});
break;
}
case 'includes': {
const start = item.name.toLowerCase().search(this.name);
chars.forEach((it, idx)=>{
if (idx + item.nameOffset < start) {
it.classList.remove('matched');
} else if (idx + item.nameOffset < start + item.name.length) {
it.classList.add('matched');
} else {
it.classList.remove('matched');
}
});
break;
}
case 'fuzzy': {
item.name.replace(this.fuzzyRegex, (_, ...parts)=>{
parts.splice(-2, 2);
if (parts.length == 2) {
chars.forEach(c=>c.classList.remove('matched'));
} else {
let cIdx = item.nameOffset;
parts.forEach((it, idx)=>{
if (it === null || it.length == 0) return '';
if (idx % 2 == 1) {
chars.slice(cIdx, cIdx + it.length).forEach(c=>c.classList.add('matched'));
} else {
chars.slice(cIdx, cIdx + it.length).forEach(c=>c.classList.remove('matched'));
}
cIdx += it.length;
});
}
return '';
});
}
}
return item;
}
/**
* Calculate score for the fuzzy match.
* @param {AutoCompleteOption} option
* @returns The option.
*/
fuzzyScore(option) {
const parts = this.fuzzyRegex.exec(option.name).slice(1, -1);
let start = null;
let consecutive = [];
let current = '';
let offset = 0;
parts.forEach((part, idx) => {
if (idx % 2 == 0) {
if (part.length > 0) {
if (current.length > 0) {
consecutive.push(current);
}
current = '';
}
} else {
if (start === null) {
start = offset;
}
current += part;
}
offset += part.length;
});
if (current.length > 0) {
consecutive.push(current);
}
consecutive.sort((a,b)=>b.length - a.length);
option.score = new AutoCompleteFuzzyScore(start, consecutive[0]?.length ?? 0);
return option;
}
/**
* Compare two auto complete options by their fuzzy score.
* @param {AutoCompleteOption} a
* @param {AutoCompleteOption} b
*/
fuzzyScoreCompare(a, b) {
if (a.score.start < b.score.start) return -1;
if (a.score.start > b.score.start) return 1;
if (a.score.longestConsecutive > b.score.longestConsecutive) return -1;
if (a.score.longestConsecutive < b.score.longestConsecutive) return 1;
return a.name.localeCompare(b.name);
}
basicAutoHideCheck() {
// auto hide only if at least one char has been typed after the name + space
return this.textarea.selectionStart > this.parserResult.start
+ this.parserResult.name.length
+ (this.startQuote ? 1 : 0)
+ (this.endQuote ? 1 : 0)
+ 1
;
}
/**
* Show the autocomplete.
* @param {boolean} isInput Whether triggered by input.
* @param {boolean} isForced Whether force-showing (ctrl+space).
* @param {boolean} isSelect Whether an autocomplete option was just selected.
*/
async show(isInput = false, isForced = false, isSelect = false) {
//TODO check if isInput and isForced are both required
this.text = this.textarea.value;
this.isReplaceable = false;
if (document.activeElement != this.textarea) {
// only show with textarea in focus
return this.hide();
}
if (!this.checkIfActivate()) {
// only show if provider wants to
return this.hide();
}
// disable force-hide if trigger was forced
if (isForced) this.isForceHidden = false;
// request provider to get name result (potentially "incomplete", i.e. not an actual existing name) for
// cursor position
this.parserResult = await this.getNameAt(this.text, this.textarea.selectionStart);
this.secondaryParserResult = null;
if (!this.parserResult) {
// don't show if no name result found, e.g., cursor's area is not a command
return this.hide();
}
// need to know if name can be inside quotes, and then check if quotes are already there
if (this.parserResult.canBeQuoted) {
this.startQuote = this.text[this.parserResult.start] == '"';
this.endQuote = this.startQuote && this.text[this.parserResult.start + this.parserResult.name.length + 1] == '"';
} else {
this.startQuote = false;
this.endQuote = false;
}
// use lowercase name for matching
this.name = this.parserResult.name.toLowerCase() ?? '';
const isCursorInNamePart = this.textarea.selectionStart >= this.parserResult.start && this.textarea.selectionStart <= this.parserResult.start + this.parserResult.name.length + (this.startQuote ? 1 : 0);
if (isForced || isInput) {
// if forced (ctrl+space) or user input...
if (isCursorInNamePart) {
// ...and cursor is somewhere in the name part (including right behind the final char)
// -> show autocomplete for the (partial if cursor in the middle) name
this.name = this.name.slice(0, this.textarea.selectionStart - (this.parserResult.start) - (this.startQuote ? 1 : 0));
this.parserResult.name = this.name;
this.isReplaceable = true;
this.isForceHidden = false;
this.canBeAutoHidden = false;
} else {
this.isReplaceable = false;
this.canBeAutoHidden = this.basicAutoHideCheck();
}
} else {
// if not forced and no user input -> just show details
this.isReplaceable = false;
this.canBeAutoHidden = this.basicAutoHideCheck();
}
if (isForced || isInput || isSelect) {
// is forced or user input or just selected autocomplete option...
if (!isCursorInNamePart) {
// ...and cursor is not somwehere in the main name part -> check for secondary options (e.g., named arguments)
const result = this.parserResult.getSecondaryNameAt(this.text, this.textarea.selectionStart, isSelect);
if (result && (isForced || result.isRequired)) {
this.secondaryParserResult = result;
this.name = this.secondaryParserResult.name;
this.isReplaceable = isForced || this.secondaryParserResult.isRequired;
this.isForceHidden = false;
this.canBeAutoHidden = false;
} else {
this.isReplaceable = false;
this.canBeAutoHidden = this.basicAutoHideCheck();
}
}
}
if (this.matchType == 'fuzzy') {
// only build the fuzzy regex if match type is set to fuzzy
this.fuzzyRegex = new RegExp(`^(.*?)${this.name.split('').map(char=>`(${escapeRegex(char)})`).join('(.*?)')}(.*?)$`, 'i');
}
//TODO maybe move the matchers somewhere else; a single match function? matchType is available as property
const matchers = {
'strict': (name) => name.toLowerCase().startsWith(this.name),
'includes': (name) => name.toLowerCase().includes(this.name),
'fuzzy': (name) => this.fuzzyRegex.test(name),
};
this.result = this.effectiveParserResult.optionList
// filter the list of options by the partial name according to the matching type
.filter(it => this.isReplaceable || it.name == '' ? matchers[this.matchType](it.name) : it.name.toLowerCase() == this.name)
// remove aliases
.filter((it,idx,list) => list.findIndex(opt=>opt.value == it.value) == idx);
if (this.result.length == 0 && this.effectiveParserResult != this.parserResult && isForced) {
// no matching secondary results and forced trigger -> show current command details
this.secondaryParserResult = null;
this.result = [this.effectiveParserResult.optionList.find(it=>it.name == this.effectiveParserResult.name)];
this.name = this.effectiveParserResult.name;
this.fuzzyRegex = /(.*)(.*)(.*)/;
}
this.result = this.result
// update remaining options
.map(option => {
// build element
option.dom = this.makeItem(option);
// update replacer and add quotes if necessary
if (this.effectiveParserResult.canBeQuoted) {
option.replacer = option.name.includes(' ') || this.startQuote || this.endQuote ? `"${option.name}"` : `${option.name}`;
} else {
option.replacer = option.name;
}
// calculate fuzzy score if matching is fuzzy
if (this.matchType == 'fuzzy') this.fuzzyScore(option);
// update the name to highlight the matched chars
this.updateName(option);
return option;
})
// sort by fuzzy score or alphabetical
.toSorted(this.matchType == 'fuzzy' ? this.fuzzyScoreCompare : (a, b) => a.name.localeCompare(b.name))
;
if (this.isForceHidden) {
// hidden with escape
return this.hide();
}
if (this.autoHide && this.canBeAutoHidden && !isForced && this.effectiveParserResult == this.parserResult && this.result.length == 1) {
// auto hide user setting enabled and somewhere after name part and would usually show command details
return this.hide();
}
if (this.result.length == 0) {
if (!isInput) {
// no result and no input? hide autocomplete
return this.hide();
}
// otherwise add "no match" notice
const option = new BlankAutoCompleteOption(
this.name.length ?
this.effectiveParserResult.makeNoMatchText()
: this.effectiveParserResult.makeNoOptionstext()
,
);
this.result.push(option);
} else if (this.result.length == 1 && this.effectiveParserResult && this.result[0].name == this.effectiveParserResult.name) {
// only one result that is exactly the current value? just show hint, no autocomplete
this.isReplaceable = false;
this.isShowingDetails = false;
} else if (!this.isReplaceable && this.result.length > 1) {
return this.hide();
}
this.selectedItem = this.result[0];
this.isActive = true;
this.wasForced = isForced;
this.renderDebounced();
}
/**
* Hide autocomplete.
*/
hide() {
this.domWrap?.remove();
this.detailsWrap?.remove();
this.isActive = false;
this.isShowingDetails = false;
this.wasForced = false;
}
/**
* Create updated DOM.
*/
render() {
if (!this.isActive) return this.domWrap.remove();
if (this.isReplaceable) {
this.dom.innerHTML = '';
const frag = document.createDocumentFragment();
for (const item of this.result) {
if (item == this.selectedItem) {
item.dom.classList.add('selected');
} else {
item.dom.classList.remove('selected');
}
frag.append(item.dom);
}
this.dom.append(frag);
this.updatePosition();
document.body.append(this.domWrap);
} else {
this.domWrap.remove();
}
this.renderDetailsDebounced();
}
/**
* Create updated DOM for details.
*/
renderDetails() {
if (!this.isActive) return this.detailsWrap.remove();
if (!this.isShowingDetails && this.isReplaceable) return this.detailsWrap.remove();
this.detailsDom.innerHTML = '';
this.detailsDom.append(this.selectedItem?.renderDetails() ?? 'NO ITEM');
document.body.append(this.detailsWrap);
this.updateDetailsPositionDebounced();
}
/**
* Update position of DOM.
*/
updatePosition() {
if (this.isFloating) {
this.updateFloatingPosition();
} else {
const rect = {};
rect[AUTOCOMPLETE_WIDTH.INPUT] = this.textarea.getBoundingClientRect();
rect[AUTOCOMPLETE_WIDTH.CHAT] = document.querySelector('#sheld').getBoundingClientRect();
rect[AUTOCOMPLETE_WIDTH.FULL] = document.body.getBoundingClientRect();
this.domWrap.style.setProperty('--bottom', `${window.innerHeight - rect[AUTOCOMPLETE_WIDTH.INPUT].top}px`);
this.dom.style.setProperty('--bottom', `${window.innerHeight - rect[AUTOCOMPLETE_WIDTH.INPUT].top}px`);
this.domWrap.style.bottom = `${window.innerHeight - rect[AUTOCOMPLETE_WIDTH.INPUT].top}px`;
if (this.isShowingDetails) {
this.domWrap.style.setProperty('--leftOffset', '1vw');
this.domWrap.style.setProperty('--leftOffset', `max(1vw, ${rect[power_user.stscript.autocomplete.width.left].left}px)`);
this.domWrap.style.setProperty('--rightOffset', `calc(100vw - min(${rect[power_user.stscript.autocomplete.width.right].right}px, ${this.isShowingDetails ? 74 : 0}vw)`);
} else {
this.domWrap.style.setProperty('--leftOffset', `max(1vw, ${rect[power_user.stscript.autocomplete.width.left].left}px)`);
this.domWrap.style.setProperty('--rightOffset', `calc(100vw - min(99vw, ${rect[power_user.stscript.autocomplete.width.right].right}px)`);
}
this.updateDetailsPosition();
}
}
/**
* Update position of details DOM.
*/
updateDetailsPosition() {
if (this.isShowingDetails || !this.isReplaceable) {
if (this.isFloating) {
this.updateFloatingDetailsPosition();
} else {
const rect = {};
rect[AUTOCOMPLETE_WIDTH.INPUT] = this.textarea.getBoundingClientRect();
rect[AUTOCOMPLETE_WIDTH.CHAT] = document.querySelector('#sheld').getBoundingClientRect();
rect[AUTOCOMPLETE_WIDTH.FULL] = document.body.getBoundingClientRect();
if (this.isReplaceable) {
this.detailsWrap.classList.remove('full');
const selRect = this.selectedItem.dom.children[0].getBoundingClientRect();
this.detailsWrap.style.setProperty('--targetOffset', `${selRect.top}`);
this.detailsWrap.style.setProperty('--rightOffset', '1vw');
this.detailsWrap.style.setProperty('--bottomOffset', `calc(100vh - ${rect[AUTOCOMPLETE_WIDTH.INPUT].top}px)`);
this.detailsWrap.style.setProperty('--leftOffset', `calc(100vw - ${this.domWrap.style.getPropertyValue('--rightOffset')}`);
} else {
this.detailsWrap.classList.add('full');
this.detailsWrap.style.setProperty('--targetOffset', `${rect[AUTOCOMPLETE_WIDTH.INPUT].top}`);
this.detailsWrap.style.setProperty('--bottomOffset', `calc(100vh - ${rect[AUTOCOMPLETE_WIDTH.INPUT].top}px)`);
this.detailsWrap.style.setProperty('--leftOffset', `${rect[power_user.stscript.autocomplete.width.left].left}px`);
this.detailsWrap.style.setProperty('--rightOffset', `calc(100vw - ${rect[power_user.stscript.autocomplete.width.right].right}px)`);
}
}
}
}
/**
* Update position of floating autocomplete.
*/
updateFloatingPosition() {
const location = this.getCursorPosition();
const rect = this.textarea.getBoundingClientRect();
// cursor is out of view -> hide
if (location.bottom < rect.top || location.top > rect.bottom || location.left < rect.left || location.left > rect.right) {
return this.hide();
}
const left = Math.max(rect.left, location.left);
this.domWrap.style.setProperty('--targetOffset', `${left}`);
if (location.top <= window.innerHeight / 2) {
// if cursor is in lower half of window, show list above line
this.domWrap.style.top = `${location.bottom}px`;
this.domWrap.style.bottom = 'auto';
this.domWrap.style.maxHeight = `calc(${location.bottom}px - 1vh)`;
} else {
// if cursor is in upper half of window, show list below line
this.domWrap.style.top = 'auto';
this.domWrap.style.bottom = `calc(100vh - ${location.top}px)`;
this.domWrap.style.maxHeight = `calc(${location.top}px - 1vh)`;
}
}
updateFloatingDetailsPosition(location = null) {
if (!location) location = this.getCursorPosition();
const rect = this.textarea.getBoundingClientRect();
if (location.bottom < rect.top || location.top > rect.bottom || location.left < rect.left || location.left > rect.right) {
return this.hide();
}
const left = Math.max(rect.left, location.left);
this.detailsWrap.style.setProperty('--targetOffset', `${left}`);
if (this.isReplaceable) {
this.detailsWrap.classList.remove('full');
if (left < window.innerWidth / 4) {
// if cursor is in left part of screen, show details on right of list
this.detailsWrap.classList.add('right');
this.detailsWrap.classList.remove('left');
} else {
// if cursor is in right part of screen, show details on left of list
this.detailsWrap.classList.remove('right');
this.detailsWrap.classList.add('left');
}
} else {
this.detailsWrap.classList.remove('left');
this.detailsWrap.classList.remove('right');
this.detailsWrap.classList.add('full');
}
if (location.top <= window.innerHeight / 2) {
// if cursor is in lower half of window, show list above line
this.detailsWrap.style.top = `${location.bottom}px`;
this.detailsWrap.style.bottom = 'auto';
this.detailsWrap.style.maxHeight = `calc(${location.bottom}px - 1vh)`;
} else {
// if cursor is in upper half of window, show list below line
this.detailsWrap.style.top = 'auto';
this.detailsWrap.style.bottom = `calc(100vh - ${location.top}px)`;
this.detailsWrap.style.maxHeight = `calc(${location.top}px - 1vh)`;
}
}
/**
* Calculate (keyboard) cursor coordinates within textarea.
* @returns {{left:number, top:number, bottom:number}}
*/
getCursorPosition() {
const inputRect = this.textarea.getBoundingClientRect();
const style = window.getComputedStyle(this.textarea);
if (!this.clone) {
this.clone = document.createElement('div');
for (const key of style) {
this.clone.style[key] = style[key];
}
this.clone.style.position = 'fixed';
this.clone.style.visibility = 'hidden';
document.body.append(this.clone);
const mo = new MutationObserver(muts=>{
if (muts.find(it=>Array.from(it.removedNodes).includes(this.textarea))) {
this.clone.remove();
}
});
mo.observe(this.textarea.parentElement, { childList:true });
}
this.clone.style.height = `${inputRect.height}px`;
this.clone.style.left = `${inputRect.left}px`;
this.clone.style.top = `${inputRect.top}px`;
this.clone.style.whiteSpace = style.whiteSpace;
this.clone.style.tabSize = style.tabSize;
const text = this.textarea.value;
const before = text.slice(0, this.textarea.selectionStart);
this.clone.textContent = before;
const locator = document.createElement('span');
locator.textContent = text[this.textarea.selectionStart];
this.clone.append(locator);
this.clone.append(text.slice(this.textarea.selectionStart + 1));
this.clone.scrollTop = this.textarea.scrollTop;
this.clone.scrollLeft = this.textarea.scrollLeft;
const locatorRect = locator.getBoundingClientRect();
const location = {
left: locatorRect.left,
top: locatorRect.top,
bottom: locatorRect.bottom,
};
return location;
}
/**
* Toggle details view alongside autocomplete list.
*/
toggleDetails() {
this.isShowingDetails = !this.isShowingDetails;
this.renderDetailsDebounced();
this.updatePosition();
}
/**
* Select an item for autocomplete and put text into textarea.
*/
async select() {
if (this.isReplaceable && this.selectedItem.value !== null) {
this.textarea.value = `${this.text.slice(0, this.effectiveParserResult.start)}${this.selectedItem.replacer}${this.text.slice(this.effectiveParserResult.start + this.effectiveParserResult.name.length + (this.startQuote ? 1 : 0) + (this.endQuote ? 1 : 0))}`;
this.textarea.selectionStart = this.effectiveParserResult.start + this.selectedItem.replacer.length;
this.textarea.selectionEnd = this.textarea.selectionStart;
this.show(false, false, true);
} else {
const selectionStart = this.textarea.selectionStart;
const selectionEnd = this.textarea.selectionDirection;
this.textarea.selectionStart = selectionStart;
this.textarea.selectionDirection = selectionEnd;
}
this.wasForced = false;
this.textarea.dispatchEvent(new Event('input', { bubbles:true }));
}
/**
* Mark the item at newIdx in the autocomplete list as selected.
* @param {number} newIdx
*/
selectItemAtIndex(newIdx) {
this.selectedItem.dom.classList.remove('selected');
this.selectedItem = this.result[newIdx];
this.selectedItem.dom.classList.add('selected');
const rect = this.selectedItem.dom.children[0].getBoundingClientRect();
const rectParent = this.dom.getBoundingClientRect();
if (rect.top < rectParent.top || rect.bottom > rectParent.bottom ) {
this.dom.scrollTop += rect.top < rectParent.top ? rect.top - rectParent.top : rect.bottom - rectParent.bottom;
}
this.renderDetailsDebounced();
}
/**
* Handle keyboard events.
* @param {KeyboardEvent} evt The event.
*/
async handleKeyDown(evt) {
// autocomplete is shown and cursor at end of current command name (or inside name and typed or forced)
if (this.isActive && this.isReplaceable) {
// actions in the list
switch (evt.key) {
case 'ArrowUp': {
// select previous item
if (evt.ctrlKey || evt.altKey || evt.shiftKey) return;
evt.preventDefault();
evt.stopPropagation();
const idx = this.result.indexOf(this.selectedItem);
let newIdx;
if (idx == 0) newIdx = this.result.length - 1;
else newIdx = idx - 1;
this.selectItemAtIndex(newIdx);
return;
}
case 'ArrowDown': {
// select next item
if (evt.ctrlKey || evt.altKey || evt.shiftKey) return;
evt.preventDefault();
evt.stopPropagation();
const idx = this.result.indexOf(this.selectedItem);
const newIdx = (idx + 1) % this.result.length;
this.selectItemAtIndex(newIdx);
return;
}
case 'Enter': {
// pick the selected item to autocomplete
if (evt.ctrlKey || evt.altKey || evt.shiftKey || this.selectedItem.value == '') break;
if (this.selectedItem.name == this.name) break;
evt.preventDefault();
evt.stopImmediatePropagation();
this.select();
return;
}
case 'Tab': {
// pick the selected item to autocomplete
if (evt.ctrlKey || evt.altKey || evt.shiftKey || this.selectedItem.value == '') break;
evt.preventDefault();
evt.stopImmediatePropagation();
this.select();
return;
}
}
}
// details are shown, cursor can be anywhere
if (this.isActive) {
switch (evt.key) {
case 'Escape': {
// close autocomplete
if (evt.ctrlKey || evt.altKey || evt.shiftKey) return;
evt.preventDefault();
evt.stopPropagation();
this.isForceHidden = true;
this.wasForced = false;
this.hide();
return;
}
case 'Enter': {
// hide autocomplete on enter (send, execute, ...)
if (!evt.shiftKey) {
this.hide();
return;
}
break;
}
}
}
// autocomplete shown or not, cursor anywhere
switch (evt.key) {
case ' ': {
if (evt.ctrlKey) {
if (this.isActive && this.isReplaceable) {
// ctrl-space to toggle details for selected item
this.toggleDetails();
} else {
// ctrl-space to force show autocomplete
this.show(false, true);
}
return;
}
break;
}
}
if (['Control', 'Shift', 'Alt'].includes(evt.key)) {
// ignore keydown on modifier keys
return;
}
switch (evt.key) {
case 'ArrowUp':
case 'ArrowDown':
case 'ArrowRight':
case 'ArrowLeft': {
if (this.isActive) {
// keyboard navigation, wait for keyup to complete cursor move
const oldText = this.textarea.value;
await new Promise(resolve=>{
window.addEventListener('keyup', resolve, { once:true });
});
if (this.selectionStart != this.textarea.selectionStart) {
this.selectionStart = this.textarea.selectionStart;
this.show(this.isReplaceable || oldText != this.textarea.value);
}
}
break;
}
default: {
if (this.isActive) {
this.text != this.textarea.value && this.show(this.isReplaceable);
}
break;
}
}
}
}

View File

@ -0,0 +1,16 @@
export class AutoCompleteFuzzyScore {
/**@type {number}*/ start;
/**@type {number}*/ longestConsecutive;
/**
* @param {number} start
* @param {number} longestConsecutive
*/
constructor(start, longestConsecutive) {
this.start = start;
this.longestConsecutive = longestConsecutive;
}
}

View File

@ -0,0 +1,44 @@
import { SlashCommandNamedArgumentAutoCompleteOption } from '../slash-commands/SlashCommandNamedArgumentAutoCompleteOption.js';
import { AutoCompleteOption } from './AutoCompleteOption.js';
// import { AutoCompleteSecondaryNameResult } from './AutoCompleteSecondaryNameResult.js';
export class AutoCompleteNameResult {
/**@type {string} */ name;
/**@type {number} */ start;
/**@type {AutoCompleteOption[]} */ optionList = [];
/**@type {boolean} */ canBeQuoted = false;
/**@type {()=>string} */ makeNoMatchText = ()=>`No matches found for "${this.name}"`;
/**@type {()=>string} */ makeNoOptionstext = ()=>'No options';
/**
* @param {string} name Name (potentially partial) of the name at the requested index.
* @param {number} start Index where the name starts.
* @param {AutoCompleteOption[]} optionList A list of autocomplete options found in the current scope.
* @param {boolean} canBeQuoted Whether the name can be inside quotes.
* @param {()=>string} makeNoMatchText Function that returns text to show when no matches where found.
* @param {()=>string} makeNoOptionsText Function that returns text to show when no options are available to match against.
*/
constructor(name, start, optionList = [], canBeQuoted = false, makeNoMatchText = null, makeNoOptionsText = null) {
this.name = name;
this.start = start;
this.optionList = optionList;
this.canBeQuoted = canBeQuoted;
this.noMatchText = makeNoMatchText ?? this.makeNoMatchText;
this.noOptionstext = makeNoOptionsText ?? this.makeNoOptionstext;
}
/**
*
* @param {string} text The whole text
* @param {number} index Cursor index within text
* @param {boolean} isSelect Whether autocomplete was triggered by selecting an autocomplete option
* @returns {AutoCompleteSecondaryNameResult}
*/
getSecondaryNameAt(text, index, isSelect) {
return null;
}
}

View File

@ -0,0 +1,206 @@
import { SlashCommand } from '../slash-commands/SlashCommand.js';
import { AutoCompleteFuzzyScore } from './AutoCompleteFuzzyScore.js';
export class AutoCompleteOption {
/**@type {string}*/ name;
/**@type {string}*/ typeIcon;
/**@type {number}*/ nameOffset = 0;
/**@type {AutoCompleteFuzzyScore}*/ score;
/**@type {string}*/ replacer;
/**@type {HTMLElement}*/ dom;
/**
* Used as a comparison value when removing duplicates (e.g., when a SlashCommand has aliases).
* @type {any}
* */
get value() {
return this.name;
}
/**
* @param {string} name
*/
constructor(name, typeIcon = ' ') {
this.name = name;
this.typeIcon = typeIcon;
}
makeItem(key, typeIcon, noSlash, namedArguments = [], unnamedArguments = [], returnType = 'void', helpString = '', aliasList = []) {
const li = document.createElement('li'); {
li.classList.add('item');
const type = document.createElement('span'); {
type.classList.add('type');
type.classList.add('monospace');
type.textContent = typeIcon;
li.append(type);
}
const specs = document.createElement('span'); {
specs.classList.add('specs');
const name = document.createElement('span'); {
name.classList.add('name');
name.classList.add('monospace');
name.textContent = noSlash ? '' : '/';
key.split('').forEach(char=>{
const span = document.createElement('span'); {
span.textContent = char;
name.append(span);
}
});
specs.append(name);
}
const body = document.createElement('span'); {
body.classList.add('body');
const args = document.createElement('span'); {
args.classList.add('arguments');
for (const arg of namedArguments) {
const argItem = document.createElement('span'); {
argItem.classList.add('argument');
argItem.classList.add('namedArgument');
if (!arg.isRequired || (arg.defaultValue ?? false)) argItem.classList.add('optional');
if (arg.acceptsMultiple) argItem.classList.add('multiple');
const name = document.createElement('span'); {
name.classList.add('argument-name');
name.textContent = arg.name;
argItem.append(name);
}
if (arg.enumList.length > 0) {
const enums = document.createElement('span'); {
enums.classList.add('argument-enums');
for (const e of arg.enumList) {
const enumItem = document.createElement('span'); {
enumItem.classList.add('argument-enum');
enumItem.textContent = e;
enums.append(enumItem);
}
}
argItem.append(enums);
}
} else {
const types = document.createElement('span'); {
types.classList.add('argument-types');
for (const t of arg.typeList) {
const type = document.createElement('span'); {
type.classList.add('argument-type');
type.textContent = t;
types.append(type);
}
}
argItem.append(types);
}
}
args.append(argItem);
}
}
for (const arg of unnamedArguments) {
const argItem = document.createElement('span'); {
argItem.classList.add('argument');
argItem.classList.add('unnamedArgument');
if (!arg.isRequired || (arg.defaultValue ?? false)) argItem.classList.add('optional');
if (arg.acceptsMultiple) argItem.classList.add('multiple');
if (arg.enumList.length > 0) {
const enums = document.createElement('span'); {
enums.classList.add('argument-enums');
for (const e of arg.enumList) {
const enumItem = document.createElement('span'); {
enumItem.classList.add('argument-enum');
enumItem.textContent = e;
enums.append(enumItem);
}
}
argItem.append(enums);
}
} else {
const types = document.createElement('span'); {
types.classList.add('argument-types');
for (const t of arg.typeList) {
const type = document.createElement('span'); {
type.classList.add('argument-type');
type.textContent = t;
types.append(type);
}
}
argItem.append(types);
}
}
args.append(argItem);
}
}
body.append(args);
}
const returns = document.createElement('span'); {
returns.classList.add('returns');
returns.textContent = returnType ?? 'void';
// body.append(returns);
}
specs.append(body);
}
li.append(specs);
}
const help = document.createElement('span'); {
help.classList.add('help');
const content = document.createElement('span'); {
content.classList.add('helpContent');
content.innerHTML = helpString;
const text = content.textContent;
content.innerHTML = '';
content.textContent = text;
help.append(content);
}
li.append(help);
}
if (aliasList.length > 0) {
const aliases = document.createElement('span'); {
aliases.classList.add('aliases');
aliases.append(' (alias: ');
for (const aliasName of aliasList) {
const alias = document.createElement('span'); {
alias.classList.add('monospace');
alias.textContent = `/${aliasName}`;
aliases.append(alias);
}
}
aliases.append(')');
// li.append(aliases);
}
}
}
return li;
}
/**
* @returns {HTMLElement}
*/
renderItem() {
// throw new Error(`${this.constructor.name}.renderItem() is not implemented`);
let li;
li = this.makeItem(this.name, this.typeIcon, true);
li.setAttribute('data-name', this.name);
return li;
}
/**
* @returns {DocumentFragment}
*/
renderDetails() {
// throw new Error(`${this.constructor.name}.renderDetails() is not implemented`);
const frag = document.createDocumentFragment();
const specs = document.createElement('div'); {
specs.classList.add('specs');
const name = document.createElement('div'); {
name.classList.add('name');
name.classList.add('monospace');
name.textContent = this.name;
specs.append(name);
}
frag.append(specs);
}
return frag;
}
}

View File

@ -0,0 +1,5 @@
import { AutoCompleteNameResult } from './AutoCompleteNameResult.js';
export class AutoCompleteSecondaryNameResult extends AutoCompleteNameResult {
/**@type {boolean}*/ isRequired = false;
}

View File

@ -0,0 +1,29 @@
import { AutoCompleteOption } from './AutoCompleteOption.js';
export class BlankAutoCompleteOption extends AutoCompleteOption {
/**
* @param {string} name
*/
constructor(name) {
super(name);
this.dom = this.renderItem();
}
get value() { return null; }
renderItem() {
const li = document.createElement('li'); {
li.classList.add('item');
li.classList.add('blank');
li.textContent = this.name;
}
return li;
}
renderDetails() {
const frag = document.createDocumentFragment();
return frag;
}
}

View File

@ -0,0 +1,44 @@
import { AutoCompleteOption } from './AutoCompleteOption.js';
export class MacroAutoCompleteOption extends AutoCompleteOption {
/**@type {string}*/ fullName;
/**@type {string}*/ description;
constructor(name, fullName, description) {
super(name, '{}');
this.fullName = fullName;
this.description = description;
this.nameOffset = 2;
}
renderItem() {
let li;
li = this.makeItem(`${this.fullName}`, '{}', true, [], [], null, this.description);
li.setAttribute('data-name', this.name);
li.setAttribute('data-option-type', 'macro');
return li;
}
renderDetails() {
const frag = document.createDocumentFragment();
const specs = document.createElement('div'); {
specs.classList.add('specs');
const name = document.createElement('div'); {
name.classList.add('name');
name.classList.add('monospace');
name.textContent = this.fullName;
specs.append(name);
}
frag.append(specs);
}
const help = document.createElement('span'); {
help.classList.add('help');
help.innerHTML = this.description;
frag.append(help);
}
return frag;
}
}

View File

@ -1,6 +1,7 @@
import { callPopup, chat_metadata, eventSource, event_types, generateQuietPrompt, getCurrentChatId, getRequestHeaders, getThumbnailUrl, saveSettingsDebounced } from '../script.js';
import { saveMetadataDebounced } from './extensions.js';
import { registerSlashCommand } from './slash-commands.js';
import { SlashCommand } from './slash-commands/SlashCommand.js';
import { SlashCommandParser } from './slash-commands/SlashCommandParser.js';
import { flashHighlight, stringFormat } from './utils.js';
const BG_METADATA_KEY = 'custom_background';
@ -480,7 +481,20 @@ export function initBackgrounds() {
$('#auto_background').on('click', autoBackgroundCommand);
$('#add_bg_button').on('change', onBackgroundUploadSelected);
$('#bg-filter').on('input', onBackgroundFilterInput);
registerSlashCommand('lockbg', onLockBackgroundClick, ['bglock'], ' locks a background for the currently selected chat', true, true);
registerSlashCommand('unlockbg', onUnlockBackgroundClick, ['bgunlock'], ' unlocks a background for the currently selected chat', true, true);
registerSlashCommand('autobg', autoBackgroundCommand, ['bgauto'], ' automatically changes the background based on the chat context using the AI request prompt', true, true);
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'lockbg',
callback: onLockBackgroundClick,
aliases: ['bglock'],
helpString: 'Locks a background for the currently selected chat',
}));
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'unlockbg',
callback: onUnlockBackgroundClick,
aliases: ['bgunlock'],
helpString: 'Unlocks a background for the currently selected chat',
}));
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'autobg',
callback: autoBackgroundCommand,
aliases: ['bgauto'],
helpString: 'Automatically changes the background based on the chat context using the AI request prompt',
}));
}

View File

@ -0,0 +1,98 @@
/**
* @typedef {object} v2DataWorldInfoEntry
* @property {string[]} keys - An array of primary keys associated with the entry.
* @property {string[]} secondary_keys - An array of secondary keys associated with the entry (optional).
* @property {string} comment - A human-readable description or explanation for the entry.
* @property {string} content - The main content or data associated with the entry.
* @property {boolean} constant - Indicates if the entry's content is fixed and unchangeable.
* @property {boolean} selective - Indicates if the entry's inclusion is controlled by specific conditions.
* @property {number} insertion_order - Defines the order in which the entry is inserted during processing.
* @property {boolean} enabled - Controls whether the entry is currently active and used.
* @property {string} position - Specifies the location or context where the entry applies.
* @property {v2DataWorldInfoEntryExtensionInfos} extensions - An object containing additional details for extensions associated with the entry.
* @property {number} id - A unique identifier assigned to the entry.
*/
/**
* @typedef {object} v2DataWorldInfoEntryExtensionInfos
* @property {number} position - The order in which the extension is applied relative to other extensions.
* @property {boolean} exclude_recursion - Prevents the extension from being applied recursively.
* @property {number} probability - The chance (between 0 and 1) of the extension being applied.
* @property {boolean} useProbability - Determines if the `probability` property is used.
* @property {number} depth - The maximum level of nesting allowed for recursive application of the extension.
* @property {number} selectiveLogic - Defines the logic used to determine if the extension is applied selectively.
* @property {string} group - A category or grouping for the extension.
* @property {boolean} group_override - Overrides any existing group assignment for the extension.
* @property {number} group_weight - A value used for prioritizing extensions within the same group.
* @property {boolean} prevent_recursion - Completely disallows recursive application of the extension.
* @property {boolean} delay_until_recursion - Will only be checked during recursion.
* @property {number} scan_depth - The maximum depth to search for matches when applying the extension.
* @property {boolean} match_whole_words - Specifies if only entire words should be matched during extension application.
* @property {boolean} use_group_scoring - Indicates if group weight is considered when selecting extensions.
* @property {boolean} case_sensitive - Controls whether case sensitivity is applied during matching for the extension.
* @property {string} automation_id - An identifier used for automation purposes related to the extension.
* @property {number} role - The specific function or purpose of the extension.
* @property {boolean} vectorized - Indicates if the extension is optimized for vectorized processing.
* @property {number} display_index - The order in which the extension should be displayed for user interfaces.
*/
/**
* @typedef {object} v2WorldInfoBook
* @property {string} name - the name of the book
* @property {v2DataWorldInfoEntry[]} entries - the entries of the book
*/
/**
* @typedef {object} v2CharData
* @property {string} name - The character's name.
* @property {string} description - A brief description of the character.
* @property {string} character_version - The character's data version.
* @property {string} personality - A short summary of the character's personality traits.
* @property {string} scenario - A description of the character's background or setting.
* @property {string} first_mes - The character's opening message in a conversation.
* @property {string} mes_example - An example message demonstrating the character's conversation style.
* @property {string} creator_notes - Internal notes or comments left by the character's creator.
* @property {string[]} tags - A list of keywords or labels associated with the character.
* @property {string} system_prompt - The system prompt used to interact with the character.
* @property {string} post_history_instructions - Instructions for handling the character's conversation history.
* @property {string} creator - The name of the person who created the character.
* @property {string[]} alternate_greetings - Additional greeting messages the character can use.
* @property {v2WorldInfoBook} character_book - Data about the character's world or story (if applicable).
* @property {v2CharDataExtensionInfos} extensions - Additional details specific to the character.
*/
/**
* @typedef {object} v2CharDataExtensionInfos
* @property {number} talkativeness - A numerical value indicating the character's propensity to talk.
* @property {boolean} fav - A flag indicating whether the character is a favorite.
* @property {string} world - The fictional world or setting where the character exists (if applicable).
* @property {object} depth_prompt - Prompts used to explore the character's depth and complexity.
* @property {number} depth_prompt.depth - The level of detail or nuance targeted by the prompt.
* @property {string} depth_prompt.prompt - The actual prompt text used for deeper character interaction.
* @property {"system" | "user" | "assistant"} depth_prompt.role - The role the character takes on during the prompted interaction (system, user, or assistant).
* // Non-standard extensions added by external tools
* @property {string} [pygmalion_id] - The unique identifier assigned to the character by the Pygmalion.chat.
* @property {string} [github_repo] - The gitHub repository associated with the character.
* @property {string} [source_url] - The source URL associated with the character.
* @property {{full_path: string}} [chub] - The Chub-specific data associated with the character.
* @property {{source: string[]}} [risuai] - The RisuAI-specific data associated with the character.
*/
/**
* @typedef {object} v1CharData
* @property {string} name - the name of the character
* @property {string} description - the description of the character
* @property {string} personality - a short personality description of the character
* @property {string} scenario - a scenario description of the character
* @property {string} first_mes - the first message in the conversation
* @property {string} mes_example - the example message in the conversation
* @property {string} creatorcomment - creator's notes of the character
* @property {string[]} tags - the tags of the character
* @property {number} talkativeness - talkativeness
* @property {boolean|string} fav - fav
* @property {string} create_date - create_date
* @property {v2CharData} data - v2 data extension
* // Non-standard extensions added by the ST server (not part of the original data)
* @property {string} chat - name of the current chat file chat
* @property {string} avatar - file name of the avatar image (acts as a unique identifier)
* @property {string} json_data - the full raw JSON data of the character
*/
export default 0;// now this file is a module

View File

@ -0,0 +1,9 @@
<div class="characterAsset">
<div class="characterAssetName">{{name}}</div>
<img class="characterAssetImage" alt="{{name}}" src="{{url}}" />
<div class="characterAssetDescription" title="{{description}}">{{description}}</div>
<div class="characterAssetButtons flex-container">
<div class="characterAssetDownloadButton right_menu_button fa-fw fa-solid fa-download" title="Download"></div>
<div class="characterAssetCheckMark right_menu_button fa-fw fa-solid fa-check" title="Installed"></div>
</div>
</div>

View File

@ -3,8 +3,9 @@ TODO:
*/
//const DEBUG_TONY_SAMA_FORK_MODE = true
import { getRequestHeaders, callPopup, processDroppedFiles } from '../../../script.js';
import { getRequestHeaders, callPopup, processDroppedFiles, eventSource, event_types } from '../../../script.js';
import { deleteExtension, extensionNames, getContext, installExtension, renderExtensionTemplateAsync } from '../../extensions.js';
import { POPUP_TYPE, callGenericPopup } from '../../popup.js';
import { executeSlashCommands } from '../../slash-commands.js';
import { getStringHash, isValidUrl } from '../../utils.js';
export { MODULE_NAME };
@ -108,7 +109,7 @@ function downloadAssetsList(url) {
</div>`);
}
for (const i in availableAssets[assetType]) {
for (const i in availableAssets[assetType].sort((a, b) => a?.name && b?.name && a['name'].localeCompare(b['name']))) {
const asset = availableAssets[assetType][i];
const elemId = `assets_install_${assetType}_${i}`;
let element = $('<div />', { id: elemId, class: 'asset-download-button right_menu_button' });
@ -199,6 +200,9 @@ function downloadAssetsList(url) {
</div>`);
if (assetType === 'character') {
if (asset.highlight) {
assetBlock.find('.asset-name').append('<i class="fa-solid fa-sm fa-trophy"></i>');
}
assetBlock.find('.asset-name').prepend(`<div class="avatar"><img src="${asset['url']}" alt="${displayName}"></div>`);
}
@ -328,6 +332,41 @@ async function deleteAsset(assetType, filename) {
}
}
async function openCharacterBrowser(forceDefault) {
const url = forceDefault ? ASSETS_JSON_URL : String($('#assets-json-url-field').val());
const fetchResult = await fetch(url, { cache: 'no-cache' });
const json = await fetchResult.json();
const characters = json.filter(x => x.type === 'character');
if (!characters.length) {
toastr.error('No characters found in the assets list', 'Character browser');
return;
}
const template = $(await renderExtensionTemplateAsync(MODULE_NAME, 'market', {}));
for (const character of characters.sort((a, b) => a.name.localeCompare(b.name))) {
const listElement = template.find(character.highlight ? '.contestWinnersList' : '.featuredCharactersList');
const characterElement = $(await renderExtensionTemplateAsync(MODULE_NAME, 'character', character));
const downloadButton = characterElement.find('.characterAssetDownloadButton');
const checkMark = characterElement.find('.characterAssetCheckMark');
const isInstalled = isAssetInstalled('character', character.id);
downloadButton.toggle(!isInstalled).on('click', async () => {
downloadButton.toggleClass('fa-download fa-spinner fa-spin');
await installAsset(character.url, 'character', character.id);
downloadButton.hide();
checkMark.show();
});
checkMark.toggle(isInstalled);
listElement.append(characterElement);
}
callGenericPopup(template, POPUP_TYPE.TEXT, '', { okButton: 'Close', wide: true, large: true, allowVerticalScrolling: true, allowHorizontalScrolling: false });
}
//#############################//
// API Calls //
//#############################//
@ -361,6 +400,11 @@ jQuery(async () => {
const assetsJsonUrl = windowHtml.find('#assets-json-url-field');
assetsJsonUrl.val(ASSETS_JSON_URL);
const charactersButton = windowHtml.find('#assets-characters-button');
charactersButton.on('click', async function () {
openCharacterBrowser(false);
});
const connectButton = windowHtml.find('#assets-connect-button');
connectButton.on('click', async function () {
const url = String(assetsJsonUrl.val());
@ -397,4 +441,8 @@ jQuery(async () => {
windowHtml.find('#assets_filters').hide();
$('#extensions_settings').append(windowHtml);
eventSource.on(event_types.OPEN_CHARACTER_LIBRARY, async (forceDefault) => {
openCharacterBrowser(forceDefault);
});
});

View File

@ -0,0 +1,19 @@
<div class="flex-container flexFlowColumn padding5">
<div class="contestWinners flex-container flexFlowColumn">
<h3 class="flex-container alignItemsBaseline justifyCenter" data-i18n="[title]These characters are the winners of character design contests and have outstandable quality." title="These characters are the winners of character design contests and have outstandable quality.">
<span data-i18n="Contest Winners">Contest Winners</span>
<i class="fa-solid fa-star"></i>
</h3>
<div class="contestWinnersList characterAssetList">
</div>
</div>
<hr>
<div class="featuredCharacters flex-container flexFlowColumn">
<h3 class="flex-container alignItemsBaseline justifyCenter" data-i18n="[title]These characters are the finalists of character design contests and have remarkable quality." title="These characters are the finalists of character design contests and have remarkable quality.">
<span data-i18n="Featured Characters">Featured Characters</span>
<i class="fa-solid fa-thumbs-up"></i>
</h3>
<div class="featuredCharactersList characterAssetList">
</div>
</div>
</div>

View File

@ -105,3 +105,54 @@
transform: rotate(1turn);
}
}
.characterAssetList {
display: flex;
flex-wrap: wrap;
justify-content: space-evenly;
}
.characterAsset {
display: flex;
flex-direction: column;
align-items: center;
padding: 10px;
gap: 10px;
border: 1px solid var(--SmartThemeBorderColor);
background-color: var(--black30a);
border-radius: 10px;
width: 17%;
min-width: 150px;
margin: 5px;
overflow: hidden;
}
.characterAssetName {
font-size: 1.2em;
font-weight: bold;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.characterAssetImage {
max-height: 140px;
object-fit: scale-down;
border-radius: 5px;
}
.characterAssetDescription {
font-size: 0.75em;
overflow: hidden;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 4;
flex: 1;
}
.characterAssetButtons {
display: flex;
flex-direction: row;
gap: 5px;
align-items: center;
}

View File

@ -14,6 +14,10 @@
<select id="assets_type_select" class="text_pole flex1">
</select>
<input id="assets_search" class="text_pole flex1" placeholder="Search" type="search">
<div id="assets-characters-button" class="menu_button menu_button_icon">
<i class="fa-solid fa-image-portrait"></i>
Characters
</div>
</div>
<div class="inline-drawer-content" id="assets_menu">
</div>

View File

@ -4,7 +4,7 @@
Enter a URL or the ID of a Fandom wiki page to scrape:
</label>
<small>
<span data-i18n=Examples:">Examples:</span>
<span data-i18n="Examples:">Examples:</span>
<code>https://harrypotter.fandom.com/</code>
<span data-i18n="or">or</span>
<code>harrypotter</code>

View File

@ -1,9 +1,15 @@
import { renderExtensionTemplateAsync } from '../../extensions.js';
import { registerSlashCommand } from '../../slash-commands.js';
import { SlashCommand } from '../../slash-commands/SlashCommand.js';
import { SlashCommandParser } from '../../slash-commands/SlashCommandParser.js';
jQuery(async () => {
const buttons = await renderExtensionTemplateAsync('attachments', 'buttons', {});
$('#extensionsMenu').prepend(buttons);
registerSlashCommand('db', () => document.getElementById('manageAttachments')?.click(), ['databank', 'data-bank'], ' open the data bank', true, true);
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'db',
callback: () => document.getElementById('manageAttachments')?.click(),
aliases: ['databank', 'data-bank'],
helpString: 'Open the data bank',
}));
});

View File

@ -0,0 +1,54 @@
<div>
<div class="flex-container flexFlowColumn">
<label for="scrapeInput" data-i18n="Enter a base URL of the MediaWiki to scrape.">
Enter a <strong>base URL</strong> of the MediaWiki to scrape.
</label>
<i data-i18n="Don't include the page name!">
Don't include the page name!
</i>
<small>
<span data-i18n="Examples:">Examples:</span>
<code>https://streetcat.wiki/index.php</code>
<span data-i18n="or">or</span>
<code>https://tcrf.net</code>
</small>
<input type="text" id="scrapeInput" name="scrapeInput" class="text_pole" placeholder="">
</div>
<div class="flex-container flexFlowColumn">
<label for="scrapeFilter">
Optional regex to pick the content by its title:
</label>
<small>
<span data-i18n="Example:">Example:</span>
<code>/Mr. (Fresh|Snack)/gi</code>
</small>
<input type="text" id="scrapeFilter" name="scrapeFilter" class="text_pole" placeholder="">
</div>
<div class="flex-container flexFlowColumn">
<label>
Output format:
</label>
<label class="checkbox_label justifyLeft" for="scrapeOutputSingle">
<input id="scrapeOutputSingle" type="radio" name="scrapeOutput" value="single" checked>
<div class="flex-container flexFlowColumn flexNoGap">
<span data-i18n="Single file">
Single file
</span>
<small data-i18n="All articles will be concatenated into a single file.">
All articles will be concatenated into a single file.
</small>
</div>
</label>
<label class="checkbox_label justifyLeft" for="scrapeOutputMulti">
<input id="scrapeOutputMulti" type="radio" name="scrapeOutput" value="multi">
<div class="flex-container flexFlowColumn flexNoGap">
<span data-i18n="File per article">
File per article
</span>
<small data-i18n="Each article will be saved as a separate file.">
Not recommended. Each article will be saved as a separate file.
</small>
</div>
</label>
</div>
</div>

View File

@ -5,7 +5,9 @@ import { getMessageTimeStamp } from '../../RossAscends-mods.js';
import { SECRET_KEYS, secret_state } from '../../secrets.js';
import { getMultimodalCaption } from '../shared.js';
import { textgen_types, textgenerationwebui_settings } from '../../textgen-settings.js';
import { registerSlashCommand } from '../../slash-commands.js';
import { SlashCommandParser } from '../../slash-commands/SlashCommandParser.js';
import { SlashCommand } from '../../slash-commands/SlashCommand.js';
import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from '../../slash-commands/SlashCommandArgument.js';
export { MODULE_NAME };
const MODULE_NAME = 'caption';
@ -254,6 +256,19 @@ async function onSelectImage(e, prompt, quiet) {
return '';
}
const caption = await getCaptionForFile(file, prompt, quiet);
form && form.reset();
return caption;
}
/**
* Gets a caption for an image file.
* @param {File} file Input file
* @param {string} prompt Caption prompt
* @param {boolean} quiet Suppresses sending a message
* @returns {Promise<string>} Generated caption
*/
async function getCaptionForFile(file, prompt, quiet) {
try {
setSpinnerIcon();
const context = getContext();
@ -273,7 +288,6 @@ async function onSelectImage(e, prompt, quiet) {
return '';
}
finally {
form && form.reset();
setImageIcon();
}
}
@ -288,9 +302,26 @@ function onRefineModeInput() {
* @param {object} args Named parameters
* @param {string} prompt Caption prompt
*/
function captionCommandCallback(args, prompt) {
async function captionCommandCallback(args, prompt) {
const quiet = isTrueBoolean(args?.quiet);
const id = args?.id;
if (!isNaN(Number(id))) {
const message = getContext().chat[id];
if (message?.extra?.image) {
try {
const fetchResult = await fetch(message.extra.image);
const blob = await fetchResult.blob();
const file = new File([blob], 'image.jpg', { type: blob.type });
return await getCaptionForFile(file, prompt, quiet);
} catch (error) {
toastr.error('Failed to get image from the message. Make sure the image is accessible.');
return '';
}
}
}
return new Promise(resolve => {
const quiet = isTrueBoolean(args?.quiet);
const input = document.createElement('input');
input.type = 'file';
input.accept = 'image/*';
@ -404,12 +435,17 @@ jQuery(function () {
<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="openai" value="gpt-4-turbo">gpt-4-turbo</option>
<option data-type="openai" value="gpt-4o">gpt-4o</option>
<option data-type="anthropic" value="claude-3-opus-20240229">claude-3-opus-20240229</option>
<option data-type="anthropic" value="claude-3-sonnet-20240229">claude-3-sonnet-20240229</option>
<option data-type="anthropic" value="claude-3-haiku-20240307">claude-3-haiku-20240307</option>
<option data-type="google" value="gemini-pro-vision">gemini-pro-vision</option>
<option data-type="google" value="gemini-1.5-flash-latest">gemini-1.5-flash-latest</option>
<option data-type="openrouter" value="openai/gpt-4-vision-preview">openai/gpt-4-vision-preview</option>
<option data-type="openrouter" value="openai/gpt-4o">openai/gpt-4o</option>
<option data-type="openrouter" value="openai/gpt-4-turbo">openai/gpt-4-turbo</option>
<option data-type="openrouter" value="haotian-liu/llava-13b">haotian-liu/llava-13b</option>
<option data-type="openrouter" value="fireworks/firellava-13b">fireworks/firellava-13b</option>
<option data-type="openrouter" value="anthropic/claude-3-haiku">anthropic/claude-3-haiku</option>
<option data-type="openrouter" value="anthropic/claude-3-sonnet">anthropic/claude-3-sonnet</option>
<option data-type="openrouter" value="anthropic/claude-3-opus">anthropic/claude-3-opus</option>
@ -418,6 +454,8 @@ jQuery(function () {
<option data-type="openrouter" value="anthropic/claude-3-opus:beta">anthropic/claude-3-opus:beta</option>
<option data-type="openrouter" value="nousresearch/nous-hermes-2-vision-7b">nousresearch/nous-hermes-2-vision-7b</option>
<option data-type="openrouter" value="google/gemini-pro-vision">google/gemini-pro-vision</option>
<option data-type="openrouter" value="google/gemini-flash-1.5">google/gemini-flash-1.5</option>
<option data-type="openrouter" value="liuhaotian/llava-yi-34b">liuhaotian/llava-yi-34b</option>
<option data-type="ollama" value="ollama_current">[Currently selected]</option>
<option data-type="ollama" value="bakllava:latest">bakllava:latest</option>
<option data-type="ollama" value="llava:latest">llava:latest</option>
@ -492,5 +530,35 @@ jQuery(function () {
saveSettingsDebounced();
});
registerSlashCommand('caption', captionCommandCallback, [], '<span class="monospace">quiet=true/false [prompt]</span> - caption an image with an optional prompt and passes the caption down the pipe. Only multimodal sources support custom prompts. Set the "quiet" argument to true to suppress sending a captioned message, default: false.', true, true);
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'caption',
callback: captionCommandCallback,
returns: 'caption',
namedArgumentList: [
new SlashCommandNamedArgument(
'quiet', 'suppress sending a captioned message', [ARGUMENT_TYPE.BOOLEAN], false, false, 'false', ['true', 'false'],
),
new SlashCommandNamedArgument(
'id', 'get image from a message with this ID', [ARGUMENT_TYPE.NUMBER], false, false,
),
],
unnamedArgumentList: [
new SlashCommandArgument(
'prompt', [ARGUMENT_TYPE.STRING], false,
),
],
helpString: `
<div>
Caption an image with an optional prompt and passes the caption down the pipe.
</div>
<div>
Only multimodal sources support custom prompts.
</div>
<div>
Provide a message ID to get an image from a message instead of uploading one.
</div>
<div>
Set the "quiet" argument to true to suppress sending a captioned message, default: false.
</div>
`,
}));
});

View File

@ -2,11 +2,13 @@ import { callPopup, eventSource, event_types, generateQuietPrompt, getRequestHea
import { dragElement, isMobile } from '../../RossAscends-mods.js';
import { getContext, getApiUrl, modules, extension_settings, ModuleWorkerWrapper, doExtrasFetch, renderExtensionTemplateAsync } from '../../extensions.js';
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';
import { isJsonSchemaSupported } from '../../textgen-settings.js';
import { debounce_timeout } from '../../constants.js';
import { SlashCommandParser } from '../../slash-commands/SlashCommandParser.js';
import { SlashCommand } from '../../slash-commands/SlashCommand.js';
import { ARGUMENT_TYPE, SlashCommandArgument } from '../../slash-commands/SlashCommandArgument.js';
export { MODULE_NAME };
const MODULE_NAME = 'expressions';
@ -906,8 +908,10 @@ async function setSpriteSetCommand(_, folder) {
$('#expression_override').val(folder.trim());
onClickExpressionOverrideButton();
removeExpression();
moduleWorker();
// removeExpression();
// moduleWorker();
const vnMode = isVisualNovelMode();
await sendExpressionCall(folder, lastExpression, true, vnMode);
}
async function classifyCommand(_, text) {
@ -972,8 +976,8 @@ function sampleClassifyText(text) {
return text;
}
// Remove asterisks and quotes
let result = text.replace(/[*"]/g, '');
// Replace macros, remove asterisks and quotes
let result = substituteParams(text).replace(/[*"]/g, '');
const SAMPLE_THRESHOLD = 500;
const HALF_SAMPLE_THRESHOLD = SAMPLE_THRESHOLD / 2;
@ -1016,12 +1020,12 @@ function parseLlmResponse(emotionResponse, labels) {
const parsedEmotion = JSON.parse(emotionResponse);
return parsedEmotion?.emotion ?? fallbackExpression;
} catch {
const fuse = new Fuse([emotionResponse]);
for (const label of labels) {
const result = fuse.search(label);
if (result.length > 0) {
return label;
}
const fuse = new Fuse(labels, { includeScore: true });
console.debug('Using fuzzy search in labels:', labels);
const result = fuse.search(emotionResponse);
if (result.length > 0) {
console.debug(`fuzzy search found: ${result[0].item} as closest for the LLM response:`, emotionResponse);
return result[0].item;
}
}
@ -1967,9 +1971,61 @@ function migrateSettings() {
});
eventSource.on(event_types.MOVABLE_PANELS_RESET, updateVisualNovelModeDebounced);
eventSource.on(event_types.GROUP_UPDATED, updateVisualNovelModeDebounced);
registerSlashCommand('sprite', setSpriteSlashCommand, ['emote'], '<span class="monospace">(spriteId)</span> force sets the sprite for the current character', true, true);
registerSlashCommand('spriteoverride', setSpriteSetCommand, ['costume'], '<span class="monospace">(optional folder)</span> sets an override sprite folder for the current character. If the name starts with a slash or a backslash, selects a sub-folder in the character-named folder. Empty value to reset to default.', true, true);
registerSlashCommand('lastsprite', (_, value) => lastExpression[value.trim()] ?? '', [], '<span class="monospace">(charName)</span> Returns the last set sprite / expression for the named character.', true, true);
registerSlashCommand('th', toggleTalkingHeadCommand, ['talkinghead'], ' Character Expressions: toggles <i>Image Type - talkinghead (extras)</i> on/off.', true, true);
registerSlashCommand('classify', classifyCommand, [], '<span class="monospace">(text)</span> performs an emotion classification of the given text and returns a label.', true, true);
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'sprite',
aliases: ['emote'],
callback: setSpriteSlashCommand,
unnamedArgumentList: [
new SlashCommandArgument(
'spriteId', [ARGUMENT_TYPE.STRING], true,
),
],
helpString: 'Force sets the sprite for the current character.',
}));
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'spriteoverride',
aliases: ['costume'],
callback: setSpriteSetCommand,
unnamedArgumentList: [
new SlashCommandArgument(
'optional folder', [ARGUMENT_TYPE.STRING], false,
),
],
helpString: 'Sets an override sprite folder for the current character. If the name starts with a slash or a backslash, selects a sub-folder in the character-named folder. Empty value to reset to default.',
}));
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'lastsprite',
callback: (_, value) => lastExpression[value.trim()] ?? '',
returns: 'sprite',
unnamedArgumentList: [
new SlashCommandArgument(
'charName', [ARGUMENT_TYPE.STRING], true,
),
],
helpString: 'Returns the last set sprite / expression for the named character.',
}));
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'th',
callback: toggleTalkingHeadCommand,
aliases: ['talkinghead'],
helpString: 'Character Expressions: toggles <i>Image Type - talkinghead (extras)</i> on/off.',
}));
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'classify',
callback: classifyCommand,
unnamedArgumentList: [
new SlashCommandArgument(
'text', [ARGUMENT_TYPE.STRING], true,
),
],
returns: 'emotion classification label for the given text',
helpString: `
<div>
Performs an emotion classification of the given text and returns a label.
</div>
<div>
<strong>Example:</strong>
<ul>
<li>
<pre><code>/classify I am so happy today!</code></pre>
</li>
</ul>
</div>
`,
}));
})();

View File

@ -64,10 +64,6 @@
<input id="expression_override" type="text" class="text_pole" placeholder="Override folder name" />
<input id="expression_override_button" class="menu_button" type="submit" value="Submit" />
</div>
<h3 id="image_list_header">
<strong>Sprite set:</strong>&nbsp;<span id="image_list_header_name"></span>
</h3>
<div id="image_list"></div>
<div class="expression_buttons flex-container spaceEvenly">
<div id="expression_upload_pack_button" class="menu_button">
<i class="fa-solid fa-file-zipper"></i>
@ -80,6 +76,11 @@
</div>
<p class="hint"><b>Hint:</b> <i>Create new folder in the <b>/characters/</b> folder of your user data directory and name it as the name of the character.
Put images with expressions there. File names should follow the pattern: <tt>[expression_label].[image_format]</tt></i></p>
<h3 id="image_list_header">
<strong>Sprite set:</strong>&nbsp;<span id="image_list_header_name"></span>
</h3>
<div id="image_list"></div>
</div>
</div>
</div>

View File

@ -14,7 +14,6 @@
display: flex;
height: calc(100vh - var(--topBarBlockSize));
width: 100vw;
position: relative;
overflow: hidden;
}

View File

@ -8,7 +8,9 @@ import { groups, selected_group } from '../../group-chats.js';
import { loadFileToDocument, delay } from '../../utils.js';
import { loadMovingUIState } from '../../power-user.js';
import { dragElement } from '../../RossAscends-mods.js';
import { registerSlashCommand } from '../../slash-commands.js';
import { SlashCommandParser } from '../../slash-commands/SlashCommandParser.js';
import { SlashCommand } from '../../slash-commands/SlashCommand.js';
import { ARGUMENT_TYPE, SlashCommandNamedArgument } from '../../slash-commands/SlashCommandArgument.js';
const extensionName = 'gallery';
const extensionFolderPath = `scripts/extensions/${extensionName}/`;
@ -415,8 +417,26 @@ function viewWithDragbox(items) {
// Registers a simple command for opening the char gallery.
registerSlashCommand('show-gallery', showGalleryCommand, ['sg'], ' shows the gallery', true, true);
registerSlashCommand('list-gallery', listGalleryCommand, ['lg'], '<span class="monospace">[optional char=charName] [optional group=groupName]</span> list images in the gallery of the current char / group or a specified char / group', true, true);
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'show-gallery',
aliases: ['sg'],
callback: showGalleryCommand,
helpString: 'Shows the gallery.',
}));
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'list-gallery',
aliases: ['lg'],
callback: listGalleryCommand,
returns: 'list of images',
namedArgumentList: [
new SlashCommandNamedArgument(
'char', 'character name', [ARGUMENT_TYPE.STRING], false,
),
new SlashCommandNamedArgument(
'group', 'group name', [ARGUMENT_TYPE.STRING], false,
),
],
helpString: 'List images in the gallery of the current char / group or a specified char / group.',
}));
function showGalleryCommand(args) {
showCharGallery();

View File

@ -16,11 +16,14 @@ import {
getMaxContextSize,
} from '../../../script.js';
import { is_group_generating, selected_group } from '../../group-chats.js';
import { registerSlashCommand } from '../../slash-commands.js';
import { loadMovingUIState } from '../../power-user.js';
import { dragElement } from '../../RossAscends-mods.js';
import { getTextTokens, getTokenCountAsync, tokenizers } from '../../tokenizers.js';
import { debounce_timeout } from '../../constants.js';
import { SlashCommandParser } from '../../slash-commands/SlashCommandParser.js';
import { SlashCommand } from '../../slash-commands/SlashCommand.js';
import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from '../../slash-commands/SlashCommandArgument.js';
import { resolveVariable } from '../../variables.js';
export { MODULE_NAME };
const MODULE_NAME = '1_memory';
@ -416,7 +419,7 @@ async function forceSummarizeChat() {
console.log(`Skipping WIAN? ${skipWIAN}`);
if (!context.chatId) {
toastr.warning('No chat selected');
return;
return '';
}
toastr.info('Summarizing chat...', 'Please wait');
@ -424,7 +427,42 @@ async function forceSummarizeChat() {
if (!value) {
toastr.warning('Failed to summarize chat');
return;
return '';
}
return value;
}
/**
* Callback for the summarize command.
* @param {object} args Command arguments
* @param {string} text Text to summarize
*/
async function summarizeCallback(args, text) {
text = text.trim();
// Using forceSummarizeChat to summarize the current chat
if (!text) {
return await forceSummarizeChat();
}
const source = args.source || extension_settings.memory.source;
const prompt = substituteParams((resolveVariable(args.prompt) || extension_settings.memory.prompt)?.replace(/{{words}}/gi, extension_settings.memory.promptWords));
try {
switch (source) {
case summary_sources.extras:
return await callExtrasSummarizeAPI(text);
case summary_sources.main:
return await generateRaw(text, '', false, false, prompt, extension_settings.memory.overrideResponseLength);
default:
toastr.warning('Invalid summarization source specified');
return '';
}
} catch (error) {
toastr.error(String(error), 'Failed to summarize text');
console.log(error);
return '';
}
}
@ -668,37 +706,18 @@ async function summarizeChatExtras(context) {
// perform the summarization API call
try {
inApiCall = true;
const url = new URL(getApiUrl());
url.pathname = '/api/summarize';
const summary = await callExtrasSummarizeAPI(resultingString);
const newContext = getContext();
const apiResult = await doExtrasFetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Bypass-Tunnel-Reminder': 'bypass',
},
body: JSON.stringify({
text: resultingString,
params: {},
}),
});
if (apiResult.ok) {
const data = await apiResult.json();
const summary = data.summary;
const newContext = getContext();
// something changed during summarization request
if (newContext.groupId !== context.groupId
|| newContext.chatId !== context.chatId
|| (!newContext.groupId && (newContext.characterId !== context.characterId))) {
console.log('Context changed, summary discarded');
return;
}
setMemoryContext(summary, true);
// something changed during summarization request
if (newContext.groupId !== context.groupId
|| newContext.chatId !== context.chatId
|| (!newContext.groupId && (newContext.characterId !== context.characterId))) {
console.log('Context changed, summary discarded');
return;
}
setMemoryContext(summary, true);
}
catch (error) {
console.log(error);
@ -708,6 +727,40 @@ async function summarizeChatExtras(context) {
}
}
/**
* Call the Extras API to summarize the provided text.
* @param {string} text Text to summarize
* @returns {Promise<string>} Summarized text
*/
async function callExtrasSummarizeAPI(text) {
if (!modules.includes('summarize')) {
throw new Error('Summarize module is not enabled in Extras API');
}
const url = new URL(getApiUrl());
url.pathname = '/api/summarize';
const apiResult = await doExtrasFetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Bypass-Tunnel-Reminder': 'bypass',
},
body: JSON.stringify({
text: text,
params: {},
}),
});
if (apiResult.ok) {
const data = await apiResult.json();
const summary = data.summary;
return summary;
}
throw new Error('Extras API call failed');
}
function onMemoryRestoreClick() {
const context = getContext();
const content = $('#memory_contents').val();
@ -751,10 +804,7 @@ function setMemoryContext(value, saveToMessage, index = null) {
const context = getContext();
context.setExtensionPrompt(MODULE_NAME, formatMemoryValue(value), extension_settings.memory.position, extension_settings.memory.depth, false, extension_settings.memory.role);
$('#memory_contents').val(value);
console.log('Summary set to: ' + value);
console.debug('Position: ' + extension_settings.memory.position);
console.debug('Depth: ' + extension_settings.memory.depth);
console.debug('Role: ' + extension_settings.memory.role);
console.log('Summary set to: ' + value, 'Position: ' + extension_settings.memory.position, 'Depth: ' + extension_settings.memory.depth, 'Role: ' + extension_settings.memory.role);
if (saveToMessage && context.chat.length) {
const idx = index ?? context.chat.length - 2;
@ -865,5 +915,16 @@ jQuery(async function () {
eventSource.on(event_types.MESSAGE_EDITED, onChatEvent);
eventSource.on(event_types.MESSAGE_SWIPED, onChatEvent);
eventSource.on(event_types.CHAT_CHANGED, onChatEvent);
registerSlashCommand('summarize', forceSummarizeChat, [], ' forces the summarization of the current chat using the Main API', true, true);
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
name: 'summarize',
callback: summarizeCallback,
namedArgumentList: [
new SlashCommandNamedArgument('source', 'API to use for summarization', [ARGUMENT_TYPE.STRING], false, false, '', ['main', 'extras']),
new SlashCommandNamedArgument('prompt', 'prompt to use for summarization', [ARGUMENT_TYPE.STRING, ARGUMENT_TYPE.VARIABLE_NAME], false, false, ''),
],
unnamedArgumentList: [
new SlashCommandArgument('text to summarize', [ARGUMENT_TYPE.STRING], false, false, ''),
],
helpString: 'Summarizes the given text. If no text is provided, the current chat will be summarized. Can specify the source and the prompt to use.',
}));
});

View File

@ -9,73 +9,73 @@
</div>
<div class="inline-drawer-content">
<div id="summaryExtensionDrawerContents">
<label for="summary_source">Summarize with:</label>
<label for="summary_source" data-i18n="ext_sum_with">Summarize with:</label>
<select id="summary_source">
<option value="main">Main API</option>
<option value="main" data-i18n="ext_sum_main_api">Main API</option>
<option value="extras">Extras API</option>
</select><br>
<div class="flex-container justifyspacebetween alignitemscenter">
<span class="flex1">Current summary:</span>
<span class="flex1" data-i18n="ext_sum_current_summary">Current summary:</span>
<div id="memory_restore" class="menu_button flex1 margin0">
<span>Restore Previous</span>
<span data-i18n="ext_sum_restore_previous">Restore Previous</span>
</div>
</div>
<textarea id="memory_contents" class="text_pole textarea_compact" rows="6" placeholder="Summary will be generated here..."></textarea>
<textarea id="memory_contents" class="text_pole textarea_compact" rows="6" data-i18n="[placeholder]ext_sum_memory_placeholder" placeholder="Summary will be generated here..."></textarea>
<div class="memory_contents_controls">
<div id="memory_force_summarize" data-summary-source="main" class="menu_button menu_button_icon" title="Trigger a summary update right now." data-i18n="Trigger a summary update right now.">
<div id="memory_force_summarize" data-summary-source="main" class="menu_button menu_button_icon" data-i18n="[title]ext_sum_force_tip" title="Trigger a summary update right now." data-i18n="Trigger a summary update right now.">
<i class="fa-solid fa-database"></i>
<span>Summarize now</span>
<span data-i18n="ext_sum_force_text">Summarize now</span>
</div>
<label for="memory_frozen" title="Disable automatic summary updates. While paused, the summary remains as-is. You can still force an update by pressing the Summarize now button (which is only available with the Main API)." data-i18n="[title]Disable automatic summary updates. While paused, the summary remains as-is. You can still force an update by pressing the Summarize now button (which is only available with the Main API)."><input id="memory_frozen" type="checkbox" />Pause</label>
<label for="memory_frozen" title="Disable automatic summary updates. While paused, the summary remains as-is. You can still force an update by pressing the Summarize now button (which is only available with the Main API)." data-i18n="[title]Disable automatic summary updates. While paused, the summary remains as-is. You can still force an update by pressing the Summarize now button (which is only available with the Main API)."><input id="memory_frozen" type="checkbox" /><span data-i18n="ext_sum_pause">Pause</span></label>
<label data-summary-source="main" for="memory_skipWIAN" title="Omit World Info and Author's Note from text to be summarized. Only has an effect when using the Main API. The Extras API always omits WI/AN." data-i18n="[title]Omit World Info and Author's Note from text to be summarized. Only has an effect when using the Main API. The Extras API always omits WI/AN.">
<input id="memory_skipWIAN" type="checkbox" />
<span>No WI/AN</span>
<span data-i18n="ext_sum_no_wi_an">No WI/AN</span>
</label>
</div>
<div class="memory_contents_controls">
<div id="summarySettingsBlockToggle" class="menu_button menu_button_icon" title="Edit summarization prompt, insertion position, etc.">
<div id="summarySettingsBlockToggle" class="menu_button menu_button_icon" data-i18n="[title]ext_sum_settings_tip" title="Edit summarization prompt, insertion position, etc.">
<i class="fa-solid fa-cog"></i>
<span>Summary Settings</span>
<span data-i18n="ext_sum_settings">Summary Settings</span>
</div>
</div>
<div id="summarySettingsBlock" style="display:none;">
<div data-summary-source="main">
<label>
<label data-i18n="ext_sum_prompt_builder">
Prompt builder
</label>
<label class="checkbox_label" for="memory_prompt_builder_raw_blocking" title="Extension will build its own prompt using messages that were not summarized yet. Blocks the chat until the summary is generated.">
<label class="checkbox_label" for="memory_prompt_builder_raw_blocking" data-i18n="[title]ext_sum_prompt_builder_1_desc" title="Extension will build its own prompt using messages that were not summarized yet. Blocks the chat until the summary is generated.">
<input id="memory_prompt_builder_raw_blocking" type="radio" name="memory_prompt_builder" value="1" />
<span>Raw, blocking</span>
<span data-i18n="ext_sum_prompt_builder_1">Raw, blocking</span>
</label>
<label class="checkbox_label" for="memory_prompt_builder_raw_non_blocking" title="Extension will build its own prompt using messages that were not summarized yet. Does not block the chat while the summary is being generated. Not all backends support this mode.">
<label class="checkbox_label" for="memory_prompt_builder_raw_non_blocking" data-i18n="[title]ext_sum_prompt_builder_2_desc" title="Extension will build its own prompt using messages that were not summarized yet. Does not block the chat while the summary is being generated. Not all backends support this mode.">
<input id="memory_prompt_builder_raw_non_blocking" type="radio" name="memory_prompt_builder" value="2" />
<span>Raw, non-blocking</span>
<span data-i18n="ext_sum_prompt_builder_2">Raw, non-blocking</span>
</label>
<label class="checkbox_label" id="memory_prompt_builder_default" title="Extension will use the regular main prompt builder and add the summary request to it as the last system message.">
<label class="checkbox_label" id="memory_prompt_builder_default" data-i18n="[title]ext_sum_prompt_builder_3_desc" title="Extension will use the regular main prompt builder and add the summary request to it as the last system message.">
<input id="memory_prompt_builder_default" type="radio" name="memory_prompt_builder" value="0" />
<span>Classic, blocking</span>
<span data-i18n="ext_sum_prompt_builder_3">Classic, blocking</span>
</label>
</div>
<div data-summary-source="main">
<label for="memory_prompt" class="title_restorable">
<span data-i18n="Summary Prompt">Summary Prompt</span>
<div id="memory_prompt_restore" title="Restore default prompt" class="right_menu_button">
<div id="memory_prompt_restore" data-i18n="[title]ext_sum_restore_default_prompt_tip" title="Restore default prompt" class="right_menu_button">
<div class="fa-solid fa-clock-rotate-left"></div>
</div>
</label>
<textarea id="memory_prompt" class="text_pole textarea_compact" rows="6" placeholder="This prompt will be sent to AI to request the summary generation. &lcub;&lcub;words&rcub;&rcub; will resolve to the 'Number of words' parameter."></textarea>
<label for="memory_prompt_words">Target summary length (<span id="memory_prompt_words_value"></span> words)</label>
<textarea id="memory_prompt" class="text_pole textarea_compact" rows="6" data-i18n="[placeholder]ext_sum_prompt_placeholder" placeholder="This prompt will be sent to AI to request the summary generation. &lcub;&lcub;words&rcub;&rcub; will resolve to the 'Number of words' parameter."></textarea>
<label for="memory_prompt_words"><span data-i18n="ext_sum_target_length_1">Target summary length</span> <span data-i18n="ext_sum_target_length_2">(</span><span id="memory_prompt_words_value"></span><span data-i18n="ext_sum_target_length_3"> words)</span></label>
<input id="memory_prompt_words" type="range" value="{{defaultSettings.promptWords}}" min="{{defaultSettings.promptMinWords}}" max="{{defaultSettings.promptMaxWords}}" step="{{defaultSettings.promptWordsStep}}" />
<label for="memory_override_response_length">
API response length (<span id="memory_override_response_length_value"></span> tokens)
<small class="memory_disabled_hint">0 = default</small>
<span data-i18n="ext_sum_api_response_length_1">API response length</span> <span data-i18n="ext_sum_api_response_length_2">(</span><span id="memory_override_response_length_value"></span><span data-i18n="ext_sum_api_response_length_3"> tokens)</span>
<small class="memory_disabled_hint" data-i18n="ext_sum_0_default">0 = default</small>
</label>
<input id="memory_override_response_length" type="range" value="{{defaultSettings.overrideResponseLength}}" min="{{defaultSettings.overrideResponseLengthMin}}" max="{{defaultSettings.overrideResponseLengthMax}}" step="{{defaultSettings.overrideResponseLengthStep}}" />
<label for="memory_max_messages_per_request">
[Raw] Max messages per request (<span id="memory_max_messages_per_request_value"></span>)
<small class="memory_disabled_hint">0 = unlimited</small>
<span data-i18n="ext_sum_raw_max_msg">[Raw] Max messages per request</span> (<span id="memory_max_messages_per_request_value"></span>)
<small class="memory_disabled_hint" data-i18n="ext_sum_0_unlimited">0 = unlimited</small>
</label>
<input id="memory_max_messages_per_request" type="range" value="{{defaultSettings.maxMessagesPerRequest}}" min="{{defaultSettings.maxMessagesPerRequestMin}}" max="{{defaultSettings.maxMessagesPerRequestMax}}" step="{{defaultSettings.maxMessagesPerRequestStep}}" />
<h4 data-i18n="Update frequency" class="textAlignCenter">
@ -83,49 +83,49 @@
</h4>
<label for="memory_prompt_interval" class="title_restorable">
<span>
Update every <span id="memory_prompt_interval_value"></span> messages
<small class="memory_disabled_hint">0 = disable</small>
<span data-i18n="ext_sum_update_every_messages_1">Update every</span> <span id="memory_prompt_interval_value"></span><span data-i18n="ext_sum_update_every_messages_2"> messages</span>
<small class="memory_disabled_hint" data-i18n="ext_sum_0_disable">0 = disable</small>
</span>
<div id="memory_prompt_interval_auto" title="Try to automatically adjust the interval based on the chat metrics." class="right_menu_button">
<div id="memory_prompt_interval_auto" data-i18n="[title]ext_sum_auto_adjust_desc" title="Try to automatically adjust the interval based on the chat metrics." class="right_menu_button">
<div class="fa-solid fa-wand-magic-sparkles"></div>
</div>
</label>
<input id="memory_prompt_interval" type="range" value="{{defaultSettings.promptInterval}}" min="{{defaultSettings.promptMinInterval}}" max="{{defaultSettings.promptMaxInterval}}" step="{{defaultSettings.promptIntervalStep}}" />
<label for="memory_prompt_words_force" class="title_restorable">
<span>
Update every <span id="memory_prompt_words_force_value"></span> words
<small class="memory_disabled_hint">0 = disable</small>
<span data-i18n="ext_sum_update_every_words_1">Update every</span> <span id="memory_prompt_words_force_value"></span><span data-i18n="ext_sum_update_every_words_2"> words</span>
<small class="memory_disabled_hint" data-i18n="ext_sum_0_disable">0 = disable</small>
</span>
<div id="memory_prompt_words_auto" title="Try to automatically adjust the interval based on the chat metrics." class="right_menu_button">
<div id="memory_prompt_words_auto" data-i18n="[title]ext_sum_auto_adjust_desc" title="Try to automatically adjust the interval based on the chat metrics." class="right_menu_button">
<div class="fa-solid fa-wand-magic-sparkles"></div>
</div>
</label>
<input id="memory_prompt_words_force" type="range" value="{{defaultSettings.promptForceWords}}" min="{{defaultSettings.promptMinForceWords}}" max="{{defaultSettings.promptMaxForceWords}}" step="{{defaultSettings.promptForceWordsStep}}" />
<small>If both sliders are non-zero, then both will trigger summary updates at their respective intervals.</small>
<small data-i18n="ext_sum_both_sliders">If both sliders are non-zero, then both will trigger summary updates at their respective intervals.</small>
<hr>
</div>
<div class="memory_template">
<label for="memory_template">Injection Template</label>
<textarea id="memory_template" class="text_pole textarea_compact" rows="2" placeholder="&lcub;&lcub;summary&rcub;&rcub; will resolve to the current summary contents."></textarea>
<label for="memory_template" data-i18n="ext_sum_injection_template">Injection Template</label>
<textarea id="memory_template" class="text_pole textarea_compact" rows="2" data-i18n="[placeholder]ext_sum_memory_template_placeholder" placeholder="&lcub;&lcub;summary&rcub;&rcub; will resolve to the current summary contents."></textarea>
</div>
<label for="memory_position">Injection Position</label>
<label for="memory_position" data-i18n="ext_sum_injection_position">Injection Position</label>
<div class="radio_group">
<label>
<input type="radio" name="memory_position" value="2" />
Before Main Prompt / Story String
<span data-i18n="Before Main Prompt / Story String">Before Main Prompt / Story String</span>
</label>
<label>
<input type="radio" name="memory_position" value="0" />
After Main Prompt / Story String
<span data-i18n="After Main Prompt / Story String">After Main Prompt / Story String</span>
</label>
<label class="flex-container alignItemsCenter" title="How many messages before the current end of the chat." data-i18n="[title]How many messages before the current end of the chat.">
<input type="radio" name="memory_position" value="1" />
In-chat @ Depth <input id="memory_depth" class="text_pole widthUnset" type="number" min="0" max="999" />
as
<span data-i18n="In-chat @ Depth">In-chat @ Depth</span> <input id="memory_depth" class="text_pole widthUnset" type="number" min="0" max="999" />
<span data-i18n="as">as</span>
<select id="memory_role" class="text_pole widthNatural">
<option value="0">System</option>
<option value="1">User</option>
<option value="2">Assistant</option>
<option value="0" data-i18n="System">System</option>
<option value="1" data-i18n="User">User</option>
<option value="2" data-i18n="Assistant">Assistant</option>
</select>
</label>
</div>

View File

@ -30,7 +30,10 @@
<span>Ctrl+Enter to execute</span>
</label>
</div>
<textarea class="monospace" id="qr--modal-message"></textarea>
<div id="qr--modal-messageHolder">
<pre id="qr--modal-messageSyntax"><code id="qr--modal-messageSyntaxInner" class="hljs language-stscript"></code></pre>
<textarea class="monospace" id="qr--modal-message" spellcheck="false"></textarea>
</div>
</div>
</div>
@ -94,14 +97,27 @@
<h3>Testing</h3>
<div id="qr--modal-execute" class="menu_button" title="Execute the quick reply now">
<i class="fa-solid fa-play"></i>
Execute
<div id="qr--modal-executeButtons">
<div id="qr--modal-execute" class="qr--modal-executeButton menu_button" title="Execute the quick reply now">
<i class="fa-solid fa-play"></i>
Execute
</div>
<div id="qr--modal-pause" class="qr--modal-executeButton menu_button" title="Pause / continue execution">
<span class="qr--modal-executeComboIcon">
<i class="fa-solid fa-play"></i>
<i class="fa-solid fa-pause"></i>
</span>
</div>
<div id="qr--modal-stop" class="qr--modal-executeButton menu_button" title="Abort execution">
<i class="fa-solid fa-stop"></i>
</div>
</div>
<div id="qr--modal-executeProgress"></div>
<label class="checkbox_label">
<input type="checkbox" id="qr--modal-executeHide">
<span> Hide editor while executing</span>
</label>
<div id="qr--modal-executeErrors"></div>
<div id="qr--modal-executeResult"></div>
</div>
</div>

View File

@ -183,14 +183,16 @@ const init = async () => {
;
if (!qr) {
let [setName, ...qrName] = name.split('.');
name = qrName.join('.');
qrName = qrName.join('.');
let qrs = QuickReplySet.get(setName);
if (qrs) {
qr = qrs.qrList.find(it=>it.label == name);
qr = qrs.qrList.find(it=>it.label == qrName);
}
}
if (qr && qr.onExecute) {
return await qr.execute(args);
return await qr.execute(args, false, true);
} else {
throw new Error(`No Quick Reply found for "${name}".`);
}
};

View File

@ -1,5 +1,9 @@
import { POPUP_TYPE, Popup } from '../../../popup.js';
import { getSortableDelay } from '../../../utils.js';
import { setSlashCommandAutoComplete } from '../../../slash-commands.js';
import { SlashCommandAbortController } from '../../../slash-commands/SlashCommandAbortController.js';
import { SlashCommandParserError } from '../../../slash-commands/SlashCommandParserError.js';
import { SlashCommandScope } from '../../../slash-commands/SlashCommandScope.js';
import { debounce, getSortableDelay } from '../../../utils.js';
import { log, warn } from '../index.js';
import { QuickReplyContextLink } from './QuickReplyContextLink.js';
import { QuickReplySet } from './QuickReplySet.js';
@ -47,9 +51,14 @@ export class QuickReply {
/**@type {Popup}*/ editorPopup;
/**@type {HTMLElement}*/ editorExecuteBtn;
/**@type {HTMLElement}*/ editorExecuteBtnPause;
/**@type {HTMLElement}*/ editorExecuteBtnStop;
/**@type {HTMLElement}*/ editorExecuteProgress;
/**@type {HTMLElement}*/ editorExecuteErrors;
/**@type {HTMLElement}*/ editorExecuteResult;
/**@type {HTMLInputElement}*/ editorExecuteHide;
/**@type {Promise}*/ editorExecutePromise;
/**@type {SlashCommandAbortController}*/ abortController;
get hasContext() {
@ -225,15 +234,43 @@ export class QuickReply {
const updateWrap = () => {
if (wrap.checked) {
message.style.whiteSpace = 'pre-wrap';
messageSyntaxInner.style.whiteSpace = 'pre-wrap';
} else {
message.style.whiteSpace = 'pre';
messageSyntaxInner.style.whiteSpace = 'pre';
}
updateScrollDebounced();
};
const updateScroll = (evt) => {
let left = message.scrollLeft;
let top = message.scrollTop;
if (evt) {
evt.preventDefault();
left = message.scrollLeft + evt.deltaX;
top = message.scrollTop + evt.deltaY;
message.scrollTo({
behavior: 'instant',
left,
top,
});
}
messageSyntaxInner.scrollTo({
behavior: 'instant',
left,
top,
});
};
const updateScrollDebounced = updateScroll;
const updateSyntax = ()=>{
messageSyntaxInner.innerHTML = hljs.highlight(`${message.value}${message.value.slice(-1) == '\n' ? ' ' : ''}`, { language:'stscript', ignoreIllegals:true })?.value;
};
/**@type {HTMLInputElement}*/
const tabSize = dom.querySelector('#qr--modal-tabSize');
tabSize.value = JSON.parse(localStorage.getItem('qr--tabSize') ?? '4');
const updateTabSize = () => {
message.style.tabSize = tabSize.value;
messageSyntaxInner.style.tabSize = tabSize.value;
updateScrollDebounced();
};
tabSize.addEventListener('change', () => {
localStorage.setItem('qr--tabSize', JSON.stringify(Number(tabSize.value)));
@ -247,14 +284,15 @@ export class QuickReply {
});
/**@type {HTMLTextAreaElement}*/
const message = dom.querySelector('#qr--modal-message');
updateWrap();
updateTabSize();
message.value = this.message;
message.addEventListener('input', () => {
updateSyntax();
this.updateMessage(message.value);
updateScrollDebounced();
});
setSlashCommandAutoComplete(message, true);
//TODO move tab support for textarea into its own helper(?) and use for both this and .editor_maximize
message.addEventListener('keydown', (evt) => {
message.addEventListener('keydown', async(evt) => {
if (evt.key == 'Tab' && !evt.shiftKey && !evt.ctrlKey && !evt.altKey) {
evt.preventDefault();
const start = message.selectionStart;
@ -265,12 +303,12 @@ export class QuickReply {
message.value = `${message.value.substring(0, lineStart)}${message.value.substring(lineStart, end).replace(/\n/g, '\n\t')}${message.value.substring(end)}`;
message.selectionStart = start + 1;
message.selectionEnd = end + count;
this.updateMessage(message.value);
updateSyntax();
} else {
message.value = `${message.value.substring(0, start)}\t${message.value.substring(end)}`;
message.selectionStart = start + 1;
message.selectionEnd = end + 1;
this.updateMessage(message.value);
updateSyntax();
}
} else if (evt.key == 'Tab' && evt.shiftKey && !evt.ctrlKey && !evt.altKey) {
evt.preventDefault();
@ -281,15 +319,47 @@ export class QuickReply {
message.value = `${message.value.substring(0, lineStart)}${message.value.substring(lineStart, end).replace(/\n\t/g, '\n')}${message.value.substring(end)}`;
message.selectionStart = start - 1;
message.selectionEnd = end - count;
this.updateMessage(message.value);
updateSyntax();
} else if (evt.key == 'Enter' && evt.ctrlKey && !evt.shiftKey && !evt.altKey) {
evt.stopPropagation();
evt.preventDefault();
if (executeShortcut.checked) {
this.executeFromEditor();
const selectionStart = message.selectionStart;
const selectionEnd = message.selectionEnd;
message.blur();
await this.executeFromEditor();
if (document.activeElement != message) {
message.focus();
message.selectionStart = selectionStart;
message.selectionEnd = selectionEnd;
}
}
}
});
message.addEventListener('wheel', (evt)=>{
updateScrollDebounced(evt);
});
message.addEventListener('scroll', (evt)=>{
updateScrollDebounced();
});
/** @type {any} */
const resizeListener = debounce((evt) => {
updateSyntax();
updateScrollDebounced(evt);
if (document.activeElement == message) {
message.blur();
message.focus();
}
});
window.addEventListener('resize', resizeListener);
message.style.color = 'transparent';
message.style.background = 'transparent';
message.style.setProperty('text-shadow', 'none', 'important');
/**@type {HTMLElement}*/
const messageSyntaxInner = dom.querySelector('#qr--modal-messageSyntaxInner');
updateSyntax();
updateWrap();
updateTabSize();
// context menu
/**@type {HTMLTemplateElement}*/
@ -414,9 +484,15 @@ export class QuickReply {
this.updateContext();
});
/**@type {HTMLElement}*/
const executeProgress = dom.querySelector('#qr--modal-executeProgress');
this.editorExecuteProgress = executeProgress;
/**@type {HTMLElement}*/
const executeErrors = dom.querySelector('#qr--modal-executeErrors');
this.editorExecuteErrors = executeErrors;
/**@type {HTMLElement}*/
const executeResult = dom.querySelector('#qr--modal-executeResult');
this.editorExecuteResult = executeResult;
/**@type {HTMLInputElement}*/
const executeHide = dom.querySelector('#qr--modal-executeHide');
this.editorExecuteHide = executeHide;
@ -426,8 +502,30 @@ export class QuickReply {
executeBtn.addEventListener('click', async()=>{
await this.executeFromEditor();
});
/**@type {HTMLElement}*/
const executeBtnPause = dom.querySelector('#qr--modal-pause');
this.editorExecuteBtnPause = executeBtnPause;
executeBtnPause.addEventListener('click', async()=>{
if (this.abortController) {
if (this.abortController.signal.paused) {
this.abortController.continue('Continue button clicked');
this.editorExecuteProgress.classList.remove('qr--paused');
} else {
this.abortController.pause('Pause button clicked');
this.editorExecuteProgress.classList.add('qr--paused');
}
}
});
/**@type {HTMLElement}*/
const executeBtnStop = dom.querySelector('#qr--modal-stop');
this.editorExecuteBtnStop = executeBtnStop;
executeBtnStop.addEventListener('click', async()=>{
this.abortController?.abort('Stop button clicked');
});
await popupResult;
window.removeEventListener('resize', resizeListener);
} else {
warn('failed to fetch qrEditor template');
}
@ -436,21 +534,54 @@ export class QuickReply {
async executeFromEditor() {
if (this.editorExecutePromise) return;
this.editorExecuteBtn.classList.add('qr--busy');
this.editorExecuteProgress.style.setProperty('--prog', '0');
this.editorExecuteErrors.classList.remove('qr--hasErrors');
this.editorExecuteResult.classList.remove('qr--hasResult');
this.editorExecuteProgress.classList.remove('qr--error');
this.editorExecuteProgress.classList.remove('qr--success');
this.editorExecuteProgress.classList.remove('qr--paused');
this.editorExecuteProgress.classList.remove('qr--aborted');
this.editorExecuteErrors.innerHTML = '';
this.editorExecuteResult.innerHTML = '';
if (this.editorExecuteHide.checked) {
this.editorPopup.dom.classList.add('qr--hide');
}
try {
this.editorExecutePromise = this.execute();
await this.editorExecutePromise;
this.editorExecutePromise = this.execute({}, true);
const result = await this.editorExecutePromise;
if (this.abortController?.signal?.aborted) {
this.editorExecuteProgress.classList.add('qr--aborted');
} else {
this.editorExecuteResult.textContent = result?.toString();
this.editorExecuteResult.classList.add('qr--hasResult');
this.editorExecuteProgress.classList.add('qr--success');
}
this.editorExecuteProgress.classList.remove('qr--paused');
} catch (ex) {
this.editorExecuteErrors.textContent = ex.message;
this.editorExecuteErrors.classList.add('qr--hasErrors');
this.editorExecuteProgress.classList.add('qr--error');
this.editorExecuteProgress.classList.remove('qr--paused');
if (ex instanceof SlashCommandParserError) {
this.editorExecuteErrors.innerHTML = `
<div>${ex.message}</div>
<div>Line: ${ex.line} Column: ${ex.column}</div>
<pre style="text-align:left;">${ex.hint}</pre>
`;
} else {
this.editorExecuteErrors.innerHTML = `
<div>${ex.message}</div>
`;
}
}
this.editorExecutePromise = null;
this.editorExecuteBtn.classList.remove('qr--busy');
this.editorPopup.dom.classList.remove('qr--hide');
}
updateEditorProgress(done, total) {
this.editorExecuteProgress.style.setProperty('--prog', `${done / total * 100}`);
}
@ -526,12 +657,22 @@ export class QuickReply {
}
async execute(args = {}) {
async execute(args = {}, isEditor = false, isRun = false) {
if (this.message?.length > 0 && this.onExecute) {
const message = this.message.replace(/\{\{arg::([^}]+)\}\}/g, (_, key) => {
return args[key] ?? '';
const scope = new SlashCommandScope();
for (const key of Object.keys(args)) {
scope.setMacro(`arg::${key}`, args[key]);
}
if (isEditor) {
this.abortController = new SlashCommandAbortController();
}
return await this.onExecute(this, {
message:this.message,
isAutoExecute: args.isAutoExecute ?? false,
isEditor,
isRun,
scope,
});
return await this.onExecute(this, message, args.isAutoExecute ?? false);
}
}

View File

@ -1,5 +1,6 @@
import { getRequestHeaders, substituteParams } from '../../../../script.js';
import { executeSlashCommands } from '../../../slash-commands.js';
import { executeSlashCommands, executeSlashCommandsOnChatInput, executeSlashCommandsWithOptions } from '../../../slash-commands.js';
import { SlashCommandScope } from '../../../slash-commands/SlashCommandScope.js';
import { debounceAsync, warn } from '../index.js';
import { QuickReply } from './QuickReply.js';
@ -100,15 +101,29 @@ export class QuickReplySet {
/**
* @param {QuickReply} qr
* @param {String} [message] - optional altered message to be used
*
* @param {QuickReply} qr The QR to execute.
* @param {object} options
* @param {string} [options.message] (null) altered message to be used
* @param {boolean} [options.isAutoExecute] (false) whether the execution is triggered by auto execute
* @param {boolean} [options.isEditor] (false) whether the execution is triggered by the QR editor
* @param {boolean} [options.isRun] (false) whether the execution is triggered by /run or /: (window.executeQuickReplyByName)
* @param {SlashCommandScope} [options.scope] (null) scope to be used when running the command
* @returns
*/
async execute(qr, message = null, isAutoExecute = false) {
async executeWithOptions(qr, options = {}) {
options = Object.assign({
message:null,
isAutoExecute:false,
isEditor:false,
isRun:false,
scope:null,
}, options);
/**@type {HTMLTextAreaElement}*/
const ta = document.querySelector('#send_textarea');
const finalMessage = message ?? qr.message;
const finalMessage = options.message ?? qr.message;
let input = ta.value;
if (!isAutoExecute && this.injectInput && input.length > 0) {
if (!options.isAutoExecute && !options.isEditor && !options.isRun && this.injectInput && input.length > 0) {
if (this.placeBeforeInput) {
input = `${finalMessage} ${input}`;
} else {
@ -119,7 +134,24 @@ export class QuickReplySet {
}
if (input[0] == '/' && !this.disableSend) {
const result = await executeSlashCommands(input);
let result;
if (options.isAutoExecute || options.isRun) {
result = await executeSlashCommandsWithOptions(input, {
handleParserErrors: true,
scope: options.scope,
});
} else if (options.isEditor) {
result = await executeSlashCommandsWithOptions(input, {
handleParserErrors: false,
scope: options.scope,
abortController: qr.abortController,
onProgress: (done, total) => qr.updateEditorProgress(done, total),
});
} else {
result = await executeSlashCommandsOnChatInput(input, {
scope: options.scope,
});
}
return typeof result === 'object' ? result?.pipe : '';
}
@ -131,6 +163,18 @@ export class QuickReplySet {
document.querySelector('#send_but').click();
}
}
/**
* @param {QuickReply} qr
* @param {String} [message] - optional altered message to be used
* @param {SlashCommandScope} [scope] - optional scope to be used when running the command
*/
async execute(qr, message = null, isAutoExecute = false, scope = null) {
return this.executeWithOptions(qr, {
message,
isAutoExecute,
scope,
});
}
@ -152,7 +196,7 @@ export class QuickReplySet {
}
hookQuickReply(qr) {
qr.onExecute = (_, message, isAutoExecute)=>this.execute(qr, message, isAutoExecute);
qr.onExecute = (_, options)=>this.executeWithOptions(qr, options);
qr.onDelete = ()=>this.removeQuickReply(qr);
qr.onUpdate = ()=>this.save();
}

View File

@ -1,4 +1,6 @@
import { registerSlashCommand } from '../../../slash-commands.js';
import { SlashCommand } from '../../../slash-commands/SlashCommand.js';
import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from '../../../slash-commands/SlashCommandArgument.js';
import { SlashCommandParser } from '../../../slash-commands/SlashCommandParser.js';
import { isTrueBoolean } from '../../../utils.js';
// eslint-disable-next-line no-unused-vars
import { QuickReplyApi } from '../api/QuickReplyApi.js';
@ -17,46 +19,331 @@ export class SlashCommandHandler {
init() {
registerSlashCommand('qr', (_, value) => this.executeQuickReplyByIndex(Number(value)), [], '<span class="monospace">(number)</span> activates the specified Quick Reply', true, true);
registerSlashCommand('qrset', ()=>toastr.warning('The command /qrset has been deprecated. Use /qr-set, /qr-set-on, and /qr-set-off instead.'), [], '<strong>DEPRECATED</strong> The command /qrset has been deprecated. Use /qr-set, /qr-set-on, and /qr-set-off instead.', true, true);
registerSlashCommand('qr-set', (args, value)=>this.toggleGlobalSet(value, args), [], '<span class="monospace">[visible=true] (number)</span> toggle global QR set', true, true);
registerSlashCommand('qr-set-on', (args, value)=>this.addGlobalSet(value, args), [], '<span class="monospace">[visible=true] (number)</span> activate global QR set', true, true);
registerSlashCommand('qr-set-off', (_, value)=>this.removeGlobalSet(value), [], '<span class="monospace">(number)</span> deactivate global QR set', true, true);
registerSlashCommand('qr-chat-set', (args, value)=>this.toggleChatSet(value, args), [], '<span class="monospace">[visible=true] (number)</span> toggle chat QR set', true, true);
registerSlashCommand('qr-chat-set-on', (args, value)=>this.addChatSet(value, args), [], '<span class="monospace">[visible=true] (number)</span> activate chat QR set', true, true);
registerSlashCommand('qr-chat-set-off', (_, value)=>this.removeChatSet(value), [], '<span class="monospace">(number)</span> deactivate chat QR set', true, true);
registerSlashCommand('qr-set-list', (_, value)=>this.listSets(value ?? 'all'), [], '(all|global|chat) gets a list of the names of all quick reply sets', true, true);
registerSlashCommand('qr-list', (_, value)=>this.listQuickReplies(value), [], '(set name) gets a list of the names of all quick replies in this quick reply set', true, true);
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'qr',
callback: (_, value) => this.executeQuickReplyByIndex(Number(value)),
unnamedArgumentList: [
new SlashCommandArgument(
'number', [ARGUMENT_TYPE.NUMBER], true,
),
],
helpString: 'Activates the specified Quick Reply',
}));
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'qrset',
callback: () => toastr.warning('The command /qrset has been deprecated. Use /qr-set, /qr-set-on, and /qr-set-off instead.'),
helpString: '<strong>DEPRECATED</strong> The command /qrset has been deprecated. Use /qr-set, /qr-set-on, and /qr-set-off instead.',
}));
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'qr-set',
callback: (args, value) => this.toggleGlobalSet(value, args),
namedArgumentList: [
new SlashCommandNamedArgument(
'visible', 'set visibility', [ARGUMENT_TYPE.BOOLEAN], false, false, 'true',
),
],
unnamedArgumentList: [
new SlashCommandArgument(
'QR set name', [ARGUMENT_TYPE.STRING], true,
),
],
helpString: 'Toggle global QR set',
}));
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'qr-set-on',
callback: (args, value) => this.addGlobalSet(value, args),
namedArgumentList: [
new SlashCommandNamedArgument(
'visible', 'set visibility', [ARGUMENT_TYPE.BOOLEAN], false, false, 'true',
),
],
unnamedArgumentList: [
new SlashCommandArgument(
'QR set name', [ARGUMENT_TYPE.STRING], true,
),
],
helpString: 'Activate global QR set',
}));
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'qr-set-off',
callback: (_, value) => this.removeGlobalSet(value),
unnamedArgumentList: [
new SlashCommandArgument(
'QR set name', [ARGUMENT_TYPE.STRING], true,
),
],
helpString: 'Deactivate global QR set',
}));
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'qr-chat-set',
callback: (args, value) => this.toggleChatSet(value, args),
namedArgumentList: [
new SlashCommandNamedArgument(
'visible', 'set visibility', [ARGUMENT_TYPE.BOOLEAN], false, false, 'true',
),
],
unnamedArgumentList: [
new SlashCommandArgument(
'QR set name', [ARGUMENT_TYPE.STRING], true,
),
],
helpString: 'Toggle chat QR set',
}));
const qrArgs = `
label - string - text on the button, e.g., label=MyButton
set - string - name of the QR set, e.g., set=PresetName1
hidden - bool - whether the button should be hidden, e.g., hidden=true
startup - bool - auto execute on app startup, e.g., startup=true
user - bool - auto execute on user message, e.g., user=true
bot - bool - auto execute on AI message, e.g., bot=true
load - bool - auto execute on chat load, e.g., load=true
group - bool - auto execute on group member selection, e.g., group=true
title - string - title / tooltip to be shown on button, e.g., title="My Fancy Button"
`.trim();
const qrUpdateArgs = `
newlabel - string - new text for the button, e.g. newlabel=MyRenamedButton
${qrArgs}
`.trim();
registerSlashCommand('qr-create', (args, message)=>this.createQuickReply(args, message), [], `<span class="monospace" style="white-space:pre-line;">[arguments] (message)\n arguments:\n ${qrArgs}</span> creates a new Quick Reply, example: <tt>/qr-create set=MyPreset label=MyButton /echo 123</tt>`, true, true);
registerSlashCommand('qr-update', (args, message)=>this.updateQuickReply(args, message), [], `<span class="monospace" style="white-space:pre-line;">[arguments] (message)\n arguments:\n ${qrUpdateArgs}</span> updates Quick Reply, example: <tt>/qr-update set=MyPreset label=MyButton newlabel=MyRenamedButton /echo 123</tt>`, true, true);
registerSlashCommand('qr-delete', (args, name)=>this.deleteQuickReply(args, name), [], '<span class="monospace">set=string [label]</span> deletes Quick Reply', true, true);
registerSlashCommand('qr-contextadd', (args, name)=>this.createContextItem(args, name), [], '<span class="monospace">set=string label=string [chain=false] (preset name)</span> add context menu preset to a QR, example: <tt>/qr-contextadd set=MyPreset label=MyButton chain=true MyOtherPreset</tt>', true, true);
registerSlashCommand('qr-contextdel', (args, name)=>this.deleteContextItem(args, name), [], '<span class="monospace">set=string label=string (preset name)</span> remove context menu preset from a QR, example: <tt>/qr-contextdel set=MyPreset label=MyButton MyOtherPreset</tt>', true, true);
registerSlashCommand('qr-contextclear', (args, label)=>this.clearContextMenu(args, label), [], '<span class="monospace">set=string (label)</span> remove all context menu presets from a QR, example: <tt>/qr-contextclear set=MyPreset MyButton</tt>', true, true);
const presetArgs = `
nosend - bool - disable send / insert in user input (invalid for slash commands)
before - bool - place QR before user input
inject - bool - inject user input automatically (if disabled use {{input}})
`.trim();
registerSlashCommand('qr-set-create', (args, name)=>this.createSet(name, args), ['qr-presetadd'], `<span class="monospace" style="white-space:pre-line;">[arguments] (name)\n arguments:\n ${presetArgs}</span> create a new preset (overrides existing ones), example: <tt>/qr-set-add MyNewPreset</tt>`, true, true);
registerSlashCommand('qr-set-update', (args, name)=>this.updateSet(name, args), ['qr-presetupdate'], `<span class="monospace" style="white-space:pre-line;">[arguments] (name)\n arguments:\n ${presetArgs}</span> update an existing preset, example: <tt>/qr-set-update enabled=false MyPreset</tt>`, true, true);
registerSlashCommand('qr-set-delete', (args, name)=>this.deleteSet(name), ['qr-presetdelete'], `<span class="monospace" style="white-space:pre-line;">(name)\n arguments:\n ${presetArgs}</span> delete an existing preset, example: <tt>/qr-set-delete MyPreset</tt>`, true, true);
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'qr-chat-set-on',
callback: (args, value) => this.addChatSet(value, args),
namedArgumentList: [
new SlashCommandNamedArgument(
'visible', 'whether the QR set should be visible', [ARGUMENT_TYPE.BOOLEAN], false, false, 'true', ['true', 'false'],
),
],
unnamedArgumentList: [
new SlashCommandArgument(
'QR set name', [ARGUMENT_TYPE.STRING], true,
),
],
helpString: 'Activate chat QR set',
}));
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'qr-chat-set-off',
callback: (_, value) => this.removeChatSet(value),
unnamedArgumentList: [
new SlashCommandArgument(
'QR set name', [ARGUMENT_TYPE.STRING], true,
),
],
helpString: 'Deactivate chat QR set',
}));
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'qr-set-list',
callback: (_, value) => this.listSets(value ?? 'all'),
returns: 'list of QR sets',
namedArgumentList: [],
unnamedArgumentList: [
new SlashCommandArgument(
'set type', [ARGUMENT_TYPE.STRING], false, false, null, ['all', 'global', 'chat'],
),
],
helpString: 'Gets a list of the names of all quick reply sets.',
}));
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'qr-list',
callback: (_, value) => this.listQuickReplies(value),
returns: 'list of QRs',
namedArgumentList: [],
unnamedArgumentList: [
new SlashCommandArgument(
'set name', [ARGUMENT_TYPE.STRING], true,
),
],
helpString: 'Gets a list of the names of all quick replies in this quick reply set.',
}));
const qrArgs = [
new SlashCommandNamedArgument('label', 'text on the button, e.g., label=MyButton', [ARGUMENT_TYPE.STRING]),
new SlashCommandNamedArgument('set', 'name of the QR set, e.g., set=PresetName1', [ARGUMENT_TYPE.STRING]),
new SlashCommandNamedArgument('hidden', 'whether the button should be hidden, e.g., hidden=true', [ARGUMENT_TYPE.BOOLEAN], false, false, 'false'),
new SlashCommandNamedArgument('startup', 'auto execute on app startup, e.g., startup=true', [ARGUMENT_TYPE.BOOLEAN], false, false, 'false'),
new SlashCommandNamedArgument('user', 'auto execute on user message, e.g., user=true', [ARGUMENT_TYPE.BOOLEAN], false, false, 'false'),
new SlashCommandNamedArgument('bot', 'auto execute on AI message, e.g., bot=true', [ARGUMENT_TYPE.BOOLEAN], false, false, 'false'),
new SlashCommandNamedArgument('load', 'auto execute on chat load, e.g., load=true', [ARGUMENT_TYPE.BOOLEAN], false, false, 'false'),
new SlashCommandNamedArgument('group', 'auto execute on group member selection, e.g., group=true', [ARGUMENT_TYPE.BOOLEAN], false, false, 'false'),
new SlashCommandNamedArgument('title', 'title / tooltip to be shown on button, e.g., title="My Fancy Button"', [ARGUMENT_TYPE.STRING], false),
];
const qrUpdateArgs = [
new SlashCommandNamedArgument('newlabel', 'new text for the button', [ARGUMENT_TYPE.STRING], false),
];
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'qr-create',
callback: (args, message) => this.createQuickReply(args, message),
namedArgumentList: qrArgs,
unnamedArgumentList: [
new SlashCommandArgument(
'command', [ARGUMENT_TYPE.STRING], true,
),
],
helpString: `
<div>Creates a new Quick Reply.</div>
<div>
<strong>Example:</strong>
<ul>
<li>
<pre><code>/qr-create set=MyPreset label=MyButton /echo 123</code></pre>
</li>
</ul>
</div>
`,
}));
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'qr-update',
callback: (args, message) => this.updateQuickReply(args, message),
returns: 'updated quick reply',
namedArgumentList: [...qrUpdateArgs, ...qrArgs],
helpString: `
<div>
Updates Quick Reply.
</div>
<div>
<strong>Example:</strong>
<ul>
<li>
<pre><code>/qr-update set=MyPreset label=MyButton newlabel=MyRenamedButton /echo 123</code></pre>
</li>
</ul>
</div>
`,
}));
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'qr-delete',
callback: (args, name) => this.deleteQuickReply(args, name),
namedArgumentList: [
new SlashCommandNamedArgument(
'set', 'Quick Reply set', [ARGUMENT_TYPE.STRING], true,
),
new SlashCommandNamedArgument(
'label', 'Quick Reply label', [ARGUMENT_TYPE.STRING], false,
),
],
helpString: 'Deletes a Quick Reply from the specified set. If no label is provided, the entire set is deleted.',
}));
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'qr-contextadd',
callback: (args, name) => this.createContextItem(args, name),
namedArgumentList: [
new SlashCommandNamedArgument(
'set', 'string', [ARGUMENT_TYPE.STRING], true,
),
new SlashCommandNamedArgument(
'label', 'string', [ARGUMENT_TYPE.STRING], true,
),
new SlashCommandNamedArgument(
'chain', 'boolean', [ARGUMENT_TYPE.BOOLEAN], false, false, 'false',
),
],
unnamedArgumentList: [
new SlashCommandArgument(
'preset name', [ARGUMENT_TYPE.STRING], true,
),
],
helpString: `
<div>
Add context menu preset to a QR.
</div>
<div>
<strong>Example:</strong>
<ul>
<li>
<pre><code>/qr-contextadd set=MyPreset label=MyButton chain=true MyOtherPreset</code></pre>
</li>
</ul>
</div>
`,
}));
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'qr-contextdel',
callback: (args, name) => this.deleteContextItem(args, name),
namedArgumentList: [
new SlashCommandNamedArgument(
'set', 'string', [ARGUMENT_TYPE.STRING], true,
),
new SlashCommandNamedArgument(
'label', 'string', [ARGUMENT_TYPE.STRING], true,
),
],
unnamedArgumentList: [
new SlashCommandArgument(
'preset name', [ARGUMENT_TYPE.STRING], true,
),
],
helpString: `
<div>
Remove context menu preset from a QR.
</div>
<div>
<strong>Example:</strong>
<ul>
<li>
<pre><code>/qr-contextdel set=MyPreset label=MyButton MyOtherPreset</code></pre>
</li>
</ul>
</div>
`,
}));
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'qr-contextclear',
callback: (args, label) => this.clearContextMenu(args, label),
namedArgumentList: [
new SlashCommandNamedArgument(
'set', 'context menu preset name', [ARGUMENT_TYPE.STRING], true,
),
],
unnamedArgumentList: [
new SlashCommandArgument(
'label', [ARGUMENT_TYPE.STRING], true,
),
],
helpString: `
<div>
Remove all context menu presets from a QR.
</div>
<div>
<strong>Example:</strong>
<ul>
<li>
<pre><code>/qr-contextclear set=MyPreset MyButton</code></pre>
</li>
</ul>
</div>
`,
}));
const presetArgs = [
new SlashCommandNamedArgument('nosend', 'disable send / insert in user input (invalid for slash commands)', [ARGUMENT_TYPE.BOOLEAN], false),
new SlashCommandNamedArgument('before', 'place QR before user input', [ARGUMENT_TYPE.BOOLEAN], false),
new SlashCommandNamedArgument('inject', 'inject user input automatically (if disabled use {{input}})', [ARGUMENT_TYPE.BOOLEAN], false),
];
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'qr-set-create',
callback: (args, name) => this.createSet(name, args),
aliases: ['qr-presetadd'],
namedArgumentList: presetArgs,
unnamedArgumentList: [
new SlashCommandArgument(
'name', [ARGUMENT_TYPE.STRING], true,
),
],
helpString: `
<div>
Create a new preset (overrides existing ones).
</div>
<div>
<strong>Example:</strong>
<ul>
<li>
<pre><code>/qr-set-add MyNewPreset</code></pre>
</li>
</ul>
</div>
`,
}));
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'qr-set-update',
callback: (args, name) => this.updateSet(name, args),
aliases: ['qr-presetupdate'],
namedArgumentList: presetArgs,
unnamedArgumentList: [
new SlashCommandArgument('name', [ARGUMENT_TYPE.STRING], true),
],
helpString: `
<div>
Update an existing preset.
</div>
<div>
<strong>Example:</strong>
<pre><code>/qr-set-update enabled=false MyPreset</code></pre>
</div>
`,
}));
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'qr-set-delete',
callback: (args, name) => this.deleteSet(name),
aliases: ['qr-presetdelete'],
unnamedArgumentList: [
new SlashCommandArgument('name', [ARGUMENT_TYPE.STRING], true),
],
helpString: `
<div>
Delete an existing preset.
</div>
<div>
<strong>Example:</strong>
<pre><code>/qr-set-delete MyPreset</code></pre>
</div>
`,
}));
}

View File

@ -357,6 +357,7 @@ export class SettingsUi {
a.download = `${this.currentQrSet.name}.json`;
a.click();
}
URL.revokeObjectURL(url);
}
selectQrSet(qrs) {

View File

@ -209,6 +209,10 @@
justify-content: center;
padding-bottom: 0.5em;
}
#qr--qrOptions {
display: flex;
flex-direction: column;
}
#qr--qrOptions > #qr--ctxEditor .qr--ctxItem {
display: flex;
flex-direction: row;
@ -218,12 +222,17 @@
@media screen and (max-width: 750px) {
body .dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor {
flex-direction: column;
overflow: auto;
}
body .dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor > #qr--main {
flex: 0 0 auto;
}
body .dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor > #qr--main > .qr--labels {
flex-direction: column;
}
body .dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor > #qr--main > .qr--modal-messageContainer > #qr--modal-message {
min-height: 90svh;
body .dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor > #qr--main > .qr--modal-messageContainer > #qr--modal-messageHolder {
min-height: 50svh;
height: 50svh;
}
}
.dialogue_popup:has(#qr--modalEditor) {
@ -238,11 +247,13 @@
display: flex;
flex-direction: row;
gap: 1em;
overflow: hidden;
}
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor > #qr--main {
flex: 1 1 auto;
display: flex;
flex-direction: column;
overflow: hidden;
}
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor > #qr--main > .qr--labels {
flex: 0 0 auto;
@ -268,6 +279,7 @@
flex: 1 1 auto;
display: flex;
flex-direction: column;
overflow: hidden;
}
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor > #qr--main > .qr--modal-messageContainer > .qr--modal-editorSettings {
display: flex;
@ -283,17 +295,167 @@
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor > #qr--main > .qr--modal-messageContainer > .qr--modal-editorSettings > .checkbox_label > input {
font-size: inherit;
}
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor > #qr--main > .qr--modal-messageContainer > #qr--modal-message {
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor > #qr--main > .qr--modal-messageContainer > #qr--modal-messageHolder {
flex: 1 1 auto;
display: grid;
text-align: left;
overflow: hidden;
}
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor #qr--modal-execute {
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor > #qr--main > .qr--modal-messageContainer > #qr--modal-messageHolder > #qr--modal-messageSyntax {
grid-column: 1;
grid-row: 1;
padding: 0;
margin: 0;
border: none;
overflow: hidden;
min-width: 100%;
width: 0;
}
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor > #qr--main > .qr--modal-messageContainer > #qr--modal-messageHolder > #qr--modal-messageSyntax > #qr--modal-messageSyntaxInner {
height: 100%;
}
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor > #qr--main > .qr--modal-messageContainer > #qr--modal-messageHolder > #qr--modal-message {
grid-column: 1;
grid-row: 1;
caret-color: white;
mix-blend-mode: difference;
}
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor > #qr--main > .qr--modal-messageContainer > #qr--modal-messageHolder > #qr--modal-message::-webkit-scrollbar,
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor > #qr--main > .qr--modal-messageContainer > #qr--modal-messageHolder > #qr--modal-message::-webkit-scrollbar-thumb {
visibility: hidden;
cursor: default;
}
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor > #qr--main > .qr--modal-messageContainer > #qr--modal-messageHolder #qr--modal-message,
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor > #qr--main > .qr--modal-messageContainer > #qr--modal-messageHolder #qr--modal-messageSyntaxInner {
padding: 0.75em;
margin: 0;
border: none;
resize: none;
line-height: 1.2;
border: 1px solid var(--SmartThemeBorderColor);
border-radius: 5px;
}
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor #qr--modal-executeButtons {
display: flex;
gap: 1em;
}
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor #qr--modal-executeButtons .qr--modal-executeButton {
border-width: 2px;
border-style: solid;
display: flex;
flex-direction: row;
gap: 0.5em;
padding: 0.5em 0.75em;
}
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor #qr--modal-execute.qr--busy {
opacity: 0.5;
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor #qr--modal-executeButtons .qr--modal-executeButton .qr--modal-executeComboIcon {
display: flex;
}
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor #qr--modal-executeButtons #qr--modal-execute {
transition: 200ms;
filter: grayscale(0);
}
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor #qr--modal-executeButtons #qr--modal-execute.qr--busy {
cursor: wait;
opacity: 0.5;
filter: grayscale(1);
}
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor #qr--modal-executeButtons #qr--modal-execute {
border-color: #51a351;
}
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor #qr--modal-executeButtons #qr--modal-pause,
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor #qr--modal-executeButtons #qr--modal-stop {
cursor: default;
opacity: 0.5;
filter: grayscale(1);
pointer-events: none;
}
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor #qr--modal-executeButtons .qr--busy ~ #qr--modal-pause,
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor #qr--modal-executeButtons .qr--busy ~ #qr--modal-stop {
cursor: pointer;
opacity: 1;
filter: grayscale(0);
pointer-events: all;
}
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor #qr--modal-executeButtons #qr--modal-pause {
border-color: #92befc;
}
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor #qr--modal-executeButtons #qr--modal-stop {
border-color: #d78872;
}
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor #qr--modal-executeProgress {
--prog: 0;
--progColor: #92befc;
--progFlashColor: #d78872;
--progSuccessColor: #51a351;
--progErrorColor: #bd362f;
--progAbortedColor: #d78872;
height: 0.5em;
background-color: var(--black50a);
position: relative;
}
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor #qr--modal-executeProgress:after {
content: '';
background-color: var(--progColor);
position: absolute;
inset: 0;
right: calc(100% - var(--prog) * 1%);
transition: 200ms;
}
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor #qr--modal-executeProgress.qr--paused:after {
animation-name: qr--progressPulse;
animation-duration: 1500ms;
animation-timing-function: ease-in-out;
animation-delay: 0s;
animation-iteration-count: infinite;
}
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor #qr--modal-executeProgress.qr--aborted:after {
background-color: var(--progAbortedColor);
}
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor #qr--modal-executeProgress.qr--success:after {
background-color: var(--progSuccessColor);
}
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor #qr--modal-executeProgress.qr--error:after {
background-color: var(--progErrorColor);
}
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor #qr--modal-executeErrors {
display: none;
text-align: left;
font-size: smaller;
background-color: #bd362f;
color: white;
padding: 0.5em;
overflow: auto;
min-width: 100%;
width: 0;
}
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor #qr--modal-executeErrors.qr--hasErrors {
display: block;
}
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor #qr--modal-executeResult {
display: none;
text-align: left;
font-size: smaller;
background-color: #51a351;
color: white;
padding: 0.5em;
overflow: auto;
min-width: 100%;
width: 0;
}
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor #qr--modal-executeResult.qr--hasResult {
display: block;
}
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor #qr--modal-executeResult:before {
content: 'Result: ';
}
@keyframes qr--progressPulse {
0%,
100% {
background-color: var(--progColor);
}
50% {
background-color: var(--progFlashColor);
}
}
.shadow_popup.qr--hide {
opacity: 0 !important;

View File

@ -229,6 +229,8 @@
#qr--qrOptions {
display: flex;
flex-direction: column;
> #qr--ctxEditor {
.qr--ctxItem {
display: flex;
@ -244,11 +246,16 @@
@media screen and (max-width: 750px) {
body .dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor {
flex-direction: column;
overflow: auto;
> #qr--main {
flex: 0 0 auto;
}
> #qr--main > .qr--labels {
flex-direction: column;
}
> #qr--main > .qr--modal-messageContainer > #qr--modal-message {
min-height: 90svh;
> #qr--main > .qr--modal-messageContainer > #qr--modal-messageHolder {
min-height: 50svh;
height: 50svh;
}
}
}
@ -264,11 +271,13 @@
display: flex;
flex-direction: row;
gap: 1em;
overflow: hidden;
> #qr--main {
flex: 1 1 auto;
display: flex;
flex-direction: column;
overflow: hidden;
> .qr--labels {
flex: 0 0 auto;
display: flex;
@ -293,6 +302,7 @@
flex: 1 1 auto;
display: flex;
flex-direction: column;
overflow: hidden;
> .qr--modal-editorSettings {
display: flex;
flex-direction: row;
@ -307,24 +317,169 @@
}
}
}
> #qr--modal-message {
> #qr--modal-messageHolder {
flex: 1 1 auto;
display: grid;
text-align: left;
overflow: hidden;
> #qr--modal-messageSyntax {
grid-column: 1;
grid-row: 1;
padding: 0;
margin: 0;
border: none;
overflow: hidden;
min-width: 100%;
width: 0;
> #qr--modal-messageSyntaxInner {
height: 100%;
}
}
> #qr--modal-message {
grid-column: 1;
grid-row: 1;
caret-color: white;
mix-blend-mode: difference;
&::-webkit-scrollbar, &::-webkit-scrollbar-thumb {
visibility: hidden;
cursor: default;
}
}
#qr--modal-message, #qr--modal-messageSyntaxInner {
padding: 0.75em;
margin: 0;
border: none;
resize: none;
line-height: 1.2;
border: 1px solid var(--SmartThemeBorderColor);
border-radius: 5px;
}
}
}
}
#qr--modal-execute {
#qr--modal-executeButtons {
display: flex;
flex-direction: row;
gap: 0.5em;
&.qr--busy {
opacity: 0.5;
cursor: wait;
gap: 1em;
.qr--modal-executeButton {
border-width: 2px;
border-style: solid;
display: flex;
flex-direction: row;
gap: 0.5em;
padding: 0.5em 0.75em;
.qr--modal-executeComboIcon {
display: flex;
}
}
#qr--modal-execute {
transition: 200ms;
filter: grayscale(0);
&.qr--busy {
cursor: wait;
opacity: 0.5;
filter: grayscale(1);
}
}
#qr--modal-execute {
border-color: rgb(81, 163, 81);
}
#qr--modal-pause, #qr--modal-stop {
cursor: default;
opacity: 0.5;
filter: grayscale(1);
pointer-events: none;
}
.qr--busy {
~ #qr--modal-pause, ~ #qr--modal-stop {
cursor: pointer;
opacity: 1;
filter: grayscale(0);
pointer-events: all;
}
}
#qr--modal-pause {
border-color: rgb(146, 190, 252);
}
#qr--modal-stop {
border-color: rgb(215, 136, 114);
}
}
#qr--modal-executeProgress {
--prog: 0;
--progColor: rgb(146, 190, 252);
--progFlashColor: rgb(215, 136, 114);
--progSuccessColor: rgb(81, 163, 81);
--progErrorColor: rgb(189, 54, 47);
--progAbortedColor: rgb(215, 136, 114);
height: 0.5em;
background-color: var(--black50a);
position: relative;
&:after {
content: '';
background-color: var(--progColor);
position: absolute;
inset: 0;
right: calc(100% - var(--prog) * 1%);
transition: 200ms;
}
&.qr--paused:after {
animation-name: qr--progressPulse;
animation-duration: 1500ms;
animation-timing-function: ease-in-out;
animation-delay: 0s;
animation-iteration-count: infinite;
}
&.qr--aborted:after {
background-color: var(--progAbortedColor);
}
&.qr--success:after {
background-color: var(--progSuccessColor);
}
&.qr--error:after {
background-color: var(--progErrorColor);
}
}
#qr--modal-executeErrors {
display: none;
&.qr--hasErrors {
display: block;
}
text-align: left;
font-size: smaller;
background-color: rgb(189, 54, 47);
color: white;
padding: 0.5em;
overflow: auto;
min-width: 100%;
width: 0;
}
#qr--modal-executeResult {
display: none;
&.qr--hasResult {
display: block;
}
&:before { content: 'Result: '; }
text-align: left;
font-size: smaller;
background-color: rgb(81, 163, 81);
color: white;
padding: 0.5em;
overflow: auto;
min-width: 100%;
width: 0;
}
}
}
}
@keyframes qr--progressPulse {
0%, 100% {
background-color: var(--progColor);
}
50% {
background-color: var(--progFlashColor);
}
}
.shadow_popup.qr--hide {
opacity: 0 !important;

View File

@ -8,16 +8,16 @@
<div class="flex-container">
<div id="open_regex_editor" class="menu_button">
<i class="fa-solid fa-pen-to-square"></i>
<span>Open Editor</span>
<span data-i18n="ext_regex_open_editor">Open Editor</span>
</div>
<div id="import_regex" class="menu_button">
<i class="fa-solid fa-file-import"></i>
<span>Import Script</span>
<span data-i18n="ext_regex_import_script">Import Script</span>
</div>
<input type="file" id="import_regex_file" hidden accept="*.json" />
<input type="file" id="import_regex_file" hidden accept="*.json" multiple />
</div>
<hr />
<label>Saved Scripts</label>
<label data-i18n="ext_regex_saved_scripts">Saved Scripts</label>
<div id="saved_regex_scripts" class="flex-container regex-script-container flexFlowColumn"></div>
</div>
</div>

View File

@ -11,7 +11,7 @@
</div>
</h3>
<small class="flex-container extensions_info">
<small class="flex-container extensions_info" data-i18n="ext_regex_desc">
Regex is a tool to find/replace strings using regular expressions. If you want to learn more, click on the ? next to the title.
</small>
<hr />
@ -21,13 +21,13 @@
<label class="title_restorable" for="regex_test_input">
<small data-i18n="Input">Input</small>
</label>
<textarea id="regex_test_input" class="text_pole textarea_compact" rows="4" placeholder="Type here..."></textarea>
<textarea id="regex_test_input" class="text_pole textarea_compact" rows="4" data-i18n="[placeholder]ext_regex_test_input_placeholder" placeholder="Type here..."></textarea>
</div>
<div class="flex1">
<label class="title_restorable" for="regex_test_output">
<small data-i18n="Output">Output</small>
</label>
<textarea id="regex_test_output" class="text_pole textarea_compact" rows="4" placeholder="Empty" readonly></textarea>
<textarea id="regex_test_output" class="text_pole textarea_compact" rows="4" data-i18n="[placeholder]ext_regex_output_placeholder" placeholder="Empty" readonly></textarea>
</div>
<hr>
</div>
@ -56,6 +56,7 @@
<div>
<textarea
class="regex_replace_string text_pole wide100p textarea_compact"
data-i18n="[placeholder]ext_regex_replace_string_placeholder"
placeholder="Use {{match}} to include the matched text from the Find Regex or $1, $2, etc. for capture groups."
rows="2"
></textarea>
@ -67,7 +68,7 @@
</label>
<div>
<textarea
class="regex_trim_strings text_pole wide100p textarea_compact"
class="regex_trim_strings text_pole wide100p textarea_compact" data-i18n="[placeholder]ext_regex_trim_placeholder"
placeholder="Globally trims any unwanted parts from a regex match before replacement. Separate each element by an enter."
rows="3"
></textarea>
@ -77,53 +78,59 @@
<div class="flex-container">
<div class="flex1 wi-enter-footer-text flex-container flexFlowColumn flexNoGap alignitemsstart">
<small>Affects</small>
<div>
<small data-i18n="ext_regex_affects">Affects</small>
<div title="Messages sent by the user.">
<label class="checkbox flex-container">
<input type="checkbox" name="replace_position" value="1">
<span data-i18n="Before Char">User Input</span>
<span data-i18n="ext_regex_user_input">User Input</span>
</label>
</div>
<div>
<div title="Messages received from the Generation API.">
<label class="checkbox flex-container">
<input type="checkbox" name="replace_position" value="2">
<span data-i18n="After Char">AI Output</span>
<span data-i18n="ext_regex_ai_output">AI Output</span>
</label>
</div>
<div>
<div title="Messages sent using STscript commands.">
<label class="checkbox flex-container">
<input type="checkbox" name="replace_position" value="3">
<span data-i18n="Slash Commands">Slash Commands</span>
</label>
</div>
<div title="Lorebook/World Info entry contents. Requires 'Only Format Prompt' to be checked!">
<label class="checkbox flex-container">
<input type="checkbox" name="replace_position" value="5">
<span data-i18n="World Info">World Info</span>
</label>
</div>
<div class="flex-container wide100p marginTop5">
<div class="flex1 flex-container flexNoGap">
<small title="When applied to prompts or display, only affect messages that are at least N levels deep. 0 = last message, 1 = penultimate message, etc. Only counts usable messages, i.e. not hidden or system.">
<small data-i18n="[title]ext_regex_min_depth_desc" title="When applied to prompts or display, only affect messages that are at least N levels deep. 0 = last message, 1 = penultimate message, etc. Only counts WI entries @Depth and usable messages, i.e. not hidden or system.">
<span data-i18n="Min Depth">Min Depth</span>
<span class="fa-solid fa-circle-question note-link-span"></span>
</small>
<input name="min_depth" class="text_pole textarea_compact" type="number" min="0" max="999" placeholder="Unlimited" />
<input name="min_depth" class="text_pole textarea_compact" type="number" min="0" max="999" data-i18n="[placeholder]ext_regex_min_depth_placeholder" placeholder="Unlimited" />
</div>
<div class="flex1 flex-container flexNoGap">
<small title="When applied to prompts or display, only affect messages no more than N levels deep. 0 = last message, 1 = penultimate message, etc. Only counts usable messages, i.e. not hidden or system.">
<small data-i18n="[title]ext_regex_max_depth_desc" title="When applied to prompts or display, only affect messages no more than N levels deep. 0 = last message, 1 = penultimate message, etc. Only counts WI entries @Depth and usable messages, i.e. not hidden or system.">
<span data-i18n="Max Depth">Max Depth</span>
<span class="fa-solid fa-circle-question note-link-span"></span>
</small>
<input name="max_depth" class="text_pole textarea_compact" type="number" min="0" max="999" placeholder="Unlimited" />
<input name="max_depth" class="text_pole textarea_compact" type="number" min="0" max="999" data-i18n="[placeholder]ext_regex_min_depth_placeholder" placeholder="Unlimited" />
</div>
</div>
</div>
<div class="flex1 wi-enter-footer-text flex-container flexFlowColumn flexNoGap alignitemsstart">
<small>Other Options</small>
<small data-i18n="ext_regex_other_options">Other Options</small>
<label class="checkbox flex-container">
<input type="checkbox" name="disabled" />
<span data-i18n="Disabled">Disabled</span>
</label>
<label class="checkbox flex-container">
<label class="checkbox flex-container" title="Chat history won't change, only the message rendered in the UI.">
<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)">
<label class="checkbox flex-container" data-i18n="[title]ext_regex_only_format_prompt_desc" title="Chat history won't change, only the prompt as the request is sent (on generation).">
<input type="checkbox" name="only_format_prompt"/>
<span>
<span data-i18n="Only Format Prompt (?)">Only Format Prompt</span>
@ -134,7 +141,7 @@
<input type="checkbox" name="run_on_edit" />
<span data-i18n="Run On Edit">Run On Edit</span>
</label>
<label class="checkbox flex-container" title="Substitute {{macros}} in Find Regex before running it">
<label class="checkbox flex-container" data-i18n="[title]ext_regex_substitute_regex_desc" title="Substitute &lcub;&lcub;macros&rcub;&rcub; in Find Regex before running it">
<input type="checkbox" name="substitute_regex" />
<span>
<span data-i18n="Substitute Regex">Substitute Regex</span>

View File

@ -1,5 +1,6 @@
import { substituteParams } from '../../../script.js';
import { extension_settings } from '../../extensions.js';
import { regexFromString } from '../../utils.js';
export {
regex_placement,
getRegexedString,
@ -17,31 +18,10 @@ const regex_placement = {
USER_INPUT: 1,
AI_OUTPUT: 2,
SLASH_COMMAND: 3,
// 4 - sendAs (legacy)
WORLD_INFO: 5,
};
/**
* Instantiates a regular expression from a string.
* @param {string} input The input string.
* @returns {RegExp} The regular expression instance.
* @copyright Originally from: https://github.com/IonicaBizau/regex-parser.js/blob/master/lib/index.js
*/
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 {
return;
}
}
/**
* Parent function to fetch a regexed version of a raw string
* @param {string} rawString The raw string to be regexed

View File

@ -1,6 +1,8 @@
import { callPopup, getCurrentChatId, reloadCurrentChat, saveSettingsDebounced } from '../../../script.js';
import { extension_settings, renderExtensionTemplateAsync } from '../../extensions.js';
import { registerSlashCommand } from '../../slash-commands.js';
import { SlashCommand } from '../../slash-commands/SlashCommand.js';
import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from '../../slash-commands/SlashCommandArgument.js';
import { SlashCommandParser } from '../../slash-commands/SlashCommandParser.js';
import { download, getFileText, getSortableDelay, uuidv4 } from '../../utils.js';
import { resolveVariable } from '../../variables.js';
import { regex_placement, runRegexScript } from './engine.js';
@ -323,7 +325,9 @@ jQuery(async () => {
});
$('#import_regex_file').on('change', async function () {
const inputElement = this instanceof HTMLInputElement && this;
await onRegexImportFileChange(inputElement.files[0]);
for (const file of inputElement.files) {
await onRegexImportFileChange(file);
}
inputElement.value = '';
});
$('#import_regex').on('click', function () {
@ -353,5 +357,20 @@ jQuery(async () => {
await loadRegexScripts();
$('#saved_regex_scripts').sortable('enable');
registerSlashCommand('regex', runRegexCallback, [], '(name=scriptName [input]) runs a Regex extension script by name on the provided string. The script must be enabled.', true, true);
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'regex',
callback: runRegexCallback,
returns: 'replaced text',
namedArgumentList: [
new SlashCommandNamedArgument(
'name', 'script name', [ARGUMENT_TYPE.STRING], true,
),
],
unnamedArgumentList: [
new SlashCommandArgument(
'input', [ARGUMENT_TYPE.STRING], false,
),
],
helpString: 'Runs a Regex extension script by name on the provided string. The script must be enabled.',
}));
});

View File

@ -4,16 +4,16 @@
<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>
<span class="regex-toggle-on fa-solid fa-toggle-on" data-i18n="[title]ext_regex_disable_script" title="Disable script"></span>
<span class="regex-toggle-off fa-solid fa-toggle-off" data-i18n="[title]ext_regex_enable_script" title="Enable script"></span>
</label>
<div class="edit_existing_regex menu_button" title="Edit script">
<div class="edit_existing_regex menu_button" data-i18n="[title]ext_regex_edit_script" title="Edit script">
<i class="fa-solid fa-pencil"></i>
</div>
<div class="export_regex menu_button" title="Export script">
<div class="export_regex menu_button" data-i18n="[title]ext_regex_export_script" title="Export script">
<i class="fa-solid fa-file-export"></i>
</div>
<div class="delete_regex menu_button" title="Delete script">
<div class="delete_regex menu_button" data-i18n="[title]ext_regex_delete_script" title="Delete script">
<i class="fa-solid fa-trash"></i>
</div>
</div>

View File

@ -25,16 +25,12 @@ import { getMessageTimeStamp, humanizedDateTime } from '../../RossAscends-mods.j
import { SECRET_KEYS, secret_state } from '../../secrets.js';
import { getNovelUnlimitedImageGeneration, getNovelAnlas, loadNovelSubscriptionData } from '../../nai-settings.js';
import { getMultimodalCaption } from '../shared.js';
import { registerSlashCommand } from '../../slash-commands.js';
import { SlashCommandParser } from '../../slash-commands/SlashCommandParser.js';
import { SlashCommand } from '../../slash-commands/SlashCommand.js';
import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from '../../slash-commands/SlashCommandArgument.js';
import { resolveVariable } from '../../variables.js';
export { MODULE_NAME };
// Wraps a string into monospace font-face span
const m = x => `<span class="monospace">${x}</span>`;
// Joins an array of strings with ' / '
const j = a => a.join(' / ');
// Wraps a string into paragraph block
const p = a => `<p>${a}</p>`;
const MODULE_NAME = 'sd';
const UPDATE_INTERVAL = 1000;
// This is a 1x1 transparent PNG
@ -97,7 +93,7 @@ const triggerWords = {
};
const messageTrigger = {
activationRegex: /\b(send|mail|imagine|generate|make|create|draw|paint|render)\b.*\b(pic|picture|image|drawing|painting|photo|photograph)\b(?:\s+of)?(?:\s+(?:a|an|the|this|that|those)?)?(.+)/i,
activationRegex: /\b(send|mail|imagine|generate|make|create|draw|paint|render)\b.{0,10}\b(pic|picture|image|drawing|painting|photo|photograph)\b(?:\s+of)?(?:\s+(?:a|an|the|this|that|those)?)?(.+)/i,
specialCases: {
[generationMode.CHARACTER]: ['you', 'yourself'],
[generationMode.USER]: ['me', 'myself'],
@ -148,11 +144,6 @@ const promptTemplates = {
[generationMode.USER_MULTIMODAL]: 'Provide an exhaustive comma-separated list of tags describing the appearance of the character on this image in great detail. Start with "full body portrait".',
};
const helpString = [
`${m('[quiet=false/true] (argument)')} requests to generate an image and posts it to chat (unless quiet=true argument is specified). Supported arguments: ${m(j(Object.values(triggerWords).flat()))}.`,
'Anything else would trigger a "free mode" to make generate whatever you prompted. Example: \'/imagine apple tree\' would generate a picture of an apple tree. Returns a link to the generated image.',
].join(' ');
const defaultPrefix = 'best quality, absurdres, aesthetic,';
const defaultNegative = 'lowres, bad anatomy, bad hands, text, error, cropped, worst quality, low quality, normal quality, jpeg artifacts, signature, watermark, username, blurry';
@ -473,7 +464,8 @@ function addPromptTemplates() {
for (const [name, prompt] of Object.entries(extension_settings.sd.prompts)) {
const label = $('<label></label>')
.text(modeLabels[name])
.attr('for', `sd_prompt_${name}`);
.attr('for', `sd_prompt_${name}`)
.attr('data-i18n', `sd_prompt_${name}`);
const textarea = $('<textarea></textarea>')
.addClass('textarea_compact text_pole')
.attr('id', `sd_prompt_${name}`)
@ -485,6 +477,7 @@ function addPromptTemplates() {
const button = $('<button></button>')
.addClass('menu_button fa-solid fa-undo')
.attr('title', 'Restore default')
.attr('data-i18n', 'Restore default')
.on('click', () => {
textarea.val(promptTemplates[name]);
extension_settings.sd.prompts[name] = promptTemplates[name];
@ -589,15 +582,17 @@ async function expandPrompt(prompt) {
* Modifies prompt based on auto-expansion and user inputs.
* @param {string} prompt Prompt to refine
* @param {boolean} allowExpand Whether to allow auto-expansion
* @param {boolean} isNegative Whether the prompt is a negative one
* @returns {Promise<string>} Refined prompt
*/
async function refinePrompt(prompt, allowExpand) {
async function refinePrompt(prompt, allowExpand, isNegative = false) {
if (allowExpand && extension_settings.sd.expand) {
prompt = await expandPrompt(prompt);
}
if (extension_settings.sd.refine_mode) {
const refinedPrompt = await callPopup('<h3>Review and edit the prompt:</h3>Press "Cancel" to abort the image generation.', 'input', prompt.trim(), { rows: 5, okButton: 'Generate' });
const text = isNegative ? '<h3>Review and edit the <i>negative</i> prompt:</h3>' : '<h3>Review and edit the prompt:</h3>';
const refinedPrompt = await callPopup(text + 'Press "Cancel" to abort the image generation.', 'input', prompt.trim(), { rows: 5, okButton: 'Continue' });
if (refinedPrompt) {
return refinedPrompt;
@ -1892,7 +1887,7 @@ function processReply(str) {
str = str.replaceAll('“', '');
str = str.replaceAll('.', ',');
str = str.replaceAll('\n', ', ');
str = str.replace(/[^a-zA-Z0-9,:()\-']+/g, ' '); // Replace everything except alphanumeric characters and commas with spaces
str = str.replace(/[^a-zA-Z0-9,:_(){}[\]\-']+/g, ' ');
str = str.replace(/\s+/g, ' '); // Collapse multiple whitespaces into one
str = str.trim();
@ -1943,7 +1938,7 @@ async function generatePicture(args, trigger, message, callback) {
}
if (!isValidState()) {
toastr.warning('Extensions API is not connected or doesn\'t provide SD module. Enable Stable Horde to generate images.');
toastr.warning('Image generation is not available. Check your settings and try again.');
return;
}
@ -1967,9 +1962,9 @@ async function generatePicture(args, trigger, message, callback) {
eventSource.emit(event_types.FORCE_SET_BACKGROUND, { url: imgUrl, path: imagePath });
if (typeof callbackOriginal === 'function') {
callbackOriginal(prompt, imagePath, generationType);
callbackOriginal(prompt, imagePath, generationType, negativePromptPrefix);
} else {
sendMessage(prompt, imagePath, generationType);
sendMessage(prompt, imagePath, generationType, negativePromptPrefix);
}
};
}
@ -1978,6 +1973,7 @@ async function generatePicture(args, trigger, message, callback) {
callback = () => { };
}
const negativePromptPrefix = resolveVariable(args?.negative) || '';
const dimensions = setTypeSpecificDimensions(generationType);
let imagePath = '';
@ -1988,7 +1984,7 @@ async function generatePicture(args, trigger, message, callback) {
context.deactivateSendButtons();
hideSwipeButtons();
imagePath = await sendGenerationRequest(generationType, prompt, characterName, callback);
imagePath = await sendGenerationRequest(generationType, prompt, negativePromptPrefix, characterName, callback);
} catch (err) {
console.trace(err);
throw new Error('SD prompt text generation failed.');
@ -2183,17 +2179,26 @@ async function generatePrompt(quietPrompt) {
return processedReply;
}
async function sendGenerationRequest(generationType, prompt, characterName = null, callback) {
/**
* Sends a request to image generation endpoint and processes the result.
* @param {number} generationType Type of image generation
* @param {string} prompt Prompt to be used for image generation
* @param {string} additionalNegativePrefix Additional negative prompt to be used for image generation
* @param {string} [characterName] Name of the character
* @param {function} [callback] Callback function to be called after image generation
* @returns
*/
async function sendGenerationRequest(generationType, prompt, additionalNegativePrefix, characterName = null, callback) {
const noCharPrefix = [generationMode.FREE, generationMode.BACKGROUND, generationMode.USER, generationMode.USER_MULTIMODAL];
const prefix = noCharPrefix.includes(generationType)
? extension_settings.sd.prompt_prefix
: combinePrefixes(extension_settings.sd.prompt_prefix, getCharacterPrefix());
const negativePrefix = noCharPrefix.includes(generationType)
? extension_settings.sd.negative_prompt
: combinePrefixes(extension_settings.sd.negative_prompt, getCharacterNegativePrefix());
const prefixedPrompt = substituteParams(combinePrefixes(prefix, prompt, '{prompt}'));
const negativePrompt = substituteParams(noCharPrefix.includes(generationType)
? extension_settings.sd.negative_prompt
: combinePrefixes(extension_settings.sd.negative_prompt, getCharacterNegativePrefix()));
const negativePrompt = substituteParams(combinePrefixes(additionalNegativePrefix, negativePrefix));
let result = { format: '', data: '' };
const currentChatId = getCurrentChatId();
@ -2249,7 +2254,7 @@ async function sendGenerationRequest(generationType, prompt, characterName = nul
const filename = `${characterName}_${humanizedDateTime()}`;
const base64Image = await saveBase64AsFile(result.data, characterName, filename, result.format);
callback ? callback(prompt, base64Image, generationType) : sendMessage(prompt, base64Image, generationType);
callback ? callback(prompt, base64Image, generationType, additionalNegativePrefix) : sendMessage(prompt, base64Image, generationType, additionalNegativePrefix);
return base64Image;
}
@ -2722,6 +2727,9 @@ async function onComfyOpenWorkflowEditorClick() {
$('#sd_comfy_workflow_editor_placeholder_list_custom').append(el);
el.find('.sd_comfy_workflow_editor_custom_find').val(placeholder.find);
el.find('.sd_comfy_workflow_editor_custom_find').on('input', function () {
if (!(this instanceof HTMLInputElement)) {
return;
}
placeholder.find = this.value;
el.find('.sd_comfy_workflow_editor_custom_final').text(`"%${this.value}%"`);
el.attr('data-placeholder', `${this.value}`);
@ -2730,6 +2738,9 @@ async function onComfyOpenWorkflowEditorClick() {
});
el.find('.sd_comfy_workflow_editor_custom_replace').val(placeholder.replace);
el.find('.sd_comfy_workflow_editor_custom_replace').on('input', function () {
if (!(this instanceof HTMLInputElement)) {
return;
}
placeholder.replace = this.value;
saveSettingsDebounced();
});
@ -2819,19 +2830,29 @@ async function onComfyDeleteWorkflowClick() {
onComfyWorkflowChange();
}
async function sendMessage(prompt, image, generationType) {
/**
* Sends a chat message with the generated image.
* @param {string} prompt Prompt used for the image generation
* @param {string} image Base64 encoded image
* @param {number} generationType Generation type of the image
* @param {string} additionalNegativePrefix Additional negative prompt used for the image generation
*/
async function sendMessage(prompt, image, generationType, additionalNegativePrefix) {
const context = getContext();
const messageText = `[${context.name2} sends a picture that contains: ${prompt}]`;
const name = context.groupId ? systemUserName : context.name2;
const messageText = `[${name} sends a picture that contains: ${prompt}]`;
const message = {
name: context.groupId ? systemUserName : context.name2,
name: name,
is_user: false,
is_system: true,
send_date: getMessageTimeStamp(),
mes: context.groupId ? p(messageText) : messageText,
mes: messageText,
extra: {
image: image,
title: prompt,
generationType: generationType,
negative: additionalNegativePrefix,
inline_image: false,
},
};
context.chat.push(message);
@ -2952,6 +2973,7 @@ async function sdMessageButton(e) {
const characterFileName = context.characterId ? context.characters[context.characterId].name : context.groups[Object.keys(context.groups).filter(x => context.groups[x].id === context.groupId)[0]]?.id?.toString();
const messageText = message?.mes;
const hasSavedImage = message?.extra?.image && message?.extra?.title;
const hasSavedNegative = message?.extra?.negative;
if ($icon.hasClass(busyClass)) {
console.log('Previous image is still being generated...');
@ -2963,13 +2985,14 @@ async function sdMessageButton(e) {
try {
setBusyIcon(true);
if (hasSavedImage) {
const prompt = await refinePrompt(message.extra.title, false);
const prompt = await refinePrompt(message.extra.title, false, false);
const negative = hasSavedNegative ? await refinePrompt(message.extra.negative, false, true) : '';
message.extra.title = prompt;
const generationType = message?.extra?.generationType ?? generationMode.FREE;
console.log('Regenerating an image, using existing prompt:', prompt);
dimensions = setTypeSpecificDimensions(generationType);
await sendGenerationRequest(generationType, prompt, characterFileName, saveGeneratedImage);
await sendGenerationRequest(generationType, prompt, negative, characterFileName, saveGeneratedImage);
}
else {
console.log('doing /sd raw last');
@ -2987,7 +3010,7 @@ async function sdMessageButton(e) {
}
}
function saveGeneratedImage(prompt, image, generationType) {
function saveGeneratedImage(prompt, image, generationType, negative) {
// Some message sources may not create the extra object
if (typeof message.extra !== 'object') {
message.extra = {};
@ -2998,6 +3021,7 @@ async function sdMessageButton(e) {
message.extra.image = image;
message.extra.title = prompt;
message.extra.generationType = generationType;
message.extra.negative = negative;
appendMediaToMessage(message, $mes);
context.saveChat();
@ -3025,8 +3049,43 @@ $('#sd_dropdown [id]').on('click', function () {
});
jQuery(async () => {
registerSlashCommand('imagine', generatePicture, ['sd', 'img', 'image'], helpString, true, true);
registerSlashCommand('imagine-comfy-workflow', changeComfyWorkflow, ['icw'], '(workflowName) - change the workflow to be used for image generation with ComfyUI, e.g. <tt>/imagine-comfy-workflow MyWorkflow</tt>');
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'imagine',
callback: generatePicture,
aliases: ['sd', 'img', 'image'],
namedArgumentList: [
new SlashCommandNamedArgument(
'quiet', 'whether to post the generated image to chat', [ARGUMENT_TYPE.BOOLEAN], false, false, 'false', ['false', 'true'],
),
new SlashCommandNamedArgument(
'negative', 'negative prompt prefix', [ARGUMENT_TYPE.STRING], false, false, '',
),
],
unnamedArgumentList: [
new SlashCommandArgument(
'argument', [ARGUMENT_TYPE.STRING], false, false, null, Object.values(triggerWords).flat(),
),
],
helpString: `
<div>
Requests to generate an image and posts it to chat (unless quiet=true argument is specified). Supported arguments: <code>${Object.values(triggerWords).flat().join(', ')}</code>.
</div>
<div>
Anything else would trigger a "free mode" to make generate whatever you prompted. Example: <code>/imagine apple tree</code> would generate a picture of an apple tree. Returns a link to the generated image.
</div>
`,
}));
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'imagine-comfy-workflow',
callback: changeComfyWorkflow,
aliases: ['icw'],
unnamedArgumentList: [
new SlashCommandArgument(
'workflowName', [ARGUMENT_TYPE.STRING], true,
),
],
helpString: '(workflowName) - change the workflow to be used for image generation with ComfyUI, e.g. <pre><code>/imagine-comfy-workflow MyWorkflow</code></pre>',
}));
const template = await renderExtensionTemplateAsync('stable-diffusion', 'settings', defaultSettings);
$('#extensions_settings').append(template);

View File

@ -2,35 +2,35 @@
<div class="inline-drawer">
<div class="inline-drawer-toggle inline-drawer-header">
<b>
Image Generation
<a href="https://docs.sillytavern.app/extras/extensions/stable-diffusion/" class="notes-link" target="_blank">
<span data-i18n="Image Generation">Image Generation</span>
<a href="https://docs.sillytavern.app/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">
<label for="sd_refine_mode" class="checkbox_label" title="Allow to edit prompts manually before sending them to generation API">
<label for="sd_refine_mode" class="checkbox_label" data-i18n="[title]sd_refine_mode" title="Allow to edit prompts manually before sending them to generation API">
<input id="sd_refine_mode" type="checkbox" />
Edit prompts before generation
<span data-i18n="sd_refine_mode_txt">Edit prompts before generation</span>
</label>
<label for="sd_interactive_mode" class="checkbox_label" title="Automatically generate images when sending messages like 'send me a picture of cat'.">
<label for="sd_interactive_mode" class="checkbox_label" data-i18n="[title]sd_interactive_mode" title="Automatically generate images when sending messages like 'send me a picture of cat'.">
<input id="sd_interactive_mode" type="checkbox" />
Interactive mode
<span data-i18n="sd_interactive_mode_txt">Interactive mode</span>
</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.">
<label for="sd_multimodal_captioning" class="checkbox_label" data-i18n="[title]sd_multimodal_captioning" 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
<span data-i18n="sd_multimodal_captioning_txt">Use multimodal captioning for portraits</span>
</label>
<label for="sd_expand" class="checkbox_label" title="Automatically extend prompts using text generation model">
<label for="sd_expand" class="checkbox_label" data-i18n="[title]sd_expand" title="Automatically extend prompts using text generation model">
<input id="sd_expand" type="checkbox" />
Auto-enhance prompts
<span data-i18n="sd_expand_txt">Auto-enhance prompts</span>
</label>
<label for="sd_snap" class="checkbox_label" title="Snap generation requests with a forced aspect ratio (portraits, backgrounds) to the nearest known resolution, while trying to preserve the absolute pixel counts (recommended for SDXL).">
<label for="sd_snap" class="checkbox_label" data-i18n="[title]sd_snap" title="Snap generation requests with a forced aspect ratio (portraits, backgrounds) to the nearest known resolution, while trying to preserve the absolute pixel counts (recommended for SDXL).">
<input id="sd_snap" type="checkbox" />
Snap auto-adjusted resolutions
<span data-i18n="sd_snap_txt">Snap auto-adjusted resolutions</span>
</label>
<label for="sd_source">Source</label>
<label for="sd_source" data-i18n="Source">Source</label>
<select id="sd_source">
<option value="comfy">ComfyUI</option>
<option value="drawthings">DrawThings HTTP API</option>
@ -46,7 +46,7 @@
<div data-sd-source="auto">
<label for="sd_auto_url">SD Web UI URL</label>
<div class="flex-container flexnowrap">
<input id="sd_auto_url" type="text" class="text_pole" placeholder="Example: {{auto_url}}" value="{{auto_url}}" />
<input id="sd_auto_url" type="text" class="text_pole" data-i18n="[placeholder]sd_auto_url" placeholder="Example: {{auto_url}}" value="{{auto_url}}" />
<div id="sd_auto_validate" class="menu_button menu_button_icon">
<i class="fa-solid fa-check"></i>
<span data-i18n="Connect">
@ -54,14 +54,15 @@
</span>
</div>
</div>
<label for="sd_auto_auth">Authentication (optional)</label>
<input id="sd_auto_auth" type="text" class="text_pole" placeholder="Example: username:password" value="" />
<i><b>Important:</b> run SD Web UI with the <tt>--api</tt> flag! The server must be accessible from the SillyTavern host machine.</i>
<label for="sd_auto_auth" data-i18n="Authentication (optional)">Authentication (optional)</label>
<input id="sd_auto_auth" type="text" class="text_pole" data-i18n="[placeholder]Example: username:password" placeholder="Example: username:password" value="" />
<!-- (Original Text)<b>Important:</b> run SD Web UI with the <tt>--api</tt> flag! The server must be accessible from the SillyTavern host machine. -->
<i><b data-i18n="Important:">Important:</b></i><i data-i18n="sd_auto_auth_warning_1"> run SD Web UI with the </i><i><tt>--api</tt></i><i data-i18n="sd_auto_auth_warning_2"> flag! The server must be accessible from the SillyTavern host machine.</i>
</div>
<div data-sd-source="drawthings">
<label for="sd_drawthings_url">DrawThings API URL</label>
<div class="flex-container flexnowrap">
<input id="sd_drawthings_url" type="text" class="text_pole" placeholder="Example: {{drawthings_url}}" value="{{drawthings_url}}" />
<input id="sd_drawthings_url" type="text" class="text_pole" data-i18n="[placeholder]sd_drawthings_url" placeholder="Example: {{drawthings_url}}" value="{{drawthings_url}}" />
<div id="sd_drawthings_validate" class="menu_button menu_button_icon">
<i class="fa-solid fa-check"></i>
<span data-i18n="Connect">
@ -69,14 +70,15 @@
</span>
</div>
</div>
<label for="sd_drawthings_auth">Authentication (optional)</label>
<input id="sd_drawthings_auth" type="text" class="text_pole" placeholder="Example: username:password" value="" />
<i><b>Important:</b> run DrawThings app with HTTP API switch enabled in the UI! The server must be accessible from the SillyTavern host machine.</i>
<label for="sd_drawthings_auth" data-i18n="Authentication (optional)">Authentication (optional)</label>
<input id="sd_drawthings_auth" type="text" class="text_pole" data-i18n="[placeholder]Example: username:password" placeholder="Example: username:password" value="" />
<!-- (Original Text)<b>Important:</b> run DrawThings app with HTTP API switch enabled in the UI! The server must be accessible from the SillyTavern host machine. -->
<i><b data-i18n="Important:">Important:</b></i><i data-i18n="sd_drawthings_auth_txt"> run DrawThings app with HTTP API switch enabled in the UI! The server must be accessible from the SillyTavern host machine.</i>
</div>
<div data-sd-source="vlad">
<label for="sd_vlad_url">SD.Next API URL</label>
<div class="flex-container flexnowrap">
<input id="sd_vlad_url" type="text" class="text_pole" placeholder="Example: {{vlad_url}}" value="{{vlad_url}}" />
<input id="sd_vlad_url" type="text" class="text_pole" data-i18n="[placeholder]sd_vlad_url" placeholder="Example: {{vlad_url}}" value="{{vlad_url}}" />
<div id="sd_vlad_validate" class="menu_button menu_button_icon">
<i class="fa-solid fa-check"></i>
<span data-i18n="Connect">
@ -84,12 +86,12 @@
</span>
</div>
</div>
<label for="sd_vlad_auth">Authentication (optional)</label>
<input id="sd_vlad_auth" type="text" class="text_pole" placeholder="Example: username:password" value="" />
<i>The server must be accessible from the SillyTavern host machine.</i>
<label for="sd_vlad_auth" data-i18n="Authentication (optional)">Authentication (optional)</label>
<input id="sd_vlad_auth" type="text" class="text_pole" data-i18n="[placeholder]Example: username:password" placeholder="Example: username:password" value="" />
<i data-i18n="The server must be accessible from the SillyTavern host machine.">The server must be accessible from the SillyTavern host machine.</i>
</div>
<div data-sd-source="horde">
<i>Hint: Save an API key in Horde KoboldAI API settings to use it here.</i>
<i data-i18n="Hint: Save an API key in Horde KoboldAI API settings to use it here.">Hint: Save an API key in Horde KoboldAI API settings to use it here.</i>
<label for="sd_horde_nsfw" class="checkbox_label">
<input id="sd_horde_nsfw" type="checkbox" />
<span data-i18n="Allow NSFW images from Horde">
@ -105,38 +107,38 @@
</div>
<div data-sd-source="novel">
<div class="flex-container flexFlowColumn">
<label for="sd_novel_anlas_guard" class="checkbox_label flex1" title="Automatically adjust generation parameters to ensure free image generations.">
<label for="sd_novel_anlas_guard" class="checkbox_label flex1" data-i18n="[title]Automatically adjust generation parameters to ensure free image generations." title="Automatically adjust generation parameters to ensure free image generations.">
<input id="sd_novel_anlas_guard" type="checkbox" />
<span data-i18n="Avoid spending Anlas">
Avoid spending Anlas
</span>
<span data-i18n="Opus tier" class="toggle-description">(Opus tier)</span>
</label>
<div id="sd_novel_view_anlas" class="menu_button menu_button_icon">
<div id="sd_novel_view_anlas" class="menu_button menu_button_icon" data-i18n="View my Anlas">
View my Anlas
</div>
</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>
<small data-i18n="These settings only apply to DALL-E 3">These settings only apply to DALL-E 3</small>
<div class="flex-container">
<label for="sd_openai_style">Image Style</label>
<label for="sd_openai_style" data-i18n="Image 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>
<label for="sd_openai_quality" data-i18n="Image Quality">Image Quality</label>
<select id="sd_openai_quality">
<option value="standard">Standard</option>
<option value="hd">HD</option>
<option value="standard" data-i18n="Standard">Standard</option>
<option value="hd" data-i18n="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}}" />
<input id="sd_comfy_url" type="text" class="text_pole" data-i18n="[placeholder]sd_comfy_url" 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">
@ -144,17 +146,17 @@
</span>
</div>
</div>
<p><i><b>Important:</b> The server must be accessible from the SillyTavern host machine.</i></p>
<p><i><b data-i18n="Important:">Important:</b></i><i data-i18n="The server must be accessible from the SillyTavern host machine."> 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">
<div id="sd_comfy_open_workflow_editor" class="menu_button menu_button_icon" data-i18n="[title]Open workflow editor" 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">
<div id="sd_comfy_new_workflow" class="menu_button menu_button_icon" data-i18n="[title]Create new workflow" 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">
<div id="sd_comfy_delete_workflow" class="menu_button menu_button_icon" data-i18n="[title]Delete workflow" title="Delete workflow">
<i class="fa-solid fa-trash-can"></i>
</div>
</div>
@ -178,17 +180,17 @@
</label>
</div>
</div>
<label for="sd_scale">CFG Scale (<span id="sd_scale_value"></span>)</label>
<label for="sd_scale" data-i18n="CFG 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>
<label for="sd_steps" data-i18n="Sampling steps">Sampling steps (<span id="sd_steps_value"></span>)</label>
<input id="sd_steps" type="range" min="{{steps_min}}" max="{{steps_max}}" step="{{steps_step}}" value="{{steps}}" />
<label for="sd_width">Width (<span id="sd_width_value"></span>)</label>
<label for="sd_width" data-i18n="Width">Width (<span id="sd_width_value"></span>)</label>
<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>
<label for="sd_height" data-i18n="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">Model</label>
<label for="sd_model" data-i18n="Model">Model</label>
<select id="sd_model"></select>
<label for="sd_sampler">Sampling method</label>
<label for="sd_sampler" data-i18n="Sampling method">Sampling method</label>
<select id="sd_sampler"></select>
<label data-sd-source="horde" for="sd_horde_karras" class="checkbox_label">
<input id="sd_horde_karras" type="checkbox" />
@ -197,23 +199,23 @@
</span>
</label>
<div data-sd-source="novel" class="flex-container">
<label class="flex1 checkbox_label" title="SMEA versions of samplers are modified to perform better at high resolution.">
<label class="flex1 checkbox_label" data-i18n="[title]SMEA versions of samplers are modified to perform better at high resolution." title="SMEA versions of samplers are modified to perform better at high resolution.">
<input id="sd_novel_sm" type="checkbox" />
<span data-i18n="SMEA">
SMEA
</span>
</label>
<label class="flex1 checkbox_label" title="DYN variants of SMEA samplers often lead to more varied output, but may fail at very high resolutions.">
<label class="flex1 checkbox_label" data-i18n="[title]DYN variants of SMEA samplers often lead to more varied output, but may fail at very high resolutions." title="DYN variants of SMEA samplers often lead to more varied output, but may fail at very high resolutions.">
<input id="sd_novel_sm_dyn" type="checkbox" />
<span data-i18n="DYN">
DYN
</span>
</label>
</div>
<label for="sd_resolution">Resolution</label>
<label for="sd_resolution" data-i18n="Resolution">Resolution</label>
<select id="sd_resolution"><!-- Populated in JS --></select>
<div data-sd-source="comfy">
<label for="sd_scheduler">Scheduler</label>
<label for="sd_scheduler" data-i18n="Scheduler">Scheduler</label>
<select id="sd_scheduler"></select>
</div>
<div data-sd-source="comfy">
@ -223,54 +225,54 @@
<div class="flex-container marginTop10 margin-bot-10px">
<label class="flex1 checkbox_label">
<input id="sd_restore_faces" type="checkbox" />
Restore Faces
<span data-i18n="Restore Faces">Restore Faces</span>
</label>
<label class="flex1 checkbox_label">
<input id="sd_enable_hr" type="checkbox" />
Hires. Fix
<span data-i18n="Hires. Fix">Hires. Fix</span>
</label>
</div>
<div data-sd-source="auto,vlad">
<label for="sd_hr_upscaler">Upscaler</label>
<label for="sd_hr_upscaler" data-i18n="Upscaler">Upscaler</label>
<select id="sd_hr_upscaler"></select>
<label for="sd_hr_scale">Upscale by (<span id="sd_hr_scale_value"></span>)</label>
<label for="sd_hr_scale"><span data-i18n="Upscale by">Upscale by</span> (<span id="sd_hr_scale_value"></span>)</label>
<input id="sd_hr_scale" type="range" min="{{hr_scale_min}}" max="{{hr_scale_max}}" step="{{hr_scale_step}}" value="{{hr_scale}}" />
<label for="sd_denoising_strength">Denoising strength (<span id="sd_denoising_strength_value"></span>)</label>
<label for="sd_denoising_strength"><span data-i18n="Denoising strength">Denoising strength</span> (<span id="sd_denoising_strength_value"></span>)</label>
<input id="sd_denoising_strength" type="range" min="{{denoising_strength_min}}" max="{{denoising_strength_max}}" step="{{denoising_strength_step}}" value="{{denoising_strength}}" />
<label for="sd_hr_second_pass_steps">Hires steps (2nd pass) (<span id="sd_hr_second_pass_steps_value"></span>)</label>
<label for="sd_hr_second_pass_steps"><span data-i18n="Hires steps (2nd pass)">Hires steps (2nd pass)</span> (<span id="sd_hr_second_pass_steps_value"></span>)</label>
<input id="sd_hr_second_pass_steps" type="range" min="{{hr_second_pass_steps_min}}" max="{{hr_second_pass_steps_max}}" step="{{hr_second_pass_steps_step}}" value="{{hr_second_pass_steps}}" />
</div>
<div data-sd-source="novel">
<label for="sd_novel_upscale_ratio">Upscale by (<span id="sd_novel_upscale_ratio_value"></span>)</label>
<label for="sd_novel_upscale_ratio"><span data-i18n="Upscale by">Upscale by</span> (<span id="sd_novel_upscale_ratio_value"></span>)</label>
<input id="sd_novel_upscale_ratio" type="range" min="{{novel_upscale_ratio_min}}" max="{{novel_upscale_ratio_max}}" step="{{novel_upscale_ratio_step}}" value="{{novel_upscale_ratio}}" />
</div>
<hr>
<h4 title="Preset for prompt prefix and negative prompt">
Style
<h4 data-i18n="[title]Preset for prompt prefix and negative prompt" title="Preset for prompt prefix and negative prompt">
<span data-i18n="Style">Style</span>
</h4>
<div class="flex-container">
<select id="sd_style" class="flex1 text_pole"></select>
<div id="sd_save_style" title="Save style" class="menu_button">
<div id="sd_save_style" data-i18n="[title]Save style" title="Save style" class="menu_button">
<i class="fa-solid fa-save"></i>
</div>
</div>
<label for="sd_prompt_prefix">Common prompt prefix</label>
<textarea id="sd_prompt_prefix" class="text_pole textarea_compact" rows="3" placeholder="Use {prompt} to specify where the generated prompt will be inserted"></textarea>
<label for="sd_negative_prompt">Negative prompt</label>
<label for="sd_prompt_prefix" data-i18n="Common prompt prefix">Common prompt prefix</label>
<textarea id="sd_prompt_prefix" class="text_pole textarea_compact" rows="3" data-i18n="[placeholder]sd_prompt_prefix_placeholder" placeholder="Use {prompt} to specify where the generated prompt will be inserted"></textarea>
<label for="sd_negative_prompt" data-i18n="Negative common prompt prefix">Negative common prompt prefix</label>
<textarea id="sd_negative_prompt" class="text_pole textarea_compact" rows="3"></textarea>
<div id="sd_character_prompt_block">
<label for="sd_character_prompt">Character-specific prompt prefix</label>
<small>Won't be used in groups.</small>
<textarea id="sd_character_prompt" class="text_pole textarea_compact" rows="3" placeholder="Any characteristics that describe the currently selected character. Will be added after a common prefix.&#10;Example: female, green eyes, brown hair, pink shirt"></textarea>
<label for="sd_character_negative_prompt">Character-specific negative prompt prefix</label>
<small>Won't be used in groups.</small>
<textarea id="sd_character_negative_prompt" class="text_pole textarea_compact" rows="3" placeholder="Any characteristics that should not appear for the selected character. Will be added after a negative common prefix.&#10;Example: jewellery, shoes, glasses"></textarea>
<label for="sd_character_prompt" data-i18n="Character-specific prompt prefix">Character-specific prompt prefix</label>
<small data-i18n="Won't be used in groups.">Won't be used in groups.</small>
<textarea id="sd_character_prompt" class="text_pole textarea_compact" rows="3" data-i18n="[placeholder]sd_character_prompt_placeholder" placeholder="Any characteristics that describe the currently selected character. Will be added after a common prompt prefix.&#10;Example: female, green eyes, brown hair, pink shirt"></textarea>
<label for="sd_character_negative_prompt" data-i18n="Character-specific negative prompt prefix">Character-specific negative prompt prefix</label>
<small data-i18n="Won't be used in groups.">Won't be used in groups.</small>
<textarea id="sd_character_negative_prompt" class="text_pole textarea_compact" rows="3" data-i18n="[placeholder]sd_character_negative_prompt_placeholder" placeholder="Any characteristics that should not appear for the selected character. Will be added after a negative common prompt prefix.&#10;Example: jewellery, shoes, glasses"></textarea>
</div>
</div>
</div>
<div class="inline-drawer">
<div class="inline-drawer-toggle inline-drawer-header">
<b>Image Prompt Templates</b>
<b data-i18n="Image Prompt Templates">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

@ -1,6 +1,7 @@
import { callPopup, main_api } from '../../../script.js';
import { getContext } from '../../extensions.js';
import { registerSlashCommand } from '../../slash-commands.js';
import { SlashCommand } from '../../slash-commands/SlashCommand.js';
import { SlashCommandParser } from '../../slash-commands/SlashCommandParser.js';
import { getFriendlyTokenizerName, getTextTokens, getTokenCountAsync, tokenizers } from '../../tokenizers.js';
import { resetScrollHeight, debounce } from '../../utils.js';
import { debounce_timeout } from '../../constants.js';
@ -132,5 +133,10 @@ jQuery(() => {
</div>`;
$('#extensionsMenu').prepend(buttonHtml);
$('#token_counter').on('click', doTokenCounter);
registerSlashCommand('count', doCount, [], ' counts the number of tokens in the current chat', true, false);
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'count',
callback: doCount,
returns: 'number of tokens',
helpString: 'Counts the number of tokens in the current chat.',
}));
});

View File

@ -642,11 +642,11 @@ jQuery(() => {
loadSettings();
eventSource.on(event_types.CHARACTER_MESSAGE_RENDERED, handleIncomingMessage);
eventSource.makeFirst(event_types.CHARACTER_MESSAGE_RENDERED, handleIncomingMessage);
eventSource.makeFirst(event_types.USER_MESSAGE_RENDERED, handleOutgoingMessage);
eventSource.on(event_types.MESSAGE_SWIPED, handleIncomingMessage);
eventSource.on(event_types.USER_MESSAGE_RENDERED, handleOutgoingMessage);
eventSource.on(event_types.IMPERSONATE_READY, handleImpersonateReady);
eventSource.on(event_types.MESSAGE_EDITED, handleMessageEdit);
eventSource.on(event_types.MESSAGE_UPDATED, handleMessageEdit);
document.body.classList.add('translate');
});

View File

@ -0,0 +1,207 @@
import { callPopup, getRequestHeaders } from '../../../script.js';
import { SECRET_KEYS, findSecret, secret_state, writeSecret } from '../../secrets.js';
import { getPreviewString, saveTtsProviderSettings } from './index.js';
export { AzureTtsProvider };
class AzureTtsProvider {
//########//
// Config //
//########//
settings;
voices = [];
separator = ' . ';
audioElement = document.createElement('audio');
defaultSettings = {
region: '',
voiceMap: {},
};
get settingsHtml() {
let html = `
<div class="azure_tts_settings">
<div class="flex-container alignItemsBaseline">
<h4 for="azure_tts_key" class="flex1 margin0">
<a href="https://portal.azure.com/" target="_blank">Azure TTS Key</a>
</h4>
<div id="azure_tts_key" class="menu_button menu_button_icon">
<i class="fa-solid fa-key"></i>
<span>Click to set</span>
</div>
</div>
<label for="azure_tts_region">Region:</label>
<input id="azure_tts_region" type="text" class="text_pole" placeholder="e.g. westus" />
<hr>
</div>
`;
return html;
}
onSettingsChange() {
// Update dynamically
this.settings.region = String($('#azure_tts_region').val());
// Reset voices
this.voices = [];
saveTtsProviderSettings();
}
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}`;
}
}
$('#azure_tts_region').val(this.settings.region).on('input', () => this.onSettingsChange());
$('#azure_tts_key').toggleClass('success', secret_state[SECRET_KEYS.AZURE_TTS]);
$('#azure_tts_key').on('click', async () => {
const popupText = 'Azure TTS API Key';
const savedKey = secret_state[SECRET_KEYS.AZURE_TTS] ? await findSecret(SECRET_KEYS.AZURE_TTS) : '';
const key = await callPopup(popupText, 'input', savedKey);
if (key == false || key == '') {
return;
}
await writeSecret(SECRET_KEYS.AZURE_TTS, key);
toastr.success('API Key saved');
$('#azure_tts_key').addClass('success');
await this.onRefreshClick();
});
try {
await this.checkReady();
console.debug('Azure: Settings loaded');
} catch {
console.debug('Azure: Settings loaded, but not ready');
}
}
// Perform a simple readiness check by trying to fetch voiceIds
async checkReady() {
if (secret_state[SECRET_KEYS.AZURE_TTS]) {
await this.fetchTtsVoiceObjects();
} else {
this.voices = [];
}
}
async onRefreshClick() {
await this.checkReady();
}
//#################//
// TTS Interfaces //
//#################//
async getVoice(voiceName) {
if (this.voices.length == 0) {
this.voices = await this.fetchTtsVoiceObjects();
}
const match = this.voices.filter(
voice => voice.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() {
if (!secret_state[SECRET_KEYS.AZURE_TTS]) {
console.warn('Azure TTS API Key not set');
return [];
}
if (!this.settings.region) {
console.warn('Azure TTS region not set');
return [];
}
const response = await fetch('/api/azure/list', {
method: 'POST',
headers: getRequestHeaders(),
body: JSON.stringify({
region: this.settings.region,
}),
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${await response.text()}`);
}
let responseJson = await response.json();
responseJson = responseJson
.sort((a, b) => a.Locale.localeCompare(b.Locale) || a.ShortName.localeCompare(b.ShortName))
.map(x => ({ name: x.ShortName, voice_id: x.ShortName, preview_url: false, lang: x.Locale }));
return responseJson;
}
/**
* Preview TTS for a given voice ID.
* @param {string} id Voice ID
*/
async previewTtsVoice(id) {
this.audioElement.pause();
this.audioElement.currentTime = 0;
const voice = await this.getVoice(id);
const text = getPreviewString(voice.lang);
const response = await this.fetchTtsGeneration(text, id);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${await response.text()}`);
}
const audio = await response.blob();
const url = URL.createObjectURL(audio);
this.audioElement.src = url;
this.audioElement.play();
URL.revokeObjectURL(url);
}
async fetchTtsGeneration(text, voiceId) {
if (!secret_state[SECRET_KEYS.AZURE_TTS]) {
throw new Error('Azure TTS API Key not set');
}
if (!this.settings.region) {
throw new Error('Azure TTS region not set');
}
const response = await fetch('/api/azure/generate', {
method: 'POST',
headers: getRequestHeaders(),
body: JSON.stringify({
text: text,
voice: voiceId,
region: this.settings.region,
}),
});
if (!response.ok) {
toastr.error(response.statusText, 'TTS Generation Failed');
throw new Error(`HTTP ${response.status}: ${await response.text()}`);
}
return response;
}
}

View File

@ -155,6 +155,7 @@ class EdgeTtsProvider {
const url = URL.createObjectURL(audio);
this.audioElement.src = url;
this.audioElement.play();
URL.revokeObjectURL(url);
}
/**

View File

@ -0,0 +1,267 @@
import { saveTtsProviderSettings } from './index.js';
export { GSVITtsProvider };
class GSVITtsProvider {
//########//
// Config //
//########//
settings;
ready = false;
separator = '. ';
characterList = {};
voices = [];
/**
* Perform any text processing before passing to TTS engine.
* @param {string} text Input text
* @returns {string} Processed text
*/
processText(text) {
text = text.replace('<br>', '\n'); // Replace <br> with newline
return text;
}
languageLabels = {
'Multilingual': '多语种混合',
'Chinese': '中文',
'English': '英文',
'Japanese': '日文',
'Chinese-English': '中英混合',
'Japanese-English': '日英混合',
};
defaultSettings = {
provider_endpoint: 'http://127.0.0.1:5000',
language: '多语种混合',
cha_name: '',
character_emotion: 'default',
speed: 1,
top_k: 6,
top_p: 0.85,
temperature: 0.75,
batch_size: 10,
stream: false,
stream_chunk_size: 100,
};
// Added new methods to obtain characters and emotions
async fetchCharacterList() {
const response = await fetch(this.settings.provider_endpoint + '/character_list');
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${await response.text()}`);
}
const characterList = await response.json();
this.characterList = characterList;
this.voices = Object.keys(characterList);
}
get settingsHtml() {
let html = `
<label for="gsvi_api_language">Text Language</label>
<select id="gsvi_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>GSVI Settings:</label><br/>
<label for="gsvi_tts_endpoint">Provider Endpoint:</label>
<input id="gsvi_tts_endpoint" type="text" class="text_pole" maxlength="250" value="${this.defaultSettings.provider_endpoint}"/>
<label for="gsvi_speed">Speed: <span id="gsvi_tts_speed_output">${this.defaultSettings.speed}</span></label>
<input id="gsvi_speed" type="range" value="${this.defaultSettings.speed}" min="0.5" max="2" step="0.01" />
<label for="gsvi_top_k">Top K: <span id="gsvi_top_k_output">${this.defaultSettings.top_k}</span></label>
<input id="gsvi_top_k" type="range" value="${this.defaultSettings.top_k}" min="0" max="100" step="1" />
<label for="gsvi_top_p">Top P: <span id="gsvi_top_p_output">${this.defaultSettings.top_p}</span></label>
<input id="gsvi_top_p" type="range" value="${this.defaultSettings.top_p}" min="0" max="1" step="0.01" />
<label for="gsvi_temperature">Temperature: <span id="gsvi_tts_temperature_output">${this.defaultSettings.temperature}</span></label>
<input id="gsvi_temperature" type="range" value="${this.defaultSettings.temperature}" min="0.01" max="1" step="0.01" />
<label for="gsvi_batch_size">Batch Size: <span id="gsvi_batch_size_output">${this.defaultSettings.batch_size}</span></label>
<input id="gsvi_batch_size" type="range" value="${this.defaultSettings.batch_size}" min="1" max="35" step="1" />
<label for="gsvi_tts_streaming" class="checkbox_label">
<input id="gsvi_tts_streaming" type="checkbox" ${this.defaultSettings.stream ? 'checked' : ''}/>
<span>Streaming</span>
</label>
<label for="gsvi_stream_chunk_size">Stream Chunk Size: <span id="gsvi_stream_chunk_size_output">${this.defaultSettings.stream_chunk_size}</span></label>
<input id="gsvi_stream_chunk_size" type="range" value="${this.defaultSettings.stream_chunk_size}" min="100" max="400" step="1" />
<p>
For more information, visit the
<a href="https://github.com/X-T-E-R/GPT-SoVITS-Inference" target="_blank">GSVI project page</a>.
</p>
`;
return html;
}
onSettingsChange() {
// Update provider settings based on input fields
this.settings.provider_endpoint = $('#gsvi_tts_endpoint').val();
this.settings.language = $('#gsvi_api_language').val();
// Update the rest of TTS settings based on input fields
this.settings.speed = parseFloat($('#gsvi_speed').val());
this.settings.temperature = parseFloat($('#gsvi_temperature').val());
this.settings.top_k = parseInt($('#gsvi_top_k').val(), 10);
this.settings.top_p = parseFloat($('#gsvi_top_p').val());
this.settings.batch_size = parseInt($('#gsvi_batch_size').val(), 10);
this.settings.stream = $('#gsvi_tts_streaming').is(':checked');
this.settings.stream_chunk_size = parseInt($('#gsvi_stream_chunk_size').val(), 10);
// Update UI to reflect changes
$('#gsvi_tts_speed_output').text(this.settings.speed);
$('#gsvi_tts_temperature_output').text(this.settings.temperature);
$('#gsvi_top_k_output').text(this.settings.top_k);
$('#gsvi_top_p_output').text(this.settings.top_p);
$('#gsvi_stream_chunk_size_output').text(this.settings.stream_chunk_size);
$('#gsvi_batch_size_output').text(this.settings.batch_size);
// Persist settings changes
saveTtsProviderSettings();
}
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, ...settings };
// Fetch character and emotion list
// Set initial values from the settings
$('#gsvi_tts_endpoint').val(this.settings.provider_endpoint);
$('#gsvi_api_language').val(this.settings.language);
$('#gsvi_speed').val(this.settings.speed);
$('#gsvi_temperature').val(this.settings.temperature);
$('#gsvi_top_k').val(this.settings.top_k);
$('#gsvi_top_p').val(this.settings.top_p);
$('#gsvi_batch_size').val(this.settings.batch_size);
$('#gsvi_tts_streaming').prop('checked', this.settings.stream);
$('#gsvi_stream_chunk_size').val(this.settings.stream_chunk_size);
// Update UI to reflect initial settings
$('#gsvi_tts_speed_output').text(this.settings.speed);
$('#gsvi_tts_temperature_output').text(this.settings.temperature);
$('#gsvi_top_k_output').text(this.settings.top_k);
$('#gsvi_top_p_output').text(this.settings.top_p);
$('#gsvi_stream_chunk_size_output').text(this.settings.stream_chunk_size);
// Register event listeners to update settings on user interaction
// (Similar to before, ensure event listeners for character and emotion selection are included)
// Register input/change event listeners to update settings on user interaction
$('#gsvi_tts_endpoint').on('input', () => { this.onSettingsChange(); });
$('#gsvi_api_language').on('change', () => { this.onSettingsChange(); });
$('#gsvi_speed').on('input', () => { this.onSettingsChange(); });
$('#gsvi_temperature').on('input', () => { this.onSettingsChange(); });
$('#gsvi_top_k').on('input', () => { this.onSettingsChange(); });
$('#gsvi_top_p').on('input', () => { this.onSettingsChange(); });
$('#gsvi_batch_size').on('input', () => { this.onSettingsChange(); });
$('#gsvi_tts_streaming').on('change', () => { this.onSettingsChange(); });
$('#gsvi_stream_chunk_size').on('input', () => { this.onSettingsChange(); });
await this.checkReady();
console.debug('GSVI: Settings loaded');
}
// Perform a simple readiness check by trying to fetch voiceIds
async checkReady() {
await Promise.allSettled([this.fetchCharacterList()]);
}
async onRefreshClick() {
return;
}
//#################//
// TTS Interfaces //
//#################//
async getVoice(voiceName) {
if (this.voices.length == 0) {
this.fetchCharacterList();
}
if (!this.voices.includes(voiceName)) {
throw `TTS Voice name ${voiceName} not found`;
}
return { name: voiceName, voice_id: voiceName, preview_url: false, lang: 'zh-CN' };
}
async generateTts(text, voiceId) {
const response = await this.fetchTtsGeneration(text, voiceId);
return response;
}
//###########//
// API CALLS //
//###########//
async fetchTtsVoiceObjects() {
if (this.voices.length == 0) {
await this.fetchCharacterList();
}
console.log(this.voices);
const voices = this.voices.map(x => ({ name: x, voice_id: x, preview_url: false, lang: 'zh-CN' }));
return voices;
}
async fetchTtsGeneration(inputText, voiceId) {
console.info(`Generating new TTS for voice_id ${voiceId}`);
const params = new URLSearchParams();
params.append('text', inputText);
params.append('cha_name', voiceId);
params.append('text_language', this.settings.language);
params.append('batch_size', this.settings.batch_size.toString());
params.append('speed', this.settings.speed.toString());
params.append('top_k', this.settings.top_k.toString());
params.append('top_p', this.settings.top_p.toString());
params.append('temperature', this.settings.temperature.toString());
params.append('stream', this.settings.stream.toString());
return `${this.settings.provider_endpoint}/tts?${params.toString()}`;
}
// Interface not used by GSVI TTS
async fetchTtsFromHistory(history_item_id) {
return Promise.resolve(history_item_id);
}
}

View File

@ -8,11 +8,15 @@ 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';
import { GSVITtsProvider } from './gsvi.js';
import { AllTalkTtsProvider } from './alltalk.js';
import { SpeechT5TtsProvider } from './speecht5.js';
import { AzureTtsProvider } from './azure.js';
import { SlashCommandParser } from '../../slash-commands/SlashCommandParser.js';
import { SlashCommand } from '../../slash-commands/SlashCommand.js';
import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from '../../slash-commands/SlashCommandArgument.js';
export { talkingAnimation };
const UPDATE_INTERVAL = 1000;
@ -72,6 +76,7 @@ const ttsProviders = {
ElevenLabs: ElevenLabsTtsProvider,
Silero: SileroTtsProvider,
XTTSv2: XTTSTtsProvider,
GSVI: GSVITtsProvider,
System: SystemTtsProvider,
Coqui: CoquiTtsProvider,
Edge: EdgeTtsProvider,
@ -79,6 +84,7 @@ const ttsProviders = {
OpenAI: OpenAITtsProvider,
AllTalk: AllTalkTtsProvider,
SpeechT5: SpeechT5TtsProvider,
Azure: AzureTtsProvider,
};
let ttsProvider;
let ttsProviderName;
@ -259,6 +265,7 @@ async function playAudioData(audioJob) {
audioElement.addEventListener('ended', completeCurrentAudioJob);
audioElement.addEventListener('canplay', () => {
console.debug('Starting TTS playback');
audioElement.playbackRate = extension_settings.tts.playback_rate;
audioElement.play();
});
}
@ -527,6 +534,10 @@ function loadSettings() {
$('#tts_pass_asterisks').prop('checked', extension_settings.tts.pass_asterisks);
$('#tts_skip_codeblocks').prop('checked', extension_settings.tts.skip_codeblocks);
$('#tts_skip_tags').prop('checked', extension_settings.tts.skip_tags);
$('#playback_rate').val(extension_settings.tts.playback_rate);
$('#playback_rate_counter').val(Number(extension_settings.tts.playback_rate).toFixed(2));
$('#playback_rate_block').toggle(extension_settings.tts.currentProvider !== 'System');
$('body').toggleClass('tts', extension_settings.tts.enabled);
}
@ -536,6 +547,7 @@ const defaultSettings = {
currentProvider: 'ElevenLabs',
auto_generation: true,
narrate_user: false,
playback_rate: 1,
};
function setTtsStatus(status, success) {
@ -647,6 +659,7 @@ async function loadTtsProvider(provider) {
function onTtsProviderChange() {
const ttsProviderSelection = $('#tts_provider').val();
extension_settings.tts.currentProvider = ttsProviderSelection;
$('#playback_rate_block').toggle(extension_settings.tts.currentProvider !== 'System');
loadTtsProvider(ttsProviderSelection);
}
@ -1020,6 +1033,20 @@ $(document).ready(function () {
<small>Pass Asterisks to TTS Engine</small>
</label>
</div>
<div id="playback_rate_block" class="range-block">
<hr>
<div class="range-block-title justifyLeft" data-i18n="Audio Playback Speed">
<small>Audio Playback Speed</small>
</div>
<div class="range-block-range-and-counter">
<div class="range-block-range">
<input type="range" id="playback_rate" name="volume" min="0" max="3" step="0.05">
</div>
<div class="range-block-counter">
<input type="number" min="0" max="3" step="0.05" data-for="playback_rate" id="playback_rate_counter">
</div>
</div>
</div>
<div id="tts_voicemap_block">
</div>
<hr>
@ -1044,6 +1071,15 @@ $(document).ready(function () {
$('#tts_pass_asterisks').on('click', onPassAsterisksClick);
$('#tts_auto_generation').on('click', onAutoGenerationClick);
$('#tts_narrate_user').on('click', onNarrateUserClick);
$('#playback_rate').on('input', function () {
const value = $(this).val();
const formattedValue = Number(value).toFixed(2);
extension_settings.tts.playback_rate = value;
$('#playback_rate_counter').val(formattedValue);
saveSettingsDebounced();
});
$('#tts_voices').on('click', onTtsVoicesClick);
for (const provider in ttsProviders) {
$('#tts_provider').append($('<option />').val(provider).text(provider));
@ -1061,8 +1097,38 @@ $(document).ready(function () {
eventSource.on(event_types.CHAT_CHANGED, onChatChanged);
eventSource.on(event_types.MESSAGE_DELETED, onMessageDeleted);
eventSource.on(event_types.GROUP_UPDATED, onChatChanged);
eventSource.on(event_types.MESSAGE_SENT, onMessageEvent);
eventSource.on(event_types.MESSAGE_RECEIVED, onMessageEvent);
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);
eventSource.makeLast(event_types.CHARACTER_MESSAGE_RENDERED, onMessageEvent);
eventSource.makeLast(event_types.USER_MESSAGE_RENDERED, onMessageEvent);
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'speak',
callback: onNarrateText,
aliases: ['narrate', 'tts'],
namedArgumentList: [
new SlashCommandNamedArgument(
'voice', 'character voice name', [ARGUMENT_TYPE.STRING], false,
),
],
unnamedArgumentList: [
new SlashCommandArgument(
'text', [ARGUMENT_TYPE.STRING], true,
),
],
helpString: `
<div>
Narrate any text using currently selected character's voice.
</div>
<div>
Use <code>voice="Character Name"</code> argument to set other voice from the voice map.
</div>
<div>
<strong>Example:</strong>
<ul>
<li>
<pre><code>/speak voice="Donald Duck" Quack!</code></pre>
</li>
</ul>
</div>
`,
}));
document.body.appendChild(audioElement);
});

View File

@ -180,6 +180,7 @@ class NovelTtsProvider {
const url = URL.createObjectURL(audio);
this.audioElement.src = url;
this.audioElement.play();
URL.revokeObjectURL(url);
}
async* fetchTtsGeneration(inputText, voiceId) {

View File

@ -60,6 +60,7 @@ class SpeechT5TtsProvider {
const url = URL.createObjectURL(audio);
this.audioElement.src = url;
this.audioElement.play();
URL.revokeObjectURL(url);
}
async loadSettings(settings) {

View File

@ -916,6 +916,28 @@ async function onVectorizeAllFilesClick() {
const chatAttachments = getContext().chat.filter(x => x.extra?.file).map(x => x.extra.file);
const allFiles = [...dataBank, ...chatAttachments];
/**
* Gets the chunk size for a file attachment.
* @param file {import('../../chats.js').FileAttachment} File attachment
* @returns {number} Chunk size for the file
*/
function getChunkSize(file) {
if (chatAttachments.includes(file)) {
// Convert kilobytes to string length
const thresholdLength = settings.size_threshold * 1024;
return file.size > thresholdLength ? settings.chunk_size : -1;
}
if (dataBank.includes(file)) {
// Convert kilobytes to string length
const thresholdLength = settings.size_threshold_db * 1024;
// Use chunk size from settings if file is larger than threshold
return file.size > thresholdLength ? settings.chunk_size_db : -1;
}
return -1;
}
let allSuccess = true;
for (const file of allFiles) {
@ -928,7 +950,8 @@ async function onVectorizeAllFilesClick() {
continue;
}
const result = await vectorizeFile(text, file.name, collectionId, settings.chunk_size);
const chunkSize = getChunkSize(file);
const result = await vectorizeFile(text, file.name, collectionId, chunkSize);
if (!result) {
allSuccess = false;

View File

@ -258,9 +258,8 @@ export class FilterHelper {
*/
folderFilter(data) {
const state = this.filterData[FILTER_TYPES.FOLDER];
// Slightly different than the other filters, as a positive folder filter means it doesn't filter anything (folders get "not hidden" at another place),
// while a negative state should then filter out all folders.
const isFolder = entity => isFilterState(state, FILTER_STATES.SELECTED) ? true : entity.type === 'tag';
// Filter directly on folder. Special rules on still displaying characters with active folder filter are implemented in 'getEntitiesList' directly.
const isFolder = entity => entity.type === 'tag';
return this.filterDataByState(data, state, isFolder);
}
@ -342,15 +341,40 @@ export class FilterHelper {
* Applies all filters to the given data.
* @param {any[]} data - The data to filter.
* @param {object} options - Optional call parameters
* @param {boolean|FilterType} [options.clearScoreCache=true] - Whether the score
* @param {boolean} [options.clearScoreCache=true] - Whether the score cache should be cleared.
* @param {Object.<FilterType, any>} [options.tempOverrides={}] - Temporarily override specific filters for this filter application
* @returns {any[]} The filtered data.
*/
applyFilters(data, { clearScoreCache = true } = {}) {
applyFilters(data, { clearScoreCache = true, tempOverrides = {} } = {}) {
if (clearScoreCache) this.clearScoreCache();
return Object.values(this.filterFunctions)
.reduce((data, fn) => fn(data), data);
// Save original filter states
const originalStates = {};
for (const key in tempOverrides) {
originalStates[key] = this.filterData[key];
this.filterData[key] = tempOverrides[key];
}
try {
const result = Object.values(this.filterFunctions)
.reduce((data, fn) => fn(data), data);
// Restore original filter states
for (const key in originalStates) {
this.filterData[key] = originalStates[key];
}
return result;
} catch (error) {
// Restore original filter states in case of an error
for (const key in originalStates) {
this.filterData[key] = originalStates[key];
}
throw error;
}
}
/**
* Cache scores for a specific filter type
* @param {FilterType} type - The type of data being cached

View File

@ -40,7 +40,6 @@ import {
online_status,
talkativeness_default,
selectRightMenuWithAnimation,
default_ch_mes,
deleteLastMessage,
showSwipeButtons,
hideSwipeButtons,
@ -204,6 +203,12 @@ export async function getGroupChat(groupId, reload = false) {
}
const mes = await getFirstCharacterMessage(character);
// No first message
if (!(mes?.mes)) {
continue;
}
chat.push(mes);
await eventSource.emit(event_types.MESSAGE_RECEIVED, (chat.length - 1));
addOneMessage(mes);
@ -452,7 +457,7 @@ async function getFirstCharacterMessage(character) {
mes['extra'] = { 'gen_id': Date.now() * Math.random() * 1000000 };
mes['mes'] = messageText
? substituteParams(messageText.trim(), name1, character.name)
: default_ch_mes;
: '';
mes['force_avatar'] =
character.avatar != 'none'
? getThumbnailUrl('avatar', character.avatar)
@ -637,7 +642,7 @@ function isValidImageUrl(url) {
if (Object.keys(url).length === 0) {
return false;
}
return isDataURL(url) || (url && url.startsWith('user'));
return isDataURL(url) || (url && (url.startsWith('user') || url.startsWith('/user')));
}
function getGroupAvatar(group) {
@ -788,7 +793,6 @@ async function generateGroupWrapper(by_auto_mode, type = null, params = {}) {
}
}
else if (type === 'impersonate') {
$('#send_textarea').attr('disabled', true);
activatedMembers = activateImpersonate(group.members);
}
else if (activationStrategy === group_activation_strategy.NATURAL) {
@ -805,7 +809,7 @@ async function generateGroupWrapper(by_auto_mode, type = null, params = {}) {
const bias = getBiasStrings(userInput, type);
await sendMessageAsUser(userInput, bias.messageBias);
await saveChatConditional();
$('#send_textarea').val('').trigger('input');
$('#send_textarea').val('')[0].dispatchEvent(new Event('input', { bubbles:true }));
}
// now the real generation begins: cycle through every activated character
@ -840,7 +844,6 @@ async function generateGroupWrapper(by_auto_mode, type = null, params = {}) {
typingIndicator.hide();
is_group_generating = false;
$('#send_textarea').attr('disabled', false);
setSendButtonState(false);
setCharacterId(undefined);
setCharacterName('');
@ -1420,6 +1423,10 @@ function select_group_chats(groupId, skipAnimation) {
* @returns {Promise<void>} - A promise that resolves when the processing and upload is complete.
*/
async function uploadGroupAvatar(event) {
if (!(event.target instanceof HTMLInputElement) || !event.target.files.length) {
return;
}
const file = event.target.files[0];
if (!file) {

View File

@ -5,7 +5,9 @@ const storageKey = 'language';
const overrideLanguage = localStorage.getItem(storageKey);
const localeFile = String(overrideLanguage || navigator.language || navigator.userLanguage || 'en').toLowerCase();
const langs = await fetch('/locales/lang.json').then(response => response.json());
const localeData = await getLocaleData(localeFile);
// Don't change to let/const! It will break module loading.
// eslint-disable-next-line prefer-const
var localeData = await getLocaleData(localeFile);
/**
* Fetches the locale data for the given language.
@ -107,12 +109,12 @@ export function applyLocale(root = document) {
const attributeMatch = key.match(/\[(\S+)\](.+)/); // [attribute]key
if (attributeMatch) { // attribute-tagged key
const localizedValue = localeData?.[attributeMatch[2]];
if (localizedValue) {
if (localizedValue || localizedValue == '') {
$(this).attr(attributeMatch[1], localizedValue);
}
} else { // No attribute tag, treat as 'text'
const localizedValue = localeData?.[key];
if (localizedValue) {
if (localizedValue || localizedValue == '') {
$(this).text(localizedValue);
}
}
@ -126,16 +128,17 @@ export function applyLocale(root = document) {
function addLanguagesToDropdown() {
const uiLanguageSelects = $('#ui_language_select, #onboarding_ui_language_select');
for (const langObj of langs) { // Set the value to the language code
const option = document.createElement('option');
option.value = langObj['lang']; // Set the value to the language code
option.innerText = langObj['display']; // Set the display text to the language name
$('#ui_language_select').append(option);
uiLanguageSelects.append(option);
}
const selectedLanguage = localStorage.getItem(storageKey);
if (selectedLanguage) {
$('#ui_language_select').val(selectedLanguage);
uiLanguageSelects.val(selectedLanguage);
}
}
@ -144,7 +147,7 @@ export function initLocales() {
addLanguagesToDropdown();
updateSecretDisplay();
$('#ui_language_select').on('change', async function () {
$('#ui_language_select, #onboarding_ui_language_select').on('change', async function () {
const language = String($(this).val());
if (language) {

View File

@ -7,7 +7,7 @@ import {
power_user,
context_presets,
} from './power-user.js';
import { resetScrollHeight } from './utils.js';
import { regexFromString, resetScrollHeight } from './utils.js';
/**
* @type {any[]} Instruct mode presets.
@ -189,10 +189,10 @@ export function autoSelectInstructPreset(modelId) {
// If activation regex is set, check if it matches the model id
if (preset.activation_regex) {
try {
const regex = new RegExp(preset.activation_regex, 'i');
const regex = regexFromString(preset.activation_regex);
// Stop on first match so it won't cycle back and forth between presets if multiple regexes match
if (regex.test(modelId)) {
if (regex instanceof RegExp && regex.test(modelId)) {
selectInstructPreset(preset.name);
return true;
@ -340,8 +340,11 @@ export function formatInstructModeChat(name, mes, isUser, isNarrator, forceAvata
}
const separator = power_user.instruct.wrap ? '\n' : '';
const textArray = includeNames ? [prefix, `${name}: ${mes}` + suffix] : [prefix, mes + suffix];
// Don't include the name if it's empty
const textArray = includeNames && name ? [prefix, `${name}: ${mes}` + suffix] : [prefix, mes + suffix];
const text = textArray.filter(x => x).join(separator);
return text;
}

View File

@ -259,6 +259,26 @@ function diceRollReplace(input, invalidRollPlaceholder = '') {
});
}
/**
* Returns the difference between two times. Works with any time format acceptable by moment().
* Can work with {{date}} {{time}} macros
* @param {string} input - The string to replace time difference macros in.
* @returns {string} The string with replaced time difference macros.
*/
function timeDiffReplace(input) {
const timeDiffPattern = /{{timeDiff::(.*?)::(.*?)}}/gi;
const output = input.replace(timeDiffPattern, (_match, matchPart1, matchPart2) => {
const time1 = moment(matchPart1);
const time2 = moment(matchPart2);
const timeDifference = moment.duration(time1.diff(time2));
return timeDifference.humanize();
});
return output;
}
/**
* Substitutes {{macro}} parameters in a string.
* @param {string} content - The string to substitute parameters in.
@ -327,6 +347,7 @@ export function evaluateMacros(content, env) {
const utcTime = moment().utc().utcOffset(utcOffset).format('LT');
return utcTime;
});
content = timeDiffReplace(content);
content = bannedWordsReplace(content);
content = randomReplace(content);
content = pickReplace(content, rawContent);

View File

@ -32,7 +32,6 @@ import {
this_chid,
} from '../script.js';
import { selected_group } from './group-chats.js';
import { registerSlashCommand } from './slash-commands.js';
import {
chatCompletionDefaultPrompts,
@ -51,6 +50,7 @@ import {
download,
getBase64Async,
getFileText,
getImageSizeFromDataURL,
getSortableDelay,
isDataURL,
parseJsonFile,
@ -66,6 +66,9 @@ import {
} from './instruct-mode.js';
import { isMobile } from './RossAscends-mods.js';
import { saveLogprobsForActiveMessage } from './logprobs.js';
import { SlashCommandParser } from './slash-commands/SlashCommandParser.js';
import { SlashCommand } from './slash-commands/SlashCommand.js';
import { ARGUMENT_TYPE, SlashCommandArgument } from './slash-commands/SlashCommandArgument.js';
export {
openai_messages_count,
@ -114,6 +117,7 @@ const max_4k = 4095;
const max_8k = 8191;
const max_16k = 16383;
const max_32k = 32767;
const max_64k = 65535;
const max_128k = 128 * 1000;
const max_200k = 200 * 1000;
const max_1mil = 1000 * 1000;
@ -173,6 +177,7 @@ export const chat_completion_sources = {
CUSTOM: 'custom',
COHERE: 'cohere',
PERPLEXITY: 'perplexity',
GROQ: 'groq',
};
const character_names_behavior = {
@ -240,6 +245,7 @@ const default_settings = {
mistralai_model: 'mistral-medium-latest',
cohere_model: 'command-r',
perplexity_model: 'llama-3-70b-instruct',
groq_model: 'llama3-70b-8192',
custom_model: '',
custom_url: '',
custom_include_body: '',
@ -251,6 +257,7 @@ const default_settings = {
openrouter_force_instruct: false,
openrouter_group_models: false,
openrouter_sort_models: 'alphabetically',
openrouter_providers: [],
jailbreak_system: false,
reverse_proxy: '',
chat_completion_source: chat_completion_sources.OPENAI,
@ -259,6 +266,7 @@ const default_settings = {
show_external_models: false,
proxy_password: '',
assistant_prefill: '',
assistant_impersonation: '',
human_sysprompt_message: default_claude_human_sysprompt_message,
use_ai21_tokenizer: false,
use_google_tokenizer: false,
@ -267,6 +275,7 @@ const default_settings = {
use_alt_scale: false,
squash_system_messages: false,
image_inlining: false,
inline_image_quality: 'low',
bypass_status_check: false,
continue_prefill: false,
names_behavior: character_names_behavior.NONE,
@ -313,6 +322,7 @@ const oai_settings = {
mistralai_model: 'mistral-medium-latest',
cohere_model: 'command-r',
perplexity_model: 'llama-3-70b-instruct',
groq_model: 'llama3-70b-8192',
custom_model: '',
custom_url: '',
custom_include_body: '',
@ -324,6 +334,7 @@ const oai_settings = {
openrouter_force_instruct: false,
openrouter_group_models: false,
openrouter_sort_models: 'alphabetically',
openrouter_providers: [],
jailbreak_system: false,
reverse_proxy: '',
chat_completion_source: chat_completion_sources.OPENAI,
@ -332,6 +343,7 @@ const oai_settings = {
show_external_models: false,
proxy_password: '',
assistant_prefill: '',
assistant_impersonation: '',
human_sysprompt_message: default_claude_human_sysprompt_message,
use_ai21_tokenizer: false,
use_google_tokenizer: false,
@ -340,6 +352,7 @@ const oai_settings = {
use_alt_scale: false,
squash_system_messages: false,
image_inlining: false,
inline_image_quality: 'low',
bypass_status_check: false,
continue_prefill: false,
names_behavior: character_names_behavior.NONE,
@ -899,7 +912,7 @@ function getPromptRole(role) {
/**
* Populate a chat conversation by adding prompts to the conversation and managing system and user prompts.
*
* @param {PromptCollection} prompts - PromptCollection containing all prompts where the key is the prompt identifier and the value is the prompt object.
* @param {import('./PromptManager.js').PromptCollection} prompts - PromptCollection containing all prompts where the key is the prompt identifier and the value is the prompt object.
* @param {ChatCompletion} chatCompletion - An instance of ChatCompletion class that will be populated with the prompts.
* @param {Object} options - An object with optional settings.
* @param {string} options.bias - A bias to be added in the conversation.
@ -911,7 +924,7 @@ function getPromptRole(role) {
* @param {object[]} options.messageExamples - Array containing all message examples.
* @returns {Promise<void>}
*/
async function populateChatCompletion(prompts, chatCompletion, { bias, quietPrompt, quietImage, type, cyclePrompt, messages, messageExamples } = {}) {
async function populateChatCompletion(prompts, chatCompletion, { bias, quietPrompt, quietImage, type, cyclePrompt, messages, messageExamples }) {
// Helper function for preparing a prompt, that already exists within the prompt collection, for completion
const addToChatCompletion = (source, target = null) => {
// We need the prompts array to determine a position for the source.
@ -1046,21 +1059,22 @@ async function populateChatCompletion(prompts, chatCompletion, { bias, quietProm
/**
* Combines system prompts with prompt manager prompts
*
* @param {string} Scenario - The scenario or context of the dialogue.
* @param {string} charPersonality - Description of the character's personality.
* @param {string} name2 - The second name to be used in the messages.
* @param {string} worldInfoBefore - The world info to be added before the main conversation.
* @param {string} worldInfoAfter - The world info to be added after the main conversation.
* @param {string} charDescription - Description of the character.
* @param {string} quietPrompt - The quiet prompt to be used in the conversation.
* @param {string} bias - The bias to be added in the conversation.
* @param {Object} extensionPrompts - An object containing additional prompts.
* @param {string} systemPromptOverride
* @param {string} jailbreakPromptOverride
* @param {string} personaDescription
* @param {Object} options - An object with optional settings.
* @param {string} options.Scenario - The scenario or context of the dialogue.
* @param {string} options.charPersonality - Description of the character's personality.
* @param {string} options.name2 - The second name to be used in the messages.
* @param {string} options.worldInfoBefore - The world info to be added before the main conversation.
* @param {string} options.worldInfoAfter - The world info to be added after the main conversation.
* @param {string} options.charDescription - Description of the character.
* @param {string} options.quietPrompt - The quiet prompt to be used in the conversation.
* @param {string} options.bias - The bias to be added in the conversation.
* @param {Object} options.extensionPrompts - An object containing additional prompts.
* @param {string} options.systemPromptOverride
* @param {string} options.jailbreakPromptOverride
* @param {string} options.personaDescription
* @returns {Object} prompts - The prepared and merged system and user-defined prompts.
*/
function preparePromptsForChatCompletion({ Scenario, charPersonality, name2, worldInfoBefore, worldInfoAfter, charDescription, quietPrompt, bias, extensionPrompts, systemPromptOverride, jailbreakPromptOverride, personaDescription } = {}) {
function preparePromptsForChatCompletion({ Scenario, charPersonality, name2, worldInfoBefore, worldInfoAfter, charDescription, quietPrompt, bias, extensionPrompts, systemPromptOverride, jailbreakPromptOverride, personaDescription }) {
const scenarioText = Scenario && oai_settings.scenario_format ? substituteParams(oai_settings.scenario_format) : '';
const charPersonalityText = charPersonality && oai_settings.personality_format ? substituteParams(oai_settings.personality_format) : '';
const groupNudge = substituteParams(oai_settings.group_nudge_prompt);
@ -1178,12 +1192,16 @@ function preparePromptsForChatCompletion({ Scenario, charPersonality, name2, wor
* @param {string} content.bias - The bias to be added in the conversation.
* @param {string} content.type - The type of the chat, can be 'impersonate'.
* @param {string} content.quietPrompt - The quiet prompt to be used in the conversation.
* @param {string} content.quietImage - Image prompt for extras
* @param {string} content.cyclePrompt - The last prompt used for chat message continuation.
* @param {Array} content.extensionPrompts - An array of additional prompts.
* @param {string} content.systemPromptOverride - The system prompt override.
* @param {string} content.jailbreakPromptOverride - The jailbreak prompt override.
* @param {string} content.personaDescription - The persona description.
* @param {object} content.extensionPrompts - An array of additional prompts.
* @param {object[]} content.messages - An array of messages to be used as chat history.
* @param {string[]} content.messageExamples - An array of messages to be used as dialogue examples.
* @param dryRun - Whether this is a live call or not.
* @returns {(*[]|boolean)[]} An array where the first element is the prepared chat and the second element is a boolean flag.
* @returns {Promise<(any[]|boolean)[]>} An array where the first element is the prepared chat and the second element is a boolean flag.
*/
export async function prepareOpenAIMessages({
name2,
@ -1203,7 +1221,7 @@ export async function prepareOpenAIMessages({
personaDescription,
messages,
messageExamples,
} = {}, dryRun) {
}, dryRun) {
// Without a character selected, there is no way to accurately calculate tokens
if (!promptManager.activeCharacter && dryRun) return [null, false];
@ -1432,6 +1450,8 @@ function getChatCompletionModel() {
return oai_settings.cohere_model;
case chat_completion_sources.PERPLEXITY:
return oai_settings.perplexity_model;
case chat_completion_sources.GROQ:
return oai_settings.groq_model;
default:
throw new Error(`Unknown chat completion source: ${oai_settings.chat_completion_source}`);
}
@ -1658,6 +1678,7 @@ async function sendOpenAIRequest(type, messages, signal) {
const isCustom = oai_settings.chat_completion_source == chat_completion_sources.CUSTOM;
const isCohere = oai_settings.chat_completion_source == chat_completion_sources.COHERE;
const isPerplexity = oai_settings.chat_completion_source == chat_completion_sources.PERPLEXITY;
const isGroq = oai_settings.chat_completion_source == chat_completion_sources.GROQ;
const isTextCompletion = (isOAI && textCompletionModels.includes(oai_settings.openai_model)) || (isOpenRouter && oai_settings.openrouter_force_instruct && power_user.instruct.enabled);
const isQuiet = type === 'quiet';
const isImpersonate = type === 'impersonate';
@ -1748,7 +1769,7 @@ async function sendOpenAIRequest(type, messages, signal) {
generate_data['human_sysprompt_message'] = substituteParams(oai_settings.human_sysprompt_message);
// Don't add a prefill on quiet gens (summarization)
if (!isQuiet) {
generate_data['assistant_prefill'] = substituteParams(oai_settings.assistant_prefill);
generate_data['assistant_prefill'] = isImpersonate ? substituteParams(oai_settings.assistant_impersonation) : substituteParams(oai_settings.assistant_prefill);
}
}
@ -1758,6 +1779,7 @@ async function sendOpenAIRequest(type, messages, signal) {
generate_data['repetition_penalty'] = Number(oai_settings.repetition_penalty_openai);
generate_data['top_a'] = Number(oai_settings.top_a_openai);
generate_data['use_fallback'] = oai_settings.openrouter_use_fallback;
generate_data['provider'] = oai_settings.openrouter_providers;
if (isTextCompletion) {
generate_data['stop'] = getStoppingStrings(isImpersonate, isContinue);
@ -1815,10 +1837,20 @@ async function sendOpenAIRequest(type, messages, signal) {
delete generate_data['stop'];
}
// https://console.groq.com/docs/openai
if (isGroq) {
delete generate_data.logprobs;
delete generate_data.logit_bias;
delete generate_data.top_logprobs;
delete generate_data.n;
}
if ((isOAI || isOpenRouter || isMistral || isCustom || isCohere) && oai_settings.seed >= 0) {
generate_data['seed'] = oai_settings.seed;
}
await eventSource.emit(event_types.CHAT_COMPLETION_SETTINGS_READY, generate_data);
const generate_url = '/api/backends/chat-completions/generate';
const response = await fetch(generate_url, {
method: 'POST',
@ -2163,12 +2195,47 @@ class Message {
}
}
const quality = oai_settings.inline_image_quality || default_settings.inline_image_quality;
this.content = [
{ type: 'text', text: textContent },
{ type: 'image_url', image_url: { 'url': image, 'detail': 'low' } },
{ type: 'image_url', image_url: { 'url': image, 'detail': quality } },
];
this.tokens += Message.tokensPerImage;
const tokens = await this.getImageTokenCost(image, quality);
this.tokens += tokens;
}
async getImageTokenCost(dataUrl, quality) {
if (quality === 'low') {
return Message.tokensPerImage;
}
const size = await getImageSizeFromDataURL(dataUrl);
// If the image is small enough, we can use the low quality token cost
if (quality === 'auto' && size.width <= 512 && size.height <= 512) {
return Message.tokensPerImage;
}
/*
* Images are first scaled to fit within a 2048 x 2048 square, maintaining their aspect ratio.
* Then, they are scaled such that the shortest side of the image is 768px long.
* Finally, we count how many 512px squares the image consists of.
* Each of those squares costs 170 tokens. Another 85 tokens are always added to the final total.
* https://platform.openai.com/docs/guides/vision/calculating-costs
*/
const scale = 2048 / Math.min(size.width, size.height);
const scaledWidth = Math.round(size.width * scale);
const scaledHeight = Math.round(size.height * scale);
const finalScale = 768 / Math.min(scaledWidth, scaledHeight);
const finalWidth = Math.round(scaledWidth * finalScale);
const finalHeight = Math.round(scaledHeight * finalScale);
const squares = Math.ceil(finalWidth / 512) * Math.ceil(finalHeight / 512);
const tokens = squares * 170 + 85;
return tokens;
}
/**
@ -2682,6 +2749,7 @@ function loadOpenAISettings(data, settings) {
oai_settings.mistralai_model = settings.mistralai_model ?? default_settings.mistralai_model;
oai_settings.cohere_model = settings.cohere_model ?? default_settings.cohere_model;
oai_settings.perplexity_model = settings.perplexity_model ?? default_settings.perplexity_model;
oai_settings.groq_model = settings.groq_model ?? default_settings.groq_model;
oai_settings.custom_model = settings.custom_model ?? default_settings.custom_model;
oai_settings.custom_url = settings.custom_url ?? default_settings.custom_url;
oai_settings.custom_include_body = settings.custom_include_body ?? default_settings.custom_include_body;
@ -2694,8 +2762,10 @@ function loadOpenAISettings(data, settings) {
oai_settings.show_external_models = settings.show_external_models ?? default_settings.show_external_models;
oai_settings.proxy_password = settings.proxy_password ?? default_settings.proxy_password;
oai_settings.assistant_prefill = settings.assistant_prefill ?? default_settings.assistant_prefill;
oai_settings.assistant_impersonation = settings.assistant_impersonation ?? default_settings.assistant_impersonation;
oai_settings.human_sysprompt_message = settings.human_sysprompt_message ?? default_settings.human_sysprompt_message;
oai_settings.image_inlining = settings.image_inlining ?? default_settings.image_inlining;
oai_settings.inline_image_quality = settings.inline_image_quality ?? default_settings.inline_image_quality;
oai_settings.bypass_status_check = settings.bypass_status_check ?? default_settings.bypass_status_check;
oai_settings.seed = settings.seed ?? default_settings.seed;
oai_settings.n = settings.n ?? default_settings.n;
@ -2729,10 +2799,14 @@ function loadOpenAISettings(data, settings) {
$('#api_url_scale').val(oai_settings.api_url_scale);
$('#openai_proxy_password').val(oai_settings.proxy_password);
$('#claude_assistant_prefill').val(oai_settings.assistant_prefill);
$('#claude_assistant_impersonation').val(oai_settings.assistant_impersonation);
$('#claude_human_sysprompt_textarea').val(oai_settings.human_sysprompt_message);
$('#openai_image_inlining').prop('checked', oai_settings.image_inlining);
$('#openai_bypass_status_check').prop('checked', oai_settings.bypass_status_check);
$('#openai_inline_image_quality').val(oai_settings.inline_image_quality);
$(`#openai_inline_image_quality option[value="${oai_settings.inline_image_quality}"]`).prop('selected', true);
$('#model_openai_select').val(oai_settings.openai_model);
$(`#model_openai_select option[value="${oai_settings.openai_model}"`).attr('selected', true);
$('#model_claude_select').val(oai_settings.claude_model);
@ -2749,6 +2823,8 @@ function loadOpenAISettings(data, settings) {
$(`#model_cohere_select option[value="${oai_settings.cohere_model}"`).attr('selected', true);
$('#model_perplexity_select').val(oai_settings.perplexity_model);
$(`#model_perplexity_select option[value="${oai_settings.perplexity_model}"`).attr('selected', true);
$('#model_groq_select').val(oai_settings.groq_model);
$(`#model_groq_select option[value="${oai_settings.groq_model}"`).attr('selected', true);
$('#custom_model_id').val(oai_settings.custom_model);
$('#custom_api_url_text').val(oai_settings.custom_url);
$('#openai_max_context').val(oai_settings.openai_max_context);
@ -2770,6 +2846,7 @@ function loadOpenAISettings(data, settings) {
$('#openrouter_use_fallback').prop('checked', oai_settings.openrouter_use_fallback);
$('#openrouter_force_instruct').prop('checked', oai_settings.openrouter_force_instruct);
$('#openrouter_group_models').prop('checked', oai_settings.openrouter_group_models);
$('#openrouter_providers_chat').val(oai_settings.openrouter_providers).trigger('change');
$('#squash_system_messages').prop('checked', oai_settings.squash_system_messages);
$('#continue_prefill').prop('checked', oai_settings.continue_prefill);
if (settings.impersonation_prompt !== undefined) oai_settings.impersonation_prompt = settings.impersonation_prompt;
@ -2898,7 +2975,14 @@ async function getStatusOpen() {
return resultCheckStatus();
}
const noValidateSources = [chat_completion_sources.SCALE, chat_completion_sources.CLAUDE, chat_completion_sources.AI21, chat_completion_sources.MAKERSUITE, chat_completion_sources.PERPLEXITY];
const noValidateSources = [
chat_completion_sources.SCALE,
chat_completion_sources.CLAUDE,
chat_completion_sources.AI21,
chat_completion_sources.MAKERSUITE,
chat_completion_sources.PERPLEXITY,
chat_completion_sources.GROQ,
];
if (noValidateSources.includes(oai_settings.chat_completion_source)) {
let status = 'Unable to verify key; press "Test Message" to validate.';
setOnlineStatus(status);
@ -2986,10 +3070,12 @@ async function saveOpenAIPreset(name, settings, triggerUi = true) {
openrouter_force_instruct: settings.openrouter_force_instruct,
openrouter_group_models: settings.openrouter_group_models,
openrouter_sort_models: settings.openrouter_sort_models,
openrouter_providers: settings.openrouter_providers,
ai21_model: settings.ai21_model,
mistralai_model: settings.mistralai_model,
cohere_model: settings.cohere_model,
perplexity_model: settings.perplexity_model,
groq_model: settings.groq_model,
custom_model: settings.custom_model,
custom_url: settings.custom_url,
custom_include_body: settings.custom_include_body,
@ -3033,6 +3119,7 @@ async function saveOpenAIPreset(name, settings, triggerUi = true) {
api_url_scale: settings.api_url_scale,
show_external_models: settings.show_external_models,
assistant_prefill: settings.assistant_prefill,
assistant_impersonation: settings.assistant_impersonation,
human_sysprompt_message: settings.human_sysprompt_message,
use_ai21_tokenizer: settings.use_ai21_tokenizer,
use_google_tokenizer: settings.use_google_tokenizer,
@ -3041,6 +3128,7 @@ async function saveOpenAIPreset(name, settings, triggerUi = true) {
use_alt_scale: settings.use_alt_scale,
squash_system_messages: settings.squash_system_messages,
image_inlining: settings.image_inlining,
inline_image_quality: settings.inline_image_quality,
bypass_status_check: settings.bypass_status_check,
continue_prefill: settings.continue_prefill,
continue_postfix: settings.continue_postfix,
@ -3381,10 +3469,12 @@ function onSettingsPresetChange() {
openrouter_force_instruct: ['#openrouter_force_instruct', 'openrouter_force_instruct', true],
openrouter_group_models: ['#openrouter_group_models', 'openrouter_group_models', false],
openrouter_sort_models: ['#openrouter_sort_models', 'openrouter_sort_models', false],
openrouter_providers: ['#openrouter_providers_chat', 'openrouter_providers', false],
ai21_model: ['#model_ai21_select', 'ai21_model', false],
mistralai_model: ['#model_mistralai_select', 'mistralai_model', false],
cohere_model: ['#model_cohere_select', 'cohere_model', false],
perplexity_model: ['#model_perplexity_select', 'perplexity_model', false],
groq_model: ['#model_groq_select', 'groq_model', false],
custom_model: ['#custom_model_id', 'custom_model', false],
custom_url: ['#custom_api_url_text', 'custom_url', false],
custom_include_body: ['#custom_include_body', 'custom_include_body', false],
@ -3416,6 +3506,7 @@ function onSettingsPresetChange() {
show_external_models: ['#openai_show_external_models', 'show_external_models', true],
proxy_password: ['#openai_proxy_password', 'proxy_password', false],
assistant_prefill: ['#claude_assistant_prefill', 'assistant_prefill', false],
assistant_impersonation: ['#claude_assistant_impersonation', 'assistant_impersonation', false],
human_sysprompt_message: ['#claude_human_sysprompt_textarea', 'human_sysprompt_message', false],
use_ai21_tokenizer: ['#use_ai21_tokenizer', 'use_ai21_tokenizer', true],
use_google_tokenizer: ['#use_google_tokenizer', 'use_google_tokenizer', true],
@ -3424,6 +3515,7 @@ function onSettingsPresetChange() {
use_alt_scale: ['#use_alt_scale', 'use_alt_scale', true],
squash_system_messages: ['#squash_system_messages', 'squash_system_messages', true],
image_inlining: ['#openai_image_inlining', 'image_inlining', true],
inline_image_quality: ['#openai_inline_image_quality', 'inline_image_quality', false],
continue_prefill: ['#continue_prefill', 'continue_prefill', true],
continue_postfix: ['#continue_postfix', 'continue_postfix', false],
seed: ['#seed_openai', 'seed', false],
@ -3440,6 +3532,11 @@ function onSettingsPresetChange() {
preset.names_behavior = character_names_behavior.COMPLETION;
}
// Claude: Assistant Impersonation Prefill = Inherit from Assistant Prefill
if (preset.assistant_prefill !== undefined && preset.assistant_impersonation === undefined) {
preset.assistant_impersonation = preset.assistant_prefill;
}
const updateInput = (selector, value) => $(selector).val(value).trigger('input');
const updateCheckbox = (selector, value) => $(selector).prop('checked', value).trigger('input');
@ -3464,6 +3561,7 @@ function onSettingsPresetChange() {
$('#chat_completion_source').trigger('change');
$('#openai_logit_bias_preset').trigger('change');
$('#openrouter_providers_chat').trigger('change');
saveSettingsDebounced();
eventSource.emit(event_types.OAI_PRESET_CHANGED_AFTER);
@ -3474,7 +3572,7 @@ function getMaxContextOpenAI(value) {
if (oai_settings.max_context_unlocked) {
return unlocked_max;
}
else if (value.includes('gpt-4-turbo') || value.includes('gpt-4-1106') || value.includes('gpt-4-0125') || value.includes('gpt-4-vision')) {
else if (value.includes('gpt-4-turbo') || value.includes('gpt-4o') || value.includes('gpt-4-1106') || value.includes('gpt-4-0125') || value.includes('gpt-4-vision')) {
return max_128k;
}
else if (value.includes('gpt-3.5-turbo-1106')) {
@ -3613,6 +3711,11 @@ async function onModelChange() {
oai_settings.perplexity_model = value;
}
if ($(this).is('#model_groq_select')) {
console.log('Groq model changed to', value);
oai_settings.groq_model = value;
}
if (value && $(this).is('#model_custom_select')) {
console.log('Custom model changed to', value);
oai_settings.custom_model = value;
@ -3633,7 +3736,7 @@ async function onModelChange() {
if (oai_settings.chat_completion_source == chat_completion_sources.MAKERSUITE) {
if (oai_settings.max_context_unlocked) {
$('#openai_max_context').attr('max', max_1mil);
} else if (value === 'gemini-1.5-pro-latest') {
} else if (value === 'gemini-1.5-pro-latest' || value.includes('gemini-1.5-flash')) {
$('#openai_max_context').attr('max', max_1mil);
} else if (value === 'gemini-ultra' || value === 'gemini-1.0-pro-latest' || value === 'gemini-pro' || value === 'gemini-1.0-ultra-latest') {
$('#openai_max_context').attr('max', max_32k);
@ -3728,7 +3831,11 @@ async function onModelChange() {
}
if (oai_settings.chat_completion_source === chat_completion_sources.MISTRALAI) {
$('#openai_max_context').attr('max', max_32k);
if (oai_settings.mistralai_model.includes('mixtral-8x22b')) {
$('#openai_max_context').attr('max', max_64k);
} else {
$('#openai_max_context').attr('max', max_32k);
}
oai_settings.openai_max_context = Math.min(oai_settings.openai_max_context, Number($('#openai_max_context').attr('max')));
$('#openai_max_context').val(oai_settings.openai_max_context).trigger('input');
@ -3762,6 +3869,12 @@ async function onModelChange() {
if (oai_settings.max_context_unlocked) {
$('#openai_max_context').attr('max', unlocked_max);
}
else if (['llama-3-sonar-small-32k-chat', 'llama-3-sonar-large-32k-chat'].includes(oai_settings.perplexity_model)) {
$('#openai_max_context').attr('max', max_32k);
}
else if (['llama-3-sonar-small-32k-online', 'llama-3-sonar-large-32k-online'].includes(oai_settings.perplexity_model)) {
$('#openai_max_context').attr('max', 28000);
}
else if (['sonar-small-chat', 'sonar-medium-chat', 'codellama-70b-instruct', 'mistral-7b-instruct', 'mixtral-8x7b-instruct', 'mixtral-8x22b-instruct'].includes(oai_settings.perplexity_model)) {
$('#openai_max_context').attr('max', max_16k);
}
@ -3780,6 +3893,25 @@ async function onModelChange() {
$('#temp_openai').attr('max', oai_max_temp).val(oai_settings.temp_openai).trigger('input');
}
if (oai_settings.chat_completion_source == chat_completion_sources.GROQ) {
if (oai_settings.max_context_unlocked) {
$('#openai_max_context').attr('max', unlocked_max);
}
else if (['llama3-8b-8192', 'llama3-70b-8192', 'gemma-7b-it'].includes(oai_settings.groq_model)) {
$('#openai_max_context').attr('max', max_8k);
}
else if (['mixtral-8x7b-32768'].includes(oai_settings.groq_model)) {
$('#openai_max_context').attr('max', max_32k);
}
else {
$('#openai_max_context').attr('max', max_4k);
}
oai_settings.openai_max_context = Math.min(Number($('#openai_max_context').attr('max')), oai_settings.openai_max_context);
$('#openai_max_context').val(oai_settings.openai_max_context).trigger('input');
oai_settings.temp_openai = Math.min(oai_max_temp, oai_settings.temp_openai);
$('#temp_openai').attr('max', oai_max_temp).val(oai_settings.temp_openai).trigger('input');
}
if (oai_settings.chat_completion_source == chat_completion_sources.AI21) {
if (oai_settings.max_context_unlocked) {
$('#openai_max_context').attr('max', unlocked_max);
@ -4000,6 +4132,19 @@ async function onConnectButtonClick(e) {
}
}
if (oai_settings.chat_completion_source == chat_completion_sources.GROQ) {
const api_key_groq = String($('#api_key_groq').val()).trim();
if (api_key_groq.length) {
await writeSecret(SECRET_KEYS.GROQ, api_key_groq);
}
if (!secret_state[SECRET_KEYS.GROQ]) {
console.log('No secret key saved for Groq');
return;
}
}
startStatusLoading();
saveSettingsDebounced();
await getStatusOpen();
@ -4041,6 +4186,9 @@ function toggleChatCompletionForms() {
else if (oai_settings.chat_completion_source == chat_completion_sources.PERPLEXITY) {
$('#model_perplexity_select').trigger('change');
}
else if (oai_settings.chat_completion_source == chat_completion_sources.GROQ) {
$('#model_groq_select').trigger('change');
}
else if (oai_settings.chat_completion_source == chat_completion_sources.CUSTOM) {
$('#model_custom_select').trigger('change');
}
@ -4148,11 +4296,14 @@ export function isImageInliningSupported() {
// gultra just isn't being offered as multimodal, thanks google.
const visionSupportedModels = [
'gpt-4-vision',
'gemini-1.5-flash-latest',
'gemini-1.5-flash',
'gemini-1.0-pro-vision-latest',
'gemini-1.5-pro-latest',
'gemini-pro-vision',
'claude-3',
'gpt-4-turbo',
'gpt-4o',
];
switch (oai_settings.chat_completion_source) {
@ -4294,7 +4445,18 @@ function runProxyCallback(_, value) {
return foundName;
}
registerSlashCommand('proxy', runProxyCallback, [], '<span class="monospace">(name)</span> sets a proxy preset by name');
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'proxy',
callback: runProxyCallback,
returns: 'current proxy',
namedArgumentList: [],
unnamedArgumentList: [
new SlashCommandArgument(
'name', [ARGUMENT_TYPE.STRING], true,
),
],
helpString: 'Sets a proxy preset by name.',
}));
$(document).ready(async function () {
$('#test_api_button').on('click', testApiConnection);
@ -4570,6 +4732,11 @@ $(document).ready(async function () {
saveSettingsDebounced();
});
$('#claude_assistant_impersonation').on('input', function () {
oai_settings.assistant_impersonation = String($(this).val());
saveSettingsDebounced();
});
$('#claude_human_sysprompt_textarea').on('input', function () {
oai_settings.human_sysprompt_message = String($('#claude_human_sysprompt_textarea').val());
saveSettingsDebounced();
@ -4605,6 +4772,11 @@ $(document).ready(async function () {
saveSettingsDebounced();
});
$('#openai_inline_image_quality').on('input', function () {
oai_settings.inline_image_quality = String($(this).val());
saveSettingsDebounced();
});
$('#continue_prefill').on('input', function () {
oai_settings.continue_prefill = !!$(this).prop('checked');
saveSettingsDebounced();
@ -4697,6 +4869,19 @@ $(document).ready(async function () {
});
}
$('#openrouter_providers_chat').on('change', function () {
const selectedProviders = $(this).val();
// Not a multiple select?
if (!Array.isArray(selectedProviders)) {
return;
}
oai_settings.openrouter_providers = selectedProviders;
saveSettingsDebounced();
});
$('#api_button_openai').on('click', onConnectButtonClick);
$('#openai_reverse_proxy').on('input', onReverseProxyInput);
$('#model_openai_select').on('change', onModelChange);
@ -4711,6 +4896,7 @@ $(document).ready(async function () {
$('#model_mistralai_select').on('change', onModelChange);
$('#model_cohere_select').on('change', onModelChange);
$('#model_perplexity_select').on('change', onModelChange);
$('#model_groq_select').on('change', onModelChange);
$('#model_custom_select').on('change', onModelChange);
$('#settings_preset_openai').on('change', onSettingsPresetChange);
$('#new_oai_preset').on('click', onNewPresetClick);

View File

@ -27,6 +27,8 @@ import { selected_group } from './group-chats.js';
let savePersonasPage = 0;
const GRID_STORAGE_KEY = 'Personas_GridView';
const DEFAULT_DEPTH = 2;
const DEFAULT_ROLE = 0;
export let user_avatar = '';
export const personasFilter = new FilterHelper(debounce(getUserAvatars, debounce_timeout.quick));
@ -119,7 +121,7 @@ function verifyPersonaSearchSortRule() {
flashHighlight(selector);
}
// If search got cleared, we make sure to hide the option and go back to the one before
if (!searchTerm && !isHidden) {
if (!searchTerm) {
searchOption.attr('hidden', '');
selector.val(power_user.persona_sort_order);
}
@ -371,6 +373,8 @@ export function initPersona(avatarId, personaName, personaDescription) {
power_user.persona_descriptions[avatarId] = {
description: personaDescription || '',
position: persona_description_positions.IN_PROMPT,
depth: DEFAULT_DEPTH,
role: DEFAULT_ROLE,
};
saveSettingsDebounced();
@ -415,6 +419,8 @@ export async function convertCharacterToPersona(characterId = null) {
power_user.persona_descriptions[overwriteName] = {
description: description,
position: persona_description_positions.IN_PROMPT,
depth: DEFAULT_DEPTH,
role: DEFAULT_ROLE,
};
// If the user is currently using this persona, update the description
@ -447,11 +453,17 @@ export function setPersonaDescription() {
power_user.persona_description_position = persona_description_positions.IN_PROMPT;
}
$('#persona_depth_position_settings').toggle(power_user.persona_description_position === persona_description_positions.AT_DEPTH);
$('#persona_description').val(power_user.persona_description);
$('#persona_depth_value').val(power_user.persona_description_depth ?? DEFAULT_DEPTH);
$('#persona_description_position')
.val(power_user.persona_description_position)
.find(`option[value='${power_user.persona_description_position}']`)
.find(`option[value="${power_user.persona_description_position}"]`)
.attr('selected', String(true));
$('#persona_depth_role')
.val(power_user.persona_description_role)
.find(`option[value="${power_user.persona_description_role}"]`)
.prop('selected', String(true));
countPersonaDescriptionTokens();
}
@ -473,12 +485,20 @@ export function autoSelectPersona(name) {
async function updatePersonaNameIfExists(avatarId, newName) {
if (avatarId in power_user.personas) {
power_user.personas[avatarId] = newName;
await getUserAvatars(true, avatarId);
saveSettingsDebounced();
console.log(`Updated persona name for ${avatarId} to ${newName}`);
} else {
console.log(`Persona name ${avatarId} was not updated because it does not exist`);
power_user.personas[avatarId] = newName;
power_user.persona_descriptions[avatarId] = {
description: '',
position: persona_description_positions.IN_PROMPT,
depth: DEFAULT_DEPTH,
role: DEFAULT_ROLE,
};
console.log(`Created persona name for ${avatarId} as ${newName}`);
}
await getUserAvatars(true, avatarId);
saveSettingsDebounced();
}
async function bindUserNameToPersona(e) {
@ -511,6 +531,8 @@ async function bindUserNameToPersona(e) {
power_user.persona_descriptions[avatarId] = {
description: isCurrentPersona ? power_user.persona_description : '',
position: isCurrentPersona ? power_user.persona_description_position : persona_description_positions.IN_PROMPT,
depth: isCurrentPersona ? power_user.persona_description_depth : DEFAULT_DEPTH,
role: isCurrentPersona ? power_user.persona_description_role : DEFAULT_ROLE,
};
}
@ -551,12 +573,16 @@ function selectCurrentPersona() {
const descriptor = power_user.persona_descriptions[user_avatar];
if (descriptor) {
power_user.persona_description = descriptor.description;
power_user.persona_description_position = descriptor.position;
power_user.persona_description = descriptor.description ?? '';
power_user.persona_description_position = descriptor.position ?? persona_description_positions.IN_PROMPT;
power_user.persona_description_depth = descriptor.depth ?? DEFAULT_DEPTH;
power_user.persona_description_role = descriptor.role ?? DEFAULT_ROLE;
} else {
power_user.persona_description = '';
power_user.persona_description_position = persona_description_positions.IN_PROMPT;
power_user.persona_descriptions[user_avatar] = { description: '', position: persona_description_positions.IN_PROMPT };
power_user.persona_description_depth = DEFAULT_DEPTH;
power_user.persona_description_role = DEFAULT_ROLE;
power_user.persona_descriptions[user_avatar] = { description: '', position: persona_description_positions.IN_PROMPT, depth: DEFAULT_DEPTH, role: DEFAULT_ROLE };
}
setPersonaDescription();
@ -661,6 +687,8 @@ function onPersonaDescriptionInput() {
object = {
description: power_user.persona_description,
position: Number($('#persona_description_position').find(':selected').val()),
depth: Number($('#persona_depth_value').val()),
role: Number($('#persona_depth_role').find(':selected').val()),
};
power_user.persona_descriptions[user_avatar] = object;
}
@ -674,26 +702,55 @@ function onPersonaDescriptionInput() {
saveSettingsDebounced();
}
function onPersonaDescriptionDepthValueInput() {
power_user.persona_description_depth = Number($('#persona_depth_value').val());
if (power_user.personas[user_avatar]) {
const object = getOrCreatePersonaDescriptor();
object.depth = power_user.persona_description_depth;
}
saveSettingsDebounced();
}
function onPersonaDescriptionDepthRoleInput() {
power_user.persona_description_role = Number($('#persona_depth_role').find(':selected').val());
if (power_user.personas[user_avatar]) {
const object = getOrCreatePersonaDescriptor();
object.role = power_user.persona_description_role;
}
saveSettingsDebounced();
}
function onPersonaDescriptionPositionInput() {
power_user.persona_description_position = Number(
$('#persona_description_position').find(':selected').val(),
);
if (power_user.personas[user_avatar]) {
let object = power_user.persona_descriptions[user_avatar];
if (!object) {
object = {
description: power_user.persona_description,
position: power_user.persona_description_position,
};
power_user.persona_descriptions[user_avatar] = object;
}
const object = getOrCreatePersonaDescriptor();
object.position = power_user.persona_description_position;
}
saveSettingsDebounced();
$('#persona_depth_position_settings').toggle(power_user.persona_description_position === persona_description_positions.AT_DEPTH);
}
function getOrCreatePersonaDescriptor() {
let object = power_user.persona_descriptions[user_avatar];
if (!object) {
object = {
description: power_user.persona_description,
position: power_user.persona_description_position,
depth: power_user.persona_description_depth,
role: power_user.persona_description_role,
};
power_user.persona_descriptions[user_avatar] = object;
}
return object;
}
async function setDefaultPersona(e) {
@ -919,6 +976,8 @@ export function initPersonas() {
$('#create_dummy_persona').on('click', createDummyPersona);
$('#persona_description').on('input', onPersonaDescriptionInput);
$('#persona_description_position').on('input', onPersonaDescriptionPositionInput);
$('#persona_depth_value').on('input', onPersonaDescriptionDepthValueInput);
$('#persona_depth_role').on('input', onPersonaDescriptionDepthRoleInput);
$('#personas_backup').on('click', onBackupPersonas);
$('#personas_restore').on('click', () => $('#personas_restore_input').trigger('click'));
$('#personas_restore_input').on('change', onPersonasRestoreInput);

View File

@ -69,7 +69,7 @@ export class Popup {
if (allowVerticalScrolling) dlg.classList.add('vertical_scrolling_dialogue_popup');
this.ok.textContent = okButton ?? 'OK';
this.cancel.textContent = cancelButton ?? 'Cancel';
this.cancel.textContent = cancelButton ?? template.getAttribute('popup_text_cancel');
switch (type) {
case POPUP_TYPE.TEXT: {
@ -79,13 +79,13 @@ export class Popup {
}
case POPUP_TYPE.CONFIRM: {
this.input.style.display = 'none';
this.ok.textContent = okButton ?? 'Yes';
this.cancel.textContent = cancelButton ?? 'No';
this.ok.textContent = okButton ?? template.getAttribute('popup_text_yes');
this.cancel.textContent = cancelButton ?? template.getAttribute('popup_text_no');
break;
}
case POPUP_TYPE.INPUT: {
this.input.style.display = 'block';
this.ok.textContent = okButton ?? 'Save';
this.ok.textContent = okButton ?? template.getAttribute('popup_text_save');
break;
}
default: {

View File

@ -35,7 +35,6 @@ import {
selectInstructPreset,
} from './instruct-mode.js';
import { registerSlashCommand } from './slash-commands.js';
import { getTagsList, tag_map, tags } from './tags.js';
import { tokenizers } from './tokenizers.js';
import { BIAS_CACHE } from './logit-bias.js';
@ -43,6 +42,10 @@ import { renderTemplateAsync } from './templates.js';
import { countOccurrences, debounce, delay, download, getFileText, isOdd, onlyUnique, resetScrollHeight, shuffle, sortMoments, stringToRange, timestampToMoment } from './utils.js';
import { FILTER_TYPES } from './filters.js';
import { PARSER_FLAG, SlashCommandParser } from './slash-commands/SlashCommandParser.js';
import { SlashCommand } from './slash-commands/SlashCommand.js';
import { ARGUMENT_TYPE, SlashCommandArgument } from './slash-commands/SlashCommandArgument.js';
import { AUTOCOMPLETE_WIDTH } from './autocomplete/AutoComplete.js';
export {
loadPowerUserSettings,
@ -99,6 +102,7 @@ export const persona_description_positions = {
AFTER_CHAR: 1,
TOP_AN: 2,
BOTTOM_AN: 3,
AT_DEPTH: 4,
};
let power_user = {
@ -241,6 +245,8 @@ let power_user = {
persona_description: '',
persona_description_position: persona_description_positions.IN_PROMPT,
persona_description_role: 0,
persona_description_depth: 2,
persona_show_notifications: true,
persona_sort_order: 'asc',
@ -253,6 +259,24 @@ let power_user = {
zoomed_avatar_magnification: false,
show_tag_filters: false,
aux_field: 'character_version',
stscript: {
matching: 'fuzzy',
autocomplete: {
autoHide: false,
style: 'theme',
font: {
scale: 0.8,
},
width: {
left: AUTOCOMPLETE_WIDTH.CHAT,
right: AUTOCOMPLETE_WIDTH.CHAT,
},
},
parser: {
/**@type {Object.<PARSER_FLAG,boolean>} */
flags: {},
},
},
restore_user_input: true,
reduced_motion: false,
compact_input_area: true,
@ -1368,7 +1392,7 @@ async function applyTheme(name) {
}
async function applyMovingUIPreset(name) {
resetMovablePanels('quiet');
await resetMovablePanels('quiet');
const movingUIPreset = movingUIPresets.find(x => x.name == name);
if (!movingUIPreset) {
@ -1380,6 +1404,7 @@ async function applyMovingUIPreset(name) {
console.log('MovingUI Preset applied: ' + name);
loadMovingUIState();
saveSettingsDebounced();
}
/**
@ -1430,11 +1455,32 @@ function getExampleMessagesBehavior() {
}
function loadPowerUserSettings(settings, data) {
const defaultStscript = JSON.parse(JSON.stringify(power_user.stscript));
// Load from settings.json
if (settings.power_user !== undefined) {
Object.assign(power_user, settings.power_user);
}
if (power_user.stscript === undefined) {
power_user.stscript = defaultStscript;
} else {
if (power_user.stscript.autocomplete === undefined) {
power_user.stscript.autocomplete = defaultStscript.autocomplete;
} else {
if (power_user.stscript.autocomplete.width === undefined) {
power_user.stscript.autocomplete.width = defaultStscript.autocomplete.width;
}
if (power_user.stscript.autocomplete.font === undefined) {
power_user.stscript.autocomplete.font = defaultStscript.autocomplete.font;
}
}
if (power_user.stscript.parser === undefined) {
power_user.stscript.parser = defaultStscript.parser;
} else if (power_user.stscript.parser.flags === undefined) {
power_user.stscript.parser.flags = defaultStscript.parser.flags;
}
}
if (data.themes !== undefined) {
themes = data.themes;
}
@ -1576,6 +1622,21 @@ function loadPowerUserSettings(settings, data) {
$('#chat_width_slider').val(power_user.chat_width);
$('#token_padding').val(power_user.token_padding);
$('#aux_field').val(power_user.aux_field);
$('#stscript_autocomplete_autoHide').prop('checked', power_user.stscript.autocomplete.autoHide ?? false).trigger('input');
$('#stscript_matching').val(power_user.stscript.matching ?? 'fuzzy');
$('#stscript_autocomplete_style').val(power_user.stscript.autocomplete_style ?? 'theme');
document.body.setAttribute('data-stscript-style', power_user.stscript.autocomplete_style);
$('#stscript_parser_flag_strict_escaping').prop('checked', power_user.stscript.parser.flags[PARSER_FLAG.STRICT_ESCAPING] ?? false);
$('#stscript_parser_flag_replace_getvar').prop('checked', power_user.stscript.parser.flags[PARSER_FLAG.REPLACE_GETVAR] ?? false);
$('#stscript_autocomplete_font_scale').val(power_user.stscript.autocomplete.font.scale ?? defaultStscript.autocomplete.font.scale);
$('#stscript_autocomplete_font_scale_counter').val(power_user.stscript.autocomplete.font.scale ?? defaultStscript.autocomplete.font.scale);
document.body.style.setProperty('--ac-font-scale', power_user.stscript.autocomplete.font.scale ?? defaultStscript.autocomplete.font.scale.toString());
$('#stscript_autocomplete_width_left').val(power_user.stscript.autocomplete.width.left ?? AUTOCOMPLETE_WIDTH.CHAT);
document.querySelector('#stscript_autocomplete_width_left').dispatchEvent(new Event('input', { bubbles: true }));
$('#stscript_autocomplete_width_right').val(power_user.stscript.autocomplete.width.right ?? AUTOCOMPLETE_WIDTH.CHAT);
document.querySelector('#stscript_autocomplete_width_right').dispatchEvent(new Event('input', { bubbles: true }));
$('#restore_user_input').prop('checked', power_user.restore_user_input);
$('#chat_truncation').val(power_user.chat_truncation);
@ -1865,7 +1926,7 @@ function highlightDefaultContext() {
/**
* Fuzzy search characters by a search term
* @param {string} searchValue - The search term
* @returns {{item?: *, refIndex: number, score: number}[]} Results as items with their score
* @returns {FuseResult[]} Results as items with their score
*/
export function fuzzySearchCharacters(searchValue) {
// @ts-ignore
@ -1898,7 +1959,7 @@ export function fuzzySearchCharacters(searchValue) {
* Fuzzy search world info entries by a search term
* @param {*[]} data - WI items data array
* @param {string} searchValue - The search term
* @returns {{item?: *, refIndex: number, score: number}[]} Results as items with their score
* @returns {FuseResult[]} Results as items with their score
*/
export function fuzzySearchWorldInfo(data, searchValue) {
// @ts-ignore
@ -1927,7 +1988,7 @@ export function fuzzySearchWorldInfo(data, searchValue) {
* Fuzzy search persona entries by a search term
* @param {*[]} data - persona data array
* @param {string} searchValue - The search term
* @returns {{item?: *, refIndex: number, score: number}[]} Results as items with their score
* @returns {FuseResult[]} Results as items with their score
*/
export function fuzzySearchPersonas(data, searchValue) {
data = data.map(x => ({ key: x, name: power_user.personas[x] ?? '', description: power_user.persona_descriptions[x]?.description ?? '' }));
@ -1951,7 +2012,7 @@ export function fuzzySearchPersonas(data, searchValue) {
/**
* Fuzzy search tags by a search term
* @param {string} searchValue - The search term
* @returns {{item?: *, refIndex: number, score: number}[]} Results as items with their score
* @returns {FuseResult[]} Results as items with their score
*/
export function fuzzySearchTags(searchValue) {
// @ts-ignore
@ -1973,7 +2034,7 @@ export function fuzzySearchTags(searchValue) {
/**
* Fuzzy search groups by a search term
* @param {string} searchValue - The search term
* @returns {{item?: *, refIndex: number, score: number}[]} Results as items with their score
* @returns {FuseResult[]} Results as items with their score
*/
export function fuzzySearchGroups(searchValue) {
// @ts-ignore
@ -2935,13 +2996,20 @@ $(document).ready(() => {
});
const reportZoomLevelDebounced = debounce(() => {
const zoomLevel = Number(window.devicePixelRatio).toFixed(2);
const zoomLevel = Number(window.devicePixelRatio).toFixed(2) || 1;
const winWidth = window.innerWidth;
const winHeight = window.innerHeight;
console.debug(`Zoom: ${zoomLevel}, X:${winWidth}, Y:${winHeight}`);
const originalWidth = winWidth * zoomLevel;
const originalHeight = winHeight * zoomLevel;
console.debug(`Zoom: ${zoomLevel}, X:${winWidth}, Y:${winHeight}, original: ${originalWidth}x${originalHeight} `);
return zoomLevel;
});
var coreTruthWinWidth = window.innerWidth;
var coreTruthWinHeight = window.innerHeight;
$(window).on('resize', async () => {
console.log(`Window resize: ${coreTruthWinWidth}x${coreTruthWinHeight} -> ${window.innerWidth}x${window.innerHeight}`)
adjustAutocompleteDebounced();
setHotswapsDebounced();
@ -2951,9 +3019,54 @@ $(document).ready(() => {
reportZoomLevelDebounced();
//attempt to scale movingUI elements naturally across window resizing/zooms
//this will still break if the zoom level causes mobile styles to come into play.
const scaleY = Number(window.innerHeight / coreTruthWinHeight).toFixed(4);
const scaleX = Number(window.innerWidth / coreTruthWinWidth).toFixed(4);
if (Object.keys(power_user.movingUIState).length > 0) {
resetMovablePanels('resize');
for (var elmntName of Object.keys(power_user.movingUIState)) {
var elmntState = power_user.movingUIState[elmntName];
var oldHeight = elmntState.height;
var oldWidth = elmntState.width;
var oldLeft = elmntState.left;
var oldTop = elmntState.top;
var oldBottom = elmntState.bottom;
var oldRight = elmntState.right;
var newHeight, newWidth, newTop, newBottom, newLeft, newRight;
newHeight = Number(oldHeight * scaleY).toFixed(0);
newWidth = Number(oldWidth * scaleX).toFixed(0);
newLeft = Number(oldLeft * scaleX).toFixed(0);
newTop = Number(oldTop * scaleY).toFixed(0);
newBottom = Number(oldBottom * scaleY).toFixed(0);
newRight = Number(oldRight * scaleX).toFixed(0);
try {
var elmnt = $('#' + $.escapeSelector(elmntName));
if (elmnt.length) {
console.log(`scaling ${elmntName} by ${scaleX}x${scaleY} to ${newWidth}x${newHeight}`);
elmnt.css('height', newHeight);
elmnt.css('width', newWidth);
elmnt.css('inset', `${newTop}px ${newRight}px ${newBottom}px ${newLeft}px`);
power_user.movingUIState[elmntName].height = newHeight;
power_user.movingUIState[elmntName].width = newWidth;
power_user.movingUIState[elmntName].top = newTop;
power_user.movingUIState[elmntName].bottom = newBottom;
power_user.movingUIState[elmntName].left = newLeft;
power_user.movingUIState[elmntName].right = newRight;
} else {
console.log(`skipping ${elmntName} because it doesn't exist in the DOM`);
}
} catch (err) {
console.log(`error occurred while processing ${elmntName}: ${err}`);
}
}
} else {
console.log('aborting MUI reset', Object.keys(power_user.movingUIState).length)
}
saveSettingsDebounced();
coreTruthWinWidth = window.innerWidth;
coreTruthWinHeight = window.innerHeight;
});
// Settings that go to settings.json
@ -3591,6 +3704,69 @@ $(document).ready(() => {
saveSettingsDebounced();
});
$('#stscript_autocomplete_autoHide').on('input', function () {
power_user.stscript.autocomplete.autoHide = !!$(this).prop('checked');
saveSettingsDebounced();
});
$('#stscript_matching').on('change', function () {
const value = $(this).find(':selected').val();
power_user.stscript.matching = String(value);
saveSettingsDebounced();
});
$('#stscript_autocomplete_style').on('change', function () {
const value = $(this).find(':selected').val();
power_user.stscript.autocomplete_style = String(value);
document.body.setAttribute('data-stscript-style', power_user.stscript.autocomplete_style);
saveSettingsDebounced();
});
$('#stscript_autocomplete_font_scale').on('input', function () {
const value = $(this).val();
$('#stscript_autocomplete_font_scale_counter').val(value);
power_user.stscript.autocomplete.font.scale = Number(value);
document.body.style.setProperty('--ac-font-scale', value.toString());
window.dispatchEvent(new Event('resize', { bubbles: true }));
saveSettingsDebounced();
});
$('#stscript_autocomplete_font_scale_counter').on('input', function () {
const value = $(this).val();
$('#stscript_autocomplete_font_scale').val(value);
power_user.stscript.autocomplete.font.scale = Number(value);
document.body.style.setProperty('--ac-font-scale', value.toString());
window.dispatchEvent(new Event('resize', { bubbles: true }));
saveSettingsDebounced();
});
$('#stscript_autocomplete_width_left').on('input', function () {
const value = $(this).val();
power_user.stscript.autocomplete.width.left = Number(value);
/**@type {HTMLElement}*/(this.closest('.doubleRangeInputContainer')).style.setProperty('--value', value.toString());
window.dispatchEvent(new Event('resize', { bubbles: true }));
saveSettingsDebounced();
});
$('#stscript_autocomplete_width_right').on('input', function () {
const value = $(this).val();
power_user.stscript.autocomplete.width.right = Number(value);
/**@type {HTMLElement}*/(this.closest('.doubleRangeInputContainer')).style.setProperty('--value', value.toString());
window.dispatchEvent(new Event('resize', { bubbles: true }));
saveSettingsDebounced();
});
$('#stscript_parser_flag_strict_escaping').on('click', function () {
const value = $(this).prop('checked');
power_user.stscript.parser.flags[PARSER_FLAG.STRICT_ESCAPING] = value;
saveSettingsDebounced();
});
$('#stscript_parser_flag_replace_getvar').on('click', function () {
const value = $(this).prop('checked');
power_user.stscript.parser.flags[PARSER_FLAG.REPLACE_GETVAR] = value;
saveSettingsDebounced();
});
$('#restore_user_input').on('input', function () {
power_user.restore_user_input = !!$(this).prop('checked');
saveSettingsDebounced();
@ -3669,13 +3845,93 @@ $(document).ready(() => {
browser_has_focus = false;
});
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, [], '<span class="monospace">(optional tag name)</span> start a new chat with a random character. If an argument is provided, only considers characters that have the specified tag.', 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! Returns the text of cut messages separated by a newline.', 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);
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
name: 'vn',
callback: toggleWaifu,
helpString: 'Swaps Visual Novel Mode On/Off',
}));
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
name: 'newchat',
callback: doNewChat,
helpString: 'Start a new chat with the current character',
}));
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
name: 'random',
callback: doRandomChat,
unnamedArgumentList: [
new SlashCommandArgument(
'optional tag name', [ARGUMENT_TYPE.STRING], false,
),
],
helpString: 'Start a new chat with a random character. If an argument is provided, only considers characters that have the specified tag.',
}));
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
name: 'delmode',
callback: doDelMode,
aliases: ['del'],
unnamedArgumentList: [
new SlashCommandArgument(
'optional number', [ARGUMENT_TYPE.NUMBER], false,
),
],
helpString: 'Enter message deletion mode, and auto-deletes last N messages if numeric argument is provided.',
}));
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
name: 'cut',
callback: doMesCut,
returns: 'the text of cut messages separated by a newline',
unnamedArgumentList: [
new SlashCommandArgument(
'number or range', [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.RANGE], true,
),
],
helpString: `
<div>
Cuts the specified message or continuous chunk from the chat.
</div>
<div>
Ranges are inclusive!
</div>
<div>
<strong>Example:</strong>
<ul>
<li>
<pre><code>/cut 0-10</code></pre>
</li>
</ul>
</div>
`,
aliases: [],
}));
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
name: 'resetpanels',
callback: doResetPanels,
helpString: 'resets UI panels to original state',
aliases: ['resetui'],
}));
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
name: 'bgcol',
callback: setAvgBG,
helpString: ' WIP test of auto-bg avg coloring',
}));
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
name: 'theme',
callback: setThemeCallback,
unnamedArgumentList: [
new SlashCommandArgument(
'name', [ARGUMENT_TYPE.STRING], true,
),
],
helpString: 'sets a UI theme by name',
}));
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
name: 'movingui',
callback: setmovingUIPreset,
unnamedArgumentList: [
new SlashCommandArgument(
'name', [ARGUMENT_TYPE.STRING], true,
),
],
helpString: 'activates a movingUI preset by name',
}));
});

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