mirror of
https://github.com/SillyTavern/SillyTavern.git
synced 2025-06-05 21:59:27 +02:00
Compare commits
11 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
51a43d1ff0 | ||
|
04a798b229 | ||
|
0799090a1a | ||
|
8c51ea15b2 | ||
|
3adb955a14 | ||
|
b9f31d5066 | ||
|
c5dff7b5d4 | ||
|
67e57ffd58 | ||
|
08f6f8c405 | ||
|
0d4cbf7da6 | ||
|
9cef0d8346 |
@@ -44,7 +44,6 @@ module.exports = {
|
||||
toastr: 'readonly',
|
||||
Readability: 'readonly',
|
||||
isProbablyReaderable: 'readonly',
|
||||
ePub: 'readonly',
|
||||
},
|
||||
},
|
||||
],
|
||||
|
42
.github/readme.md
vendored
42
.github/readme.md
vendored
@@ -196,45 +196,6 @@ 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]
|
||||
@@ -365,7 +326,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: MIT
|
||||
* TAI Base by Humi: Unknown license
|
||||
* Cohee's modifications and derived code: AGPL v3
|
||||
* RossAscends' additions: AGPL v3
|
||||
* Portions of CncAnon's TavernAITurbo mod: Unknown license
|
||||
@@ -386,7 +347,6 @@ 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
|
||||
|
16
.github/workflows/docker-publish.yml
vendored
16
.github/workflows/docker-publish.yml
vendored
@@ -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,12 +65,7 @@ jobs:
|
||||
id: metadata
|
||||
with:
|
||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||
# Release version tag if the workflow is triggered by a release
|
||||
# Branch name tag if the workflow is triggered by a push
|
||||
# Latest tag if the branch is release and the workflow is triggered by a push
|
||||
tags: |
|
||||
${{ github.event_name == 'release' && github.ref_name || env.BRANCH_NAME }}
|
||||
${{ github.event_name == 'push' && env.BRANCH_NAME == 'release' && 'latest' || '' }}
|
||||
tags: ${{ env.BRANCH_NAME }}
|
||||
|
||||
# Login into package repository as the person who created the release
|
||||
- name: Log in to the Container registry
|
||||
@@ -92,3 +87,10 @@ jobs:
|
||||
push: true
|
||||
tags: ${{ steps.metadata.outputs.tags }}
|
||||
labels: ${{ steps.metadata.outputs.labels }}
|
||||
|
||||
# If the workflow is triggered by a release, marks and push the image as such
|
||||
- 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
|
||||
|
33
.github/workflows/update-i18n.yaml
vendored
33
.github/workflows/update-i18n.yaml
vendored
@@ -1,33 +0,0 @@
|
||||
name: Update i18n data
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
permissions: # Job-level permissions configuration starts here
|
||||
contents: write # 'write' access to repository contents
|
||||
steps:
|
||||
- name: disable auto crlf
|
||||
uses: steve02081504/disable-autocrlf@v1
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.head_ref }}
|
||||
fetch-depth: 0 # otherwise, there would be errors pushing refs to the destination repository.
|
||||
- name: Create local changes
|
||||
run: |
|
||||
aria2c https://raw.githubusercontent.com/SillyTavern/SillyTavern-i18n/main/generate.py
|
||||
aria2c https://raw.githubusercontent.com/SillyTavern/SillyTavern-i18n/main/requirements.txt
|
||||
pip install -r ./requirements.txt
|
||||
python ./generate.py "" --sort-keys
|
||||
rm -f ./generate.py ./requirements.txt
|
||||
- name: add all
|
||||
run: git add -A
|
||||
- name: push
|
||||
uses: actions-go/push@master
|
||||
with:
|
||||
author-email: 41898282+github-actions[bot]@users.noreply.github.com
|
||||
author-name: github-actions[bot]
|
||||
commit-message: 'i18n changes'
|
||||
remote: origin
|
1
.gitignore
vendored
1
.gitignore
vendored
@@ -47,4 +47,3 @@ access.log
|
||||
public/css/user.css
|
||||
/plugins/
|
||||
/data
|
||||
/default/scaffold
|
||||
|
@@ -1,9 +0,0 @@
|
||||
# Looking for setting snapshots or chat backups?
|
||||
|
||||
Individual user backups are now located in the data directory.
|
||||
|
||||
Example for the default user under default data root:
|
||||
|
||||
/data/default-user/backups
|
||||
|
||||
This folder remains for historical purposes only.
|
BIN
default/content/default_CodingSensei.png
Normal file
BIN
default/content/default_CodingSensei.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 338 KiB |
BIN
default/content/default_FluxTheCat.png
Normal file
BIN
default/content/default_FluxTheCat.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 598 KiB |
@@ -107,6 +107,14 @@
|
||||
"filename": "default_Seraphina.png",
|
||||
"type": "character"
|
||||
},
|
||||
{
|
||||
"filename": "default_CodingSensei.png",
|
||||
"type": "character"
|
||||
},
|
||||
{
|
||||
"filename": "default_FluxTheCat.png",
|
||||
"type": "character"
|
||||
},
|
||||
{
|
||||
"filename": "Seraphina",
|
||||
"type": "sprites"
|
||||
|
@@ -231,7 +231,6 @@
|
||||
"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,
|
||||
|
@@ -33,8 +33,8 @@
|
||||
"negative_prompt": "",
|
||||
"grammar_string": "",
|
||||
"banned_tokens": "",
|
||||
"ignore_eos_token": false,
|
||||
"spaces_between_special_tokens": true,
|
||||
"ignore_eos_token_aphrodite": false,
|
||||
"spaces_between_special_tokens_aphrodite": true,
|
||||
"type": "ooba",
|
||||
"legacy_api": false,
|
||||
"sampler_order": [
|
||||
|
@@ -33,8 +33,8 @@
|
||||
"negative_prompt": "",
|
||||
"grammar_string": "",
|
||||
"banned_tokens": "",
|
||||
"ignore_eos_token": false,
|
||||
"spaces_between_special_tokens": true,
|
||||
"ignore_eos_token_aphrodite": false,
|
||||
"spaces_between_special_tokens_aphrodite": true,
|
||||
"type": "ooba",
|
||||
"legacy_api": false,
|
||||
"sampler_order": [
|
||||
|
@@ -33,8 +33,8 @@
|
||||
"negative_prompt": "",
|
||||
"grammar_string": "",
|
||||
"banned_tokens": "",
|
||||
"ignore_eos_token": false,
|
||||
"spaces_between_special_tokens": true,
|
||||
"ignore_eos_token_aphrodite": false,
|
||||
"spaces_between_special_tokens_aphrodite": true,
|
||||
"type": "ooba",
|
||||
"legacy_api": false,
|
||||
"sampler_order": [
|
||||
|
@@ -387,8 +387,14 @@
|
||||
}
|
||||
],
|
||||
"tag_map": {
|
||||
"default_FluxTheCat.png": [
|
||||
"1345561466591"
|
||||
],
|
||||
"default_Seraphina.png": [
|
||||
"1345561466591"
|
||||
],
|
||||
"default_CodingSensei.png": [
|
||||
"1345561466591"
|
||||
]
|
||||
},
|
||||
"nai_settings": {
|
||||
@@ -624,7 +630,6 @@
|
||||
"show_external_models": false,
|
||||
"proxy_password": "",
|
||||
"assistant_prefill": "",
|
||||
"assistant_impersonation": "",
|
||||
"use_ai21_tokenizer": false
|
||||
}
|
||||
}
|
||||
|
@@ -1,26 +0,0 @@
|
||||
# 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"
|
||||
}
|
||||
]
|
||||
```
|
@@ -10,5 +10,4 @@ services:
|
||||
volumes:
|
||||
- "./config:/home/node/app/config"
|
||||
- "./data:/home/node/app/data"
|
||||
- "./plugins:/home/node/app/plugins"
|
||||
restart: unless-stopped
|
||||
|
85
package-lock.json
generated
85
package-lock.json
generated
@@ -1,18 +1,19 @@
|
||||
{
|
||||
"name": "sillytavern",
|
||||
"version": "1.12.1",
|
||||
"version": "1.12.0-preview",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "sillytavern",
|
||||
"version": "1.12.1",
|
||||
"version": "1.12.0-preview",
|
||||
"hasInstallScript": true,
|
||||
"license": "AGPL-3.0",
|
||||
"dependencies": {
|
||||
"@agnai/sentencepiece-js": "^1.1.1",
|
||||
"@agnai/web-tokenizers": "^0.1.3",
|
||||
"@zeldafan0225/ai_horde": "^5.1.0",
|
||||
"@dqbd/tiktoken": "^1.0.13",
|
||||
"@zeldafan0225/ai_horde": "^4.0.1",
|
||||
"archiver": "^7.0.1",
|
||||
"bing-translate-api": "^2.9.1",
|
||||
"body-parser": "^1.20.2",
|
||||
@@ -25,6 +26,7 @@
|
||||
"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",
|
||||
@@ -44,7 +46,6 @@
|
||||
"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",
|
||||
@@ -60,9 +61,6 @@
|
||||
"@types/jquery": "^3.5.29",
|
||||
"eslint": "^8.55.0",
|
||||
"jquery": "^3.6.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
}
|
||||
},
|
||||
"node_modules/@aashutoshrathi/word-wrap": {
|
||||
@@ -84,6 +82,10 @@
|
||||
"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,
|
||||
@@ -880,14 +882,12 @@
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/@zeldafan0225/ai_horde": {
|
||||
"version": "5.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@zeldafan0225/ai_horde/-/ai_horde-5.1.0.tgz",
|
||||
"integrity": "sha512-rPC0nmmFSXK808Oon0zFPA7yGSUKBXiLtMejkmKTyfAzzOHHQt/i2lO4ccfN2e355LzX1lBLwSi+nlATVA43Sw==",
|
||||
"version": "4.0.1",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@thunder04/supermap": "^3.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
"@thunder04/supermap": "^3.0.2",
|
||||
"centra": "^2.5.0",
|
||||
"esbuild": "^0.12.28"
|
||||
}
|
||||
},
|
||||
"node_modules/abort-controller": {
|
||||
@@ -1214,6 +1214,10 @@
|
||||
"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",
|
||||
@@ -2124,6 +2128,12 @@
|
||||
"url": "https://github.com/fb55/entities?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild": {
|
||||
"name": "dry-uninstall",
|
||||
"version": "0.3.0",
|
||||
"resolved": "https://registry.npmjs.org/dry-uninstall/-/dry-uninstall-0.3.0.tgz",
|
||||
"integrity": "sha512-b8h94RVpETWkVV59x62NsY++79bM7Si6Dxq7a4iVxRcJU3ZJJ4vaiC7wUZwM8WDK0ySRL+i+T/1SMAzbJLejYA=="
|
||||
},
|
||||
"node_modules/escalade": {
|
||||
"version": "3.1.1",
|
||||
"license": "MIT",
|
||||
@@ -2729,6 +2739,16 @@
|
||||
"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",
|
||||
@@ -3869,6 +3889,7 @@
|
||||
},
|
||||
"node_modules/punycode": {
|
||||
"version": "2.3.1",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
@@ -4382,11 +4403,6 @@
|
||||
"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"
|
||||
@@ -4418,15 +4434,8 @@
|
||||
}
|
||||
},
|
||||
"node_modules/tr46": {
|
||||
"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"
|
||||
}
|
||||
"version": "0.0.3",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/truncate-utf8-bytes": {
|
||||
"version": "1.0.2",
|
||||
@@ -4575,27 +4584,19 @@
|
||||
}
|
||||
},
|
||||
"node_modules/webidl-conversions": {
|
||||
"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"
|
||||
}
|
||||
"version": "3.0.1",
|
||||
"license": "BSD-2-Clause"
|
||||
},
|
||||
"node_modules/whatwg-fetch": {
|
||||
"version": "3.6.18",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/whatwg-url": {
|
||||
"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==",
|
||||
"version": "5.0.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tr46": "^5.0.0",
|
||||
"webidl-conversions": "^7.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
"tr46": "~0.0.3",
|
||||
"webidl-conversions": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/which": {
|
||||
|
20
package.json
20
package.json
@@ -2,7 +2,8 @@
|
||||
"dependencies": {
|
||||
"@agnai/sentencepiece-js": "^1.1.1",
|
||||
"@agnai/web-tokenizers": "^0.1.3",
|
||||
"@zeldafan0225/ai_horde": "^5.1.0",
|
||||
"@dqbd/tiktoken": "^1.0.13",
|
||||
"@zeldafan0225/ai_horde": "^4.0.1",
|
||||
"archiver": "^7.0.1",
|
||||
"bing-translate-api": "^2.9.1",
|
||||
"body-parser": "^1.20.2",
|
||||
@@ -15,6 +16,7 @@
|
||||
"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",
|
||||
@@ -34,7 +36,6 @@
|
||||
"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",
|
||||
@@ -43,9 +44,6 @@
|
||||
"yargs": "^17.7.1",
|
||||
"yauzl": "^2.10.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
},
|
||||
"overrides": {
|
||||
"parse-bmfont-xml": {
|
||||
"xml2js": "^0.5.0"
|
||||
@@ -59,8 +57,8 @@
|
||||
"axios": {
|
||||
"follow-redirects": "^1.15.4"
|
||||
},
|
||||
"node-fetch": {
|
||||
"whatwg-url": "^14.0.0"
|
||||
"@zeldafan0225/ai_horde": {
|
||||
"esbuild": "npm:dry-uninstall"
|
||||
}
|
||||
},
|
||||
"name": "sillytavern",
|
||||
@@ -70,15 +68,13 @@
|
||||
"type": "git",
|
||||
"url": "https://github.com/SillyTavern/SillyTavern.git"
|
||||
},
|
||||
"version": "1.12.1",
|
||||
"version": "1.12.0-preview",
|
||||
"scripts": {
|
||||
"start": "node server.js",
|
||||
"start:no-csrf": "node server.js --disableCsrf",
|
||||
"start-multi": "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",
|
||||
"plugins:update": "node plugins update",
|
||||
"plugins:install": "node plugins install"
|
||||
"lint-fix": "eslint \"src/**/*.js\" \"public/**/*.js\" ./*.js --fix"
|
||||
},
|
||||
"bin": {
|
||||
"sillytavern": "./server.js"
|
||||
|
75
plugins.js
75
plugins.js
@@ -1,75 +0,0 @@
|
||||
// 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);
|
||||
}
|
||||
}
|
@@ -117,11 +117,6 @@
|
||||
max-width: unset;
|
||||
}
|
||||
|
||||
#wiActivationSettings,
|
||||
#wiTopBlock {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
#top-settings-holder,
|
||||
#top-bar {
|
||||
position: fixed;
|
||||
|
@@ -19,7 +19,7 @@
|
||||
|
||||
#completion_prompt_manager #completion_prompt_manager_list li {
|
||||
display: grid;
|
||||
grid-template-columns: 4fr 80px 45px;
|
||||
grid-template-columns: 4fr 80px 40px;
|
||||
margin-bottom: 0.5em;
|
||||
width: 100%
|
||||
}
|
||||
|
@@ -171,88 +171,3 @@
|
||||
.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);
|
||||
white-space: break-spaces;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.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;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
row-gap: 5px;
|
||||
}
|
||||
|
||||
.select2_multi_sameline+span.select2-container .select2-selection--multiple .select2-selection__choice {
|
||||
margin-top: 0px;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
@@ -360,14 +360,6 @@
|
||||
flex: 2 !important;
|
||||
}
|
||||
|
||||
.flex3 {
|
||||
flex: 3;
|
||||
}
|
||||
|
||||
.flex4 {
|
||||
flex: 4;
|
||||
}
|
||||
|
||||
.flexFlowColumn {
|
||||
flex-flow: column;
|
||||
}
|
||||
@@ -571,24 +563,4 @@ textarea:disabled {
|
||||
height: 30px;
|
||||
text-align: center;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
ul.li-padding-b-1 li {
|
||||
padding-bottom: 1em;
|
||||
}
|
||||
|
||||
ul.li-padding-b-2 li {
|
||||
padding-bottom: 2em;
|
||||
}
|
||||
|
||||
ul.li-padding-b-5 li {
|
||||
padding-bottom: 5em;
|
||||
}
|
||||
|
||||
ul.li-padding-bot5 li {
|
||||
padding-bottom: 5px;
|
||||
}
|
||||
|
||||
ul.li-padding-bot10 li {
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
}
|
112
public/css/stats.css
Normal file
112
public/css/stats.css
Normal file
@@ -0,0 +1,112 @@
|
||||
.rm_stat_popup_header {
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
|
||||
.rm_stats_button {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.rm_stat_block {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.rm_stat_block_data_row:hover {
|
||||
background-color: var(--grey5020a);
|
||||
filter: drop-shadow(0px 0px 5px var(--SmartThemeShadowColor));
|
||||
}
|
||||
|
||||
.rm_stat_name {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.rm_stat_values {
|
||||
flex: 2;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.rm_stat_block.rm_stat_right_spacing .rm_stat_values {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.rm_stat_name .rm_stat_header {
|
||||
height: calc(var(--mainFontSize) * 1.33333333333 + 3px);
|
||||
padding-bottom: 3px;
|
||||
border-bottom: 2px solid;
|
||||
}
|
||||
|
||||
.rm_stat_name .rm_stat_field {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.rm_stat_field.rm_stat_field_lefty {
|
||||
text-align: left;
|
||||
padding-left: 6px;
|
||||
}
|
||||
|
||||
.rm_stat_field {
|
||||
flex: 1;
|
||||
height: calc(var(--mainFontSize) * 1.33333333333);
|
||||
text-align: right;
|
||||
overflow: hidden;
|
||||
padding-left: 2px;
|
||||
padding-right: 2px;
|
||||
}
|
||||
|
||||
.rm_stat_field_smaller {
|
||||
color: var(--grey70);
|
||||
font-size: smaller;
|
||||
}
|
||||
|
||||
.rm_stat_header {
|
||||
margin-bottom: 3px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.rm_stat_spacer {
|
||||
height: 12px;
|
||||
}
|
||||
|
||||
.rm_stat_bar {
|
||||
width: 100%;
|
||||
height: calc(var(--mainFontSize) * 1.33333333333 - 4px);
|
||||
display: flex;
|
||||
margin-top: 2px;
|
||||
margin-bottom: 2px;
|
||||
padding-left: 6px;
|
||||
}
|
||||
|
||||
.rm_stat_bar_user {
|
||||
background-color: rgba(130, 178, 140, 0.9);
|
||||
}
|
||||
|
||||
.rm_stat_bar_char {
|
||||
background-color: rgba(178, 140, 130, 0.9);
|
||||
}
|
||||
|
||||
.rm_stat_block.rm_stat_right_spacing {
|
||||
margin-right: 33.33333333333%;
|
||||
}
|
||||
|
||||
.rm_stat_avatar_block {
|
||||
position: absolute;
|
||||
top: calc(10px + 1.17em + 12.5px + 2* 7px);
|
||||
right: 0px;
|
||||
height: calc(8px + calc(calc(var(--mainFontSize) * 1.33333333333) * 7) + calc(12px * 3));
|
||||
width: calc(33.33333333333% - 10px);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.rm_stat_avatar_block .avatar {
|
||||
scale: 2;
|
||||
flex: unset;
|
||||
}
|
||||
|
||||
.rm_stat_footer {
|
||||
justify-content: right;
|
||||
color: var(--grey70);
|
||||
font-size: smaller;
|
||||
font-style: italic;
|
||||
}
|
@@ -103,8 +103,7 @@
|
||||
}
|
||||
|
||||
#bulkTagsList,
|
||||
#tagList .tag,
|
||||
#groupTagList .tag {
|
||||
#tagList .tag {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
@@ -194,8 +193,7 @@
|
||||
filter: brightness(75%) saturate(0.6);
|
||||
}
|
||||
|
||||
.tag_as_folder:hover,
|
||||
.tag_as_folder.flash {
|
||||
.tag_as_folder:hover {
|
||||
filter: brightness(150%) saturate(0.6) !important;
|
||||
}
|
||||
|
||||
|
@@ -146,7 +146,6 @@ body.big-avatars .bogus_folder_select .avatar {
|
||||
body.big-avatars .avatar {
|
||||
width: calc(var(--avatar-base-width) * var(--big-avatar-width-factor));
|
||||
height: calc(var(--avatar-base-height) * var(--big-avatar-height-factor));
|
||||
/* width: unset; */
|
||||
border-style: none;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
@@ -76,12 +76,6 @@
|
||||
.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 {
|
||||
@@ -107,7 +101,7 @@
|
||||
height: auto;
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
min-height: calc(var(--mainFontSize) + 14px);
|
||||
min-height: calc(var(--mainFontSize) + 13px);
|
||||
}
|
||||
|
||||
.delete_entry_button {
|
||||
@@ -202,63 +196,3 @@
|
||||
.WIEntryHeaderTitleMobile {
|
||||
display: none;
|
||||
}
|
||||
|
||||
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);
|
||||
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;
|
||||
}
|
||||
|
||||
.select2-results__option .regex_item .regex_icon {
|
||||
margin-right: 6px;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
#wiCheckboxes {
|
||||
align-self: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
1401
public/global.d.ts
vendored
1401
public/global.d.ts
vendored
File diff suppressed because it is too large
Load Diff
@@ -1,48 +0,0 @@
|
||||
<?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>
|
Before Width: | Height: | Size: 1.9 KiB |
@@ -1,40 +0,0 @@
|
||||
<?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>
|
Before Width: | Height: | Size: 1.4 KiB |
1114
public/index.html
1114
public/index.html
File diff suppressed because it is too large
Load Diff
@@ -42,46 +42,6 @@ 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;
|
||||
|
||||
@@ -96,7 +56,7 @@ EventEmitter.prototype.removeListener = function (event, listener) {
|
||||
|
||||
EventEmitter.prototype.emit = async function (event) {
|
||||
if (localStorage.getItem('eventTracing') === 'true') {
|
||||
console.trace('Event emitted: ' + event, args);
|
||||
console.trace('Event emitted: ' + event);
|
||||
} else {
|
||||
console.debug('Event emitted: ' + event);
|
||||
}
|
||||
@@ -121,7 +81,7 @@ EventEmitter.prototype.emit = async function (event) {
|
||||
|
||||
EventEmitter.prototype.emitAndWait = function (event) {
|
||||
if (localStorage.getItem('eventTracing') === 'true') {
|
||||
console.trace('Event emitted: ' + event, args);
|
||||
console.trace('Event emitted: ' + event);
|
||||
} else {
|
||||
console.debug('Event emitted: ' + event);
|
||||
}
|
||||
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,6 @@
|
||||
[
|
||||
{ "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)" },
|
||||
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -9,13 +9,6 @@
|
||||
<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" />
|
||||
@@ -27,8 +20,8 @@
|
||||
<link rel="manifest" crossorigin="use-credentials" href="manifest.json">
|
||||
<link href="webfonts/NotoSans/stylesheet.css" rel="stylesheet">
|
||||
<!-- fontawesome webfonts-->
|
||||
<link href="css/fontawesome.min.css" rel="stylesheet">
|
||||
<link href="css/solid.min.css" rel="stylesheet">
|
||||
<link href="css/fontawesome.css" rel="stylesheet">
|
||||
<link href="css/solid.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>
|
||||
|
1254
public/script.js
1254
public/script.js
File diff suppressed because it is too large
Load Diff
@@ -4,6 +4,7 @@ import {
|
||||
characterGroupOverlay,
|
||||
callPopup,
|
||||
characters,
|
||||
deleteCharacter,
|
||||
event_types,
|
||||
eventSource,
|
||||
getCharacters,
|
||||
@@ -12,7 +13,6 @@ import {
|
||||
buildAvatarList,
|
||||
characterToEntity,
|
||||
printCharactersDebounced,
|
||||
deleteCharacter,
|
||||
} from '../script.js';
|
||||
|
||||
import { favsToHotswap } from './RossAscends-mods.js';
|
||||
@@ -115,7 +115,24 @@ class CharacterContextMenu {
|
||||
static delete = async (characterId, deleteChats = false) => {
|
||||
const character = CharacterContextMenu.#getCharacter(characterId);
|
||||
|
||||
await deleteCharacter(character.avatar, { deleteChats: deleteChats });
|
||||
return fetch('/api/characters/delete', {
|
||||
method: 'POST',
|
||||
headers: getRequestHeaders(),
|
||||
body: JSON.stringify({ avatar_url: character.avatar, delete_chats: deleteChats }),
|
||||
cache: 'no-cache',
|
||||
}).then(response => {
|
||||
if (response.ok) {
|
||||
eventSource.emit(event_types.CHARACTER_DELETED, { id: characterId, character: character });
|
||||
return deleteCharacter(character.name, character.avatar, false).then(() => {
|
||||
if (deleteChats) getPastCharacterChats(characterId).then(pastChats => {
|
||||
for (const chat of pastChats) {
|
||||
const name = chat.file_name.replace('.jsonl', '');
|
||||
eventSource.emit(event_types.CHAT_DELETED, name);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
static #getCharacter = (characterId) => characters[characterId] ?? null;
|
||||
|
@@ -6,7 +6,6 @@ import { Message, TokenHandler } from './openai.js';
|
||||
import { power_user } from './power-user.js';
|
||||
import { debounce, waitUntilCondition, escapeHtml } from './utils.js';
|
||||
import { debounce_timeout } from './constants.js';
|
||||
import { renderTemplateAsync } from './templates.js';
|
||||
|
||||
function debouncePromise(func, delay) {
|
||||
let timeoutId;
|
||||
@@ -251,7 +250,7 @@ class PromptManager {
|
||||
this.error = null;
|
||||
|
||||
/** Dry-run for generate, must return a promise */
|
||||
this.tryGenerate = async () => { };
|
||||
this.tryGenerate = () => { };
|
||||
|
||||
/** Called to persist the configuration, must return a promise */
|
||||
this.saveServiceSettings = () => { };
|
||||
@@ -696,23 +695,23 @@ class PromptManager {
|
||||
if ('character' === this.configuration.promptOrder.strategy && null === this.activeCharacter) return;
|
||||
this.error = null;
|
||||
|
||||
waitUntilCondition(() => !is_send_press && !is_group_generating, 1024 * 1024, 100).then(async () => {
|
||||
waitUntilCondition(() => !is_send_press && !is_group_generating, 1024 * 1024, 100).then(() => {
|
||||
if (true === afterTryGenerate) {
|
||||
// Executed during dry-run for determining context composition
|
||||
this.profileStart('filling context');
|
||||
this.tryGenerate().finally(async () => {
|
||||
this.tryGenerate().finally(() => {
|
||||
this.profileEnd('filling context');
|
||||
this.profileStart('render');
|
||||
await this.renderPromptManager();
|
||||
await this.renderPromptManagerListItems();
|
||||
this.renderPromptManager();
|
||||
this.renderPromptManagerListItems();
|
||||
this.makeDraggable();
|
||||
this.profileEnd('render');
|
||||
});
|
||||
} else {
|
||||
// Executed during live communication
|
||||
this.profileStart('render');
|
||||
await this.renderPromptManager();
|
||||
await this.renderPromptManagerListItems();
|
||||
this.renderPromptManager();
|
||||
this.renderPromptManagerListItems();
|
||||
this.makeDraggable();
|
||||
this.profileEnd('render');
|
||||
}
|
||||
@@ -1339,7 +1338,7 @@ class PromptManager {
|
||||
/**
|
||||
* Empties, then re-assembles the container containing the prompt list.
|
||||
*/
|
||||
async renderPromptManager() {
|
||||
renderPromptManager() {
|
||||
let selectedPromptIndex = 0;
|
||||
const existingAppendSelect = document.getElementById(`${this.configuration.prefix}prompt_manager_footer_append_prompt`);
|
||||
if (existingAppendSelect instanceof HTMLSelectElement) {
|
||||
@@ -1348,16 +1347,26 @@ class PromptManager {
|
||||
const promptManagerDiv = this.containerElement;
|
||||
promptManagerDiv.innerHTML = '';
|
||||
|
||||
const errorDiv = this.error ? `
|
||||
const errorDiv = `
|
||||
<div class="${this.configuration.prefix}prompt_manager_error">
|
||||
<span class="fa-solid tooltip fa-triangle-exclamation text_danger"></span> ${DOMPurify.sanitize(this.error)}
|
||||
<span class="fa-solid tooltip fa-triangle-exclamation text_danger"></span> ${this.error}
|
||||
</div>
|
||||
` : '';
|
||||
`;
|
||||
|
||||
const totalActiveTokens = this.tokenUsage;
|
||||
|
||||
const headerHtml = await renderTemplateAsync('promptManagerHeader', { error: this.error, errorDiv, prefix: this.configuration.prefix, totalActiveTokens });
|
||||
promptManagerDiv.insertAdjacentHTML('beforeend', headerHtml);
|
||||
promptManagerDiv.insertAdjacentHTML('beforeend', `
|
||||
<div class="range-block">
|
||||
${this.error ? errorDiv : ''}
|
||||
<div class="${this.configuration.prefix}prompt_manager_header">
|
||||
<div class="${this.configuration.prefix}prompt_manager_header_advanced">
|
||||
<span data-i18n="Prompts">Prompts</span>
|
||||
</div>
|
||||
<div>Total Tokens: ${totalActiveTokens} </div>
|
||||
</div>
|
||||
<ul id="${this.configuration.prefix}prompt_manager_list" class="text_pole"></ul>
|
||||
</div>
|
||||
`);
|
||||
|
||||
this.listElement = promptManagerDiv.querySelector(`#${this.configuration.prefix}prompt_manager_list`);
|
||||
|
||||
@@ -1375,9 +1384,22 @@ class PromptManager {
|
||||
selectedPromptIndex = 0;
|
||||
}
|
||||
|
||||
const footerHtml = `
|
||||
<div class="${this.configuration.prefix}prompt_manager_footer">
|
||||
<select id="${this.configuration.prefix}prompt_manager_footer_append_prompt" class="text_pole" name="append-prompt">
|
||||
${promptsHtml}
|
||||
</select>
|
||||
<a class="menu_button fa-chain fa-solid" title="Insert prompt" data-i18n="[title]Insert prompt"></a>
|
||||
<a class="caution menu_button fa-x fa-solid" title="Delete prompt" data-i18n="[title]Delete prompt"></a>
|
||||
<a class="menu_button fa-file-import fa-solid" id="prompt-manager-import" title="Import a prompt list" data-i18n="[title]Import a prompt list"></a>
|
||||
<a class="menu_button fa-file-export fa-solid" id="prompt-manager-export" title="Export this prompt list" data-i18n="[title]Export this prompt list"></a>
|
||||
<a class="menu_button fa-undo fa-solid" id="prompt-manager-reset-character" title="Reset current character" data-i18n="[title]Reset current character"></a>
|
||||
<a class="menu_button fa-plus-square fa-solid" title="New prompt" data-i18n="[title]New prompt"></a>
|
||||
</div>
|
||||
`;
|
||||
|
||||
const rangeBlockDiv = promptManagerDiv.querySelector('.range-block');
|
||||
const headerDiv = promptManagerDiv.querySelector('.completion_prompt_manager_header');
|
||||
const footerHtml = await renderTemplateAsync('promptManagerFooter', { promptsHtml, prefix: this.configuration.prefix });
|
||||
headerDiv.insertAdjacentHTML('afterend', footerHtml);
|
||||
rangeBlockDiv.querySelector('#prompt-manager-reset-character').addEventListener('click', this.handleCharacterReset);
|
||||
|
||||
@@ -1388,9 +1410,23 @@ class PromptManager {
|
||||
footerDiv.querySelector('select').selectedIndex = selectedPromptIndex;
|
||||
|
||||
// Add prompt export dialogue and options
|
||||
const exportForCharacter = `
|
||||
<div class="row">
|
||||
<a class="export-promptmanager-prompts-character list-group-item" data-i18n="Export for character">Export for character</a>
|
||||
<span class="tooltip fa-solid fa-info-circle" title="Export prompts for this character, including their order."></span>
|
||||
</div>`;
|
||||
const exportPopup = `
|
||||
<div id="prompt-manager-export-format-popup" class="list-group">
|
||||
<div class="prompt-manager-export-format-popup-flex">
|
||||
<div class="row">
|
||||
<a class="export-promptmanager-prompts-full list-group-item" data-i18n="Export all">Export all</a>
|
||||
<span class="tooltip fa-solid fa-info-circle" title="Export all your prompts to a file"></span>
|
||||
</div>
|
||||
${'global' === this.configuration.promptOrder.strategy ? '' : exportForCharacter}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
const exportForCharacter = await renderTemplateAsync('promptManagerExportForCharacter');
|
||||
const exportPopup = await renderTemplateAsync('promptManagerExportPopup', { isGlobalStrategy: 'global' === this.configuration.promptOrder.strategy, exportForCharacter });
|
||||
rangeBlockDiv.insertAdjacentHTML('beforeend', exportPopup);
|
||||
|
||||
// Destroy previous popper instance if it exists
|
||||
@@ -1424,7 +1460,7 @@ class PromptManager {
|
||||
/**
|
||||
* Empties, then re-assembles the prompt list
|
||||
*/
|
||||
async renderPromptManagerListItems() {
|
||||
renderPromptManagerListItems() {
|
||||
if (!this.serviceSettings.prompts) return;
|
||||
|
||||
const promptManagerList = this.listElement;
|
||||
@@ -1432,7 +1468,16 @@ class PromptManager {
|
||||
|
||||
const { prefix } = this.configuration;
|
||||
|
||||
let listItemHtml = await renderTemplateAsync('promptManagerListHeader', { prefix });
|
||||
let listItemHtml = `
|
||||
<li class="${prefix}prompt_manager_list_head">
|
||||
<span data-i18n="Name">Name</span>
|
||||
<span></span>
|
||||
<span class="prompt_manager_prompt_tokens" data-i18n="Tokens">Tokens</span>
|
||||
</li>
|
||||
<li class="${prefix}prompt_manager_list_separator">
|
||||
<hr>
|
||||
</li>
|
||||
`;
|
||||
|
||||
this.getPromptsForCharacter(this.activeCharacter).forEach(prompt => {
|
||||
if (!prompt) return;
|
||||
@@ -1506,7 +1551,7 @@ class PromptManager {
|
||||
${isImportantPrompt ? '<span class="fa-fw fa-solid fa-star" title="Important Prompt"></span>' : ''}
|
||||
${isUserPrompt ? '<span class="fa-fw fa-solid fa-user" title="User Prompt"></span>' : ''}
|
||||
${isInjectionPrompt ? '<span class="fa-fw fa-solid fa-syringe" title="In-Chat Injection"></span>' : ''}
|
||||
${this.isPromptInspectionAllowed(prompt) ? `<a title="${encodedName}" class="prompt-manager-inspect-action">${encodedName}</a>` : `<span title="${encodedName}">${encodedName}</span>`}
|
||||
${this.isPromptInspectionAllowed(prompt) ? `<a class="prompt-manager-inspect-action">${encodedName}</a>` : encodedName}
|
||||
${isInjectionPrompt ? `<small class="prompt-manager-injection-depth">@ ${prompt.injection_depth}</small>` : ''}
|
||||
${isOverriddenPrompt ? '<small class="fa-solid fa-address-card prompt-manager-overridden" title="Pulled from a character card"></small>' : ''}
|
||||
</span>
|
||||
@@ -1557,7 +1602,7 @@ class PromptManager {
|
||||
data: data,
|
||||
};
|
||||
|
||||
const serializedObject = JSON.stringify(promptExport, null, 4);
|
||||
const serializedObject = JSON.stringify(promptExport);
|
||||
const blob = new Blob([serializedObject], { type: 'application/json' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const downloadLink = document.createElement('a');
|
||||
|
@@ -81,19 +81,21 @@ observer.observe(document.documentElement, observerConfig);
|
||||
|
||||
|
||||
/**
|
||||
* Converts generation time from milliseconds to a human-readable format.
|
||||
* Converts a timespan from milliseconds to a human-readable format.
|
||||
*
|
||||
* The function takes total generation time as an input, then converts it to a format
|
||||
* The function takes a total timespan as an input, then converts it to a format
|
||||
* of "_ Days, _ Hours, _ Minutes, _ Seconds". If the generation time does not exceed a
|
||||
* particular measure (like days or hours), that measure will not be included in the output.
|
||||
*
|
||||
* @param {number} total_gen_time - The total generation time in milliseconds.
|
||||
* @returns {string} - A human-readable string that represents the time spent generating characters.
|
||||
* @param {number} timespan - The total timespan in milliseconds.
|
||||
* @param {object} [options] - Optional parameters
|
||||
* @param {boolean} [options.short=false] - Flag indicating whether short form should be used. ('2h' instead of '2 Hours')
|
||||
* @param {number} [options.onlyHighest] - Number of maximum blocks to be returned. (If, and daya is the highest matching unit, only returns days and hours, cutting of minutes and seconds)
|
||||
* @returns {string} - A human-readable string that represents the timespan.
|
||||
*/
|
||||
export function humanizeGenTime(total_gen_time) {
|
||||
|
||||
export function humanizeTimespan(timespan, { short = false, onlyHighest = 2 } = {}) {
|
||||
//convert time_spent to humanized format of "_ Hours, _ Minutes, _ Seconds" from milliseconds
|
||||
let time_spent = total_gen_time || 0;
|
||||
let time_spent = timespan || 0;
|
||||
time_spent = Math.floor(time_spent / 1000);
|
||||
let seconds = time_spent % 60;
|
||||
time_spent = Math.floor(time_spent / 60);
|
||||
@@ -102,12 +104,36 @@ export function humanizeGenTime(total_gen_time) {
|
||||
let hours = time_spent % 24;
|
||||
time_spent = Math.floor(time_spent / 24);
|
||||
let days = time_spent;
|
||||
time_spent = '';
|
||||
if (days > 0) { time_spent += `${days} Days, `; }
|
||||
if (hours > 0) { time_spent += `${hours} Hours, `; }
|
||||
if (minutes > 0) { time_spent += `${minutes} Minutes, `; }
|
||||
time_spent += `${seconds} Seconds`;
|
||||
return time_spent;
|
||||
|
||||
let parts = [
|
||||
{ singular: 'Day', plural: 'Days', short: 'd', value: days },
|
||||
{ singular: 'Hour', plural: 'Hours', short: 'h', value: hours },
|
||||
{ singular: 'Minute', plural: 'Minutes', short: 'm', value: minutes },
|
||||
{ singular: 'Second', plural: 'Seconds', short: 's', value: seconds },
|
||||
];
|
||||
|
||||
// Build the final string based on the highest significant units and respecting zeros
|
||||
let resultParts = [];
|
||||
let count = 0;
|
||||
for (let part of parts) {
|
||||
if (part.value > 0) {
|
||||
resultParts.push(part);
|
||||
}
|
||||
|
||||
// If we got a match, we count from there. Take a maximum of X elements
|
||||
if (resultParts.length) count++;
|
||||
if (count >= onlyHighest) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!resultParts.length) {
|
||||
return short ? '<1s' : 'Instant';
|
||||
}
|
||||
|
||||
return resultParts.map(part => {
|
||||
return short ? `${part.value}${part.short}` : `${part.value} ${part.value === 1 ? part.singular : part.plural}`;
|
||||
}).join(short ? ' ' : ', ');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -378,7 +404,6 @@ 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');
|
||||
@@ -424,7 +449,7 @@ function restoreUserInput() {
|
||||
|
||||
const userInput = LoadLocal('userInput');
|
||||
if (userInput) {
|
||||
$('#send_textarea').val(userInput)[0].dispatchEvent(new Event('input', { bubbles: true }));
|
||||
$('#send_textarea').val(userInput).trigger('input');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -436,8 +461,10 @@ const saveUserInputDebounced = debounce(saveUserInput);
|
||||
|
||||
// Make the DIV element draggable:
|
||||
|
||||
// THIRD UPDATE, prevent resize window breaks and smartly handle saving
|
||||
|
||||
export function dragElement(elmnt) {
|
||||
var isHeaderBeingDragged = false;
|
||||
var hasBeenDraggedByUser = false;
|
||||
var isMouseDown = false;
|
||||
|
||||
var pos1 = 0, pos2 = 0, pos3 = 0, pos4 = 0;
|
||||
@@ -448,16 +475,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) => { //listener for drag handle repositioning
|
||||
isHeaderBeingDragged = true;
|
||||
isMouseDown = true;
|
||||
elmntHeader.off('mousedown').on('mousedown', (e) => {
|
||||
hasBeenDraggedByUser = true;
|
||||
observer.observe(elmnt.get(0), { attributes: true, attributeFilter: ['style'] });
|
||||
dragMouseDown(e);
|
||||
});
|
||||
$(elmnt).off('mousedown').on('mousedown', () => { //listener for resize
|
||||
$(elmnt).off('mousedown').on('mousedown', () => {
|
||||
isMouseDown = true;
|
||||
observer.observe(elmnt.get(0), { attributes: true, attributeFilter: ['style'] });
|
||||
});
|
||||
@@ -465,19 +492,20 @@ export function dragElement(elmnt) {
|
||||
|
||||
const observer = new MutationObserver((mutations) => {
|
||||
const target = mutations[0].target;
|
||||
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
|
||||
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()
|
||||
) {
|
||||
console.debug('aborting mutator');
|
||||
return;
|
||||
}
|
||||
|
||||
const style = getComputedStyle(target);
|
||||
height = parseInt(style.height)
|
||||
width = parseInt(style.width)
|
||||
//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;
|
||||
top = parseInt(style.top);
|
||||
left = parseInt(style.left);
|
||||
right = parseInt(style.right);
|
||||
@@ -492,53 +520,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 (!isHeaderBeingDragged && isMouseDown) { //if user is dragging the resize handle (not in header)
|
||||
let imgHeight, imgWidth, imageAspectRatio;
|
||||
let containerAspectRatio = height / width;
|
||||
if (!hasBeenDraggedByUser && isMouseDown) {
|
||||
console.debug('saw resize, NOT header drag');
|
||||
|
||||
//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;
|
||||
//prevent resizing offscreen
|
||||
if (top + elmnt.height() >= winHeight) {
|
||||
console.debug('resizing height to prevent offscreen');
|
||||
elmnt.css('height', winHeight - top - 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');
|
||||
}
|
||||
if (left + elmnt.width() >= winWidth) {
|
||||
console.debug('resizing width to prevent offscreen');
|
||||
elmnt.css('width', winWidth - left - 1 + 'px');
|
||||
}
|
||||
|
||||
//prevent resizing from top left into the top bar
|
||||
if (top < topBarLastY && maxX >= topBarFirstX && left <= topBarFirstX) {
|
||||
if (top < topBarLastY && maxX >= topBarFirstX && left <= topBarFirstX
|
||||
) {
|
||||
console.debug('prevent topbar underlap resize');
|
||||
elmnt.css('width', width - 1 + 'px');
|
||||
}
|
||||
|
||||
@@ -547,11 +575,11 @@ export function dragElement(elmnt) {
|
||||
elmnt.css('top', top);
|
||||
|
||||
//set a listener for mouseup to save new width/height
|
||||
$(window).off('mouseup').on('mouseup', () => {
|
||||
console.log(`Saving ${elmntName} Height/Width`);
|
||||
elmnt.off('mouseup').on('mouseup', () => {
|
||||
console.debug(`Saving ${elmntName} Height/Width`);
|
||||
// check if the height or width actually changed
|
||||
if (power_user.movingUIState[elmntName].width === elmnt.width() && power_user.movingUIState[elmntName].height === elmnt.height()) {
|
||||
console.log('no change detected, aborting save');
|
||||
if (power_user.movingUIState[elmntName].width === width && power_user.movingUIState[elmntName].height === height) {
|
||||
console.debug('no change detected, aborting save');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -559,27 +587,12 @@ 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');
|
||||
});
|
||||
}
|
||||
|
||||
//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) {
|
||||
|
||||
//handle dragging hit detection
|
||||
if (hasBeenDraggedByUser && isMouseDown) {
|
||||
//prevent dragging offscreen
|
||||
if (top <= 0) {
|
||||
elmnt.css('top', '0px');
|
||||
} else if (maxY >= winHeight) {
|
||||
@@ -591,14 +604,27 @@ 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 reposition listener on the grabber in the header
|
||||
// Check if the element header exists and set the listener on the grabber
|
||||
if (elmntHeader.length) {
|
||||
elmntHeader.off('mousedown').on('mousedown', (e) => {
|
||||
console.debug('listener started from header');
|
||||
dragMouseDown(e);
|
||||
});
|
||||
} else { //if no header, put the listener on the elmnt itself.
|
||||
} else {
|
||||
elmnt.off('mousedown').on('mousedown', dragMouseDown);
|
||||
}
|
||||
});
|
||||
@@ -606,7 +632,7 @@ export function dragElement(elmnt) {
|
||||
function dragMouseDown(e) {
|
||||
|
||||
if (e) {
|
||||
isHeaderBeingDragged = true;
|
||||
hasBeenDraggedByUser = true;
|
||||
e.preventDefault();
|
||||
pos3 = e.clientX; //mouse X at click
|
||||
pos4 = e.clientY; //mouse Y at click
|
||||
@@ -638,20 +664,34 @@ 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.
|
||||
// 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
|
||||
// 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
|
||||
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');
|
||||
isHeaderBeingDragged = false;
|
||||
hasBeenDraggedByUser = false;
|
||||
isMouseDown = false;
|
||||
$(document).off('mouseup', closeDragElement);
|
||||
$(document).off('mousemove', elementDrag);
|
||||
@@ -661,12 +701,6 @@ 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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -693,12 +727,12 @@ const isFirefox = navigator.userAgent.toLowerCase().indexOf('firefox') > -1;
|
||||
*/
|
||||
function autoFitSendTextArea() {
|
||||
const originalScrollBottom = chatBlock.scrollHeight - (chatBlock.scrollTop + chatBlock.offsetHeight);
|
||||
if (Math.ceil(sendTextArea.scrollHeight + 3) >= Math.floor(sendTextArea.offsetHeight)) {
|
||||
if (sendTextArea.scrollHeight == 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 + 3 + 'px';
|
||||
sendTextArea.style.height = sendTextArea.scrollHeight + 0.3 + 'px';
|
||||
|
||||
if (!isFirefox) {
|
||||
const newScrollTop = Math.round(chatBlock.scrollHeight - (chatBlock.offsetHeight + originalScrollBottom));
|
||||
@@ -1124,11 +1158,6 @@ 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')
|
||||
|
@@ -9,12 +9,10 @@ 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
|
||||
@@ -457,59 +455,9 @@ export function initAuthorsNote() {
|
||||
});
|
||||
$('#option_toggle_AN').on('click', onANMenuItemClick);
|
||||
|
||||
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>
|
||||
`,
|
||||
}));
|
||||
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);
|
||||
eventSource.on(event_types.CHAT_CHANGED, onChatChanged);
|
||||
}
|
||||
|
@@ -1,792 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,16 +0,0 @@
|
||||
|
||||
|
||||
|
||||
export class AutoCompleteFuzzyScore {
|
||||
/**@type {number}*/ start;
|
||||
/**@type {number}*/ longestConsecutive;
|
||||
|
||||
/**
|
||||
* @param {number} start
|
||||
* @param {number} longestConsecutive
|
||||
*/
|
||||
constructor(start, longestConsecutive) {
|
||||
this.start = start;
|
||||
this.longestConsecutive = longestConsecutive;
|
||||
}
|
||||
}
|
@@ -1,44 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
@@ -1,206 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
@@ -1,5 +0,0 @@
|
||||
import { AutoCompleteNameResult } from './AutoCompleteNameResult.js';
|
||||
|
||||
export class AutoCompleteSecondaryNameResult extends AutoCompleteNameResult {
|
||||
/**@type {boolean}*/ isRequired = false;
|
||||
}
|
@@ -1,29 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
@@ -1,44 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
@@ -1,7 +1,6 @@
|
||||
import { callPopup, chat_metadata, eventSource, event_types, generateQuietPrompt, getCurrentChatId, getRequestHeaders, getThumbnailUrl, saveSettingsDebounced } from '../script.js';
|
||||
import { saveMetadataDebounced } from './extensions.js';
|
||||
import { SlashCommand } from './slash-commands/SlashCommand.js';
|
||||
import { SlashCommandParser } from './slash-commands/SlashCommandParser.js';
|
||||
import { registerSlashCommand } from './slash-commands.js';
|
||||
import { flashHighlight, stringFormat } from './utils.js';
|
||||
|
||||
const BG_METADATA_KEY = 'custom_background';
|
||||
@@ -481,20 +480,7 @@ export function initBackgrounds() {
|
||||
$('#auto_background').on('click', autoBackgroundCommand);
|
||||
$('#add_bg_button').on('change', onBackgroundUploadSelected);
|
||||
$('#bg-filter').on('input', onBackgroundFilterInput);
|
||||
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',
|
||||
}));
|
||||
|
||||
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);
|
||||
}
|
||||
|
@@ -12,7 +12,6 @@ import {
|
||||
getCharacters,
|
||||
chat,
|
||||
saveChatConditional,
|
||||
saveItemizedPrompts,
|
||||
} from '../script.js';
|
||||
import { humanizedDateTime } from './RossAscends-mods.js';
|
||||
import {
|
||||
@@ -200,7 +199,6 @@ async function createNewBookmark(mesId) {
|
||||
|
||||
const mainChat = selected_group ? groups?.find(x => x.id == selected_group)?.chat_id : characters[this_chid].chat;
|
||||
const newMetadata = { main_chat: mainChat };
|
||||
await saveItemizedPrompts(name);
|
||||
|
||||
if (selected_group) {
|
||||
await saveGroupBookmarkChat(selected_group, name, newMetadata, mesId);
|
||||
|
@@ -1,116 +0,0 @@
|
||||
/**
|
||||
* @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).
|
||||
* @property {RegexScriptData[]} regex_scripts - Custom regex scripts for the character.
|
||||
* // 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} RegexScriptData
|
||||
* @property {string} id - UUID of the script
|
||||
* @property {string} scriptName - The name of the script
|
||||
* @property {string} findRegex - The regex to find
|
||||
* @property {string} replaceString - The string to replace
|
||||
* @property {string[]} trimStrings - The strings to trim
|
||||
* @property {number[]} placement - The placement of the script
|
||||
* @property {boolean} disabled - Whether the script is disabled
|
||||
* @property {boolean} markdownOnly - Whether the script only applies to Markdown
|
||||
* @property {boolean} promptOnly - Whether the script only applies to prompts
|
||||
* @property {boolean} runOnEdit - Whether the script runs on edit
|
||||
* @property {boolean} substituteRegex - Whether the regex should be substituted
|
||||
* @property {number} minDepth - The minimum depth
|
||||
* @property {number} maxDepth - The maximum depth
|
||||
*/
|
||||
|
||||
/**
|
||||
* @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
|
@@ -463,50 +463,33 @@ export function encodeStyleTags(text) {
|
||||
*/
|
||||
export function decodeStyleTags(text) {
|
||||
const styleDecodeRegex = /<custom-style>(.+?)<\/custom-style>/gms;
|
||||
const mediaAllowed = isExternalMediaAllowed();
|
||||
|
||||
function sanitizeRule(rule) {
|
||||
if (Array.isArray(rule.selectors)) {
|
||||
for (let i = 0; i < rule.selectors.length; i++) {
|
||||
const selector = rule.selectors[i];
|
||||
if (selector) {
|
||||
const selectors = (selector.split(' ') ?? []).map((v) => {
|
||||
if (v.startsWith('.')) {
|
||||
return '.custom-' + v.substring(1);
|
||||
}
|
||||
return v;
|
||||
}).join(' ');
|
||||
|
||||
rule.selectors[i] = '.mes_text ' + selectors;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!mediaAllowed && Array.isArray(rule.declarations) && rule.declarations.length > 0) {
|
||||
rule.declarations = rule.declarations.filter(declaration => !declaration.value.includes('://'));
|
||||
}
|
||||
}
|
||||
|
||||
function sanitizeRuleSet(ruleSet) {
|
||||
if (Array.isArray(ruleSet.selectors) || Array.isArray(ruleSet.declarations)) {
|
||||
sanitizeRule(ruleSet);
|
||||
}
|
||||
|
||||
if (Array.isArray(ruleSet.rules)) {
|
||||
ruleSet.rules = ruleSet.rules.filter(rule => rule.type !== 'import');
|
||||
|
||||
for (const mediaRule of ruleSet.rules) {
|
||||
sanitizeRuleSet(mediaRule);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return text.replaceAll(styleDecodeRegex, (_, style) => {
|
||||
try {
|
||||
let styleCleaned = unescape(style).replaceAll(/<br\/>/g, '');
|
||||
const ast = css.parse(styleCleaned);
|
||||
const sheet = ast?.stylesheet;
|
||||
if (sheet) {
|
||||
sanitizeRuleSet(ast.stylesheet);
|
||||
const rules = ast?.stylesheet?.rules;
|
||||
if (rules) {
|
||||
for (const rule of rules) {
|
||||
|
||||
if (rule.type === 'rule') {
|
||||
if (rule.selectors) {
|
||||
for (let i = 0; i < rule.selectors.length; i++) {
|
||||
let selector = rule.selectors[i];
|
||||
if (selector) {
|
||||
let selectors = (selector.split(' ') ?? []).map((v) => {
|
||||
if (v.startsWith('.')) {
|
||||
return '.custom-' + v.substring(1);
|
||||
}
|
||||
return v;
|
||||
}).join(' ');
|
||||
|
||||
rule.selectors[i] = '.mes_text ' + selectors;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return `<style>${css.stringify(ast)}</style>`;
|
||||
} catch (error) {
|
||||
@@ -768,7 +751,7 @@ async function moveAttachment(attachment, source, callback) {
|
||||
* @param {boolean} [confirm=true] If true, show a confirmation dialog
|
||||
* @returns {Promise<void>} A promise that resolves when the attachment is deleted.
|
||||
*/
|
||||
export async function deleteAttachment(attachment, source, callback, confirm = true) {
|
||||
async function deleteAttachment(attachment, source, callback, confirm = true) {
|
||||
if (confirm) {
|
||||
const result = await callGenericPopup('Are you sure you want to delete this attachment?', POPUP_TYPE.CONFIRM);
|
||||
|
||||
@@ -855,12 +838,6 @@ async function openAttachmentManager() {
|
||||
[ATTACHMENT_SOURCE.CHAT]: '.chatAttachmentsList',
|
||||
};
|
||||
|
||||
const selected = template
|
||||
.find(sources[source])
|
||||
.find('.attachmentListItemCheckbox:checked')
|
||||
.map((_, el) => $(el).closest('.attachmentListItem').attr('data-attachment-url'))
|
||||
.get();
|
||||
|
||||
template.find(sources[source]).empty();
|
||||
|
||||
// Sort attachments by sortField and sortOrder, and apply filter
|
||||
@@ -870,8 +847,6 @@ async function openAttachmentManager() {
|
||||
const isDisabled = isAttachmentDisabled(attachment);
|
||||
const attachmentTemplate = template.find('.attachmentListItemTemplate .attachmentListItem').clone();
|
||||
attachmentTemplate.toggleClass('disabled', isDisabled);
|
||||
attachmentTemplate.attr('data-attachment-url', attachment.url);
|
||||
attachmentTemplate.attr('data-attachment-source', source);
|
||||
attachmentTemplate.find('.attachmentFileIcon').attr('title', attachment.url);
|
||||
attachmentTemplate.find('.attachmentListItemName').text(attachment.name);
|
||||
attachmentTemplate.find('.attachmentListItemSize').text(humanFileSize(attachment.size));
|
||||
@@ -884,10 +859,6 @@ async function openAttachmentManager() {
|
||||
attachmentTemplate.find('.enableAttachmentButton').toggle(isDisabled).on('click', () => enableAttachment(attachment, renderAttachments));
|
||||
attachmentTemplate.find('.disableAttachmentButton').toggle(!isDisabled).on('click', () => disableAttachment(attachment, renderAttachments));
|
||||
template.find(sources[source]).append(attachmentTemplate);
|
||||
|
||||
if (selected.includes(attachment.url)) {
|
||||
attachmentTemplate.find('.attachmentListItemCheckbox').prop('checked', true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1056,57 +1027,6 @@ async function openAttachmentManager() {
|
||||
localStorage.setItem('DataBank_sortOrder', sortOrder);
|
||||
renderAttachments();
|
||||
});
|
||||
template.find('.bulkActionDelete').on('click', async () => {
|
||||
const selectedAttachments = document.querySelectorAll('.attachmentListItemCheckboxContainer .attachmentListItemCheckbox:checked');
|
||||
|
||||
if (selectedAttachments.length === 0) {
|
||||
toastr.info('No attachments selected.', 'Data Bank');
|
||||
return;
|
||||
}
|
||||
|
||||
const confirm = await callGenericPopup('Are you sure you want to delete the selected attachments?', POPUP_TYPE.CONFIRM);
|
||||
|
||||
if (confirm !== POPUP_RESULT.AFFIRMATIVE) {
|
||||
return;
|
||||
}
|
||||
|
||||
const attachments = getDataBankAttachments();
|
||||
selectedAttachments.forEach(async (checkbox) => {
|
||||
const listItem = checkbox.closest('.attachmentListItem');
|
||||
if (!(listItem instanceof HTMLElement)) {
|
||||
return;
|
||||
}
|
||||
const url = listItem.dataset.attachmentUrl;
|
||||
const source = listItem.dataset.attachmentSource;
|
||||
const attachment = attachments.find(a => a.url === url);
|
||||
if (!attachment) {
|
||||
return;
|
||||
}
|
||||
await deleteAttachment(attachment, source, () => {}, false);
|
||||
});
|
||||
|
||||
document.querySelectorAll('.attachmentListItemCheckbox, .attachmentsBulkEditCheckbox').forEach(checkbox => {
|
||||
if (checkbox instanceof HTMLInputElement) {
|
||||
checkbox.checked = false;
|
||||
}
|
||||
});
|
||||
|
||||
await renderAttachments();
|
||||
});
|
||||
template.find('.bulkActionSelectAll').on('click', () => {
|
||||
$('.attachmentListItemCheckbox:visible').each((_, checkbox) => {
|
||||
if (checkbox instanceof HTMLInputElement) {
|
||||
checkbox.checked = true;
|
||||
}
|
||||
});
|
||||
});
|
||||
template.find('.bulkActionSelectNone').on('click', () => {
|
||||
$('.attachmentListItemCheckbox:visible').each((_, checkbox) => {
|
||||
if (checkbox instanceof HTMLInputElement) {
|
||||
checkbox.checked = false;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const cleanupFn = await renderButtons();
|
||||
await verifyAttachments();
|
||||
@@ -1179,7 +1099,7 @@ async function runScraper(scraperId, target, callback) {
|
||||
* Uploads a file attachment to the server.
|
||||
* @param {File} file File to upload
|
||||
* @param {string} target Target for the attachment
|
||||
* @returns {Promise<string>} Path to the uploaded file
|
||||
* @returns
|
||||
*/
|
||||
export async function uploadFileAttachmentToServer(file, target) {
|
||||
const isValid = await validateFile(file);
|
||||
@@ -1236,8 +1156,6 @@ export async function uploadFileAttachmentToServer(file, target) {
|
||||
saveSettingsDebounced();
|
||||
break;
|
||||
}
|
||||
|
||||
return fileUrl;
|
||||
}
|
||||
|
||||
function ensureAttachmentsExist() {
|
||||
@@ -1265,42 +1183,36 @@ function ensureAttachmentsExist() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all currently available attachments. Ignores disabled attachments by default.
|
||||
* @param {boolean} [includeDisabled=false] If true, include disabled attachments
|
||||
* Gets all currently available attachments. Ignores disabled attachments.
|
||||
* @returns {FileAttachment[]} List of attachments
|
||||
*/
|
||||
export function getDataBankAttachments(includeDisabled = false) {
|
||||
export function getDataBankAttachments() {
|
||||
ensureAttachmentsExist();
|
||||
const globalAttachments = extension_settings.attachments ?? [];
|
||||
const chatAttachments = chat_metadata.attachments ?? [];
|
||||
const characterAttachments = extension_settings.character_attachments?.[characters[this_chid]?.avatar] ?? [];
|
||||
|
||||
return [...globalAttachments, ...chatAttachments, ...characterAttachments].filter(x => includeDisabled || !isAttachmentDisabled(x));
|
||||
return [...globalAttachments, ...chatAttachments, ...characterAttachments].filter(x => !isAttachmentDisabled(x));
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all attachments for a specific source. Includes disabled attachments by default.
|
||||
* Gets all attachments for a specific source. Includes disabled attachments.
|
||||
* @param {string} source Attachment source
|
||||
* @param {boolean} [includeDisabled=true] If true, include disabled attachments
|
||||
* @returns {FileAttachment[]} List of attachments
|
||||
*/
|
||||
export function getDataBankAttachmentsForSource(source, includeDisabled = true) {
|
||||
export function getDataBankAttachmentsForSource(source) {
|
||||
ensureAttachmentsExist();
|
||||
|
||||
function getBySource() {
|
||||
switch (source) {
|
||||
case ATTACHMENT_SOURCE.GLOBAL:
|
||||
return extension_settings.attachments ?? [];
|
||||
case ATTACHMENT_SOURCE.CHAT:
|
||||
return chat_metadata.attachments ?? [];
|
||||
case ATTACHMENT_SOURCE.CHARACTER:
|
||||
return extension_settings.character_attachments?.[characters[this_chid]?.avatar] ?? [];
|
||||
}
|
||||
|
||||
return [];
|
||||
switch (source) {
|
||||
case ATTACHMENT_SOURCE.GLOBAL:
|
||||
return extension_settings.attachments ?? [];
|
||||
case ATTACHMENT_SOURCE.CHAT:
|
||||
return chat_metadata.attachments ?? [];
|
||||
case ATTACHMENT_SOURCE.CHARACTER:
|
||||
return extension_settings.character_attachments?.[characters[this_chid]?.avatar] ?? [];
|
||||
}
|
||||
|
||||
return getBySource().filter(x => includeDisabled || !isAttachmentDisabled(x));
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1462,7 +1374,6 @@ jQuery(function () {
|
||||
});
|
||||
|
||||
$(document).on('click', 'body.documentstyle .mes .mes_text', function () {
|
||||
if (window.getSelection().toString()) return;
|
||||
if ($('.edit_textarea').length) return;
|
||||
$(this).closest('.mes').find('.mes_edit').trigger('click');
|
||||
});
|
||||
|
@@ -122,9 +122,7 @@ const extension_settings = {
|
||||
custom: [],
|
||||
},
|
||||
dice: {},
|
||||
/** @type {import('./char-data.js').RegexScriptData[]} */
|
||||
regex: [],
|
||||
character_allowed_regex: [],
|
||||
tts: {},
|
||||
sd: {
|
||||
prompts: {},
|
||||
|
@@ -1,9 +0,0 @@
|
||||
<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>
|
@@ -3,9 +3,8 @@ TODO:
|
||||
*/
|
||||
//const DEBUG_TONY_SAMA_FORK_MODE = true
|
||||
|
||||
import { getRequestHeaders, callPopup, processDroppedFiles, eventSource, event_types } from '../../../script.js';
|
||||
import { getRequestHeaders, callPopup, processDroppedFiles } 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 };
|
||||
@@ -109,7 +108,7 @@ function downloadAssetsList(url) {
|
||||
</div>`);
|
||||
}
|
||||
|
||||
for (const i in availableAssets[assetType].sort((a, b) => a?.name && b?.name && a['name'].localeCompare(b['name']))) {
|
||||
for (const i in availableAssets[assetType]) {
|
||||
const asset = availableAssets[assetType][i];
|
||||
const elemId = `assets_install_${assetType}_${i}`;
|
||||
let element = $('<div />', { id: elemId, class: 'asset-download-button right_menu_button' });
|
||||
@@ -200,9 +199,6 @@ 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>`);
|
||||
}
|
||||
|
||||
@@ -332,41 +328,6 @@ 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 //
|
||||
//#############################//
|
||||
@@ -400,11 +361,6 @@ 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());
|
||||
@@ -441,8 +397,4 @@ jQuery(async () => {
|
||||
|
||||
windowHtml.find('#assets_filters').hide();
|
||||
$('#extensions_settings').append(windowHtml);
|
||||
|
||||
eventSource.on(event_types.OPEN_CHARACTER_LIBRARY, async (forceDefault) => {
|
||||
openCharacterBrowser(forceDefault);
|
||||
});
|
||||
});
|
||||
|
@@ -1,19 +0,0 @@
|
||||
<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>
|
@@ -105,54 +105,3 @@
|
||||
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;
|
||||
}
|
||||
|
@@ -14,10 +14,6 @@
|
||||
<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>
|
||||
|
@@ -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>
|
||||
|
@@ -1,301 +1,9 @@
|
||||
import { deleteAttachment, getDataBankAttachments, getDataBankAttachmentsForSource, getFileAttachment, uploadFileAttachmentToServer } from '../../chats.js';
|
||||
import { extension_settings, renderExtensionTemplateAsync } from '../../extensions.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';
|
||||
|
||||
/**
|
||||
* List of attachment sources
|
||||
* @type {string[]}
|
||||
*/
|
||||
const TYPES = ['global', 'character', 'chat'];
|
||||
const FIELDS = ['name', 'url'];
|
||||
|
||||
/**
|
||||
* Get attachments from the data bank. Includes disabled attachments.
|
||||
* @param {string} [source] Source for the attachments
|
||||
* @returns {import('../../chats').FileAttachment[]} List of attachments
|
||||
*/
|
||||
function getAttachments(source) {
|
||||
if (!source || !TYPES.includes(source)) {
|
||||
return getDataBankAttachments(true);
|
||||
}
|
||||
|
||||
return getDataBankAttachmentsForSource(source, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get attachment by a single name or URL.
|
||||
* @param {import('../../chats').FileAttachment[]} attachments List of attachments
|
||||
* @param {string} value Name or URL of the attachment
|
||||
* @returns {import('../../chats').FileAttachment} Attachment
|
||||
*/
|
||||
function getAttachmentByField(attachments, value) {
|
||||
const match = (a) => String(a).trim().toLowerCase() === String(value).trim().toLowerCase();
|
||||
const fullMatchByURL = attachments.find(it => match(it.url));
|
||||
const fullMatchByName = attachments.find(it => match(it.name));
|
||||
return fullMatchByURL || fullMatchByName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get attachment by multiple fields.
|
||||
* @param {import('../../chats').FileAttachment[]} attachments List of attachments
|
||||
* @param {string[]} values Name and URL of the attachment to search for
|
||||
* @returns
|
||||
*/
|
||||
function getAttachmentByFields(attachments, values) {
|
||||
for (const value of values) {
|
||||
const attachment = getAttachmentByField(attachments, value);
|
||||
if (attachment) {
|
||||
return attachment;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback for listing attachments in the data bank.
|
||||
* @param {object} args Named arguments
|
||||
* @returns {string} JSON string of the list of attachments
|
||||
*/
|
||||
function listDataBankAttachments(args) {
|
||||
const attachments = getAttachments(args?.source);
|
||||
const field = args?.field;
|
||||
return JSON.stringify(attachments.map(a => FIELDS.includes(field) ? a[field] : a.url));
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback for getting text from an attachment in the data bank.
|
||||
* @param {object} args Named arguments
|
||||
* @param {string} value Name or URL of the attachment
|
||||
* @returns {Promise<string>} Content of the attachment
|
||||
*/
|
||||
async function getDataBankText(args, value) {
|
||||
if (!value) {
|
||||
toastr.warning('No attachment name or URL provided.');
|
||||
return;
|
||||
}
|
||||
|
||||
const attachments = getAttachments(args?.source);
|
||||
const attachment = getAttachmentByField(attachments, value);
|
||||
|
||||
if (!attachment) {
|
||||
toastr.warning('Attachment not found.');
|
||||
return;
|
||||
}
|
||||
|
||||
const content = await getFileAttachment(attachment.url);
|
||||
return content;
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback for adding an attachment to the data bank.
|
||||
* @param {object} args Named arguments
|
||||
* @param {string} value Content of the attachment
|
||||
* @returns {Promise<string>} URL of the attachment
|
||||
*/
|
||||
async function uploadDataBankAttachment(args, value) {
|
||||
const source = args?.source && TYPES.includes(args.source) ? args.source : 'chat';
|
||||
const name = args?.name || new Date().toLocaleString();
|
||||
const file = new File([value], name, { type: 'text/plain' });
|
||||
const url = await uploadFileAttachmentToServer(file, source);
|
||||
return url;
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback for updating an attachment in the data bank.
|
||||
* @param {object} args Named arguments
|
||||
* @param {string} value Content of the attachment
|
||||
* @returns {Promise<string>} URL of the attachment
|
||||
*/
|
||||
async function updateDataBankAttachment(args, value) {
|
||||
const source = args?.source && TYPES.includes(args.source) ? args.source : 'chat';
|
||||
const attachments = getAttachments(source);
|
||||
const attachment = getAttachmentByFields(attachments, [args?.url, args?.name]);
|
||||
|
||||
if (!attachment) {
|
||||
toastr.warning('Attachment not found.');
|
||||
return '';
|
||||
}
|
||||
|
||||
await deleteAttachment(attachment, source, () => { }, false);
|
||||
const file = new File([value], attachment.name, { type: 'text/plain' });
|
||||
const url = await uploadFileAttachmentToServer(file, source);
|
||||
return url;
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback for deleting an attachment from the data bank.
|
||||
* @param {object} args Named arguments
|
||||
* @param {string} value Name or URL of the attachment
|
||||
* @returns {Promise<string>} Empty string
|
||||
*/
|
||||
async function deleteDataBankAttachment(args, value) {
|
||||
const source = args?.source && TYPES.includes(args.source) ? args.source : 'chat';
|
||||
const attachments = getAttachments(source);
|
||||
const attachment = getAttachmentByField(attachments, value);
|
||||
|
||||
if (!attachment) {
|
||||
toastr.warning('Attachment not found.');
|
||||
return '';
|
||||
}
|
||||
|
||||
await deleteAttachment(attachment, source, () => { }, false);
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback for disabling an attachment in the data bank.
|
||||
* @param {object} args Named arguments
|
||||
* @param {string} value Name or URL of the attachment
|
||||
* @returns {Promise<string>} Empty string
|
||||
*/
|
||||
async function disableDataBankAttachment(args, value) {
|
||||
const attachments = getAttachments(args?.source);
|
||||
const attachment = getAttachmentByField(attachments, value);
|
||||
|
||||
if (!attachment) {
|
||||
toastr.warning('Attachment not found.');
|
||||
return '';
|
||||
}
|
||||
|
||||
if (extension_settings.disabled_attachments.includes(attachment.url)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
extension_settings.disabled_attachments.push(attachment.url);
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback for enabling an attachment in the data bank.
|
||||
* @param {object} args Named arguments
|
||||
* @param {string} value Name or URL of the attachment
|
||||
* @returns {Promise<string>} Empty string
|
||||
*/
|
||||
async function enableDataBankAttachment(args, value) {
|
||||
const attachments = getAttachments(args?.source);
|
||||
const attachment = getAttachmentByField(attachments, value);
|
||||
|
||||
if (!attachment) {
|
||||
toastr.warning('Attachment not found.');
|
||||
return '';
|
||||
}
|
||||
|
||||
const index = extension_settings.disabled_attachments.indexOf(attachment.url);
|
||||
if (index === -1) {
|
||||
return '';
|
||||
}
|
||||
|
||||
extension_settings.disabled_attachments.splice(index, 1);
|
||||
return '';
|
||||
}
|
||||
import { renderExtensionTemplateAsync } from '../../extensions.js';
|
||||
import { registerSlashCommand } from '../../slash-commands.js';
|
||||
|
||||
jQuery(async () => {
|
||||
const buttons = await renderExtensionTemplateAsync('attachments', 'buttons', {});
|
||||
$('#extensionsMenu').prepend(buttons);
|
||||
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
|
||||
name: 'db',
|
||||
callback: () => document.getElementById('manageAttachments')?.click(),
|
||||
aliases: ['databank', 'data-bank'],
|
||||
helpString: 'Open the data bank',
|
||||
}));
|
||||
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
|
||||
name: 'db-list',
|
||||
callback: listDataBankAttachments,
|
||||
aliases: ['databank-list', 'data-bank-list'],
|
||||
helpString: 'List attachments in the Data Bank as a JSON-serialized array. Optionally, provide the source of the attachments and the field to list by.',
|
||||
namedArgumentList: [
|
||||
new SlashCommandNamedArgument('source', 'The source of the attachments.', ARGUMENT_TYPE.STRING, false, false, '', TYPES),
|
||||
new SlashCommandNamedArgument('field', 'The field to list by.', ARGUMENT_TYPE.STRING, false, false, 'url', FIELDS),
|
||||
],
|
||||
returns: ARGUMENT_TYPE.LIST,
|
||||
}));
|
||||
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
|
||||
name: 'db-get',
|
||||
callback: getDataBankText,
|
||||
aliases: ['databank-get', 'data-bank-get'],
|
||||
helpString: 'Get attachment text from the Data Bank. Either provide the name or URL of the attachment. Optionally, provide the source of the attachment.',
|
||||
namedArgumentList: [
|
||||
new SlashCommandNamedArgument('source', 'The source of the attachment.', ARGUMENT_TYPE.STRING, false, false, '', TYPES),
|
||||
],
|
||||
unnamedArgumentList: [
|
||||
new SlashCommandArgument('The name or URL of the attachment.', ARGUMENT_TYPE.STRING, true, false),
|
||||
],
|
||||
returns: ARGUMENT_TYPE.STRING,
|
||||
}));
|
||||
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
|
||||
name: 'db-add',
|
||||
callback: uploadDataBankAttachment,
|
||||
aliases: ['databank-add', 'data-bank-add'],
|
||||
helpString: 'Add an attachment to the Data Bank. If name is not provided, it will be generated automatically. Returns the URL of the attachment.',
|
||||
namedArgumentList: [
|
||||
new SlashCommandNamedArgument('source', 'The source for the attachment.', ARGUMENT_TYPE.STRING, false, false, 'chat', TYPES),
|
||||
new SlashCommandNamedArgument('name', 'The name of the attachment.', ARGUMENT_TYPE.STRING, false, false),
|
||||
],
|
||||
unnamedArgumentList: [
|
||||
new SlashCommandArgument('The content of the file attachment.', ARGUMENT_TYPE.STRING, true, false),
|
||||
],
|
||||
returns: ARGUMENT_TYPE.STRING,
|
||||
}));
|
||||
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
|
||||
name: 'db-update',
|
||||
callback: updateDataBankAttachment,
|
||||
aliases: ['databank-update', 'data-bank-update'],
|
||||
helpString: 'Update an attachment in the Data Bank, preserving its name. Returns a new URL of the attachment.',
|
||||
namedArgumentList: [
|
||||
new SlashCommandNamedArgument('source', 'The source for the attachment.', ARGUMENT_TYPE.STRING, false, false, 'chat', TYPES),
|
||||
new SlashCommandNamedArgument('name', 'The name of the attachment.', ARGUMENT_TYPE.STRING, false, false),
|
||||
new SlashCommandNamedArgument('url', 'The URL of the attachment to update.', ARGUMENT_TYPE.STRING, false, false),
|
||||
],
|
||||
unnamedArgumentList: [
|
||||
new SlashCommandArgument('The content of the file attachment.', ARGUMENT_TYPE.STRING, true, false),
|
||||
],
|
||||
returns: ARGUMENT_TYPE.STRING,
|
||||
}));
|
||||
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
|
||||
name: 'db-disable',
|
||||
callback: disableDataBankAttachment,
|
||||
aliases: ['databank-disable', 'data-bank-disable'],
|
||||
helpString: 'Disable an attachment in the Data Bank by its name or URL. Optionally, provide the source of the attachment.',
|
||||
namedArgumentList: [
|
||||
new SlashCommandNamedArgument('source', 'The source of the attachment.', ARGUMENT_TYPE.STRING, false, false, '', TYPES),
|
||||
],
|
||||
unnamedArgumentList: [
|
||||
new SlashCommandArgument('The name or URL of the attachment.', ARGUMENT_TYPE.STRING, true, false),
|
||||
],
|
||||
}));
|
||||
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
|
||||
name: 'db-enable',
|
||||
callback: enableDataBankAttachment,
|
||||
aliases: ['databank-enable', 'data-bank-enable'],
|
||||
helpString: 'Enable an attachment in the Data Bank by its name or URL. Optionally, provide the source of the attachment.',
|
||||
namedArgumentList: [
|
||||
new SlashCommandNamedArgument('source', 'The source of the attachment.', ARGUMENT_TYPE.STRING, false, false, '', TYPES),
|
||||
],
|
||||
unnamedArgumentList: [
|
||||
new SlashCommandArgument('The name or URL of the attachment.', ARGUMENT_TYPE.STRING, true, false),
|
||||
],
|
||||
}));
|
||||
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
|
||||
name: 'db-delete',
|
||||
callback: deleteDataBankAttachment,
|
||||
aliases: ['databank-delete', 'data-bank-delete'],
|
||||
helpString: 'Delete an attachment from the Data Bank.',
|
||||
namedArgumentList: [
|
||||
new SlashCommandNamedArgument('source', 'The source of the attachment.', ARGUMENT_TYPE.STRING, false, false, 'chat', TYPES),
|
||||
],
|
||||
unnamedArgumentList: [
|
||||
new SlashCommandArgument('The name or URL of the attachment.', ARGUMENT_TYPE.STRING, true, false),
|
||||
],
|
||||
}));
|
||||
registerSlashCommand('db', () => document.getElementById('manageAttachments')?.click(), ['databank', 'data-bank'], '– open the data bank', true, true);
|
||||
});
|
||||
|
@@ -1,4 +1,4 @@
|
||||
<div class="wide100p padding5 dataBankAttachments">
|
||||
<div class="wide100p padding5">
|
||||
<h2 class="marginBot5">
|
||||
<span data-i18n="Data Bank">
|
||||
Data Bank
|
||||
@@ -37,27 +37,7 @@
|
||||
Size (Largest First)
|
||||
</option>
|
||||
</select>
|
||||
<label class="margin0 menu_button menu_button_icon attachmentsBulkEditButton">
|
||||
<i class="fa-solid fa-edit"></i>
|
||||
<span data-i18n="Bulk Edit">Bulk Edit</span>
|
||||
<input type="checkbox" class="displayNone attachmentsBulkEditCheckbox" hidden>
|
||||
</label>
|
||||
</div>
|
||||
<div class="attachmentBulkActionsContainer flex-container marginTopBot5 alignItemsBaseline">
|
||||
<div class="flex-container">
|
||||
<div class="menu_button menu_button_icon bulkActionSelectAll" title="Select all *visible* attachments">
|
||||
<i class="fa-solid fa-check-square"></i>
|
||||
<span data-i18n="Select All">Select All</span>
|
||||
</div>
|
||||
<div class="menu_button menu_button_icon bulkActionSelectNone" title="Deselect all *visible* attachments">
|
||||
<i class="fa-solid fa-square"></i>
|
||||
<span data-i18n="Select None">Select None</span>
|
||||
</div>
|
||||
<div class="menu_button menu_button_icon bulkActionDelete" title="Delete selected attachments">
|
||||
<i class="fa-solid fa-trash"></i>
|
||||
<span data-i18n="Delete">Delete</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="justifyLeft globalAttachmentsBlock marginBot10">
|
||||
<h3 class="globalAttachmentsTitle margin0 title_restorable">
|
||||
@@ -122,7 +102,6 @@
|
||||
|
||||
<div class="attachmentListItemTemplate template_element">
|
||||
<div class="attachmentListItem flex-container alignItemsCenter flexGap10">
|
||||
<div class="attachmentListItemCheckboxContainer"><input type="checkbox" class="attachmentListItemCheckbox"></div>
|
||||
<div class="attachmentFileIcon fa-solid fa-file-alt"></div>
|
||||
<div class="attachmentListItemName flex1"></div>
|
||||
<small class="attachmentListItemCreated"></small>
|
||||
|
@@ -1,54 +0,0 @@
|
||||
<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>
|
@@ -37,27 +37,3 @@
|
||||
.attachmentListItemCreated {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.attachmentListItemCheckboxContainer,
|
||||
.attachmentBulkActionsContainer,
|
||||
.attachmentsBulkEditCheckbox {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@supports selector(:has(*)) {
|
||||
.dataBankAttachments:has(.attachmentsBulkEditCheckbox:checked) .attachmentsBulkEditButton {
|
||||
color: var(--golden);
|
||||
}
|
||||
|
||||
.dataBankAttachments:has(.attachmentsBulkEditCheckbox:checked) .attachmentBulkActionsContainer {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.dataBankAttachments:has(.attachmentsBulkEditCheckbox:checked) .attachmentListItemCheckboxContainer {
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
.dataBankAttachments:has(.attachmentsBulkEditCheckbox:checked) .attachmentFileIcon {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
@@ -1,13 +1,11 @@
|
||||
import { getBase64Async, isTrueBoolean, saveBase64AsFile } from '../../utils.js';
|
||||
import { getContext, getApiUrl, doExtrasFetch, extension_settings, modules, renderExtensionTemplateAsync } from '../../extensions.js';
|
||||
import { getContext, getApiUrl, doExtrasFetch, extension_settings, modules } from '../../extensions.js';
|
||||
import { callPopup, getRequestHeaders, saveSettingsDebounced, substituteParams } from '../../../script.js';
|
||||
import { getMessageTimeStamp } from '../../RossAscends-mods.js';
|
||||
import { SECRET_KEYS, secret_state } from '../../secrets.js';
|
||||
import { getMultimodalCaption } from '../shared.js';
|
||||
import { textgen_types, textgenerationwebui_settings } from '../../textgen-settings.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 { registerSlashCommand } from '../../slash-commands.js';
|
||||
export { MODULE_NAME };
|
||||
|
||||
const MODULE_NAME = 'caption';
|
||||
@@ -256,19 +254,6 @@ 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();
|
||||
@@ -288,6 +273,7 @@ async function getCaptionForFile(file, prompt, quiet) {
|
||||
return '';
|
||||
}
|
||||
finally {
|
||||
form && form.reset();
|
||||
setImageIcon();
|
||||
}
|
||||
}
|
||||
@@ -302,26 +288,9 @@ function onRefineModeInput() {
|
||||
* @param {object} args Named parameters
|
||||
* @param {string} prompt Caption 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 '';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function captionCommandCallback(args, prompt) {
|
||||
return new Promise(resolve => {
|
||||
const quiet = isTrueBoolean(args?.quiet);
|
||||
const input = document.createElement('input');
|
||||
input.type = 'file';
|
||||
input.accept = 'image/*';
|
||||
@@ -334,7 +303,7 @@ async function captionCommandCallback(args, prompt) {
|
||||
});
|
||||
}
|
||||
|
||||
jQuery(async function () {
|
||||
jQuery(function () {
|
||||
function addSendPictureButton() {
|
||||
const sendButton = $(`
|
||||
<div id="send_picture" class="list-group-item flex-container flexGap5">
|
||||
@@ -348,8 +317,8 @@ jQuery(async function () {
|
||||
(modules.includes('caption') && extension_settings.caption.source === 'extras') ||
|
||||
(extension_settings.caption.source === 'multimodal' && extension_settings.caption.multimodal_api === 'openai' && (secret_state[SECRET_KEYS.OPENAI] || extension_settings.caption.allow_reverse_proxy)) ||
|
||||
(extension_settings.caption.source === 'multimodal' && extension_settings.caption.multimodal_api === 'openrouter' && secret_state[SECRET_KEYS.OPENROUTER]) ||
|
||||
(extension_settings.caption.source === 'multimodal' && extension_settings.caption.multimodal_api === 'google' && (secret_state[SECRET_KEYS.MAKERSUITE] || extension_settings.caption.allow_reverse_proxy)) ||
|
||||
(extension_settings.caption.source === 'multimodal' && extension_settings.caption.multimodal_api === 'anthropic' && (secret_state[SECRET_KEYS.CLAUDE] || extension_settings.caption.allow_reverse_proxy)) ||
|
||||
(extension_settings.caption.source === 'multimodal' && extension_settings.caption.multimodal_api === 'google' && secret_state[SECRET_KEYS.MAKERSUITE]) ||
|
||||
(extension_settings.caption.source === 'multimodal' && extension_settings.caption.multimodal_api === 'anthropic' && secret_state[SECRET_KEYS.CLAUDE]) ||
|
||||
(extension_settings.caption.source === 'multimodal' && extension_settings.caption.multimodal_api === 'ollama' && textgenerationwebui_settings.server_urls[textgen_types.OLLAMA]) ||
|
||||
(extension_settings.caption.source === 'multimodal' && extension_settings.caption.multimodal_api === 'llamacpp' && textgenerationwebui_settings.server_urls[textgen_types.LLAMACPP]) ||
|
||||
(extension_settings.caption.source === 'multimodal' && extension_settings.caption.multimodal_api === 'ooba' && textgenerationwebui_settings.server_urls[textgen_types.OOBA]) ||
|
||||
@@ -399,12 +368,95 @@ jQuery(async function () {
|
||||
saveSettingsDebounced();
|
||||
});
|
||||
}
|
||||
async function addSettings() {
|
||||
const html = await renderExtensionTemplateAsync('caption', 'settings');
|
||||
function addSettings() {
|
||||
const html = `
|
||||
<div class="caption_settings">
|
||||
<div class="inline-drawer">
|
||||
<div class="inline-drawer-toggle inline-drawer-header">
|
||||
<b>Image Captioning</b>
|
||||
<div class="inline-drawer-icon fa-solid fa-circle-chevron-down down"></div>
|
||||
</div>
|
||||
<div class="inline-drawer-content">
|
||||
<label for="caption_source">Source</label>
|
||||
<select id="caption_source" class="text_pole">
|
||||
<option value="local">Local</option>
|
||||
<option value="multimodal">Multimodal (OpenAI / Anthropic / llama / Google)</option>
|
||||
<option value="extras">Extras</option>
|
||||
<option value="horde">Horde</option>
|
||||
</select>
|
||||
<div id="caption_multimodal_block" class="flex-container wide100p">
|
||||
<div class="flex1 flex-container flexFlowColumn flexNoGap">
|
||||
<label for="caption_multimodal_api">API</label>
|
||||
<select id="caption_multimodal_api" class="flex1 text_pole">
|
||||
<option value="anthropic">Anthropic</option>
|
||||
<option value="custom">Custom (OpenAI-compatible)</option>
|
||||
<option value="google">Google MakerSuite</option>
|
||||
<option value="koboldcpp">KoboldCpp</option>
|
||||
<option value="llamacpp">llama.cpp</option>
|
||||
<option value="ollama">Ollama</option>
|
||||
<option value="openai">OpenAI</option>
|
||||
<option value="openrouter">OpenRouter</option>
|
||||
<option value="ooba">Text Generation WebUI (oobabooga)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="flex1 flex-container flexFlowColumn flexNoGap">
|
||||
<label for="caption_multimodal_model">Model</label>
|
||||
<select id="caption_multimodal_model" class="flex1 text_pole">
|
||||
<option data-type="openai" value="gpt-4-vision-preview">gpt-4-vision-preview</option>
|
||||
<option data-type="openai" value="gpt-4-turbo">gpt-4-turbo</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="openrouter" value="openai/gpt-4-vision-preview">openai/gpt-4-vision-preview</option>
|
||||
<option data-type="openrouter" value="haotian-liu/llava-13b">haotian-liu/llava-13b</option>
|
||||
<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>
|
||||
<option data-type="openrouter" value="anthropic/claude-3-haiku:beta">anthropic/claude-3-haiku:beta</option>
|
||||
<option data-type="openrouter" value="anthropic/claude-3-sonnet:beta">anthropic/claude-3-sonnet:beta</option>
|
||||
<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="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>
|
||||
<option data-type="llamacpp" value="llamacpp_current">[Currently loaded]</option>
|
||||
<option data-type="ooba" value="ooba_current">[Currently loaded]</option>
|
||||
<option data-type="koboldcpp" value="koboldcpp_current">[Currently loaded]</option>
|
||||
<option data-type="custom" value="custom_current">[Currently selected]</option>
|
||||
</select>
|
||||
</div>
|
||||
<label data-type="openai,anthropic" class="checkbox_label flexBasis100p" for="caption_allow_reverse_proxy" title="Allow using reverse proxy if defined and valid.">
|
||||
<input id="caption_allow_reverse_proxy" type="checkbox" class="checkbox">
|
||||
Allow reverse proxy
|
||||
</label>
|
||||
<div class="flexBasis100p m-b-1">
|
||||
<small><b>Hint:</b> Set your API keys and endpoints in the 'API Connections' tab first.</small>
|
||||
</div>
|
||||
</div>
|
||||
<div id="caption_prompt_block">
|
||||
<label for="caption_prompt">Caption Prompt</label>
|
||||
<textarea id="caption_prompt" class="text_pole" rows="1" placeholder="< Use default >">${PROMPT_DEFAULT}</textarea>
|
||||
<label class="checkbox_label margin-bot-10px" for="caption_prompt_ask" title="Ask for a custom prompt every time an image is captioned.">
|
||||
<input id="caption_prompt_ask" type="checkbox" class="checkbox">
|
||||
Ask every time
|
||||
</label>
|
||||
</div>
|
||||
<label for="caption_template">Message Template <small>(use <code>{{caption}}</code> macro)</small></label>
|
||||
<textarea id="caption_template" class="text_pole" rows="2" placeholder="< Use default >">${TEMPLATE_DEFAULT}</textarea>
|
||||
<label class="checkbox_label margin-bot-10px" for="caption_refine_mode">
|
||||
<input id="caption_refine_mode" type="checkbox" class="checkbox">
|
||||
Edit captions before saving
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
$('#extensions_settings2').append(html);
|
||||
}
|
||||
|
||||
await addSettings();
|
||||
addSettings();
|
||||
addPictureSendForm();
|
||||
addSendPictureButton();
|
||||
setImageIcon();
|
||||
@@ -440,35 +492,5 @@ jQuery(async function () {
|
||||
saveSettingsDebounced();
|
||||
});
|
||||
|
||||
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>
|
||||
`,
|
||||
}));
|
||||
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);
|
||||
});
|
||||
|
@@ -1,90 +0,0 @@
|
||||
|
||||
<div class="caption_settings">
|
||||
<div class="inline-drawer">
|
||||
<div class="inline-drawer-toggle inline-drawer-header">
|
||||
<b>Image Captioning</b>
|
||||
<div class="inline-drawer-icon fa-solid fa-circle-chevron-down down"></div>
|
||||
</div>
|
||||
<div class="inline-drawer-content">
|
||||
<label for="caption_source">Source</label>
|
||||
<select id="caption_source" class="text_pole">
|
||||
<option value="local">Local</option>
|
||||
<option value="multimodal">Multimodal (OpenAI / Anthropic / llama / Google)</option>
|
||||
<option value="extras">Extras</option>
|
||||
<option value="horde">Horde</option>
|
||||
</select>
|
||||
<div id="caption_multimodal_block" class="flex-container wide100p">
|
||||
<div class="flex1 flex-container flexFlowColumn flexNoGap">
|
||||
<label for="caption_multimodal_api">API</label>
|
||||
<select id="caption_multimodal_api" class="flex1 text_pole">
|
||||
<option value="anthropic">Anthropic</option>
|
||||
<option value="custom">Custom (OpenAI-compatible)</option>
|
||||
<option value="google">Google MakerSuite</option>
|
||||
<option value="koboldcpp">KoboldCpp</option>
|
||||
<option value="llamacpp">llama.cpp</option>
|
||||
<option value="ollama">Ollama</option>
|
||||
<option value="openai">OpenAI</option>
|
||||
<option value="openrouter">OpenRouter</option>
|
||||
<option value="ooba">Text Generation WebUI (oobabooga)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="flex1 flex-container flexFlowColumn flexNoGap">
|
||||
<label for="caption_multimodal_model">Model</label>
|
||||
<select id="caption_multimodal_model" class="flex1 text_pole">
|
||||
<option data-type="openai" value="gpt-4-vision-preview">gpt-4-vision-preview</option>
|
||||
<option data-type="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>
|
||||
<option data-type="openrouter" value="anthropic/claude-3-haiku:beta">anthropic/claude-3-haiku:beta</option>
|
||||
<option data-type="openrouter" value="anthropic/claude-3-sonnet:beta">anthropic/claude-3-sonnet:beta</option>
|
||||
<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>
|
||||
<option data-type="llamacpp" value="llamacpp_current">[Currently loaded]</option>
|
||||
<option data-type="ooba" value="ooba_current">[Currently loaded]</option>
|
||||
<option data-type="koboldcpp" value="koboldcpp_current">[Currently loaded]</option>
|
||||
<option data-type="custom" value="custom_current">[Currently selected]</option>
|
||||
</select>
|
||||
</div>
|
||||
<label data-type="openai,anthropic,google" class="checkbox_label flexBasis100p" for="caption_allow_reverse_proxy" title="Allow using reverse proxy if defined and valid.">
|
||||
<input id="caption_allow_reverse_proxy" type="checkbox" class="checkbox">
|
||||
Allow reverse proxy
|
||||
</label>
|
||||
<div class="flexBasis100p m-b-1">
|
||||
<small><b>Hint:</b> Set your API keys and endpoints in the 'API Connections' tab first.</small>
|
||||
</div>
|
||||
</div>
|
||||
<div id="caption_prompt_block">
|
||||
<label for="caption_prompt">Caption Prompt</label>
|
||||
<textarea id="caption_prompt" class="text_pole" rows="1" placeholder="< Use default >">${PROMPT_DEFAULT}</textarea>
|
||||
<label class="checkbox_label margin-bot-10px" for="caption_prompt_ask" title="Ask for a custom prompt every time an image is captioned.">
|
||||
<input id="caption_prompt_ask" type="checkbox" class="checkbox">
|
||||
Ask every time
|
||||
</label>
|
||||
</div>
|
||||
<label for="caption_template">Message Template <small>(use <code>{{caption}}</code> macro)</small></label>
|
||||
<textarea id="caption_template" class="text_pole" rows="2" placeholder="< Use default >">${TEMPLATE_DEFAULT}</textarea>
|
||||
<label class="checkbox_label margin-bot-10px" for="caption_refine_mode">
|
||||
<input id="caption_refine_mode" type="checkbox" class="checkbox">
|
||||
Edit captions before saving
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
@@ -1,15 +1,12 @@
|
||||
import { callPopup, eventSource, event_types, generateQuietPrompt, getRequestHeaders, online_status, saveSettingsDebounced, substituteParams } from '../../../script.js';
|
||||
import { callPopup, eventSource, event_types, generateQuietPrompt, getRequestHeaders, saveSettingsDebounced, substituteParams } from '../../../script.js';
|
||||
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 { onlyUnique, debounce, getCharaFilename, trimToEndSentence, trimToStartSentence, waitUntilCondition } from '../../utils.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';
|
||||
import { isFunctionCallingSupported } from '../../openai.js';
|
||||
export { MODULE_NAME };
|
||||
|
||||
const MODULE_NAME = 'expressions';
|
||||
@@ -17,7 +14,6 @@ const UPDATE_INTERVAL = 2000;
|
||||
const STREAMING_UPDATE_INTERVAL = 10000;
|
||||
const TALKINGCHECK_UPDATE_INTERVAL = 500;
|
||||
const DEFAULT_FALLBACK_EXPRESSION = 'joy';
|
||||
const FUNCTION_NAME = 'set_emotion';
|
||||
const DEFAULT_LLM_PROMPT = 'Pause your roleplay. Classify the emotion of the last message. Output just one word, e.g. "joy" or "anger". Choose only one of the following labels: {{labels}}';
|
||||
const DEFAULT_EXPRESSIONS = [
|
||||
'talkinghead',
|
||||
@@ -910,10 +906,8 @@ async function setSpriteSetCommand(_, folder) {
|
||||
|
||||
$('#expression_override').val(folder.trim());
|
||||
onClickExpressionOverrideButton();
|
||||
// removeExpression();
|
||||
// moduleWorker();
|
||||
const vnMode = isVisualNovelMode();
|
||||
await sendExpressionCall(folder, lastExpression, true, vnMode);
|
||||
removeExpression();
|
||||
moduleWorker();
|
||||
}
|
||||
|
||||
async function classifyCommand(_, text) {
|
||||
@@ -978,8 +972,8 @@ function sampleClassifyText(text) {
|
||||
return text;
|
||||
}
|
||||
|
||||
// Replace macros, remove asterisks and quotes
|
||||
let result = substituteParams(text).replace(/[*"]/g, '');
|
||||
// Remove asterisks and quotes
|
||||
let result = text.replace(/[*"]/g, '');
|
||||
|
||||
const SAMPLE_THRESHOLD = 500;
|
||||
const HALF_SAMPLE_THRESHOLD = SAMPLE_THRESHOLD / 2;
|
||||
@@ -1003,10 +997,6 @@ async function getLlmPrompt(labels) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (isFunctionCallingSupported()) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const labelsString = labels.map(x => `"${x}"`).join(', ');
|
||||
const prompt = substituteParams(String(extension_settings.expressions.llmPrompt))
|
||||
.replace(/{{labels}}/gi, labelsString);
|
||||
@@ -1020,64 +1010,24 @@ async function getLlmPrompt(labels) {
|
||||
* @returns {string} The parsed emotion or the fallback expression.
|
||||
*/
|
||||
function parseLlmResponse(emotionResponse, labels) {
|
||||
const fallbackExpression = getFallbackExpression();
|
||||
|
||||
try {
|
||||
const parsedEmotion = JSON.parse(emotionResponse);
|
||||
const response = parsedEmotion?.emotion?.trim()?.toLowerCase();
|
||||
|
||||
if (!response || !labels.includes(response)) {
|
||||
console.debug(`Parsed emotion response: ${response} not in labels: ${labels}`);
|
||||
throw new Error('Emotion not in labels');
|
||||
}
|
||||
|
||||
return response;
|
||||
return parsedEmotion?.emotion ?? fallbackExpression;
|
||||
} catch {
|
||||
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;
|
||||
const fuse = new Fuse([emotionResponse]);
|
||||
for (const label of labels) {
|
||||
const result = fuse.search(label);
|
||||
if (result.length > 0) {
|
||||
return label;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error('Could not parse emotion response ' + emotionResponse);
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers the function tool for the LLM API.
|
||||
* @param {FunctionToolRegister} args Function tool register arguments.
|
||||
*/
|
||||
function onFunctionToolRegister(args) {
|
||||
if (inApiCall && extension_settings.expressions.api === EXPRESSION_API.llm && isFunctionCallingSupported()) {
|
||||
// Only trigger on quiet mode
|
||||
if (args.type !== 'quiet') {
|
||||
return;
|
||||
}
|
||||
|
||||
const emotions = DEFAULT_EXPRESSIONS.filter((e) => e != 'talkinghead');
|
||||
const jsonSchema = {
|
||||
$schema: 'http://json-schema.org/draft-04/schema#',
|
||||
type: 'object',
|
||||
properties: {
|
||||
emotion: {
|
||||
type: 'string',
|
||||
enum: emotions,
|
||||
description: `One of the following: ${JSON.stringify(emotions)}`,
|
||||
},
|
||||
},
|
||||
required: [
|
||||
'emotion',
|
||||
],
|
||||
};
|
||||
args.registerFunctionTool(
|
||||
FUNCTION_NAME,
|
||||
substituteParams('Sets the label that best describes the current emotional state of {{char}}. Only select one of the enumerated values.'),
|
||||
jsonSchema,
|
||||
true,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function onTextGenSettingsReady(args) {
|
||||
// Only call if inside an API call
|
||||
if (inApiCall && extension_settings.expressions.api === EXPRESSION_API.llm && isJsonSchemaSupported()) {
|
||||
@@ -1133,27 +1083,11 @@ async function getExpressionLabel(text) {
|
||||
} break;
|
||||
// Using LLM
|
||||
case EXPRESSION_API.llm: {
|
||||
try {
|
||||
await waitUntilCondition(() => online_status !== 'no_connection', 3000, 250);
|
||||
} catch (error) {
|
||||
console.warn('No LLM connection. Using fallback expression', error);
|
||||
return getFallbackExpression();
|
||||
}
|
||||
|
||||
const expressionsList = await getExpressionsList();
|
||||
const prompt = await getLlmPrompt(expressionsList);
|
||||
let functionResult = null;
|
||||
eventSource.once(event_types.TEXT_COMPLETION_SETTINGS_READY, onTextGenSettingsReady);
|
||||
eventSource.once(event_types.LLM_FUNCTION_TOOL_REGISTER, onFunctionToolRegister);
|
||||
eventSource.once(event_types.LLM_FUNCTION_TOOL_CALL, (/** @type {FunctionToolCall} */ args) => {
|
||||
if (args.name !== FUNCTION_NAME) {
|
||||
return;
|
||||
}
|
||||
|
||||
functionResult = args?.arguments;
|
||||
});
|
||||
const emotionResponse = await generateQuietPrompt(prompt, false, false);
|
||||
return parseLlmResponse(functionResult || emotionResponse, expressionsList);
|
||||
return parseLlmResponse(emotionResponse, expressionsList);
|
||||
}
|
||||
// Extras
|
||||
default: {
|
||||
@@ -2033,61 +1967,9 @@ function migrateSettings() {
|
||||
});
|
||||
eventSource.on(event_types.MOVABLE_PANELS_RESET, updateVisualNovelModeDebounced);
|
||||
eventSource.on(event_types.GROUP_UPDATED, updateVisualNovelModeDebounced);
|
||||
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>
|
||||
`,
|
||||
}));
|
||||
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);
|
||||
})();
|
||||
|
@@ -34,7 +34,7 @@
|
||||
<i class="fa-solid fa-clock-rotate-left fa-sm"></i>
|
||||
</div>
|
||||
</label>
|
||||
<small>Will be used if the API doesn't support JSON schemas or function calling.</small>
|
||||
<small>Will be used if the API doesn't support JSON schemas.</small>
|
||||
<textarea id="expression_llm_prompt" type="text" class="text_pole textarea_compact" rows="2" placeholder="Use {{labels}} special macro."></textarea>
|
||||
</div>
|
||||
<div class="expression_fallback_block m-b-1 m-t-1">
|
||||
@@ -64,6 +64,10 @@
|
||||
<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> <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>
|
||||
@@ -76,11 +80,6 @@
|
||||
</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> <span id="image_list_header_name"></span>
|
||||
</h3>
|
||||
<div id="image_list"></div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -14,6 +14,7 @@
|
||||
display: flex;
|
||||
height: calc(100vh - var(--topBarBlockSize));
|
||||
width: 100vw;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
|
@@ -8,9 +8,7 @@ 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 { SlashCommandParser } from '../../slash-commands/SlashCommandParser.js';
|
||||
import { SlashCommand } from '../../slash-commands/SlashCommand.js';
|
||||
import { ARGUMENT_TYPE, SlashCommandNamedArgument } from '../../slash-commands/SlashCommandArgument.js';
|
||||
import { registerSlashCommand } from '../../slash-commands.js';
|
||||
|
||||
const extensionName = 'gallery';
|
||||
const extensionFolderPath = `scripts/extensions/${extensionName}/`;
|
||||
@@ -417,26 +415,8 @@ function viewWithDragbox(items) {
|
||||
|
||||
|
||||
// Registers a simple command for opening the char gallery.
|
||||
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.',
|
||||
}));
|
||||
|
||||
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);
|
||||
|
||||
function showGalleryCommand(args) {
|
||||
showCharGallery();
|
||||
|
@@ -16,14 +16,11 @@ 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';
|
||||
@@ -419,7 +416,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');
|
||||
@@ -427,42 +424,7 @@ async function forceSummarizeChat() {
|
||||
|
||||
if (!value) {
|
||||
toastr.warning('Failed to summarize chat');
|
||||
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 '';
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -706,18 +668,37 @@ async function summarizeChatExtras(context) {
|
||||
// perform the summarization API call
|
||||
try {
|
||||
inApiCall = true;
|
||||
const summary = await callExtrasSummarizeAPI(resultingString);
|
||||
const newContext = getContext();
|
||||
const url = new URL(getApiUrl());
|
||||
url.pathname = '/api/summarize';
|
||||
|
||||
// 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;
|
||||
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);
|
||||
}
|
||||
|
||||
setMemoryContext(summary, true);
|
||||
}
|
||||
catch (error) {
|
||||
console.log(error);
|
||||
@@ -727,40 +708,6 @@ 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();
|
||||
@@ -804,7 +751,10 @@ 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, 'Position: ' + extension_settings.memory.position, 'Depth: ' + extension_settings.memory.depth, 'Role: ' + extension_settings.memory.role);
|
||||
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);
|
||||
|
||||
if (saveToMessage && context.chat.length) {
|
||||
const idx = index ?? context.chat.length - 2;
|
||||
@@ -915,17 +865,5 @@ jQuery(async function () {
|
||||
eventSource.on(event_types.MESSAGE_EDITED, onChatEvent);
|
||||
eventSource.on(event_types.MESSAGE_SWIPED, onChatEvent);
|
||||
eventSource.on(event_types.CHAT_CHANGED, onChatEvent);
|
||||
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.',
|
||||
returns: ARGUMENT_TYPE.STRING,
|
||||
}));
|
||||
registerSlashCommand('summarize', forceSummarizeChat, [], '– forces the summarization of the current chat using the Main API', true, true);
|
||||
});
|
||||
|
@@ -9,73 +9,73 @@
|
||||
</div>
|
||||
<div class="inline-drawer-content">
|
||||
<div id="summaryExtensionDrawerContents">
|
||||
<label for="summary_source" data-i18n="ext_sum_with">Summarize with:</label>
|
||||
<label for="summary_source">Summarize with:</label>
|
||||
<select id="summary_source">
|
||||
<option value="main" data-i18n="ext_sum_main_api">Main API</option>
|
||||
<option value="main">Main API</option>
|
||||
<option value="extras">Extras API</option>
|
||||
</select><br>
|
||||
|
||||
<div class="flex-container justifyspacebetween alignitemscenter">
|
||||
<span class="flex1" data-i18n="ext_sum_current_summary">Current summary:</span>
|
||||
<span class="flex1">Current summary:</span>
|
||||
<div id="memory_restore" class="menu_button flex1 margin0">
|
||||
<span data-i18n="ext_sum_restore_previous">Restore Previous</span>
|
||||
<span>Restore Previous</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
<textarea id="memory_contents" class="text_pole textarea_compact" rows="6" placeholder="Summary will be generated here..."></textarea>
|
||||
<div class="memory_contents_controls">
|
||||
<div id="memory_force_summarize" 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.">
|
||||
<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.">
|
||||
<i class="fa-solid fa-database"></i>
|
||||
<span data-i18n="ext_sum_force_text">Summarize now</span>
|
||||
<span>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" /><span data-i18n="ext_sum_pause">Pause</span></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" />Pause</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 data-i18n="ext_sum_no_wi_an">No WI/AN</span>
|
||||
<span>No WI/AN</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="memory_contents_controls">
|
||||
<div id="summarySettingsBlockToggle" class="menu_button menu_button_icon" data-i18n="[title]ext_sum_settings_tip" title="Edit summarization prompt, insertion position, etc.">
|
||||
<div id="summarySettingsBlockToggle" class="menu_button menu_button_icon" title="Edit summarization prompt, insertion position, etc.">
|
||||
<i class="fa-solid fa-cog"></i>
|
||||
<span data-i18n="ext_sum_settings">Summary Settings</span>
|
||||
<span>Summary Settings</span>
|
||||
</div>
|
||||
</div>
|
||||
<div id="summarySettingsBlock" style="display:none;">
|
||||
<div data-summary-source="main">
|
||||
<label data-i18n="ext_sum_prompt_builder">
|
||||
<label>
|
||||
Prompt builder
|
||||
</label>
|
||||
<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.">
|
||||
<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.">
|
||||
<input id="memory_prompt_builder_raw_blocking" type="radio" name="memory_prompt_builder" value="1" />
|
||||
<span data-i18n="ext_sum_prompt_builder_1">Raw, blocking</span>
|
||||
<span>Raw, blocking</span>
|
||||
</label>
|
||||
<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.">
|
||||
<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.">
|
||||
<input id="memory_prompt_builder_raw_non_blocking" type="radio" name="memory_prompt_builder" value="2" />
|
||||
<span data-i18n="ext_sum_prompt_builder_2">Raw, non-blocking</span>
|
||||
<span>Raw, non-blocking</span>
|
||||
</label>
|
||||
<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.">
|
||||
<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.">
|
||||
<input id="memory_prompt_builder_default" type="radio" name="memory_prompt_builder" value="0" />
|
||||
<span data-i18n="ext_sum_prompt_builder_3">Classic, blocking</span>
|
||||
<span>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" data-i18n="[title]ext_sum_restore_default_prompt_tip" title="Restore default prompt" class="right_menu_button">
|
||||
<div id="memory_prompt_restore" 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" data-i18n="[placeholder]ext_sum_prompt_placeholder" placeholder="This prompt will be sent to AI to request the summary generation. {{words}} 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>
|
||||
<textarea id="memory_prompt" class="text_pole textarea_compact" rows="6" placeholder="This prompt will be sent to AI to request the summary generation. {{words}} 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>
|
||||
<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">
|
||||
<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>
|
||||
API response length (<span id="memory_override_response_length_value"></span> tokens)
|
||||
<small class="memory_disabled_hint">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">
|
||||
<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>
|
||||
[Raw] Max messages per request (<span id="memory_max_messages_per_request_value"></span>)
|
||||
<small class="memory_disabled_hint">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>
|
||||
<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>
|
||||
Update every <span id="memory_prompt_interval_value"></span> messages
|
||||
<small class="memory_disabled_hint">0 = disable</small>
|
||||
</span>
|
||||
<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 id="memory_prompt_interval_auto" 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>
|
||||
<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>
|
||||
Update every <span id="memory_prompt_words_force_value"></span> words
|
||||
<small class="memory_disabled_hint">0 = disable</small>
|
||||
</span>
|
||||
<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 id="memory_prompt_words_auto" 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 data-i18n="ext_sum_both_sliders">If both sliders are non-zero, then both will trigger summary updates at their respective intervals.</small>
|
||||
<small>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" 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="{{summary}} will resolve to the current summary contents."></textarea>
|
||||
<label for="memory_template">Injection Template</label>
|
||||
<textarea id="memory_template" class="text_pole textarea_compact" rows="2" placeholder="{{summary}} will resolve to the current summary contents."></textarea>
|
||||
</div>
|
||||
<label for="memory_position" data-i18n="ext_sum_injection_position">Injection Position</label>
|
||||
<label for="memory_position">Injection Position</label>
|
||||
<div class="radio_group">
|
||||
<label>
|
||||
<input type="radio" name="memory_position" value="2" />
|
||||
<span data-i18n="Before Main Prompt / Story String">Before Main Prompt / Story String</span>
|
||||
Before Main Prompt / Story String
|
||||
</label>
|
||||
<label>
|
||||
<input type="radio" name="memory_position" value="0" />
|
||||
<span data-i18n="After Main Prompt / Story String">After Main Prompt / Story String</span>
|
||||
After Main Prompt / Story String
|
||||
</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" />
|
||||
<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>
|
||||
In-chat @ Depth <input id="memory_depth" class="text_pole widthUnset" type="number" min="0" max="999" />
|
||||
as
|
||||
<select id="memory_role" class="text_pole widthNatural">
|
||||
<option value="0" data-i18n="System">System</option>
|
||||
<option value="1" data-i18n="User">User</option>
|
||||
<option value="2" data-i18n="Assistant">Assistant</option>
|
||||
<option value="0">System</option>
|
||||
<option value="1">User</option>
|
||||
<option value="2">Assistant</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
|
@@ -30,10 +30,7 @@
|
||||
<span>Ctrl+Enter to execute</span>
|
||||
</label>
|
||||
</div>
|
||||
<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>
|
||||
<textarea class="monospace" id="qr--modal-message"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -97,27 +94,14 @@
|
||||
|
||||
|
||||
<h3>Testing</h3>
|
||||
<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 id="qr--modal-execute" class="menu_button" title="Execute the quick reply now">
|
||||
<i class="fa-solid fa-play"></i>
|
||||
Execute
|
||||
</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>
|
||||
|
@@ -183,16 +183,14 @@ const init = async () => {
|
||||
;
|
||||
if (!qr) {
|
||||
let [setName, ...qrName] = name.split('.');
|
||||
qrName = qrName.join('.');
|
||||
name = qrName.join('.');
|
||||
let qrs = QuickReplySet.get(setName);
|
||||
if (qrs) {
|
||||
qr = qrs.qrList.find(it=>it.label == qrName);
|
||||
qr = qrs.qrList.find(it=>it.label == name);
|
||||
}
|
||||
}
|
||||
if (qr && qr.onExecute) {
|
||||
return await qr.execute(args, false, true);
|
||||
} else {
|
||||
throw new Error(`No Quick Reply found for "${name}".`);
|
||||
return await qr.execute(args);
|
||||
}
|
||||
};
|
||||
|
||||
|
@@ -1,9 +1,5 @@
|
||||
import { POPUP_TYPE, Popup } from '../../../popup.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 { getSortableDelay } from '../../../utils.js';
|
||||
import { log, warn } from '../index.js';
|
||||
import { QuickReplyContextLink } from './QuickReplyContextLink.js';
|
||||
import { QuickReplySet } from './QuickReplySet.js';
|
||||
@@ -51,14 +47,9 @@ 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() {
|
||||
@@ -234,43 +225,15 @@ 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)));
|
||||
@@ -284,15 +247,14 @@ 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', async(evt) => {
|
||||
message.addEventListener('keydown', (evt) => {
|
||||
if (evt.key == 'Tab' && !evt.shiftKey && !evt.ctrlKey && !evt.altKey) {
|
||||
evt.preventDefault();
|
||||
const start = message.selectionStart;
|
||||
@@ -303,12 +265,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;
|
||||
updateSyntax();
|
||||
this.updateMessage(message.value);
|
||||
} else {
|
||||
message.value = `${message.value.substring(0, start)}\t${message.value.substring(end)}`;
|
||||
message.selectionStart = start + 1;
|
||||
message.selectionEnd = end + 1;
|
||||
updateSyntax();
|
||||
this.updateMessage(message.value);
|
||||
}
|
||||
} else if (evt.key == 'Tab' && evt.shiftKey && !evt.ctrlKey && !evt.altKey) {
|
||||
evt.preventDefault();
|
||||
@@ -319,47 +281,15 @@ 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;
|
||||
updateSyntax();
|
||||
this.updateMessage(message.value);
|
||||
} else if (evt.key == 'Enter' && evt.ctrlKey && !evt.shiftKey && !evt.altKey) {
|
||||
evt.stopPropagation();
|
||||
evt.preventDefault();
|
||||
if (executeShortcut.checked) {
|
||||
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;
|
||||
}
|
||||
this.executeFromEditor();
|
||||
}
|
||||
}
|
||||
});
|
||||
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}*/
|
||||
@@ -484,15 +414,9 @@ 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;
|
||||
@@ -502,30 +426,8 @@ 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');
|
||||
}
|
||||
@@ -534,54 +436,21 @@ 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({}, 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');
|
||||
this.editorExecutePromise = this.execute();
|
||||
await this.editorExecutePromise;
|
||||
} catch (ex) {
|
||||
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.editorExecuteErrors.textContent = ex.message;
|
||||
}
|
||||
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}`);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -657,22 +526,12 @@ export class QuickReply {
|
||||
}
|
||||
|
||||
|
||||
async execute(args = {}, isEditor = false, isRun = false) {
|
||||
async execute(args = {}) {
|
||||
if (this.message?.length > 0 && this.onExecute) {
|
||||
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,
|
||||
const message = this.message.replace(/\{\{arg::([^}]+)\}\}/g, (_, key) => {
|
||||
return args[key] ?? '';
|
||||
});
|
||||
return await this.onExecute(this, message, args.isAutoExecute ?? false);
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -1,6 +1,5 @@
|
||||
import { getRequestHeaders, substituteParams } from '../../../../script.js';
|
||||
import { executeSlashCommands, executeSlashCommandsOnChatInput, executeSlashCommandsWithOptions } from '../../../slash-commands.js';
|
||||
import { SlashCommandScope } from '../../../slash-commands/SlashCommandScope.js';
|
||||
import { executeSlashCommands } from '../../../slash-commands.js';
|
||||
import { debounceAsync, warn } from '../index.js';
|
||||
import { QuickReply } from './QuickReply.js';
|
||||
|
||||
@@ -101,29 +100,15 @@ export class QuickReplySet {
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
* @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
|
||||
* @param {QuickReply} qr
|
||||
* @param {String} [message] - optional altered message to be used
|
||||
*/
|
||||
async executeWithOptions(qr, options = {}) {
|
||||
options = Object.assign({
|
||||
message:null,
|
||||
isAutoExecute:false,
|
||||
isEditor:false,
|
||||
isRun:false,
|
||||
scope:null,
|
||||
}, options);
|
||||
async execute(qr, message = null, isAutoExecute = false) {
|
||||
/**@type {HTMLTextAreaElement}*/
|
||||
const ta = document.querySelector('#send_textarea');
|
||||
const finalMessage = options.message ?? qr.message;
|
||||
const finalMessage = message ?? qr.message;
|
||||
let input = ta.value;
|
||||
if (!options.isAutoExecute && !options.isEditor && !options.isRun && this.injectInput && input.length > 0) {
|
||||
if (!isAutoExecute && this.injectInput && input.length > 0) {
|
||||
if (this.placeBeforeInput) {
|
||||
input = `${finalMessage} ${input}`;
|
||||
} else {
|
||||
@@ -134,24 +119,7 @@ export class QuickReplySet {
|
||||
}
|
||||
|
||||
if (input[0] == '/' && !this.disableSend) {
|
||||
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,
|
||||
});
|
||||
}
|
||||
const result = await executeSlashCommands(input);
|
||||
return typeof result === 'object' ? result?.pipe : '';
|
||||
}
|
||||
|
||||
@@ -163,18 +131,6 @@ 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,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -196,7 +152,7 @@ export class QuickReplySet {
|
||||
}
|
||||
|
||||
hookQuickReply(qr) {
|
||||
qr.onExecute = (_, options)=>this.executeWithOptions(qr, options);
|
||||
qr.onExecute = (_, message, isAutoExecute)=>this.execute(qr, message, isAutoExecute);
|
||||
qr.onDelete = ()=>this.removeQuickReply(qr);
|
||||
qr.onUpdate = ()=>this.save();
|
||||
}
|
||||
|
@@ -1,6 +1,4 @@
|
||||
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 { registerSlashCommand } from '../../../slash-commands.js';
|
||||
import { isTrueBoolean } from '../../../utils.js';
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
import { QuickReplyApi } from '../api/QuickReplyApi.js';
|
||||
@@ -19,331 +17,46 @@ export class SlashCommandHandler {
|
||||
|
||||
|
||||
init() {
|
||||
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',
|
||||
}));
|
||||
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-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>
|
||||
`,
|
||||
}));
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
|
@@ -357,7 +357,6 @@ export class SettingsUi {
|
||||
a.download = `${this.currentQrSet.name}.json`;
|
||||
a.click();
|
||||
}
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
selectQrSet(qrs) {
|
||||
|
@@ -209,10 +209,6 @@
|
||||
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;
|
||||
@@ -222,17 +218,12 @@
|
||||
@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-messageHolder {
|
||||
min-height: 50svh;
|
||||
height: 50svh;
|
||||
body .dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor > #qr--main > .qr--modal-messageContainer > #qr--modal-message {
|
||||
min-height: 90svh;
|
||||
}
|
||||
}
|
||||
.dialogue_popup:has(#qr--modalEditor) {
|
||||
@@ -247,13 +238,11 @@
|
||||
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;
|
||||
@@ -279,7 +268,6 @@
|
||||
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;
|
||||
@@ -295,167 +283,17 @@
|
||||
.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-messageHolder {
|
||||
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor > #qr--main > .qr--modal-messageContainer > #qr--modal-message {
|
||||
flex: 1 1 auto;
|
||||
display: grid;
|
||||
text-align: left;
|
||||
overflow: hidden;
|
||||
}
|
||||
.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;
|
||||
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor #qr--modal-execute {
|
||||
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-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 {
|
||||
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor #qr--modal-execute.qr--busy {
|
||||
opacity: 0.5;
|
||||
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;
|
||||
|
@@ -229,8 +229,6 @@
|
||||
|
||||
|
||||
#qr--qrOptions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
> #qr--ctxEditor {
|
||||
.qr--ctxItem {
|
||||
display: flex;
|
||||
@@ -246,16 +244,11 @@
|
||||
@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-messageHolder {
|
||||
min-height: 50svh;
|
||||
height: 50svh;
|
||||
> #qr--main > .qr--modal-messageContainer > #qr--modal-message {
|
||||
min-height: 90svh;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -271,13 +264,11 @@
|
||||
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;
|
||||
@@ -302,7 +293,6 @@
|
||||
flex: 1 1 auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
> .qr--modal-editorSettings {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
@@ -317,169 +307,24 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
> #qr--modal-messageHolder {
|
||||
> #qr--modal-message {
|
||||
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-executeButtons {
|
||||
#qr--modal-execute {
|
||||
display: flex;
|
||||
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;
|
||||
flex-direction: row;
|
||||
gap: 0.5em;
|
||||
&.qr--busy {
|
||||
opacity: 0.5;
|
||||
filter: grayscale(1);
|
||||
pointer-events: none;
|
||||
cursor: wait;
|
||||
}
|
||||
.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;
|
||||
|
@@ -1,52 +1,24 @@
|
||||
<div class="regex_settings">
|
||||
<div class="inline-drawer">
|
||||
<div class="inline-drawer-toggle inline-drawer-header">
|
||||
<b data-i18n="ext_regex_title">
|
||||
Regex
|
||||
</b>
|
||||
<b>Regex</b>
|
||||
<div class="inline-drawer-icon fa-solid fa-circle-chevron-down down"></div>
|
||||
</div>
|
||||
<div class="inline-drawer-content">
|
||||
<div class="flex-container">
|
||||
<div id="open_regex_editor" class="menu_button menu_button_icon" title="New global regex script">
|
||||
<div id="open_regex_editor" class="menu_button">
|
||||
<i class="fa-solid fa-pen-to-square"></i>
|
||||
<small data-i18n="ext_regex_new_global_script">+ Global</small>
|
||||
<span>Open Editor</span>
|
||||
</div>
|
||||
<div id="open_scoped_editor" class="menu_button menu_button_icon" title="New scoped regex script">
|
||||
<i class="fa-solid fa-address-card"></i>
|
||||
<small data-i18n="ext_regex_new_scoped_script">+ Scoped</small>
|
||||
</div>
|
||||
<div id="import_regex" class="menu_button menu_button_icon">
|
||||
<div id="import_regex" class="menu_button">
|
||||
<i class="fa-solid fa-file-import"></i>
|
||||
<small data-i18n="ext_regex_import_script">Import</small>
|
||||
<span>Import Script</span>
|
||||
</div>
|
||||
<input type="file" id="import_regex_file" hidden accept="*.json" multiple />
|
||||
<input type="file" id="import_regex_file" hidden accept="*.json" />
|
||||
</div>
|
||||
<hr />
|
||||
<div id="global_scripts_block" class="padding5">
|
||||
<div>
|
||||
<strong data-i18n="ext_regex_global_scripts">Global Scripts</strong>
|
||||
</div>
|
||||
<small data-i18n="ext_regex_global_scripts_desc">
|
||||
Available for all characters. Saved to local settings.
|
||||
</small>
|
||||
<div id="saved_regex_scripts" class="flex-container regex-script-container flexFlowColumn"></div>
|
||||
</div>
|
||||
<hr />
|
||||
<div id="scoped_scripts_block" class="padding5">
|
||||
<div class="flex-container alignItemsBaseline">
|
||||
<strong class="flex1" data-i18n="ext_regex_scoped_scripts">Scoped Scripts</strong>
|
||||
<label id="toggle_scoped_regex" class="checkbox flex-container" for="regex_scoped_toggle">
|
||||
<input type="checkbox" id="regex_scoped_toggle" class="enable_scoped" />
|
||||
<span class="regex-toggle-on fa-solid fa-toggle-on fa-lg" title="Disallow using scoped regex"></span>
|
||||
<span class="regex-toggle-off fa-solid fa-toggle-off fa-lg" title="Allow using scoped regex"></span>
|
||||
</label>
|
||||
</div>
|
||||
<small data-i18n="ext_regex_scoped_scripts_desc">
|
||||
Only available for this character. Saved to the card data.
|
||||
</small>
|
||||
<div id="saved_scoped_scripts" class="flex-container regex-script-container flexFlowColumn"></div>
|
||||
</div>
|
||||
<label>Saved Scripts</label>
|
||||
<div id="saved_regex_scripts" class="flex-container regex-script-container flexFlowColumn"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -11,7 +11,7 @@
|
||||
</div>
|
||||
</h3>
|
||||
|
||||
<small class="flex-container extensions_info" data-i18n="ext_regex_desc">
|
||||
<small class="flex-container extensions_info">
|
||||
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" data-i18n="[placeholder]ext_regex_test_input_placeholder" placeholder="Type here..."></textarea>
|
||||
<textarea id="regex_test_input" class="text_pole textarea_compact" rows="4" 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" data-i18n="[placeholder]ext_regex_output_placeholder" placeholder="Empty" readonly></textarea>
|
||||
<textarea id="regex_test_output" class="text_pole textarea_compact" rows="4" placeholder="Empty" readonly></textarea>
|
||||
</div>
|
||||
<hr>
|
||||
</div>
|
||||
@@ -56,7 +56,6 @@
|
||||
<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>
|
||||
@@ -68,7 +67,7 @@
|
||||
</label>
|
||||
<div>
|
||||
<textarea
|
||||
class="regex_trim_strings text_pole wide100p textarea_compact" data-i18n="[placeholder]ext_regex_trim_placeholder"
|
||||
class="regex_trim_strings text_pole wide100p textarea_compact"
|
||||
placeholder="Globally trims any unwanted parts from a regex match before replacement. Separate each element by an enter."
|
||||
rows="3"
|
||||
></textarea>
|
||||
@@ -78,59 +77,53 @@
|
||||
|
||||
<div class="flex-container">
|
||||
<div class="flex1 wi-enter-footer-text flex-container flexFlowColumn flexNoGap alignitemsstart">
|
||||
<small data-i18n="ext_regex_affects">Affects</small>
|
||||
<div title="Messages sent by the user.">
|
||||
<small>Affects</small>
|
||||
<div>
|
||||
<label class="checkbox flex-container">
|
||||
<input type="checkbox" name="replace_position" value="1">
|
||||
<span data-i18n="ext_regex_user_input">User Input</span>
|
||||
<span data-i18n="Before Char">User Input</span>
|
||||
</label>
|
||||
</div>
|
||||
<div title="Messages received from the Generation API.">
|
||||
<div>
|
||||
<label class="checkbox flex-container">
|
||||
<input type="checkbox" name="replace_position" value="2">
|
||||
<span data-i18n="ext_regex_ai_output">AI Output</span>
|
||||
<span data-i18n="After Char">AI Output</span>
|
||||
</label>
|
||||
</div>
|
||||
<div title="Messages sent using STscript commands.">
|
||||
<div>
|
||||
<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 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.">
|
||||
<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.">
|
||||
<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" data-i18n="[placeholder]ext_regex_min_depth_placeholder" placeholder="Unlimited" />
|
||||
<input name="min_depth" class="text_pole textarea_compact" type="number" min="0" max="999" placeholder="Unlimited" />
|
||||
</div>
|
||||
<div class="flex1 flex-container flexNoGap">
|
||||
<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.">
|
||||
<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.">
|
||||
<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" data-i18n="[placeholder]ext_regex_min_depth_placeholder" placeholder="Unlimited" />
|
||||
<input name="max_depth" class="text_pole textarea_compact" type="number" min="0" max="999" placeholder="Unlimited" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex1 wi-enter-footer-text flex-container flexFlowColumn flexNoGap alignitemsstart">
|
||||
<small data-i18n="ext_regex_other_options">Other Options</small>
|
||||
<small>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" title="Chat history won't change, only the message rendered in the UI.">
|
||||
<label class="checkbox flex-container">
|
||||
<input type="checkbox" name="only_format_display" />
|
||||
<span data-i18n="Only Format Display">Only Format Display</span>
|
||||
</label>
|
||||
<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).">
|
||||
<label class="checkbox flex-container" title="Chat history won't change, only the prompt as the request is sent (on generation)">
|
||||
<input type="checkbox" name="only_format_prompt"/>
|
||||
<span>
|
||||
<span data-i18n="Only Format Prompt (?)">Only Format Prompt</span>
|
||||
@@ -141,7 +134,7 @@
|
||||
<input type="checkbox" name="run_on_edit" />
|
||||
<span data-i18n="Run On Edit">Run On Edit</span>
|
||||
</label>
|
||||
<label class="checkbox flex-container" data-i18n="[title]ext_regex_substitute_regex_desc" title="Substitute {{macros}} in Find Regex before running it">
|
||||
<label class="checkbox flex-container" title="Substitute {{macros}} in Find Regex before running it">
|
||||
<input type="checkbox" name="substitute_regex" />
|
||||
<span>
|
||||
<span data-i18n="Substitute Regex">Substitute Regex</span>
|
||||
|
@@ -1,5 +0,0 @@
|
||||
<div>
|
||||
<h3>This character has embedded regex script(s).</h3>
|
||||
<h3>Would you like to allow using them?</h3>
|
||||
<div class="m-b-1">If you want to do it later, select "Regex" from the extensions menu.</div>
|
||||
</div>
|
@@ -1,6 +1,5 @@
|
||||
import { characters, substituteParams, this_chid } from '../../../script.js';
|
||||
import { substituteParams } from '../../../script.js';
|
||||
import { extension_settings } from '../../extensions.js';
|
||||
import { regexFromString } from '../../utils.js';
|
||||
export {
|
||||
regex_placement,
|
||||
getRegexedString,
|
||||
@@ -18,24 +17,29 @@ const regex_placement = {
|
||||
USER_INPUT: 1,
|
||||
AI_OUTPUT: 2,
|
||||
SLASH_COMMAND: 3,
|
||||
// 4 - sendAs (legacy)
|
||||
WORLD_INFO: 5,
|
||||
};
|
||||
|
||||
function getScopedRegex() {
|
||||
const isAllowed = extension_settings?.character_allowed_regex?.includes(characters?.[this_chid]?.avatar);
|
||||
/**
|
||||
* 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);
|
||||
|
||||
if (!isAllowed) {
|
||||
return [];
|
||||
// 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;
|
||||
}
|
||||
|
||||
const scripts = characters[this_chid]?.data?.extensions?.regex_scripts;
|
||||
|
||||
if (!Array.isArray(scripts)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return scripts;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -58,8 +62,7 @@ function getRegexedString(rawString, placement, { characterOverride, isMarkdown,
|
||||
return finalString;
|
||||
}
|
||||
|
||||
const allRegex = [...(extension_settings.regex ?? []), ...(getScopedRegex() ?? [])];
|
||||
allRegex.forEach((script) => {
|
||||
extension_settings.regex.forEach((script) => {
|
||||
if (
|
||||
// Script applies to Markdown and input is Markdown
|
||||
(script.markdownOnly && isMarkdown) ||
|
||||
@@ -112,7 +115,7 @@ function runRegexScript(regexScript, rawString, { characterOverride } = {}) {
|
||||
}
|
||||
|
||||
// Run replacement. Currently does not support the Overlay strategy
|
||||
newString = rawString.replace(findRegex, function (match) {
|
||||
newString = rawString.replace(findRegex, function(match) {
|
||||
const args = [...arguments];
|
||||
const replaceString = regexScript.replaceString.replace(/{{match}}/gi, '$0');
|
||||
const replaceWithGroups = replaceString.replaceAll(/\$(\d+)/g, (_, num) => {
|
||||
|
@@ -1,19 +0,0 @@
|
||||
<div>
|
||||
<h3 data-i18n="ext_regex_import_target">
|
||||
Import To:
|
||||
</h3>
|
||||
<div class="flex-container flexFlowColumn wide100p padding10 justifyLeft">
|
||||
<label for="regex_import_target_global">
|
||||
<input type="radio" name="regex_import_target" id="regex_import_target_global" value="global" checked />
|
||||
<span data-i18n="ext_regex_global_scripts">
|
||||
Global Scripts
|
||||
</span>
|
||||
</label>
|
||||
<label for="regex_import_target_scoped">
|
||||
<input type="radio" name="regex_import_target" id="regex_import_target_scoped" value="scoped" />
|
||||
<span data-i18n="ext_regex_scoped_scripts">
|
||||
Scoped Scripts
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
@@ -1,28 +1,12 @@
|
||||
import { callPopup, characters, eventSource, event_types, getCurrentChatId, reloadCurrentChat, saveSettingsDebounced, this_chid } from '../../../script.js';
|
||||
import { extension_settings, renderExtensionTemplateAsync, writeExtensionField } from '../../extensions.js';
|
||||
import { selected_group } from '../../group-chats.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 { callPopup, getCurrentChatId, reloadCurrentChat, saveSettingsDebounced } from '../../../script.js';
|
||||
import { extension_settings, renderExtensionTemplateAsync } from '../../extensions.js';
|
||||
import { registerSlashCommand } from '../../slash-commands.js';
|
||||
import { download, getFileText, getSortableDelay, uuidv4 } from '../../utils.js';
|
||||
import { resolveVariable } from '../../variables.js';
|
||||
import { regex_placement, runRegexScript } from './engine.js';
|
||||
|
||||
/**
|
||||
* Saves a regex script to the extension settings or character data.
|
||||
* @param {import('../../char-data.js').RegexScriptData} regexScript
|
||||
* @param {number} existingScriptIndex Index of the existing script
|
||||
* @param {boolean} isScoped Is the script scoped to a character?
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async function saveRegexScript(regexScript, existingScriptIndex, isScoped) {
|
||||
async function saveRegexScript(regexScript, existingScriptIndex) {
|
||||
// If not editing
|
||||
const array = (isScoped ? characters[this_chid]?.data?.extensions?.regex_scripts : extension_settings.regex) ?? [];
|
||||
|
||||
// Assign a UUID if it doesn't exist
|
||||
if (!regexScript.id) {
|
||||
regexScript.id = uuidv4();
|
||||
}
|
||||
|
||||
// Is the script name undefined or empty?
|
||||
if (!regexScript.scriptName) {
|
||||
@@ -30,6 +14,22 @@ async function saveRegexScript(regexScript, existingScriptIndex, isScoped) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (existingScriptIndex === -1) {
|
||||
// Does the script name already exist?
|
||||
if (extension_settings.regex.find((e) => e.scriptName === regexScript.scriptName)) {
|
||||
toastr.error(`Could not save regex script: A script with name ${regexScript.scriptName} already exists.`);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
// Does the script name already exist somewhere else?
|
||||
// (If this fails, make it a .filter().map() to index array)
|
||||
const foundIndex = extension_settings.regex.findIndex((e) => e.scriptName === regexScript.scriptName);
|
||||
if (foundIndex !== existingScriptIndex && foundIndex !== -1) {
|
||||
toastr.error(`Could not save regex script: A script with name ${regexScript.scriptName} already exists.`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Is a find regex present?
|
||||
if (regexScript.findRegex.length === 0) {
|
||||
toastr.warning('This regex script will not work, but was saved anyway: A find regex isn\'t present.');
|
||||
@@ -41,18 +41,9 @@ async function saveRegexScript(regexScript, existingScriptIndex, isScoped) {
|
||||
}
|
||||
|
||||
if (existingScriptIndex !== -1) {
|
||||
array[existingScriptIndex] = regexScript;
|
||||
extension_settings.regex[existingScriptIndex] = regexScript;
|
||||
} else {
|
||||
array.push(regexScript);
|
||||
}
|
||||
|
||||
if (isScoped) {
|
||||
await writeExtensionField(this_chid, 'regex_scripts', array);
|
||||
|
||||
// Add the character to the allowed list
|
||||
if (!extension_settings.character_allowed_regex.includes(characters[this_chid].avatar)) {
|
||||
extension_settings.character_allowed_regex.push(characters[this_chid].avatar);
|
||||
}
|
||||
extension_settings.regex.push(regexScript);
|
||||
}
|
||||
|
||||
saveSettingsDebounced();
|
||||
@@ -65,16 +56,12 @@ async function saveRegexScript(regexScript, existingScriptIndex, isScoped) {
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteRegexScript({ id, isScoped }) {
|
||||
const array = (isScoped ? characters[this_chid]?.data?.extensions?.regex_scripts : extension_settings.regex) ?? [];
|
||||
async function deleteRegexScript({ existingId }) {
|
||||
let scriptName = $(`#${existingId}`).find('.regex_script_name').text();
|
||||
|
||||
const existingScriptIndex = array.findIndex((script) => script.id === id);
|
||||
const existingScriptIndex = extension_settings.regex.findIndex((script) => script.scriptName === scriptName);
|
||||
if (!existingScriptIndex || existingScriptIndex !== -1) {
|
||||
array.splice(existingScriptIndex, 1);
|
||||
|
||||
if (isScoped) {
|
||||
await writeExtensionField(this_chid, 'regex_scripts', array);
|
||||
}
|
||||
extension_settings.regex.splice(existingScriptIndex, 1);
|
||||
|
||||
saveSettingsDebounced();
|
||||
await loadRegexScripts();
|
||||
@@ -83,32 +70,19 @@ async function deleteRegexScript({ id, isScoped }) {
|
||||
|
||||
async function loadRegexScripts() {
|
||||
$('#saved_regex_scripts').empty();
|
||||
$('#saved_scoped_scripts').empty();
|
||||
|
||||
const scriptTemplate = $(await renderExtensionTemplateAsync('regex', 'scriptTemplate'));
|
||||
|
||||
/**
|
||||
* Renders a script to the UI.
|
||||
* @param {string} container Container to render the script to
|
||||
* @param {import('../../char-data.js').RegexScriptData} script Script data
|
||||
* @param {boolean} isScoped Script is scoped to a character
|
||||
* @param {number} index Index of the script in the array
|
||||
*/
|
||||
function renderScript(container, script, isScoped, index) {
|
||||
extension_settings.regex.forEach((script) => {
|
||||
// Have to clone here
|
||||
const scriptHtml = scriptTemplate.clone();
|
||||
const save = () => saveRegexScript(script, index, isScoped);
|
||||
|
||||
if (!script.id) {
|
||||
script.id = uuidv4();
|
||||
}
|
||||
|
||||
scriptHtml.attr('id', script.id);
|
||||
scriptHtml.attr('id', uuidv4());
|
||||
scriptHtml.find('.regex_script_name').text(script.scriptName);
|
||||
scriptHtml.find('.disable_regex').prop('checked', script.disabled ?? false)
|
||||
.on('input', async function () {
|
||||
.on('input', function () {
|
||||
script.disabled = !!$(this).prop('checked');
|
||||
await save();
|
||||
reloadCurrentChat();
|
||||
saveSettingsDebounced();
|
||||
});
|
||||
scriptHtml.find('.regex-toggle-on').on('click', function () {
|
||||
scriptHtml.find('.disable_regex').prop('checked', true).trigger('input');
|
||||
@@ -117,37 +91,7 @@ async function loadRegexScripts() {
|
||||
scriptHtml.find('.disable_regex').prop('checked', false).trigger('input');
|
||||
});
|
||||
scriptHtml.find('.edit_existing_regex').on('click', async function () {
|
||||
await onRegexEditorOpenClick(scriptHtml.attr('id'), isScoped);
|
||||
});
|
||||
scriptHtml.find('.move_to_global').on('click', async function () {
|
||||
const confirm = await callPopup('Are you sure you want to move this regex script to global?', 'confirm');
|
||||
|
||||
if (!confirm) {
|
||||
return;
|
||||
}
|
||||
|
||||
await deleteRegexScript({ id: script.id, isScoped: true });
|
||||
await saveRegexScript(script, -1, false);
|
||||
});
|
||||
scriptHtml.find('.move_to_scoped').on('click', async function () {
|
||||
if (this_chid === undefined) {
|
||||
toastr.error('No character selected.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (selected_group) {
|
||||
toastr.error('Cannot edit scoped scripts in group chats.');
|
||||
return;
|
||||
}
|
||||
|
||||
const confirm = await callPopup('Are you sure you want to move this regex script to scoped?', 'confirm');
|
||||
|
||||
if (!confirm) {
|
||||
return;
|
||||
}
|
||||
|
||||
await deleteRegexScript({ id: script.id, isScoped: false });
|
||||
await saveRegexScript(script, -1, true);
|
||||
await onRegexEditorOpenClick(scriptHtml.attr('id'));
|
||||
});
|
||||
scriptHtml.find('.export_regex').on('click', async function () {
|
||||
const fileName = `${script.scriptName.replace(/[\s.<>:"/\\|?*\x00-\x1F\x7F]/g, '_').toLowerCase()}.json`;
|
||||
@@ -161,36 +105,23 @@ async function loadRegexScripts() {
|
||||
return;
|
||||
}
|
||||
|
||||
await deleteRegexScript({ id: script.id, isScoped });
|
||||
await reloadCurrentChat();
|
||||
await deleteRegexScript({ existingId: scriptHtml.attr('id') });
|
||||
});
|
||||
|
||||
$(container).append(scriptHtml);
|
||||
}
|
||||
|
||||
extension_settings?.regex?.forEach((script, index, array) => renderScript('#saved_regex_scripts', script, false, index, array));
|
||||
characters[this_chid]?.data?.extensions?.regex_scripts?.forEach((script, index, array) => renderScript('#saved_scoped_scripts', script, true, index, array));
|
||||
|
||||
const isAllowed = extension_settings?.character_allowed_regex?.includes(characters?.[this_chid]?.avatar);
|
||||
$('#regex_scoped_toggle').prop('checked', isAllowed);
|
||||
$('#saved_regex_scripts').append(scriptHtml);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the regex editor.
|
||||
* @param {string|boolean} existingId Existing ID
|
||||
* @param {boolean} isScoped Is the script scoped to a character?
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async function onRegexEditorOpenClick(existingId, isScoped) {
|
||||
async function onRegexEditorOpenClick(existingId) {
|
||||
const editorHtml = $(await renderExtensionTemplateAsync('regex', 'editor'));
|
||||
const array = (isScoped ? characters[this_chid]?.data?.extensions?.regex_scripts : extension_settings.regex) ?? [];
|
||||
|
||||
// If an ID exists, fill in all the values
|
||||
let existingScriptIndex = -1;
|
||||
if (existingId) {
|
||||
existingScriptIndex = array.findIndex((script) => script.id === existingId);
|
||||
const existingScriptName = $(`#${existingId}`).find('.regex_script_name').text();
|
||||
existingScriptIndex = extension_settings.regex.findIndex((script) => script.scriptName === existingScriptName);
|
||||
if (existingScriptIndex !== -1) {
|
||||
const existingScript = array[existingScriptIndex];
|
||||
const existingScript = extension_settings.regex[existingScriptIndex];
|
||||
if (existingScript.scriptName) {
|
||||
editorHtml.find('.regex_script_name').val(existingScript.scriptName);
|
||||
} else {
|
||||
@@ -240,7 +171,6 @@ async function onRegexEditorOpenClick(existingId, isScoped) {
|
||||
}
|
||||
|
||||
const testScript = {
|
||||
id: uuidv4(),
|
||||
scriptName: editorHtml.find('.regex_script_name').val(),
|
||||
findRegex: editorHtml.find('.find_regex').val(),
|
||||
replaceString: editorHtml.find('.regex_replace_string').val(),
|
||||
@@ -257,10 +187,9 @@ async function onRegexEditorOpenClick(existingId, isScoped) {
|
||||
const popupResult = await callPopup(editorHtml, 'confirm', undefined, { okButton: 'Save' });
|
||||
if (popupResult) {
|
||||
const newRegexScript = {
|
||||
id: existingId ? String(existingId) : uuidv4(),
|
||||
scriptName: String(editorHtml.find('.regex_script_name').val()),
|
||||
findRegex: String(editorHtml.find('.find_regex').val()),
|
||||
replaceString: String(editorHtml.find('.regex_replace_string').val()),
|
||||
scriptName: editorHtml.find('.regex_script_name').val(),
|
||||
findRegex: editorHtml.find('.find_regex').val(),
|
||||
replaceString: editorHtml.find('.regex_replace_string').val(),
|
||||
trimStrings: editorHtml.find('.regex_trim_strings').val().split('\n').filter((e) => e.length !== 0) || [],
|
||||
placement:
|
||||
editorHtml
|
||||
@@ -278,7 +207,7 @@ async function onRegexEditorOpenClick(existingId, isScoped) {
|
||||
maxDepth: parseInt(String(editorHtml.find('input[name="max_depth"]').val())),
|
||||
};
|
||||
|
||||
saveRegexScript(newRegexScript, existingScriptIndex, isScoped);
|
||||
saveRegexScript(newRegexScript, existingScriptIndex);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -289,11 +218,6 @@ function migrateSettings() {
|
||||
|
||||
// Current: If MD Display is present in placement, remove it and add new placements/MD option
|
||||
extension_settings.regex.forEach((script) => {
|
||||
if (!script.id) {
|
||||
script.id = uuidv4();
|
||||
performSave = true;
|
||||
}
|
||||
|
||||
if (script.placement.includes(regex_placement.MD_DISPLAY)) {
|
||||
script.placement = script.placement.length === 1 ?
|
||||
Object.values(regex_placement).filter((e) => e !== regex_placement.MD_DISPLAY) :
|
||||
@@ -316,11 +240,6 @@ function migrateSettings() {
|
||||
}
|
||||
});
|
||||
|
||||
if (!extension_settings.character_allowed_regex) {
|
||||
extension_settings.character_allowed_regex = [];
|
||||
performSave = true;
|
||||
}
|
||||
|
||||
if (performSave) {
|
||||
saveSettingsDebounced();
|
||||
}
|
||||
@@ -339,9 +258,8 @@ function runRegexCallback(args, value) {
|
||||
}
|
||||
|
||||
const scriptName = String(resolveVariable(args.name));
|
||||
const scripts = [...(extension_settings.regex ?? []), ...(characters[this_chid]?.data?.extensions?.regex_scripts ?? [])];
|
||||
|
||||
for (const script of scripts) {
|
||||
for (const script of extension_settings.regex) {
|
||||
if (String(script.scriptName).toLowerCase() === String(scriptName).toLowerCase()) {
|
||||
if (script.disabled) {
|
||||
toastr.warning(`Regex script "${scriptName}" is disabled.`);
|
||||
@@ -360,9 +278,8 @@ function runRegexCallback(args, value) {
|
||||
/**
|
||||
* Performs the import of the regex file.
|
||||
* @param {File} file Input file
|
||||
* @param {boolean} isScoped Is the script scoped to a character?
|
||||
*/
|
||||
async function onRegexImportFileChange(file, isScoped) {
|
||||
async function onRegexImportFileChange(file) {
|
||||
if (!file) {
|
||||
toastr.error('No file provided.');
|
||||
return;
|
||||
@@ -375,15 +292,7 @@ async function onRegexImportFileChange(file, isScoped) {
|
||||
throw new Error('No script name provided.');
|
||||
}
|
||||
|
||||
// Assign a new UUID
|
||||
regexScript.id = uuidv4();
|
||||
|
||||
const array = (isScoped ? characters[this_chid]?.data?.extensions?.regex_scripts : extension_settings.regex) ?? [];
|
||||
array.push(regexScript);
|
||||
|
||||
if (isScoped) {
|
||||
await writeExtensionField(this_chid, 'regex_scripts', array);
|
||||
}
|
||||
extension_settings.regex.push(regexScript);
|
||||
|
||||
saveSettingsDebounced();
|
||||
await loadRegexScripts();
|
||||
@@ -395,47 +304,6 @@ async function onRegexImportFileChange(file, isScoped) {
|
||||
}
|
||||
}
|
||||
|
||||
function purgeEmbeddedRegexScripts( { character }){
|
||||
const avatar = character?.avatar;
|
||||
|
||||
if (avatar && extension_settings.character_allowed_regex?.includes(avatar)) {
|
||||
const index = extension_settings.character_allowed_regex.indexOf(avatar);
|
||||
if (index !== -1) {
|
||||
extension_settings.character_allowed_regex.splice(index, 1);
|
||||
saveSettingsDebounced();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function checkEmbeddedRegexScripts() {
|
||||
const chid = this_chid;
|
||||
|
||||
if (chid !== undefined && !selected_group) {
|
||||
const avatar = characters[chid]?.avatar;
|
||||
const scripts = characters[chid]?.data?.extensions?.regex_scripts;
|
||||
|
||||
if (Array.isArray(scripts) && scripts.length > 0) {
|
||||
if (avatar && !extension_settings.character_allowed_regex.includes(avatar)) {
|
||||
const checkKey = `AlertRegex_${characters[chid].avatar}`;
|
||||
|
||||
if (!localStorage.getItem(checkKey)) {
|
||||
localStorage.setItem(checkKey, 'true');
|
||||
const template = await renderExtensionTemplateAsync('regex', 'embeddedScripts', {});
|
||||
const result = await callPopup(template, 'confirm', '', { okButton: 'Yes' });
|
||||
|
||||
if (result) {
|
||||
extension_settings.character_allowed_regex.push(avatar);
|
||||
await reloadCurrentChat();
|
||||
saveSettingsDebounced();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
loadRegexScripts();
|
||||
}
|
||||
|
||||
// Workaround for loading in sequence with other extensions
|
||||
// NOTE: Always puts extension at the top of the list, but this is fine since it's static
|
||||
jQuery(async () => {
|
||||
@@ -451,123 +319,39 @@ jQuery(async () => {
|
||||
const settingsHtml = $(await renderExtensionTemplateAsync('regex', 'dropdown'));
|
||||
$('#extensions_settings2').append(settingsHtml);
|
||||
$('#open_regex_editor').on('click', function () {
|
||||
onRegexEditorOpenClick(false, false);
|
||||
});
|
||||
$('#open_scoped_editor').on('click', function () {
|
||||
if (this_chid === undefined) {
|
||||
toastr.error('No character selected.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (selected_group) {
|
||||
toastr.error('Cannot edit scoped scripts in group chats.');
|
||||
return;
|
||||
}
|
||||
|
||||
onRegexEditorOpenClick(false, true);
|
||||
onRegexEditorOpenClick(false);
|
||||
});
|
||||
$('#import_regex_file').on('change', async function () {
|
||||
let target = 'global';
|
||||
const template = $(await renderExtensionTemplateAsync('regex', 'importTarget'));
|
||||
template.find('#regex_import_target_global').on('input', () => target = 'global');
|
||||
template.find('#regex_import_target_scoped').on('input', () => target = 'scoped');
|
||||
|
||||
await callPopup(template, 'text');
|
||||
|
||||
const inputElement = this instanceof HTMLInputElement && this;
|
||||
for (const file of inputElement.files) {
|
||||
await onRegexImportFileChange(file, target === 'scoped');
|
||||
}
|
||||
await onRegexImportFileChange(inputElement.files[0]);
|
||||
inputElement.value = '';
|
||||
});
|
||||
$('#import_regex').on('click', function () {
|
||||
$('#import_regex_file').trigger('click');
|
||||
});
|
||||
|
||||
let sortableDatas = [
|
||||
{
|
||||
selector: '#saved_regex_scripts',
|
||||
setter: x => extension_settings.regex = x,
|
||||
getter: () => extension_settings.regex ?? [],
|
||||
$('#saved_regex_scripts').sortable({
|
||||
delay: getSortableDelay(),
|
||||
stop: function () {
|
||||
let newScripts = [];
|
||||
$('#saved_regex_scripts').children().each(function () {
|
||||
const scriptName = $(this).find('.regex_script_name').text();
|
||||
const existingScript = extension_settings.regex.find((e) => e.scriptName === scriptName);
|
||||
if (existingScript) {
|
||||
newScripts.push(existingScript);
|
||||
}
|
||||
});
|
||||
|
||||
extension_settings.regex = newScripts;
|
||||
saveSettingsDebounced();
|
||||
|
||||
console.debug('Regex scripts reordered');
|
||||
// TODO: Maybe reload regex scripts after move
|
||||
},
|
||||
{
|
||||
selector: '#saved_scoped_scripts',
|
||||
setter: x => writeExtensionField(this_chid, 'regex_scripts', x),
|
||||
getter: () => characters[this_chid]?.data?.extensions?.regex_scripts ?? [],
|
||||
},
|
||||
];
|
||||
for (const { selector, setter, getter } of sortableDatas) {
|
||||
$(selector).sortable({
|
||||
delay: getSortableDelay(),
|
||||
stop: async function () {
|
||||
const oldScripts = getter();
|
||||
const newScripts = [];
|
||||
$(selector).children().each(function () {
|
||||
const id = $(this).attr('id');
|
||||
const existingScript = oldScripts.find((e) => e.id === id);
|
||||
if (existingScript) {
|
||||
newScripts.push(existingScript);
|
||||
}
|
||||
});
|
||||
|
||||
await setter(newScripts);
|
||||
saveSettingsDebounced();
|
||||
|
||||
console.debug(`Regex scripts in ${selector} reordered`);
|
||||
await loadRegexScripts();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
$('#regex_scoped_toggle').on('input', function () {
|
||||
if (this_chid === undefined) {
|
||||
toastr.error('No character selected.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (selected_group) {
|
||||
toastr.error('Cannot edit scoped scripts in group chats.');
|
||||
return;
|
||||
}
|
||||
|
||||
const isEnable = !!$(this).prop('checked');
|
||||
const avatar = characters[this_chid].avatar;
|
||||
|
||||
if (isEnable) {
|
||||
if (!extension_settings.character_allowed_regex.includes(avatar)) {
|
||||
extension_settings.character_allowed_regex.push(avatar);
|
||||
}
|
||||
} else {
|
||||
const index = extension_settings.character_allowed_regex.indexOf(avatar);
|
||||
if (index !== -1) {
|
||||
extension_settings.character_allowed_regex.splice(index, 1);
|
||||
}
|
||||
}
|
||||
|
||||
saveSettingsDebounced();
|
||||
reloadCurrentChat();
|
||||
});
|
||||
|
||||
await loadRegexScripts();
|
||||
$('#saved_regex_scripts').sortable('enable');
|
||||
|
||||
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.',
|
||||
}));
|
||||
|
||||
eventSource.on(event_types.CHAT_CHANGED, checkEmbeddedRegexScripts);
|
||||
eventSource.on(event_types.CHARACTER_DELETED, purgeEmbeddedRegexScripts);
|
||||
registerSlashCommand('regex', runRegexCallback, [], '(name=scriptName [input]) – runs a Regex extension script by name on the provided string. The script must be enabled.', true, true);
|
||||
});
|
||||
|
@@ -4,22 +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" 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>
|
||||
<span class="regex-toggle-on fa-solid fa-toggle-on" title="Disable script"></span>
|
||||
<span class="regex-toggle-off fa-solid fa-toggle-off" title="Enable script"></span>
|
||||
</label>
|
||||
<div class="edit_existing_regex menu_button" data-i18n="[title]ext_regex_edit_script" title="Edit script">
|
||||
<div class="edit_existing_regex menu_button" title="Edit script">
|
||||
<i class="fa-solid fa-pencil"></i>
|
||||
</div>
|
||||
<div class="move_to_global menu_button" data-i18n="[title]ext_regex_move_to_global" title="Move to global scripts">
|
||||
<i class="fa-solid fa-arrow-up"></i>
|
||||
</div>
|
||||
<div class="move_to_scoped menu_button" data-i18n="[title]ext_regex_move_to_scoped" title="Move to scoped scripts">
|
||||
<i class="fa-solid fa-arrow-down"></i>
|
||||
</div>
|
||||
<div class="export_regex menu_button" data-i18n="[title]ext_regex_export_script" title="Export script">
|
||||
<div class="export_regex menu_button" title="Export script">
|
||||
<i class="fa-solid fa-file-export"></i>
|
||||
</div>
|
||||
<div class="delete_regex menu_button" data-i18n="[title]ext_regex_delete_script" title="Delete script">
|
||||
<div class="delete_regex menu_button" title="Delete script">
|
||||
<i class="fa-solid fa-trash"></i>
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -14,47 +14,6 @@
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.regex-script-container:empty::after {
|
||||
content: "No scripts found";
|
||||
font-size: 0.95em;
|
||||
opacity: 0.7;
|
||||
display: block;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#scoped_scripts_block {
|
||||
opacity: 1;
|
||||
transition: opacity 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
#scoped_scripts_block .move_to_scoped {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#global_scripts_block .move_to_global {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#scoped_scripts_block:not(:has(#regex_scoped_toggle:checked)) {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.enable_scoped:checked ~ .regex-toggle-on {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.enable_scoped:checked ~ .regex-toggle-off {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.enable_scoped:not(:checked) ~ .regex-toggle-on {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.enable_scoped:not(:checked) ~ .regex-toggle-off {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.regex-script-label {
|
||||
align-items: center;
|
||||
border: 1px solid var(--SmartThemeBorderColor);
|
||||
@@ -64,13 +23,7 @@
|
||||
margin-bottom: 1px;
|
||||
}
|
||||
|
||||
.regex-script-label:has(.disable_regex:checked) .regex_script_name {
|
||||
text-decoration: line-through;
|
||||
filter: grayscale(0.5);
|
||||
}
|
||||
|
||||
input.disable_regex,
|
||||
input.enable_scoped {
|
||||
input.disable_regex {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
@@ -78,12 +31,6 @@ input.enable_scoped {
|
||||
cursor: pointer;
|
||||
opacity: 0.5;
|
||||
filter: grayscale(0.5);
|
||||
transition: opacity 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
.regex-toggle-off:hover {
|
||||
opacity: 1;
|
||||
filter: none;
|
||||
}
|
||||
|
||||
.regex-toggle-on {
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user