Compare commits

..

11 Commits

Author SHA1 Message Date
Wolfsblvt
51a43d1ff0 Add chat stats popup 2024-05-02 05:35:07 +02:00
Wolfsblvt
04a798b229 Switch word counting to Segmenter 2024-05-02 02:02:12 +02:00
Wolfsblvt
0799090a1a Finalize stats tooltips 2024-05-02 01:52:26 +02:00
Wolfsblvt
8c51ea15b2 Merge branch 'staging' into stats-2.0 2024-05-02 00:52:38 +02:00
Wolfsblvt
3adb955a14 Persona stats with avatar, more doc improvements 2024-04-25 03:53:06 +02:00
Wolfsblvt
b9f31d5066 Stats: Implement data retrieval into char popup
- Parse json with Date objects
- Fix char directory
- Add sub info for aggregated stats
- Correctly pull names out of the chat files
- Rework humanized duration, humanized timespan, humanized filesize
- Add smart truncate and sensible round
- Implement/Fix values into the character stat popup
- Implement correct stat API calls on client side
2024-04-23 06:26:57 +02:00
Wolfsblvt
c5dff7b5d4 Refactor in endpoint changes of user-folders from neo-server 2024-04-22 00:44:15 +02:00
Wolfsblvt
67e57ffd58 Merge branch 'staging' into stats-2.0 2024-04-21 21:54:51 +02:00
Wolfsblvt
08f6f8c405 Calculate global stats, prepare stats endpoints 2024-04-20 05:56:26 +02:00
Wolfsblvt
0d4cbf7da6 Merge branch 'staging' into stats-2.0 2024-04-19 21:59:33 +02:00
Wolfsblvt
9cef0d8346 Temp commit
- Fixed "old" popup resizing and scroll bars (now actually respecting the chosen setting)
2024-04-11 20:43:20 +02:00
276 changed files with 16801 additions and 48779 deletions

View File

@@ -8,5 +8,3 @@ Start.bat
cloudflared.exe
access.log
/data
/cache
.DS_Store

View File

@@ -5,7 +5,7 @@ end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
[*.{js, conf, json, css, less, html}]
[*.{js, conf, json}]
charset = utf-8
indent_style = space
indent_size = 4

View File

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

View File

@@ -1,3 +0,0 @@
## Checklist:
- [ ] I have read the [Contributing guidelines](https://github.com/SillyTavern/SillyTavern/blob/release/CONTRIBUTING.md).

102
.github/readme.md vendored
View File

@@ -144,14 +144,12 @@ A full list of included extensions and tutorials on how to use them can be found
8. The server will then start, and SillyTavern will pop up in your browser.
## Installing via SillyTavern Launcher
1. On your keyboard: press **`WINDOWS + R`** to open Run dialog box. Then, run the following command to install git:
```shell
cmd /c winget install -e --id Git.Git
```
2. On your keyboard: press **`WINDOWS + E`** to open File Explorer, then navigate to the folder where you want to install the launcher. Once in the desired folder, type `cmd` into the address bar and press enter. Then, run the following command:
```shell
git clone https://github.com/SillyTavern/SillyTavern-Launcher.git && cd SillyTavern-Launcher && start installer.bat
```
1. Install [Git for Windows](https://gitforwindows.org/)
2. Open Windows Explorer (`Win+E`) and make or choose a folder where you wanna install the launcher to
3. Open a Command Prompt inside that folder by clicking in the 'Address Bar' at the top, typing `cmd`, and pressing Enter.
4. When you see a black box, insert the following command: `git clone https://github.com/SillyTavern/SillyTavern-Launcher.git`
5. Double-click on `installer.bat` and choose what you wanna install
6. After installation double-click on `launcher.bat`
## Installing via GitHub Desktop
(This allows git usage **only** in GitHub Desktop, if you want to use `git` on the command line too, you also need to install [Git for Windows](https://gitforwindows.org/))
@@ -185,79 +183,18 @@ For MacOS / Linux all of these will be done in a Terminal.
### For Linux users
1. Open your favorite terminal and install git
2. Git clone the Sillytavern-Launcher with:
```shell
git clone https://github.com/SillyTavern/SillyTavern-Launcher.git && cd SillyTavern-Launcher
```
3. Start the installer.sh with:
```shell
chmod +x install.sh && ./install.sh
```
4. After installation start the launcher.sh with:
```shell
chmod +x launcher.sh && ./launcher.sh
```
2. Download Sillytavern Launcher with: `git clone https://github.com/SillyTavern/SillyTavern-Launcher.git`
3. Navigate to the SillyTavern-Launcher with: `cd SillyTavern-Launcher`
4. Start the install launcher with: `chmod +x install.sh && ./install.sh` and choose what you wanna install
5. After installation start the launcher with: `chmod +x launcher.sh && ./launcher.sh`
### For Mac users
1. Open a terminal and install brew with:
```shell
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
```
2. Install git with:
```shell
brew install git
```
3. Git clone the Sillytavern-Launcher with:
```shell
git clone https://github.com/SillyTavern/SillyTavern-Launcher.git && cd SillyTavern-Launcher
```
4. Start the installer.sh with:
```shell
chmod +x install.sh && ./install.sh
```
5. After installation start the launcher.sh with:
```shell
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.
1. Open a terminal and install brew with: `/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"`
2. Then install git with: `brew install git`
3. Download Sillytavern Launcher with: `git clone https://github.com/SillyTavern/SillyTavern-Launcher.git`
4. Navigate to the SillyTavern-Launcher with: `cd SillyTavern-Launcher`
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`
## 📱 Mobile - Installing via termux
@@ -268,7 +205,7 @@ You will need two mandatory directory mappings and a port mapping to allow Silly
## API keys management
SillyTavern saves your API keys to a `secrets.json` file in the user data directory (`/data/default-user/secrets.json` is the default path).
SillyTavern saves your API keys to a `secrets.json` file in the server directory.
By default, they will not be exposed to a frontend after you enter them and reload the page.
@@ -283,7 +220,7 @@ Most often this is for people who want to use SillyTavern on their mobile phones
However, it can be used to allow remote connections from anywhere as well.
**IMPORTANT: Refer to the official guide if you want to configure SillyTavern user accounts with (optional) password protection: [Users](https://docs.sillytavern.app/installation/st-1.12.0-migration-guide/#users).**
**IMPORTANT: SillyTavern is a single-user program, so anyone who logs in will be able to see all characters and chats, and be able to change any settings inside the UI.**
### 1. Managing whitelisted IPs
@@ -389,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
@@ -410,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

View File

@@ -30,7 +30,7 @@ jobs:
run: |
echo "IMAGE_NAME=${REPO,,}" >> ${GITHUB_ENV}
# Using the following workaround because currently GitHub Actions
# Using the following workaround because currently GitHub Actions
# does not support logical AND/OR operations on triggers
# It's currently not possible to have `branches` under the `schedule` trigger
- name: Checkout the release branch (on release)
@@ -65,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

View File

@@ -1,32 +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

2
.gitignore vendored
View File

@@ -47,5 +47,3 @@ access.log
public/css/user.css
/plugins/
/data
/default/scaffold
public/scripts/extensions/third-party

View File

@@ -5,6 +5,4 @@ node_modules/
secrets.json
/dist
/backups/
/data
/cache
access.log

View File

@@ -4,8 +4,7 @@
// List of extensions which should be recommended for users of this workspace.
"recommendations": [
"dbaeumer.vscode-eslint",
"EditorConfig.EditorConfig",
"mrcrowl.easy-less"
"EditorConfig.EditorConfig"
],
// List of extensions recommended by VS Code that should not be recommended for users of this workspace.
"unwantedRecommendations": []

View File

@@ -1,32 +0,0 @@
# How to contribute to SillyTavern
## Setting up the dev environment
1. Required software: git and node.
2. Recommended editor: Visual Studio Code.
3. You can also use GitHub Codespaces which sets up everything for you.
## Getting the code ready
1. Register a GitHub account.
2. Fork this repository under your account.
3. Clone the fork onto your machine.
4. Open the cloned repository in the code editor.
5. Create a git branch (recommended).
6. Make your changes and test them locally.
7. Commit the changes and push the branch to the remote repo.
8. Go to GitHub, and open a pull request, targeting the upstream branch.
## Contribution guidelines
1. Our standards are pretty low, but make sure the code is not too ugly:
- Run VS Code's autoformat when you're done.
- Check with ESLint by running `npm run lint`, then fix the errors.
- Use common sense and follow existing naming conventions.
2. Create pull requests for the staging branch, 99% of contributions should go there. That way people could test your code before the next stable release.
3. You can still send a pull request for release in the following scenarios:
- Updating README.
- Updating GitHub Actions.
- Hotfixing a critical bug.
4. Project maintainers will test and can change your code before merging.
5. Mind the license. Your contributions will be licensed under the GNU Affero General Public License. If you don't know what that implies, consult your lawyer.

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 338 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 598 KiB

View File

@@ -11,14 +11,6 @@
"filename": "themes/Cappuccino.json",
"type": "theme"
},
{
"filename": "themes/Celestial Macaron.json",
"type": "theme"
},
{
"filename": "themes/Dark V 1.0.json",
"type": "theme"
},
{
"filename": "backgrounds/__transparent.png",
"type": "background"
@@ -115,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"
@@ -484,11 +484,7 @@
"type": "context"
},
{
"filename": "presets/context/DreamGen Role-Play V1 ChatML.json",
"type": "context"
},
{
"filename": "presets/context/DreamGen Role-Play V1 Llama3.json",
"filename": "presets/context/DreamGen Role-Play V1.json",
"type": "context"
},
{
@@ -568,11 +564,7 @@
"type": "instruct"
},
{
"filename": "presets/instruct/DreamGen Role-Play V1 ChatML.json",
"type": "instruct"
},
{
"filename": "presets/instruct/DreamGen Role-Play V1 Llama3.json",
"filename": "presets/instruct/DreamGen Role-Play V1.json",
"type": "instruct"
},
{

View File

@@ -1,12 +0,0 @@
{
"story_string": "<|start_header_id|>system<|end_header_id|>\n\n{{#if system}}{{system}}\n\n\n{{/if}}## Overall plot description:\n\n{{#if scenario}}{{scenario}}{{else}}Conversation between {{char}} and {{user}}.{{/if}}{{#if wiBefore}}\n\n{{wiBefore}}{{/if}}\n\n\n## Characters:\n\n### {{char}}\n\n{{#if description}}{{description}}\n\n{{/if}}{{#if personality}}{{personality}}\n\n{{/if}}### {{user}}\n\n{{#if persona}}{{persona}}{{else}}{{user}} is the protagonist of the role-play.{{/if}}{{#if wiAfter}}\n\n{{wiAfter}}{{/if}}{{#if mesExamples}}\n\n{{mesExamples}}{{/if}}",
"example_separator": "<|eot_id|>\n<|start_header_id|>user<|end_header_id|>\n\nWrite an example narrative / conversation that is not part of the main story.",
"chat_start": "<|eot_id|>\n<|start_header_id|>user<|end_header_id|>\n\nStart the role-play between {{char}} and {{user}}.",
"use_stop_strings": false,
"allow_jailbreak": false,
"always_force_name2": false,
"trim_sentences": true,
"include_newline": false,
"single_line": false,
"name": "DreamGen Role-Play V1 Llama3"
}

View File

@@ -8,5 +8,5 @@
"trim_sentences": true,
"include_newline": false,
"single_line": false,
"name": "DreamGen Role-Play V1 ChatML"
"name": "DreamGen Role-Play V1"
}

View File

@@ -1,18 +0,0 @@
{
"system_prompt": "You are an intelligent, skilled, versatile writer.\n\nYour task is to write a role-play based on the information below.",
"input_sequence": "<|eot_id|>\n<|start_header_id|>writer character: {{user}}<|end_header_id|>\n\n",
"output_sequence": "<|eot_id|>\n<|start_header_id|>writer character: {{char}}<|end_header_id|>\n\n",
"first_output_sequence": "",
"last_output_sequence": "",
"system_sequence_prefix": "",
"system_sequence_suffix": "",
"stop_sequence": "",
"separator_sequence": "",
"wrap": false,
"macro": true,
"names": false,
"names_force_groups": false,
"activation_regex": "",
"skip_examples": false,
"name": "DreamGen Role-Play V1 Llama3"
}

View File

@@ -20,5 +20,5 @@
"user_alignment_message": "",
"system_same_as_user": true,
"last_system_sequence": "",
"name": "DreamGen Role-Play V1 ChatML"
"name": "DreamGen Role-Play V1"
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,7 +1,7 @@
{
"name": "Cappuccino",
"blur_strength": 3,
"main_text_color": "rgba(235, 235, 235, 1)",
"main_text_color": "rgba(255, 255, 255, 1)",
"italics_text_color": "rgba(230, 210, 190, 1)",
"underline_text_color": "rgba(205, 180, 160, 1)",
"quote_text_color": "rgba(165, 140, 115, 1)",

View File

@@ -1,37 +0,0 @@
{
"name": "Celestial Macaron",
"blur_strength": 10,
"main_text_color": "rgba(229, 175, 162, 1)",
"italics_text_color": "rgba(146, 147, 161, 1)",
"underline_text_color": "rgba(157, 215, 198, 1)",
"quote_text_color": "rgba(197, 202, 206, 1)",
"blur_tint_color": "rgba(23, 36, 55, 0.9)",
"chat_tint_color": "rgba(18, 26, 40, 0.9)",
"user_mes_blur_tint_color": "rgba(51, 67, 90, 0.7)",
"bot_mes_blur_tint_color": "rgba(23, 36, 55, 0.75)",
"shadow_color": "rgba(0, 0, 0, 0.3)",
"shadow_width": 1,
"border_color": "rgba(60, 74, 110, 0.93)",
"font_scale": 1,
"fast_ui_mode": false,
"waifuMode": false,
"avatar_style": 0,
"chat_display": 1,
"noShadows": true,
"chat_width": 58,
"timer_enabled": true,
"timestamps_enabled": true,
"timestamp_model_icon": false,
"mesIDDisplay_enabled": true,
"hideChatAvatars_enabled": false,
"message_token_count_enabled": true,
"expand_message_actions": true,
"enableZenSliders": false,
"enableLabMode": false,
"hotswap_enabled": true,
"custom_css": "",
"bogus_folders": true,
"zoomed_avatar_magnification": false,
"reduced_motion": false,
"compact_input_area": true
}

View File

@@ -1,37 +0,0 @@
{
"name": "Dark V 1.0",
"blur_strength": 13,
"main_text_color": "rgba(207, 207, 197, 1)",
"italics_text_color": "rgba(145, 145, 145, 1)",
"underline_text_color": "rgba(145, 145, 145, 1)",
"quote_text_color": "rgba(198, 193, 151, 1)",
"blur_tint_color": "rgba(29, 33, 40, 0.9)",
"chat_tint_color": "rgba(29, 33, 40, 0.9)",
"user_mes_blur_tint_color": "rgba(29, 33, 40, 0.9)",
"bot_mes_blur_tint_color": "rgba(29, 33, 40, 0.9)",
"shadow_color": "rgba(0, 0, 0, 0.9)",
"shadow_width": 2,
"border_color": "rgba(0, 0, 0, 1)",
"font_scale": 1,
"fast_ui_mode": false,
"waifuMode": false,
"avatar_style": 0,
"chat_display": 0,
"noShadows": false,
"chat_width": 55,
"timer_enabled": false,
"timestamps_enabled": false,
"timestamp_model_icon": false,
"mesIDDisplay_enabled": false,
"hideChatAvatars_enabled": false,
"message_token_count_enabled": false,
"expand_message_actions": false,
"enableZenSliders": false,
"enableLabMode": false,
"hotswap_enabled": true,
"custom_css": "",
"bogus_folders": true,
"zoomed_avatar_magnification": true,
"reduced_motion": true,
"compact_input_area": false
}

View File

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

View File

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

View File

@@ -15,11 +15,6 @@
"**/node_modules/*",
"public/lib",
"backups/*",
"data/*",
"**/dist/*",
"dist/*",
"cache/*",
"src/tokenizers/*",
"docker/*",
"data/*"
]
}

92
package-lock.json generated
View File

@@ -1,18 +1,19 @@
{
"name": "sillytavern",
"version": "1.12.2",
"version": "1.12.0-preview",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "sillytavern",
"version": "1.12.2",
"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,11 +46,10 @@
"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",
"ws": "^8.17.1",
"ws": "^8.13.0",
"yaml": "^2.3.4",
"yargs": "^17.7.1",
"yauzl": "^2.10.0"
@@ -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": {
@@ -4659,9 +4660,8 @@
}
},
"node_modules/ws": {
"version": "8.17.1",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz",
"integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==",
"version": "8.13.0",
"license": "MIT",
"engines": {
"node": ">=10.0.0"
},

View File

@@ -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,18 +36,14 @@
"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",
"ws": "^8.17.1",
"ws": "^8.13.0",
"yaml": "^2.3.4",
"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.2",
"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"

View File

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

View File

@@ -1,122 +0,0 @@
/* Fade animations with opacity */
@keyframes fade-in {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
@keyframes fade-out {
0% {
opacity: 1;
}
100% {
opacity: 0;
}
}
/* Pop animations with opacity and vertical scaling */
@keyframes pop-in {
0% {
opacity: 0;
transform: scaleY(0);
}
/* Make the scaling faster on pop-in, otherwise it looks a bit weird */
33% {
transform: scaleY(1);
}
100% {
opacity: 1;
transform: scaleY(1);
}
}
@keyframes pop-out {
0% {
opacity: 1;
transform: scaleY(1);
}
100% {
opacity: 0;
transform: scaleY(0);
}
}
/* Flashing for highlighting animation */
@keyframes flash {
20%,
60%,
100% {
opacity: 1;
}
0%,
40%,
80% {
opacity: 0.2;
}
}
/* Pulsing highlight, slightly resizing the element */
@keyframes pulse {
from {
transform: scale(1);
filter: brightness(1.1);
}
to {
transform: scale(1.01);
filter: brightness(1.3);
}
}
/* Ellipsis animation */
@keyframes ellipsis {
0% {
content: ""
}
25% {
content: "."
}
50% {
content: ".."
}
75% {
content: "..."
}
}
/* HEINOUS */
@keyframes infinite-spinning {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
/* STscript animation */
@keyframes script_progress_pulse {
0%,
100% {
border-top-color: var(--progColor);
}
50% {
border-top-color: var(--progFlashColor);
}
}

View File

@@ -36,6 +36,7 @@ label[for="extensions_autoconnect"] {
.extensions_info {
text-align: left;
margin: 0 1em;
}
.extensions_info h3 {
@@ -96,11 +97,114 @@ input.extension_missing[type="checkbox"] {
flex-direction: column;
}
/* Fixes order of settings for extensions */
.extension_container {
display: contents;
/** LEFT COLUMN **/
/* Must be always on top */
#extensions_settings>#assets_ui {
order: -1;
}
#extensionsMenu>div.extension_container:empty {
display: none;
#extensions_settings>.expression_settings {
order: 2;
}
#extensions_settings>.background_settings {
order: 3;
}
#extensions_settings>.sd_settings {
order: 4;
}
#extensions_settings>#tts_settings {
order: 5;
}
#extensions_settings>#rvc_settings {
order: 6;
}
#extensions_settings>.objective-settings {
order: 7;
}
#extensions_settings>#speech_recognition_settings {
order: 8;
}
#extensions_settings>#audio_settings {
order: 9;
}
/** RIGHT COLUMN **/
#extensions_settings2>.translation_settings {
order: 1;
}
#extensions_settings2>.caption_settings {
order: 2;
}
#extensions_settings2>.quickReplySettings {
order: 3;
}
#extensions_settings2>.idle-settings {
order: 4;
}
#extensions_settings2>#memory_settings {
order: 5;
}
#extensions_settings2>.hypebot_settings {
order: 6;
}
#extensions_settings2>.regex_settings {
order: 7;
}
#extensions_settings2>.vectors_settings {
order: 8;
}
#extensions_settings2>.chromadb_settings {
order: 9;
}
#extensions_settings2>.randomizer_settings {
order: 10;
}
/** WAND MENU **/
#extensionsMenu>#ttsExtensionMenuItem {
order: 1;
}
#extensionsMenu>#sd_gen {
order: 2;
}
#extensionsMenu>#send_picture {
order: 3;
}
#extensionsMenu>#token_counter {
order: 4;
}
#extensionsMenu>#objective-task-manual-check-menu-item {
order: 5;
}
#extensionsMenu>#roll_dice {
order: 6;
}
#extensionsMenu>#translate_chat {
order: 7;
}
#extensionsMenu>#translate_input_message {
order: 8;
}

View File

@@ -98,7 +98,7 @@
font-weight: bold;
}
.logprobs_top_candidate:not([disabled]):hover {
.logprobs_top_candidate:not([disabled]):hover, .logprobs_top_candidate:not([disabled]):focus {
background-color: rgba(0, 0, 0, 0.3);
}

View File

@@ -117,11 +117,6 @@
max-width: unset;
}
#wiActivationSettings,
#wiTopBlock {
flex-direction: column;
}
#top-settings-holder,
#top-bar {
position: fixed;

View File

@@ -1,8 +0,0 @@
/* iPhone copium land */
@media screen and (max-width: 1000px) {
.ios .popup .popup-body {
height: fit-content;
max-height: 90vh;
max-height: 90svh;
}
}

View File

@@ -1,175 +0,0 @@
@import url('./popup-safari-fix.css');
dialog {
color: var(--SmartThemeBodyColor);
}
/* Closed state of the dialog */
.popup {
width: 500px;
text-align: center;
box-shadow: 0px 0px 14px var(--black70a);
border: 1px solid var(--SmartThemeBorderColor);
padding: 4px 14px;
background-color: var(--SmartThemeBlurTintColor);
border-radius: 10px;
display: flex;
flex-direction: column;
max-height: calc(100svh - 2em);
max-width: calc(100svw - 2em);
min-height: fit-content;
/* Overflow visible so elements (like toasts) can appear outside of the dialog. '.popup-body' is hiding overflow for the real content. */
overflow: visible;
/* Fix weird animation issue with font-scaling during popup open */
backface-visibility: hidden;
transform: translateZ(0);
-webkit-font-smoothing: subpixel-antialiased;
}
.popup .popup-body {
display: flex;
flex-direction: column;
overflow: hidden;
width: 100%;
height: 100%;
padding: 1px;
}
.popup .popup-content {
margin-top: 10px;
padding: 0 8px;
overflow: hidden;
flex-grow: 1;
}
.popup .popup-content h3:first-child {
/* No double spacing for the first heading needed, the .popup-content already has margin */
margin-top: 0px;
}
.popup.vertical_scrolling_dialogue_popup .popup-content {
overflow-y: auto;
}
.popup.horizontal_scrolling_dialogue_popup .popup-content {
overflow-x: auto;
}
/* Opening animation */
.popup[opening] {
animation: pop-in var(--animation-duration-slow) ease-in-out;
}
.popup[opening]::backdrop {
animation: fade-in var(--animation-duration-slow) ease-in-out;
}
/* Open state of the dialog */
.popup[open] {
color: var(--SmartThemeBodyColor);
}
.popup[open]::backdrop {
backdrop-filter: blur(calc(var(--SmartThemeBlurStrength) * 2));
-webkit-backdrop-filter: blur(calc(var(--SmartThemeBlurStrength) * 2));
background-color: var(--black30a);
}
body.no-blur .popup[open]::backdrop {
backdrop-filter: none;
-webkit-backdrop-filter: none;
}
/* Closing animation */
.popup[closing] {
animation: pop-out var(--animation-duration-slow) ease-in-out;
}
.popup[closing]::backdrop {
animation: fade-out var(--animation-duration-slow) ease-in-out;
}
.popup #toast-container {
/* Fix toastr in dialogs by actually placing it at the top of the screen via transform */
height: 100svh;
top: calc(50% + var(--topBarBlockSize));
left: 50%;
transform: translate(-50%, -50%);
/* Fix text align, popups are centered by default. toasts should not. */
text-align: left;
}
.popup-crop-wrap {
margin: 10px auto;
max-height: 75vh;
max-height: 75svh;
max-width: 100%;
}
.popup-crop-wrap img {
max-width: 100%;
/* This rule is very important, please do not ignore this! */
}
.popup-inputs {
margin-top: 10px;
font-size: smaller;
opacity: 0.7;
}
.popup-input {
margin-top: 10px;
}
.popup-controls {
margin-top: 10px;
display: flex;
align-self: center;
gap: 20px;
}
.menu_button.menu_button_default {
box-shadow: 0 0 5px var(--white20a);
}
.menu_button.popup-button-ok {
background-color: var(--crimson70a);
}
.menu_button.popup-button-ok:hover {
background-color: var(--crimson-hover);
}
.popup-controls .menu_button {
/* Popup buttons should not scale to smallest size, otherwise they will always break to multiline if multiple words */
width: unset;
/* Fix weird animation issue with fonts on brightness filter */
backface-visibility: hidden;
transform: translateZ(0);
-webkit-font-smoothing: subpixel-antialiased;
}
.popup-controls .menu_button:hover:focus-visible {
filter: brightness(1.3) saturate(1.3);
}
.popup .popup-button-close {
position: absolute;
top: -6px;
right: -6px;
width: 24px;
height: 24px;
font-size: 20px;
padding: 2px 3px 3px 2px;
filter: brightness(0.8);
/* Fix weird animation issue with font-scaling during popup open */
backface-visibility: hidden;
}

View File

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

View File

@@ -23,14 +23,6 @@
opacity: 0.8;
}
.select2-selection--single .select2-selection__placeholder {
color: var(--SmartThemeEmColor);
}
.select2-container--classic .select2-selection--single .select2-selection__placeholder {
color: var(--SmartThemeEmColor);
}
.select2-container .select2-selection--single .select2-selection__rendered {
color: var(--SmartThemeBodyColor);
line-height: revert;
@@ -57,7 +49,7 @@
color: var(--SmartThemeBodyColor);
border: 1px solid var(--SmartThemeBorderColor);
border-radius: 7px;
font-family: var(--mainFontFamily);
font-family: "Noto Sans", "Noto Color Emoji", sans-serif;
padding: 3px 5px;
}
@@ -85,7 +77,7 @@
color: var(--SmartThemeBodyColor);
border: 1px solid var(--SmartThemeBorderColor);
border-radius: 7px;
font-family: var(--mainFontFamily);
font-family: "Noto Sans", "Noto Color Emoji", sans-serif;
padding: 3px 5px;
}
@@ -179,92 +171,3 @@
.select2-results__option.select2-results__message::before {
display: none;
}
.select2-selection__choice__display {
/* Fix weird alignment of the inside block */
margin-left: 3px;
margin-right: 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;
}

View File

@@ -220,7 +220,7 @@
}
.monospace {
font-family: var(--monoFontFamily);
font-family: monospace;
}
.expander {
@@ -292,14 +292,6 @@
flex-wrap: nowrap;
}
.inline-flex {
display: inline-flex;
}
.inline-block {
display: inline-block;
}
.alignitemscenter,
.alignItemsCenter {
align-items: center;
@@ -356,10 +348,6 @@
margin-right: 5px;
}
.margin-r2 {
margin-right: 2px;
}
.flex0 {
flex: 0;
}
@@ -372,14 +360,6 @@
flex: 2 !important;
}
.flex3 {
flex: 3;
}
.flex4 {
flex: 4;
}
.flexFlowColumn {
flex-flow: column;
}
@@ -583,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
View 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;
}

View File

@@ -14,7 +14,7 @@
display: flex;
flex-direction: row;
align-items: center;
gap: 6px;
gap: 10px;
margin-bottom: 5px;
}
@@ -27,19 +27,8 @@
flex: 1;
}
.tag_view_color_picker {
position: relative;
}
.tag_view_color_picker .link_icon {
position: absolute;
top: 50%;
right: 0px;
opacity: 0.5;
}
.tag_delete {
padding: 2px 4px;
padding-right: 0;
color: var(--SmartThemeBodyColor) !important;
}
@@ -114,16 +103,7 @@
}
#bulkTagsList,
#tagList .tag,
#groupTagList .tag {
opacity: 0.6;
}
#tagList .tag:has(.tag_remove:hover) {
opacity: 1;
}
#tagList .tag:has(.tag_remove:hover) .tag_name {
#tagList .tag {
opacity: 0.6;
}
@@ -213,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;
}

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -1,59 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="363.44339"
height="375.68854"
viewBox="0 0 363.44339 375.68854"
version="1.1"
id="svg2"
sodipodi:docname="Yi_logo_icon_dark.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="defs2" />
<sodipodi:namedview
id="namedview2"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="1.1073359"
inkscape:cx="192.35355"
inkscape:cy="196.86889"
inkscape:window-width="1512"
inkscape:window-height="857"
inkscape:window-x="0"
inkscape:window-y="38"
inkscape:window-maximized="1"
inkscape:current-layer="svg2" />
<rect
x="287.14771"
y="224.04056"
width="42.3862"
height="151.64799"
rx="21.1931"
id="rect1" />
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="m 299.41969,17.362538 c -8.916,-7.5830004 -22.291,-6.503 -29.874,2.414 l -118.432,139.253002 c -3.056,3.593 -4.705,7.911 -5.001,12.281 -0.166,1.069 -0.252,2.164 -0.252,3.279 v 178.022 c 0,11.705 9.488,21.193 21.193,21.193 11.705,0 21.193,-9.488 21.193,-21.193 v -171.819 l 113.587,-133.556002 c 7.583,-8.916 6.502,-22.291 -2.414,-29.874 z"
id="path1" />
<rect
x="-18.236605"
y="8.6596518"
width="42.3862"
height="174.745"
rx="21.1931"
transform="rotate(-39.3441)"
id="rect2" />
<circle
cx="337.54071"
cy="163.28656"
r="25.9027"
id="circle2" />
</svg>

Before

Width:  |  Height:  |  Size: 1.8 KiB

View File

@@ -1,39 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
class="logo"
width="36"
height="30.9767"
viewBox="0 0 36 30.9767"
version="1.1"
id="svg2"
sodipodi:docname="featherless.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="defs2" />
<sodipodi:namedview
id="namedview2"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="4.0920245"
inkscape:cx="75.268366"
inkscape:cy="15.151424"
inkscape:window-width="1512"
inkscape:window-height="857"
inkscape:window-x="0"
inkscape:window-y="38"
inkscape:window-maximized="1"
inkscape:current-layer="svg2" />
<path
d="M 34.0866,1.68482 C 32.2902,0.5825 29.863,0 27.0672,0 22.7842,0 18.0653,1.35865 13.8276,3.72206 L 13.7979,3.71083 c 0,0 -0.0042,0.02261 -0.0065,0.0334 C 12.5086,4.4617 11.2656,5.2629 10.0981,6.15731 3.22112,11.4248 1.29519,17.6748 2.92004,21.0156 1.14142,24.0728 0.0457,27.2332 0,30.9767 3.41949,24.421 5.4719,19.108 16.6146,10.1637 13.4309,10.8501 7.9281,14.1057 4.2271,19.0459 3.87793,16.156 6.1477,11.4895 11.2033,7.6174 11.8435,7.127 12.5092,6.66864 13.1886,6.23374 12.6577,7.8934 12.8269,7.4806 11.7254,9.8076 c 1.6289,-1.551 2.7014,-2.5081 4.3096,-5.16615 2.088,-1.03181 4.2598,-1.80301 6.4132,-2.2691 -0.3563,1.18836 -1.0345,3.20231 -1.9527,4.79455 0,0 2.3303,-0.50255 4.2563,-0.38902 -1.0523,1.16802 -1.9991,2.43152 -2.9592,3.72332 -1.3149,1.7684 -2.6742,3.5971 -4.4148,5.2993 -0.2095,0.2049 -0.4098,0.3907 -0.6129,0.5825 -2.6747,-0.2576 -4.4414,0.7485 -6.0966,2.5259 1.3054,-0.6123 3.059,-1.1165 4.1583,-0.813 -2.0258,1.662 -5.216,3.8529 -7.8373,3.6725 -0.4971,0.7611 -0.5285,0.7844 -1.0749,1.7038 4.252,1.0648 9.5926,-3.2817 12.7354,-6.3561 1.8428,-1.803 3.2466,-3.6904 4.6036,-5.5149 2.7947,-3.7585 5.2082,-7.0038 10.5619,-8.2388 L 36,2.85877 Z"
class="logo-mark"
id="path1"/>
</svg>

Before

Width:  |  Height:  |  Size: 2.2 KiB

View File

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

View File

@@ -1,40 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="88.001465mm"
height="81.280983mm"
version="1.1"
id="svg9"
sodipodi:docname="huggingface.svg"
inkscape:version="1.3 (0e150ed, 2023-07-21)"
viewBox="0 0 88.001465 81.280983"
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="defs9" />
<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.68605868"
inkscape:cx="424.16197"
inkscape:cy="154.50573"
inkscape:window-width="1512"
inkscape:window-height="857"
inkscape:window-x="0"
inkscape:window-y="38"
inkscape:window-maximized="1"
inkscape:current-layer="svg9"
inkscape:clip-to-page="false"
inkscape:document-units="mm" />
<path
id="path2-9"
style="display:inline;"
d="M 40.855186,0.10840487 A 38.75,38.75 0 0 0 5.0016702,38.750983 a 38.75,38.75 0 0 0 1.7871095,11.589844 7.1,7.1 0 0 1 1.871094,0.291015 5.97,5.97 0 0 1 1.330078,-3.761718 c 0.02089,-0.02502 0.04515,-0.04576 0.06641,-0.07031 a 34.75,34.75 0 0 1 -1.0547201,-8.048831 34.750014,34.750014 0 0 1 69.5000291,0 34.75,34.75 0 0 1 -0.957032,7.630859 c 0.163358,0.152193 0.321565,0.31255 0.466797,0.488282 a 5.97,5.97 0 0 1 1.330078,3.761718 7.1,7.1 0 0 1 1.337891,-0.207031 A 38.75,38.75 0 0 0 82.501671,38.750983 38.75,38.75 0 0 0 40.855186,0.10840487 Z M 48.015342,73.165045 a 34.75,34.75 0 0 1 -8.044921,0.03906 c -0.396448,0.901178 -0.898324,1.811009 -1.529297,2.736328 -0.233308,0.342701 -0.489288,0.664577 -0.75586,0.974609 a 38.75,38.75 0 0 0 12.574219,-0.06641 c -0.245421,-0.290144 -0.482504,-0.589875 -0.699219,-0.908203 -0.639915,-0.938432 -1.14623,-1.86177 -1.544922,-2.775391 z M 73.940733,45.000983 c 1.62,0 3.07,0.66 4.07,1.87 a 5.97,5.97 0 0 1 1.33,3.76 7.1,7.1 0 0 1 1.95,-0.3 c 1.55,0 2.95,0.59 3.94,1.66 a 5.8,5.8 0 0 1 0.8,7 5.3,5.3 0 0 1 1.78,2.82 c 0.24,0.9 0.48,2.8 -0.8,4.74 a 5.22,5.22 0 0 1 0.37,5.02 c -1.02,2.32 -3.57,4.14 -8.51,6.1 -3.08,1.22 -5.9,2 -5.92,2.01 a 44.33,44.33 0 0 1 -10.93,1.6 c -5.86,0 -10.05,-1.8 -12.46,-5.34 -3.88,-5.69 -3.33,-10.9 1.7,-15.92 2.78,-2.78 4.63,-6.87 5.01,-7.77 0.78,-2.66 2.83,-5.62 6.24,-5.62 a 5.7,5.7 0 0 1 4.6,2.46 c 1,-1.26 1.98,-2.25 2.87,-2.82 a 7.4,7.4 0 0 1 3.96,-1.27 z m 0,4 c -0.51,0 -1.13,0.22 -1.82,0.65 -2.13,1.36 -6.25,8.43 -7.76,11.18 a 2.43,2.43 0 0 1 -2.14,1.31 c -1.54,0 -2.75,-1.53 -0.14,-3.48 3.91,-2.93 2.54,-7.72 0.67,-8.01 a 1.54,1.54 0 0 0 -0.24,-0.02 c -1.7,0 -2.45,2.93 -2.45,2.93 0,0 -2.2,5.52 -5.97,9.3 -3.78,3.77 -3.98,6.8 -1.22,10.83 1.87,2.75 5.47,3.58 9.15,3.58 3.82,0 7.73,-0.9 9.93,-1.46 0.1,-0.03 13.45,-3.8 11.76,-7 -0.29,-0.54 -0.75,-0.76 -1.34,-0.76 -2.38,0 -6.71,3.54 -8.57,3.54 -0.42,0 -0.71,-0.17 -0.83,-0.6 -0.8,-2.85 12.05,-4.05 10.97,-8.17 -0.19,-0.73 -0.7,-1.02 -1.44,-1.02 -3.14,0 -10.2,5.53 -11.68,5.53 -0.1,0 -0.19,-0.03 -0.23,-0.1 -0.74,-1.2 -0.34,-2.04 4.88,-5.2 5.23,-3.16 8.9,-5.06 6.8,-7.33 -0.23,-0.26 -0.57,-0.38 -0.98,-0.38 -3.18,0 -10.67,6.82 -10.67,6.82 0,0 -2.02,2.1 -3.24,2.1 a 0.74,0.74 0 0 1 -0.68,-0.38 c -0.87,-1.46 8.05,-8.22 8.55,-11.01 0.34,-1.9 -0.24,-2.85 -1.31,-2.85 z m -6.69,-15 a 3.25,3.25 0 1 0 0,-6.5 3.25,3.25 0 0 0 0,6.5 z m -46.5,0 a 3.25,3.25 0 1 0 0,-6.5 3.25,3.25 0 0 0 0,6.5 z m -6.69,11 c -1.62,0 -3.06,0.66 -4.0700003,1.87 a 5.97,5.97 0 0 0 -1.33,3.76 7.1,7.1 0 0 0 -1.94,-0.3 c -1.55,0 -2.95,0.59 -3.94,1.66 a 5.8,5.8 0 0 0 -0.8,7 5.3,5.3 0 0 0 -1.79000004,2.82 c -0.24,0.9 -0.48,2.8 0.8,4.74 a 5.22,5.22 0 0 0 -0.37,5.02 c 1.02000004,2.32 3.57000004,4.14 8.52000004,6.1 3.0700003,1.22 5.8900003,2 5.9100003,2.01 a 44.33,44.33 0 0 0 10.93,1.6 c 5.86,0 10.05,-1.8 12.46,-5.34 3.88,-5.69 3.33,-10.9 -1.7,-15.92 -2.77,-2.78 -4.62,-6.87 -5,-7.77 -0.78,-2.66 -2.84,-5.62 -6.25,-5.62 a 5.7,5.7 0 0 0 -4.6,2.46 c -1,-1.26 -1.98,-2.25 -2.86,-2.82 a 7.4,7.4 0 0 0 -3.97,-1.27 z m 0,4 c 0.51,0 1.14,0.22 1.82,0.65 2.14,1.36 6.25,8.43 7.76,11.18 0.5,0.92 1.37,1.31 2.14,1.31 1.55,0 2.75,-1.53 0.15,-3.48 -3.92,-2.93 -2.55,-7.72 -0.68,-8.01 0.08,-0.02 0.17,-0.02 0.24,-0.02 1.7,0 2.45,2.93 2.45,2.93 0,0 2.2,5.52 5.98,9.3 3.77,3.77 3.97,6.8 1.22,10.83 -1.88,2.75 -5.47,3.58 -9.16,3.58 -3.81,0 -7.73,-0.9 -9.92,-1.46 -0.11,-0.03 -13.4500003,-3.8 -11.7600003,-7 0.28,-0.54 0.75,-0.76 1.34,-0.76 2.38,0 6.7000003,3.54 8.5700003,3.54 0.41,0 0.7,-0.17 0.83,-0.6 0.79,-2.85 -12.0600003,-4.05 -10.9800003,-8.17 0.2,-0.73 0.71,-1.02 1.44,-1.02 3.14,0 10.2000003,5.53 11.6800003,5.53 0.11,0 0.2,-0.03 0.24,-0.1 0.74,-1.2 0.33,-2.04 -4.9,-5.2 -5.2100003,-3.16 -8.8800003,-5.06 -6.8000003,-7.33 0.24,-0.26 0.58,-0.38 1,-0.38 3.17,0 10.6600003,6.82 10.6600003,6.82 0,0 2.02,2.1 3.25,2.1 0.28,0 0.52,-0.1 0.68,-0.38 0.86,-1.46 -8.06,-8.22 -8.56,-11.01 -0.34,-1.9 0.24,-2.85 1.31,-2.85 z m 21.91,2 a 8.7,8.7 0 0 1 5.3,-4.49 c 0.4,-0.12 0.81,0.57 1.24,1.28 0.4,0.68 0.82,1.37 1.24,1.37 0.45,0 0.9,-0.68 1.33,-1.35 0.45,-0.7 0.89,-1.38 1.32,-1.25 a 8.61,8.61 0 0 1 5,4.17 c 3.73,-2.94 5.1,-7.74 5.1,-10.7 0,-2.34 -1.57,-1.6 -4.09,-0.36 l -0.14,0.07 c -2.31,1.15 -5.39,2.67 -8.77,2.67 -3.38,0 -6.45,-1.52 -8.77,-2.67 -2.6,-1.29 -4.23,-2.1 -4.23,0.29 0,3.05 1.46,8.06 5.47,10.97 z m 19.07,-21.7 c 1.28,0.44 1.78,3.06 3.07,2.38 a 5,5 0 1 0 -6.76,-2.07 c 0.61,1.15 2.55,-0.72 3.7,-0.32 z m -23.55,0 c -1.28,0.44 -1.79,3.06 -3.07,2.38 a 5,5 0 1 1 6.76,-2.07 c -0.61,1.15 -2.56,-0.72 -3.7,-0.32 z" />
</svg>

Before

Width:  |  Height:  |  Size: 5.6 KiB

View File

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

View File

@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<svg width="800px" height="800px" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path d="M13.3252 3.05011L8.66765 20.4323L10.5995 20.9499L15.257 3.56775L13.3252 3.05011Z" />
<path d="M7.61222 18.3608L8.97161 16.9124L8.9711 16.8933L3.87681 12.1121L8.66724 7.00798L7.20892 5.63928L1.0498 12.2017L7.61222 18.3608Z" />
<path d="M16.3883 18.3608L15.0289 16.9124L15.0294 16.8933L20.1237 12.1121L15.3333 7.00798L16.7916 5.63928L22.9507 12.2017L16.3883 18.3608Z" />
</svg>

Before

Width:  |  Height:  |  Size: 514 B

File diff suppressed because it is too large Load Diff

View File

@@ -3,7 +3,6 @@
"checkJs": true,
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "node",
"allowUmdGlobalAccess": true,
"allowSyntheticDefaultImports": true
},

View File

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

View File

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

View File

@@ -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" />
@@ -26,10 +19,9 @@
<link rel="stylesheet" type="text/css" href="css/login.css">
<link rel="manifest" crossorigin="use-credentials" href="manifest.json">
<link href="webfonts/NotoSans/stylesheet.css" rel="stylesheet">
<link href="webfonts/NotoSansMono/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>

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -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 = () => { };
@@ -685,23 +684,6 @@ class PromptManager {
this.log('Initialized');
}
/**
* Get the scroll position of the prompt manager
* @returns {number} - Scroll position of the prompt manager
*/
#getScrollPosition() {
return document.getElementById(this.configuration.prefix + 'prompt_manager')?.closest('.scrollableInner')?.scrollTop;
}
/**
* Set the scroll position of the prompt manager
* @param {number} scrollPosition - The scroll position to set
*/
#setScrollPosition(scrollPosition) {
if (scrollPosition === undefined || scrollPosition === null) return;
document.getElementById(this.configuration.prefix + 'prompt_manager')?.closest('.scrollableInner')?.scrollTo(0, scrollPosition);
}
/**
* Main rendering function
*
@@ -713,28 +695,24 @@ 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');
const scrollPosition = this.#getScrollPosition();
await this.renderPromptManager();
await this.renderPromptManagerListItems();
this.renderPromptManager();
this.renderPromptManagerListItems();
this.makeDraggable();
this.#setScrollPosition(scrollPosition);
this.profileEnd('render');
});
} else {
// Executed during live communication
this.profileStart('render');
const scrollPosition = this.#getScrollPosition();
await this.renderPromptManager();
await this.renderPromptManagerListItems();
this.renderPromptManager();
this.renderPromptManagerListItems();
this.makeDraggable();
this.#setScrollPosition(scrollPosition);
this.profileEnd('render');
}
}).catch(() => {
@@ -1360,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) {
@@ -1369,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`);
@@ -1396,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);
@@ -1409,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
@@ -1445,7 +1460,7 @@ class PromptManager {
/**
* Empties, then re-assembles the prompt list
*/
async renderPromptManagerListItems() {
renderPromptManagerListItems() {
if (!this.serviceSettings.prompts) return;
const promptManagerList = this.listElement;
@@ -1453,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;
@@ -1527,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>
@@ -1578,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');

View File

@@ -16,6 +16,7 @@ import {
eventSource,
menu_type,
substituteParams,
callPopup,
sendTextareaMessage,
} from '../script.js';
@@ -38,7 +39,6 @@ import { textgen_types, textgenerationwebui_settings as textgen_settings, getTex
import { debounce_timeout } from './constants.js';
import Bowser from '../lib/bowser.min.js';
import { Popup } from './popup.js';
var RPanelPin = document.getElementById('rm_button_panel_pin');
var LPanelPin = document.getElementById('lm_button_panel_pin');
@@ -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 ? '&lt;1s' : 'Instant';
}
return resultParts.map(part => {
return short ? `${part.value}${part.short}` : `${part.value} ${part.value === 1 ? part.singular : part.plural}`;
}).join(short ? ' ' : ', ');
}
/**
@@ -303,7 +329,7 @@ export async function favsToHotswap() {
return;
}
buildAvatarList(container, favs, { interactable: true, highlightFavs: false });
buildAvatarList(container, favs, { selectable: true, highlightFavs: false });
}
//changes input bar and send button display depending on connection status
@@ -360,7 +386,6 @@ function RA_autoconnect(PrevApi) {
|| (textgen_settings.type === textgen_types.INFERMATICAI && secret_state[SECRET_KEYS.INFERMATICAI])
|| (textgen_settings.type === textgen_types.DREAMGEN && secret_state[SECRET_KEYS.DREAMGEN])
|| (textgen_settings.type === textgen_types.OPENROUTER && secret_state[SECRET_KEYS.OPENROUTER])
|| (textgen_settings.type === textgen_types.FEATHERLESS && secret_state[SECRET_KEYS.FEATHERLESS])
) {
$('#api_button_textgenerationwebui').trigger('click');
}
@@ -379,8 +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)
|| (secret_state[SECRET_KEYS.ZEROONEAI] && oai_settings.chat_completion_source == chat_completion_sources.ZEROONEAI)
|| (isValidUrl(oai_settings.custom_url) && oai_settings.chat_completion_source == chat_completion_sources.CUSTOM)
) {
$('#api_button_openai').trigger('click');
@@ -426,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');
}
}
@@ -438,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;
@@ -450,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'] });
});
@@ -467,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);
@@ -494,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');
}
@@ -549,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;
}
@@ -561,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) {
@@ -593,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);
}
});
@@ -608,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
@@ -640,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);
@@ -663,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;
}
}
@@ -695,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));
@@ -725,10 +757,6 @@ export function initRossMods() {
RA_autoconnect();
}
if (getParsedUA()?.os?.name === 'iOS') {
document.body.classList.add('ios');
}
$('#main_api').change(function () {
var PrevAPI = main_api;
setTimeout(() => RA_autoconnect(PrevAPI), 100);
@@ -932,8 +960,8 @@ export function initRossMods() {
return false;
}
$(document).on('keydown', async function (event) {
await processHotkeys(event.originalEvent);
$(document).on('keydown', function (event) {
processHotkeys(event.originalEvent);
});
const hotkeyTargets = {
@@ -945,7 +973,7 @@ export function initRossMods() {
/**
* @param {KeyboardEvent} event
*/
async function processHotkeys(event) {
function processHotkeys(event) {
//Enter to send when send_textarea in focus
if (document.activeElement == hotkeyTargets['send_textarea']) {
const sendOnEnter = shouldSendOnEnter();
@@ -1009,17 +1037,20 @@ export function initRossMods() {
if (skipConfirm) {
doRegenerate();
} else {
let regenerateWithCtrlEnter = false;
const result = await Popup.show.confirm('Regenerate Message', 'Are you sure you want to regenerate the latest message?', {
customInputs: [{ id: 'regenerateWithCtrlEnter', label: 'Don\'t ask again' }],
onClose: (popup) => regenerateWithCtrlEnter = popup.inputResults.get('regenerateWithCtrlEnter') ?? false,
const popupText = `
<div class="marginBot10">Are you sure you want to regenerate the latest message?</div>
<label class="checkbox_label justifyCenter" for="regenerateWithCtrlEnter">
<input type="checkbox" id="regenerateWithCtrlEnter">
Don't ask again
</label>`;
callPopup(popupText, 'confirm').then(result => {
if (!result) {
return;
}
const regenerateWithCtrlEnter = $('#regenerateWithCtrlEnter').prop('checked');
SaveLocal(skipConfirmKey, regenerateWithCtrlEnter);
doRegenerate();
});
if (!result) {
return;
}
SaveLocal(skipConfirmKey, regenerateWithCtrlEnter);
doRegenerate();
}
return;
} else {
@@ -1099,9 +1130,6 @@ export function initRossMods() {
}
if (event.key == 'Escape') { //closes various panels
// Do not close panels if we are currently inside a popup
if (Popup.util.isPopupOpen())
return;
//dont override Escape hotkey functions from script.js
//"close edit box" and "cancel stream generation".
@@ -1130,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')

View File

@@ -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
@@ -38,7 +36,6 @@ const chara_note_position = {
function setNoteTextCommand(_, text) {
$('#extension_floating_prompt').val(text).trigger('input');
toastr.success('Author\'s Note text updated');
return '';
}
function setNoteDepthCommand(_, text) {
@@ -51,7 +48,6 @@ function setNoteDepthCommand(_, text) {
$('#extension_floating_depth').val(Math.abs(value)).trigger('input');
toastr.success('Author\'s Note depth updated');
return '';
}
function setNoteIntervalCommand(_, text) {
@@ -64,7 +60,6 @@ function setNoteIntervalCommand(_, text) {
$('#extension_floating_interval').val(Math.abs(value)).trigger('input');
toastr.success('Author\'s Note frequency updated');
return '';
}
function setNotePositionCommand(_, text) {
@@ -82,7 +77,6 @@ function setNotePositionCommand(_, text) {
$(`input[name="extension_floating_position"][value="${position}"]`).prop('checked', true).trigger('input');
toastr.info('Author\'s Note position updated');
return '';
}
function updateSettings() {
@@ -461,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);
}

View File

@@ -1,801 +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';
import { Popup, getTopmostModalLayer } from '../popup.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();
}
if (this.effectiveParserResult instanceof AutoCompleteSecondaryNameResult && !this.effectiveParserResult.forceMatch) {
// no result and matching is no forced? 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();
getTopmostModalLayer().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');
getTopmostModalLayer().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] = getTopmostModalLayer().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] = getTopmostModalLayer().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';
getTopmostModalLayer().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) {
// The first is a non-breaking space, the second is a regular space.
case ' ':
case ' ': {
if (evt.ctrlKey || evt.altKey) {
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);
}
evt.preventDefault();
evt.stopPropagation();
return;
}
break;
}
}
if (['Control', 'Shift', 'Alt'].includes(evt.key)) {
// ignore keydown on modifier keys
return;
}
switch (evt.key) {
case 'ArrowUp':
case 'ArrowDown':
case 'ArrowRight':
case 'ArrowLeft': {
if (this.isActive) {
// keyboard navigation, wait for keyup to complete cursor move
const oldText = this.textarea.value;
await new Promise(resolve=>{
window.addEventListener('keyup', resolve, { once:true });
});
if (this.selectionStart != this.textarea.selectionStart) {
this.selectionStart = this.textarea.selectionStart;
this.show(this.isReplaceable || oldText != this.textarea.value);
}
}
break;
}
default: {
if (this.isActive) {
this.text != this.textarea.value && this.show(this.isReplaceable);
}
break;
}
}
}
}

View File

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

View File

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

View File

@@ -1,214 +0,0 @@
import { SlashCommand } from '../slash-commands/SlashCommand.js';
import { AutoCompleteFuzzyScore } from './AutoCompleteFuzzyScore.js';
export class AutoCompleteOption {
/**@type {string}*/ name;
/**@type {string}*/ typeIcon;
/**@type {string}*/ type;
/**@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 = ' ', type = '') {
this.name = name;
this.typeIcon = typeIcon;
this.type = type;
}
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 stopgap = document.createElement('span'); {
stopgap.classList.add('stopgap');
stopgap.textContent = '';
li.append(stopgap);
}
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);
li.setAttribute('data-option-type', this.type);
return li;
}
/**
* @returns {DocumentFragment}
*/
renderDetails() {
// throw new Error(`${this.constructor.name}.renderDetails() is not implemented`);
const frag = document.createDocumentFragment();
const specs = document.createElement('div'); {
specs.classList.add('specs');
const name = document.createElement('div'); {
name.classList.add('name');
name.classList.add('monospace');
name.textContent = this.name;
specs.append(name);
}
frag.append(specs);
}
return frag;
}
}

View File

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

View File

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

View File

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

View File

@@ -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';
@@ -95,7 +94,7 @@ function onLockBackgroundClick(e) {
if (!chatName) {
toastr.warning('Select a chat to lock the background for it');
return '';
return;
}
const relativeBgImage = getUrlParameter(this);
@@ -103,7 +102,6 @@ function onLockBackgroundClick(e) {
saveBackgroundMetadata(relativeBgImage);
setCustomBackground();
highlightLockedBackground();
return '';
}
function onUnlockBackgroundClick(e) {
@@ -111,7 +109,6 @@ function onUnlockBackgroundClick(e) {
removeBackgroundMetadata();
unsetCustomBackground();
highlightLockedBackground();
return '';
}
function hasCustomBackground() {
@@ -321,7 +318,7 @@ async function autoBackgroundCommand() {
const options = bgTitles.map(x => ({ element: x, text: x.innerText.trim() })).filter(x => x.text.length > 0);
if (options.length == 0) {
toastr.warning('No backgrounds to choose from. Please upload some images to the "backgrounds" folder.');
return '';
return;
}
const list = options.map(option => `- ${option.text}`).join('\n');
@@ -332,12 +329,11 @@ async function autoBackgroundCommand() {
if (bestMatch.length == 0) {
toastr.warning('No match found. Please try again.');
return '';
return;
}
console.debug('Automatically choosing background:', bestMatch);
bestMatch[0].item.element.click();
return '';
}
export async function getBackgrounds() {
@@ -484,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);
}

View File

@@ -12,7 +12,6 @@ import {
getCharacters,
chat,
saveChatConditional,
saveItemizedPrompts,
} from '../script.js';
import { humanizedDateTime } from './RossAscends-mods.js';
import {
@@ -24,7 +23,6 @@ import {
saveGroupBookmarkChat,
selected_group,
} from './group-chats.js';
import { Popup } from './popup.js';
import { createTagMapFromList } from './tags.js';
import {
@@ -201,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);
@@ -240,7 +237,8 @@ async function convertSoloToGroupChat() {
return;
}
const confirm = await Popup.show.confirm('Convert to group chat', 'Are you sure you want to convert this chat to a group chat?<br />This cannot be reverted.');
const confirm = await callPopup('Are you sure you want to convert this chat to a group chat?', 'confirm');
if (!confirm) {
return;
}
@@ -336,7 +334,6 @@ async function convertSoloToGroupChat() {
if (!createChatResponse.ok) {
console.error('Group chat creation unsuccessful');
toastr.error('Group chat creation unsuccessful');
return;
}

View File

@@ -94,9 +94,6 @@ function enableBulkSelect() {
});
$(el).prepend(checkbox);
});
$('#rm_print_characters_block.group_overlay_mode_select .bogus_folder_select, #rm_print_characters_block.group_overlay_mode_select .group_select')
.addClass('disabled');
$('#rm_print_characters_block').addClass('bulk_select');
// We also need to disable the default click event for the character_select divs
$(document).on('click', '.bulk_select_checkbox', function (event) {
@@ -109,8 +106,6 @@ function enableBulkSelect() {
*/
function disableBulkSelect() {
$('.bulk_select_checkbox').remove();
$('#rm_print_characters_block.group_overlay_mode_select .bogus_folder_select, #rm_print_characters_block.group_overlay_mode_select .group_select')
.removeClass('disabled');
$('#rm_print_characters_block').removeClass('bulk_select');
}

View File

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

View File

@@ -4,6 +4,7 @@ import css from '../lib/css-parser.mjs';
import {
addCopyToCodeBlocks,
appendMediaToMessage,
callPopup,
characters,
chat,
eventSource,
@@ -34,10 +35,8 @@ import {
extractTextFromOffice,
} from './utils.js';
import { extension_settings, renderExtensionTemplateAsync, saveMetadataDebounced } from './extensions.js';
import { POPUP_RESULT, POPUP_TYPE, Popup, callGenericPopup } from './popup.js';
import { POPUP_RESULT, POPUP_TYPE, callGenericPopup } from './popup.js';
import { ScraperManager } from './scrapers.js';
import { DragAndDropHandler } from './dragdrop.js';
import { renderTemplateAsync } from './templates.js';
/**
* @typedef {Object} FileAttachment
@@ -185,19 +184,18 @@ export async function populateFileAttachment(message, inputId = 'file_form_input
const file = fileInput.files[0];
if (!file) return;
const slug = getStringHash(file.name);
const fileNamePrefix = `${Date.now()}_${slug}`;
const fileBase64 = await getBase64Async(file);
let base64Data = fileBase64.split(',')[1];
// If file is image
if (file.type.startsWith('image/')) {
const extension = file.type.split('/')[1];
const imageUrl = await saveBase64AsFile(base64Data, name2, fileNamePrefix, extension);
const imageUrl = await saveBase64AsFile(base64Data, name2, file.name, extension);
message.extra.image = imageUrl;
message.extra.inline_image = true;
} else {
const uniqueFileName = `${fileNamePrefix}.txt`;
const slug = getStringHash(file.name);
const uniqueFileName = `${Date.now()}_${slug}.txt`;
if (isConvertible(file.type)) {
try {
@@ -320,10 +318,12 @@ export function hasPendingFileAttachment() {
/**
* Displays file information in the message sending form.
* @param {File} file File object
* @returns {Promise<void>}
*/
async function onFileAttach(file) {
async function onFileAttach() {
const fileInput = document.getElementById('file_form_input');
if (!(fileInput instanceof HTMLInputElement)) return;
const file = fileInput.files[0];
if (!file) return;
const isValid = await validateFile(file);
@@ -418,7 +418,6 @@ function embedMessageFile(messageId, messageBlock) {
}
await populateFileAttachment(message, 'embed_file_input');
await eventSource.emit(event_types.MESSAGE_FILE_EMBEDDED, messageId);
appendMediaToMessage(message, messageBlock);
await saveChatConditional();
}
@@ -464,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) {
@@ -524,7 +506,7 @@ async function openExternalMediaOverridesDialog() {
return;
}
const template = $(await renderTemplateAsync('forbidMedia'));
const template = $('#forbid_media_override_template > .forbid_media_override').clone();
template.find('.forbid_media_global_state_forbidden').toggle(power_user.forbid_external_media);
template.find('.forbid_media_global_state_allowed').toggle(!power_user.forbid_external_media);
@@ -538,7 +520,7 @@ async function openExternalMediaOverridesDialog() {
template.find('#forbid_media_override_global').prop('checked', true);
}
callGenericPopup(template, POPUP_TYPE.TEXT, '', { wide: false, large: false });
callPopup(template, 'text', '', { wide: false, large: false });
}
export function getCurrentEntityId() {
@@ -566,7 +548,7 @@ export function isExternalMediaAllowed() {
return !power_user.forbid_external_media;
}
async function enlargeMessageImage() {
function enlargeMessageImage() {
const mesBlock = $(this).closest('.mes');
const mesId = mesBlock.attr('mesid');
const message = chat[mesId];
@@ -580,28 +562,14 @@ async function enlargeMessageImage() {
const img = document.createElement('img');
img.classList.add('img_enlarged');
img.src = imgSrc;
const imgHolder = document.createElement('div');
imgHolder.classList.add('img_enlarged_holder');
imgHolder.append(img);
const imgContainer = $('<div><pre><code></code></pre></div>');
imgContainer.prepend(imgHolder);
imgContainer.prepend(img);
imgContainer.addClass('img_enlarged_container');
imgContainer.find('code').addClass('txt').text(title);
const titleEmpty = !title || title.trim().length === 0;
imgContainer.find('pre').toggle(!titleEmpty);
addCopyToCodeBlocks(imgContainer);
const popup = new Popup(imgContainer, POPUP_TYPE.DISPLAY, '', { large: true, transparent: true });
popup.dlg.style.width = 'unset';
popup.dlg.style.height = 'unset';
img.addEventListener('click', () => {
const shouldZoom = !img.classList.contains('zoomed');
img.classList.toggle('zoomed', shouldZoom);
});
await popup.show();
callGenericPopup(imgContainer, POPUP_TYPE.TEXT, '', { wide: true, large: true });
}
async function deleteMessageImage() {
@@ -616,8 +584,6 @@ async function deleteMessageImage() {
const message = chat[mesId];
delete message.extra.image;
delete message.extra.inline_image;
delete message.extra.title;
delete message.extra.append_title;
mesBlock.find('.mes_img_container').removeClass('img_extra');
mesBlock.find('.mes_img').attr('src', '');
await saveChatConditional();
@@ -785,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);
@@ -872,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
@@ -887,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));
@@ -901,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);
}
}
}
@@ -1008,24 +962,49 @@ async function openAttachmentManager() {
template.find('.chatAttachmentsName').text(chatName);
}
const dragDropHandler = new DragAndDropHandler('.popup', async (files, event) => {
let selectedTarget = ATTACHMENT_SOURCE.GLOBAL;
const targets = getAvailableTargets();
const targetSelectTemplate = $(await renderExtensionTemplateAsync('attachments', 'files-dropped', { count: files.length, targets: targets }));
targetSelectTemplate.find('.droppedFilesTarget').on('input', function () {
selectedTarget = String($(this).val());
function addDragAndDrop() {
$(document.body).on('dragover', '.dialogue_popup', (event) => {
event.preventDefault();
event.stopPropagation();
$(event.target).closest('.dialogue_popup').addClass('dragover');
});
const result = await callGenericPopup(targetSelectTemplate, POPUP_TYPE.CONFIRM, '', { wide: false, large: false, okButton: 'Upload', cancelButton: 'Cancel' });
if (result !== POPUP_RESULT.AFFIRMATIVE) {
console.log('File upload cancelled');
return;
}
for (const file of files) {
await uploadFileAttachmentToServer(file, selectedTarget);
}
renderAttachments();
});
$(document.body).on('dragleave', '.dialogue_popup', (event) => {
event.preventDefault();
event.stopPropagation();
$(event.target).closest('.dialogue_popup').removeClass('dragover');
});
$(document.body).on('drop', '.dialogue_popup', async (event) => {
event.preventDefault();
event.stopPropagation();
$(event.target).closest('.dialogue_popup').removeClass('dragover');
const files = Array.from(event.originalEvent.dataTransfer.files);
let selectedTarget = ATTACHMENT_SOURCE.GLOBAL;
const targets = getAvailableTargets();
const targetSelectTemplate = $(await renderExtensionTemplateAsync('attachments', 'files-dropped', { count: files.length, targets: targets }));
targetSelectTemplate.find('.droppedFilesTarget').on('input', function () {
selectedTarget = String($(this).val());
});
const result = await callGenericPopup(targetSelectTemplate, POPUP_TYPE.CONFIRM, '', { wide: false, large: false, okButton: 'Upload', cancelButton: 'Cancel' });
if (result !== POPUP_RESULT.AFFIRMATIVE) {
console.log('File upload cancelled');
return;
}
for (const file of files) {
await uploadFileAttachmentToServer(file, selectedTarget);
}
renderAttachments();
});
}
function removeDragAndDrop() {
$(document.body).off('dragover', '.shadow_popup');
$(document.body).off('dragleave', '.shadow_popup');
$(document.body).off('drop', '.shadow_popup');
}
let sortField = localStorage.getItem('DataBank_sortField') || 'created';
let sortOrder = localStorage.getItem('DataBank_sortOrder') || 'desc';
@@ -1048,83 +1027,15 @@ async function openAttachmentManager() {
localStorage.setItem('DataBank_sortOrder', sortOrder);
renderAttachments();
});
function handleBulkAction(action) {
return async () => {
const selectedAttachments = document.querySelectorAll('.attachmentListItemCheckboxContainer .attachmentListItemCheckbox:checked');
if (selectedAttachments.length === 0) {
toastr.info('No attachments selected.', 'Data Bank');
return;
}
if (action.confirmMessage) {
const confirm = await callGenericPopup(action.confirmMessage, POPUP_TYPE.CONFIRM);
if (confirm !== POPUP_RESULT.AFFIRMATIVE) {
return;
}
}
const includeDisabled = true;
const attachments = getDataBankAttachments(includeDisabled);
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 action.perform(attachment, source);
});
document.querySelectorAll('.attachmentListItemCheckbox, .attachmentsBulkEditCheckbox').forEach(checkbox => {
if (checkbox instanceof HTMLInputElement) {
checkbox.checked = false;
}
});
await renderAttachments();
};
}
template.find('.bulkActionDisable').on('click', handleBulkAction({
perform: (attachment) => disableAttachment(attachment, () => { }),
}));
template.find('.bulkActionEnable').on('click', handleBulkAction({
perform: (attachment) => enableAttachment(attachment, () => { }),
}));
template.find('.bulkActionDelete').on('click', handleBulkAction({
confirmMessage: 'Are you sure you want to delete the selected attachments?',
perform: async (attachment, source) => await deleteAttachment(attachment, source, () => { }, false),
}));
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();
await renderAttachments();
await callGenericPopup(template, POPUP_TYPE.TEXT, '', { wide: true, large: true, okButton: 'Close', allowVerticalScrolling: true });
addDragAndDrop();
await callGenericPopup(template, POPUP_TYPE.TEXT, '', { wide: true, large: true, okButton: 'Close' });
cleanupFn();
dragDropHandler.destroy();
removeDragAndDrop();
}
/**
@@ -1188,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);
@@ -1245,8 +1156,6 @@ export async function uploadFileAttachmentToServer(file, target) {
saveSettingsDebounced();
break;
}
return fileUrl;
}
function ensureAttachmentsExist() {
@@ -1274,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 [];
}
/**
@@ -1467,11 +1370,10 @@ jQuery(function () {
});
}
callGenericPopup(wrapper, POPUP_TYPE.TEXT, '', { wide: true, large: true });
callPopup(wrapper, 'text', '', { wide: true, large: true });
});
$(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');
});
@@ -1505,28 +1407,8 @@ jQuery(function () {
$(document).on('click', '.mes_img_enlarge', enlargeMessageImage);
$(document).on('click', '.mes_img_delete', deleteMessageImage);
$('#file_form_input').on('change', async () => {
const fileInput = document.getElementById('file_form_input');
if (!(fileInput instanceof HTMLInputElement)) return;
const file = fileInput.files[0];
await onFileAttach(file);
});
$('#file_form_input').on('change', onFileAttach);
$('#file_form').on('reset', function () {
$('#file_form').addClass('displayNone');
});
document.getElementById('send_textarea').addEventListener('paste', async function (event) {
if (event.clipboardData.files.length === 0) {
return;
}
event.preventDefault();
event.stopPropagation();
const fileInput = document.getElementById('file_form_input');
if (!(fileInput instanceof HTMLInputElement)) return;
fileInput.files = event.clipboardData.files;
await onFileAttach(fileInput.files[0]);
});
});

View File

@@ -1,107 +0,0 @@
import { debounce_timeout } from './constants.js';
/**
* Drag and drop handler
*
* Can be used on any element, enabling drag&drop styling and callback on drop.
*/
export class DragAndDropHandler {
/** @private @type {JQuery.Selector} */ selector;
/** @private @type {(files: File[], event:JQuery.DropEvent<HTMLElement, undefined, any, any>) => void} */ onDropCallback;
/** @private @type {NodeJS.Timeout} Remark: Not actually NodeJS timeout, but it's close */ dragLeaveTimeout;
/** @private @type {boolean} */ noAnimation;
/**
* Create a DragAndDropHandler
* @param {JQuery.Selector} selector - The CSS selector for the elements to enable drag and drop
* @param {(files: File[], event:JQuery.DropEvent<HTMLElement, undefined, any, any>) => void} onDropCallback - The callback function to handle the drop event
*/
constructor(selector, onDropCallback, { noAnimation = false } = {}) {
this.selector = selector;
this.onDropCallback = onDropCallback;
this.dragLeaveTimeout = null;
this.noAnimation = noAnimation;
this.init();
}
/**
* Destroy the drag and drop functionality
*/
destroy() {
if (this.selector === 'body') {
$(document.body).off('dragover', this.handleDragOver.bind(this));
$(document.body).off('dragleave', this.handleDragLeave.bind(this));
$(document.body).off('drop', this.handleDrop.bind(this));
} else {
$(document.body).off('dragover', this.selector, this.handleDragOver.bind(this));
$(document.body).off('dragleave', this.selector, this.handleDragLeave.bind(this));
$(document.body).off('drop', this.selector, this.handleDrop.bind(this));
}
$(this.selector).remove('drop_target no_animation');
}
/**
* Initialize the drag and drop functionality
* Automatically called on construction
* @private
*/
init() {
if (this.selector === 'body') {
$(document.body).on('dragover', this.handleDragOver.bind(this));
$(document.body).on('dragleave', this.handleDragLeave.bind(this));
$(document.body).on('drop', this.handleDrop.bind(this));
} else {
$(document.body).on('dragover', this.selector, this.handleDragOver.bind(this));
$(document.body).on('dragleave', this.selector, this.handleDragLeave.bind(this));
$(document.body).on('drop', this.selector, this.handleDrop.bind(this));
}
$(this.selector).addClass('drop_target');
if (this.noAnimation) $(this.selector).addClass('no_animation');
}
/**
* @param {JQuery.DragOverEvent<HTMLElement, undefined, any, any>} event - The dragover event
* @private
*/
handleDragOver(event) {
event.preventDefault();
event.stopPropagation();
clearTimeout(this.dragLeaveTimeout);
$(this.selector).addClass('drop_target dragover');
if (this.noAnimation) $(this.selector).addClass('no_animation');
}
/**
* @param {JQuery.DragLeaveEvent<HTMLElement, undefined, any, any>} event - The dragleave event
* @private
*/
handleDragLeave(event) {
event.preventDefault();
event.stopPropagation();
// Debounce the removal of the class, so it doesn't "flicker" on dragging over
clearTimeout(this.dragLeaveTimeout);
this.dragLeaveTimeout = setTimeout(() => {
$(this.selector).removeClass('dragover');
}, debounce_timeout.quick);
}
/**
* @param {JQuery.DropEvent<HTMLElement, undefined, any, any>} event - The drop event
* @private
*/
handleDrop(event) {
event.preventDefault();
event.stopPropagation();
clearTimeout(this.dragLeaveTimeout);
$(this.selector).removeClass('dragover');
const files = Array.from(event.originalEvent.dataTransfer.files);
this.onDropCallback(files, event);
}
}

View File

@@ -1,162 +0,0 @@
/** @type {CSSStyleSheet} */
let dynamicStyleSheet = null;
/** @type {CSSStyleSheet} */
let dynamicExtensionStyleSheet = null;
/**
* An observer that will check if any new stylesheets are added to the head
* @type {MutationObserver}
*/
const observer = new MutationObserver(mutations => {
mutations.forEach(mutation => {
if (mutation.type !== 'childList') return;
mutation.addedNodes.forEach(node => {
if (node instanceof HTMLLinkElement && node.tagName === 'LINK' && node.rel === 'stylesheet') {
node.addEventListener('load', () => {
try {
applyDynamicFocusStyles(node.sheet);
} catch (e) {
console.warn('Failed to process new stylesheet:', e);
}
});
}
});
});
});
/**
* Generates dynamic focus styles based on the given stylesheet, taking its hover styles as reference
*
* @param {CSSStyleSheet} styleSheet - The stylesheet to process
* @param {object} [options] - Optional configuration options
* @param {boolean} [options.fromExtension=false] - Indicates if the styles are from an extension
*/
function applyDynamicFocusStyles(styleSheet, { fromExtension = false } = {}) {
/** @type {{baseSelector: string, rule: CSSStyleRule}[]} */
const hoverRules = [];
/** @type {Set<string>} */
const focusRules = new Set();
const PLACEHOLDER = ':__PLACEHOLDER__';
/**
* Processes the CSS rules and separates selectors for hover and focus
* @param {CSSRuleList} rules - The CSS rules to process
*/
function processRules(rules) {
Array.from(rules).forEach(rule => {
if (rule instanceof CSSImportRule) {
// Make sure that @import rules are processed recursively
processImportedStylesheet(rule.styleSheet);
} else if (rule instanceof CSSStyleRule) {
// Separate multiple selectors on a rule
const selectors = rule.selectorText.split(',').map(s => s.trim());
// We collect all hover and focus rules to be able to later decide which hover rules don't have a matching focus rule
selectors.forEach(selector => {
const isHover = selector.includes(':hover'), isFocus = selector.includes(':focus');
if (isHover && isFocus) {
// We currently do nothing here. Rules containing both hover and focus are very specific and should never be automatically touched
}
else if (isHover) {
const baseSelector = selector.replace(':hover', PLACEHOLDER).trim();
hoverRules.push({ baseSelector, rule });
} else if (isFocus) {
// We need to make sure that we remember all existing :focus, :focus-within and :focus-visible rules
const baseSelector = selector.replace(':focus-within', PLACEHOLDER).replace(':focus-visible', PLACEHOLDER).replace(':focus', PLACEHOLDER).trim();
focusRules.add(baseSelector);
}
});
} else if (rule instanceof CSSMediaRule || rule instanceof CSSSupportsRule) {
// Recursively process nested rules
processRules(rule.cssRules);
}
});
}
/**
* Processes the CSS rules of an imported stylesheet recursively
* @param {CSSStyleSheet} sheet - The imported stylesheet to process
*/
function processImportedStylesheet(sheet) {
if (sheet && sheet.cssRules) {
processRules(sheet.cssRules);
}
}
processRules(styleSheet.cssRules);
/** @type {CSSStyleSheet} */
let targetStyleSheet = null;
// Now finally create the dynamic focus rules
hoverRules.forEach(({ baseSelector, rule }) => {
if (!focusRules.has(baseSelector)) {
// Only initialize the dynamic stylesheet if needed
targetStyleSheet ??= getDynamicStyleSheet({ fromExtension });
// The closest keyboard-equivalent to :hover styling is utilizing the :focus-visible rule from modern browsers.
// It let's the browser decide whether a focus highlighting is expected and makes sense.
// So we take all :hover rules that don't have a manually defined focus rule yet, and create their
// :focus-visible counterpart, which will make the styling work the same for keyboard and mouse.
// If something like :focus-within or a more specific selector like `.blah:has(:focus-visible)` for elements inside,
// it should be manually defined in CSS.
const focusSelector = rule.selectorText.replace(/:hover/g, ':focus-visible');
const focusRule = `${focusSelector} { ${rule.style.cssText} }`;
try {
targetStyleSheet.insertRule(focusRule, targetStyleSheet.cssRules.length);
} catch (e) {
console.warn('Failed to insert focus rule:', e);
}
}
});
}
/**
* Retrieves the stylesheet that should be used for dynamic rules
*
* @param {object} options - The options object
* @param {boolean} [options.fromExtension=false] - Indicates whether the rules are coming from extensions
* @return {CSSStyleSheet} The dynamic stylesheet
*/
function getDynamicStyleSheet({ fromExtension = false } = {}) {
if (fromExtension) {
if (!dynamicExtensionStyleSheet) {
const styleSheetElement = document.createElement('style');
styleSheetElement.setAttribute('id', 'dynamic-extension-styles');
document.head.appendChild(styleSheetElement);
dynamicExtensionStyleSheet = styleSheetElement.sheet;
}
return dynamicExtensionStyleSheet;
} else {
if (!dynamicStyleSheet) {
const styleSheetElement = document.createElement('style');
styleSheetElement.setAttribute('id', 'dynamic-styles');
document.head.appendChild(styleSheetElement);
dynamicStyleSheet = styleSheetElement.sheet;
}
return dynamicStyleSheet;
}
}
/**
* Initializes dynamic styles for ST
*/
export function initDynamicStyles() {
// Start observing the head for any new added stylesheets
observer.observe(document.head, {
childList: true,
subtree: true
});
// Process all stylesheets on initial load
Array.from(document.styleSheets).forEach(sheet => {
try {
applyDynamicFocusStyles(sheet, { fromExtension: sheet.href.toLowerCase().includes('scripts/extensions') });
} catch (e) {
console.warn('Failed to process stylesheet on initial load:', e);
}
});
}

View File

@@ -1,6 +1,5 @@
import { eventSource, event_types, saveSettings, saveSettingsDebounced, getRequestHeaders, animation_duration } from '../script.js';
import { callPopup, eventSource, event_types, saveSettings, saveSettingsDebounced, getRequestHeaders, animation_duration } from '../script.js';
import { hideLoader, showLoader } from './loader.js';
import { POPUP_RESULT, POPUP_TYPE, Popup, callGenericPopup } from './popup.js';
import { renderTemplate, renderTemplateAsync } from './templates.js';
import { isSubsetOf, setValueByPath } from './utils.js';
export {
@@ -123,9 +122,7 @@ const extension_settings = {
custom: [],
},
dice: {},
/** @type {import('./char-data.js').RegexScriptData[]} */
regex: [],
character_allowed_regex: [],
tts: {},
sd: {
prompts: {},
@@ -348,12 +345,14 @@ function autoConnectInputHandler() {
saveSettingsDebounced();
}
async function addExtensionsButtonAndMenu() {
const buttonHTML = await renderTemplateAsync('wandButton');
const extensionsMenuHTML = await renderTemplateAsync('wandMenu');
function addExtensionsButtonAndMenu() {
const buttonHTML =
'<div id="extensionsMenuButton" style="display: none;" class="fa-solid fa-magic-wand-sparkles" title="Extras Extensions" /></div>';
const extensionsMenuHTML = '<div id="extensionsMenu" class="options-content" style="display: none;"></div>';
$(document.body).append(extensionsMenuHTML);
$('#leftSendForm').append(buttonHTML);
$('#leftSendForm').prepend(buttonHTML);
const button = $('#extensionsMenuButton');
const dropdown = $('#extensionsMenu');
@@ -504,7 +503,7 @@ function addExtensionScript(name, manifest) {
* @param {boolean} isDisabled - Whether the extension is disabled or not.
* @param {boolean} isExternal - Whether the extension is external or not.
* @param {string} checkboxClass - The class for the checkbox HTML element.
* @return {Promise<string>} - The HTML string that represents the extension.
* @return {string} - The HTML string that represents the extension.
*/
async function generateExtensionHtml(name, manifest, isActive, isDisabled, isExternal, checkboxClass) {
const displayName = manifest.display_name;
@@ -556,10 +555,8 @@ async function generateExtensionHtml(name, manifest, isActive, isDisabled, isExt
} else if (!isDisabled) { // Neither active nor disabled
const requirements = new Set(manifest.requires);
modules.forEach(x => requirements.delete(x));
if (requirements.size > 0) {
const requirementsString = DOMPurify.sanitize([...requirements].join(', '));
extensionHtml += `<p>Missing modules: <span class="failure">${requirementsString}</span></p>`;
}
const requirementsString = DOMPurify.sanitize([...requirements].join(', '));
extensionHtml += `<p>Missing modules: <span class="failure">${requirementsString}</span></p>`;
}
return extensionHtml;
@@ -634,18 +631,7 @@ async function showExtensionsDetails() {
${htmlDefault}
${htmlExternal}
`;
/** @type {import('./popup.js').CustomPopupButton} */
const updateAllButton = {
text: 'Update all',
appendAtEnd: true,
action: async () => {
requiresReload = true;
await autoUpdateExtensions(true);
popup.complete(POPUP_RESULT.AFFIRMATIVE);
},
};
const popup = new Popup(`<div class="extensions_info">${html}</div>`, POPUP_TYPE.TEXT, '', { okButton: 'Close', wide: true, large: true, customButtons: [updateAllButton], allowVerticalScrolling: true });
popupPromise = popup.show();
popupPromise = callPopup(`<div class="extensions_info">${html}</div>`, 'text', '', { okButton: 'Close', wide: true, large: true });
} catch (error) {
toastr.error('Error loading extensions. See browser console for details.');
console.error(error);
@@ -714,8 +700,8 @@ async function updateExtension(extensionName, quiet) {
async function onDeleteClick() {
const extensionName = $(this).data('name');
// use callPopup to create a popup for the user to confirm before delete
const confirmation = await callGenericPopup(`Are you sure you want to delete ${extensionName}?`, POPUP_TYPE.CONFIRM, '', {});
if (confirmation === POPUP_RESULT.AFFIRMATIVE) {
const confirmation = await callPopup(`Are you sure you want to delete ${extensionName}?`, 'delete_extension');
if (confirmation) {
await deleteExtension(extensionName);
}
}
@@ -811,7 +797,7 @@ async function loadExtensionSettings(settings, versionChanged) {
manifests = await getManifests(extensionNames);
if (versionChanged) {
await autoUpdateExtensions(false);
await autoUpdateExtensions();
}
await activateExtensions();
@@ -874,12 +860,7 @@ async function checkForExtensionUpdates(force) {
}
}
/**
* Updates all 3rd-party extensions that have auto-update enabled.
* @param {boolean} forceAll Force update all even if not auto-updating
* @returns {Promise<void>}
*/
async function autoUpdateExtensions(forceAll) {
async function autoUpdateExtensions() {
if (!Object.values(manifests).some(x => x.auto_update)) {
return;
}
@@ -887,7 +868,7 @@ async function autoUpdateExtensions(forceAll) {
const banner = toastr.info('Auto-updating extensions. This may take several minutes.', 'Please wait...', { timeOut: 10000, extendedTimeOut: 10000 });
const promises = [];
for (const [id, manifest] of Object.entries(manifests)) {
if ((forceAll || manifest.auto_update) && id.startsWith('third-party')) {
if (manifest.auto_update && id.startsWith('third-party')) {
console.debug(`Auto-updating 3rd-party extension: ${manifest.display_name} (${id})`);
promises.push(updateExtension(id.replace('third-party', ''), true));
}
@@ -978,8 +959,8 @@ export async function writeExtensionField(characterId, key, value) {
}
}
jQuery(async function () {
await addExtensionsButtonAndMenu();
jQuery(function () {
addExtensionsButtonAndMenu();
$('#extensionsMenuButton').css('display', 'flex');
$('#extensions_connect').on('click', connectClickHandler);
@@ -1007,14 +988,14 @@ jQuery(async function () {
<p><b>Disclaimer:</b> Please be aware that using external extensions can have unintended side effects and may pose security risks. Always make sure you trust the source before importing an extension. We are not responsible for any damage caused by third-party extensions.</p>
<br>
<p>Example: <tt> https://github.com/author/extension-name </tt></p>`;
const input = await callGenericPopup(html, POPUP_TYPE.INPUT, '');
const input = await callPopup(html, 'input');
if (!input) {
console.debug('Extension install cancelled');
return;
}
const url = String(input).trim();
const url = input.trim();
await installExtension(url);
});
});

View File

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

View File

@@ -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());
@@ -440,9 +396,5 @@ jQuery(async () => {
});
windowHtml.find('#assets_filters').hide();
$('#assets_container').append(windowHtml);
eventSource.on(event_types.OPEN_CHARACTER_LIBRARY, async (forceDefault) => {
openCharacterBrowser(forceDefault);
});
$('#extensions_settings').append(windowHtml);
});

View File

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

View File

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

View File

@@ -1,11 +1,11 @@
<div id="assets_ui">
<div class="inline-drawer">
<div class="inline-drawer-toggle inline-drawer-header">
<b data-i18n="Download Extensions & Assets">Download Extensions & Assets</b>
<b>Download Extensions & Assets</b>
<div class="inline-drawer-icon fa-solid fa-circle-chevron-down down"></div>
</div>
<div class="inline-drawer-content">
<label for="assets-json-url-field" data-i18n="Assets URL">Assets URL</label>
<label for="assets-json-url-field">Assets URL</label>
<div class="assets-connect-div">
<input id="assets-json-url-field" class="text_pole widthUnset flex1">
<i id="assets-connect-button" class="menu_button fa-solid fa-plug-circle-exclamation fa-xl redOverlayGlow"></i>
@@ -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>
<span data-i18n="Characters">Characters</span>
</div>
</div>
<div class="inline-drawer-content" id="assets_menu">
</div>

View File

@@ -1,4 +0,0 @@
<div id="attachFile" class="list-group-item flex-container flexGap5" title="Attach a file or image to a current chat.">
<div class="fa-fw fa-solid fa-paperclip extensionsMenuExtensionButton"></div>
<span data-i18n="Attach a File">Attach a File</span>
</div>

View File

@@ -1,4 +1,7 @@
<div id="attachFile" class="list-group-item flex-container flexGap5" title="Attach a file or image to a current chat.">
<div class="fa-fw fa-solid fa-paperclip extensionsMenuExtensionButton"></div>
<span data-i18n="Attach a File">Attach a File</span>
</div>
<div id="manageAttachments" class="list-group-item flex-container flexGap5" title="View global, character, or data files.">
<div class="fa-fw fa-solid fa-book-open-reader extensionsMenuExtensionButton"></div>

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