Merge pull request #1694 from SillyTavern/staging

Staging
This commit is contained in:
Cohee 2024-01-14 20:52:10 +02:00 committed by GitHub
commit 5f5c066373
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
80 changed files with 6537 additions and 2957 deletions

View File

@ -8,4 +8,7 @@ trim_trailing_whitespace = true
[*.{js, conf, json}]
charset = utf-8
indent_style = space
indent_size = 4
indent_size = 4
[*.md]
trim_trailing_whitespace = false

314
.github/readme-ja_jp.md vendored Normal file
View File

@ -0,0 +1,314 @@
[English](readme.md) | [中文](readme-zh_cn.md) | 日本語
![SillyTavern-Banner](https://github.com/SillyTavern/SillyTavern/assets/18619528/c2be4c3f-aada-4f64-87a3-ae35a68b61a4)
モバイルフレンドリーなレイアウト、マルチAPIKoboldAI/CPP、Horde、NovelAI、Ooba、OpenAI、OpenRouter、Claude、Scale、VN ライクな Waifu モード、Stable Diffusion、TTS、WorldInfo伝承本、カスタマイズ可能な UI、自動翻訳、あなたにとって必要とする以上のプロンプトオプションサードパーティの拡張機能をインストールする機能。
[TavernAI](https://github.com/TavernAI/TavernAI) 1.2.8 のフォークに基づいています
## 重要ニュース!
1. 私たちは[ドキュメント website](https://docs.sillytavern.app/) を作成し、ほとんどの質問にお答えしています。
2. アップデートしたらに拡張機能を見失った?リリースバージョン 1.10.6 以降、これまで内蔵されていた拡張機能のほとんどがダウンロード可能なアドオンに変更されました。ダウンロードは、拡張機能パネル(トップバーのスタックドブロックアイコン)にある内蔵の "Download Extensions and Assets" メニューから行えます。
### Cohee、RossAscends、SillyTavern コミュニティがお届けします
### SillyTavern または TavernAI とは何ですか?
SillyTavern は、あなたのコンピュータ(および Android スマホ)にインストールできるユーザーインターフェイスで、テキスト生成 AI と対話したり、あなたやコミュニティが作成したキャラクターとチャットやロールプレイをすることができます。
SillyTavern は TavernAI 1.2.8 のフォークで、より活発な開発が行われており、多くの主要な機能が追加されています。現時点では、これらは完全に独立したプログラムと考えることができます。
### ブランチ
SillyTavern は、すべてのユーザーにスムーズな体験を保証するために、2 つのブランチシステムを使用して開発されています。
* release -🌟 **ほとんどのユーザーにお勧め。** これは最も安定した推奨ブランチで、メジャーリリースがプッシュされた時のみ更新されます。大半のユーザーに適しています。
* staging - ⚠️ **カジュアルな使用にはお勧めしない。** このブランチには最新の機能がありますが、いつ壊れるかわからないので注意してください。パワーユーザーとマニア向けです。
git CLI の使い方に慣れていなかったり、ブランチが何なのかわからなかったりしても、心配はいりません!リリースブランチが常に望ましい選択肢となります。
### Tavern 以外に何が必要ですか?
Tavern は単なるユーザーインターフェイスなので、それだけでは役に立ちません。ロールプレイキャラクターとして機能する AI システムのバックエンドにアクセスする必要があります。様々なバックエンドがサポートされています: OpenAPI API (GPT)、KoboldAI (ローカルまたは Google Colab 上で動作)、その他。詳しくは [FAQ](https://docs.sillytavern.app/usage/faq/) をご覧ください。
### Tavern を実行するには、強力な PC が必要ですか?
Tavern は単なるユーザーインターフェイスであり、必要なハードウェアはごくわずかです。パワフルである必要があるのは、AI システムのバックエンドです。
## モバイルサポート
> **注**
> **このフォークは Termux を使って Android スマホでネイティブに実行できます。ArroganceComplex#2659 のガイドを参照してください:**
<https://rentry.org/STAI-Termux>
## ご質問やご提案
### コミュニティ Discord サーバーを開設しました
サポートを受け、お気に入りのキャラクターやプロンプトを共有する:
### [参加](https://discord.gg/RZdyAEUPvj)
***
開発者と直接連絡を取る:
* Discord: cohee または rossascends
* Reddit: /u/RossAscends または /u/sillylossy
* [GitHub issue を投稿](https://github.com/SillyTavern/SillyTavern/issues)
## このバージョンには以下が含まれる
* 大幅に修正された TavernAI 1.2.8 (コードの 50% 以上が書き換えまたは最適化されています)
* スワイプ
* グループチャット: キャラクター同士が会話できるマルチボットルーム
* チャットチェックポイント / ブランチ
* 高度なKoboldAI / TextGen生成設定と、コミュニティが作成した多くのプリセット
* ワールド情報サポート: 豊富な伝承を作成したり、キャラクターカードにトークンを保存したりできます
* [OpenRouter](https://openrouter.ai) 各種 API(Claude、GPT-4/3.5 など)の接続
* [Oobabooga's TextGen WebUI](https://github.com/oobabooga/text-generation-webui) API 接続
* [AI Horde](https://horde.koboldai.net/) 接続
* プロンプト生成フォーマットの調整
## 拡張機能
SillyTavern は拡張性をサポートしており、[SillyTavern Extras API](https://github.com/SillyTavern/SillyTavern-extras) を介していくつかの追加AIモジュールをホストしています
* 作者ノート/キャラクターバイアス
* キャラクターの感情表現(スプライト)
* チャット履歴の自動サマリー
* チャットに画像を送り、AI が内容を解釈する
* Stable Diffusion 画像生成 (5 つのチャット関連プリセットと 'free mode')
* AI 応答メッセージの音声合成ElevenLabs、Silero、または OS のシステム TTS 経由)
含まれている拡張機能の完全なリストとその使い方のチュートリアルは [Docs](https://docs.sillytavern.app/) にあります。
## RossAscends による UI/CSS/クオリティオブライフの調整
* iOS 用に最適化されたモバイル UI で、ホーム画面へのショートカット保存とフルスクリーンモードでの起動をサポート。
* ホットキー
* Up = チャットの最後のメッセージを編集する
* Ctrl+Up = チャットで最後のユーザーメッセージを編集する
* Left = 左スワイプ
* Right = 右スワイプ (注: チャットバーに何か入力されている場合、スワイプホットキーが無効になります)
* Ctrl+Left = ローカルに保存された変数を見る(ブラウザのコンソールウィンドウにて)
* Enter (チャットバー選択時) = AI にメッセージを送る
* Ctrl+Enter = 最後の AI 応答を再生成する
* ユーザー名の変更と文字の削除でページが更新されなくなりました。
* ページロード時に API に自動的に接続するかどうかを切り替えます。
* ページの読み込み時に、最近見た文字を自動的に読み込むかどうかを切り替えます。
* より良いトークンカウンター - 保存されていないキャラクターに対して機能し、永続的なトークンと一時的なトークンの両方を表示する。
* より良い過去のチャット
* 新しいチャットのファイル名は、"(文字) - (作成日)" という読みやすい形式で保存されます
* チャットのプレビューが 40 文字から 300 文字に増加。
* 文字リストの並べ替えに複数のオプション(名前順、作成日順、チャットサイズ順)があります。
* デフォルトでは、左右の設定パネルはクリックすると閉じます。
* ナビパネルのロックをクリックすると、パネルが開いたままになり、この設定はセッションをまたいで記憶されます。
* ナビパネルの開閉状態もセッションをまたいで保存されます。
* カスタマイズ可能なチャット UI:
* 新しいメッセージが届いたときにサウンドを再生する
* 丸型、長方形のアバタースタイルの切り替え
* デスクトップのチャットウィンドウを広くする
* オプションの半透明ガラス風パネル
* 'メインテキスト'、'引用テキスト'、'斜体テキスト'のページカラーをカスタマイズ可能。
* カスタマイズ可能な UI 背景色とぼかし量
## インストール
*注: このソフトウェアはローカルにインストールすることを目的としており、colab や他のクラウドノートブックサービス上では十分にテストされていません。*
> **警告**
> WINDOWS が管理しているフォルダProgram Files、System32 など)にはインストールしないでください
> START.BAT を管理者権限で実行しないでください
### Windows
Git 経由でのインストール(更新を容易にするため推奨)
きれいな写真付きのわかりやすいガイド:
<https://docs.sillytavern.app/installation/windows/>
1. [NodeJS](https://nodejs.org/en) をインストールする(最新の LTS 版を推奨)
2. [GitHub Desktop](https://central.github.com/deployments/desktop/desktop/latest/win32) をインストールする
3. Windows エクスプローラーを開く (`Win+E`)
4. Windows によって制御または監視されていないフォルダを参照または作成する。(例: C:\MySpecialFolder\
5. 上部のアドレスバーをクリックし、`cmd` と入力して Enter キーを押し、そのフォルダーの中にコマンドプロンプトを開きます。
6. 黒いボックスコマンドプロンプトがポップアップしたら、そこに以下のいずれかを入力し、Enter を押します:
* Release ブランチの場合: `git clone https://github.com/SillyTavern/SillyTavern -b release`
* Staging ブランチの場合: `git clone https://github.com/SillyTavern/SillyTavern -b staging`
7. すべてをクローンしたら、`Start.bat` をダブルクリックして、NodeJS に要件をインストールさせる。
8. サーバーが起動し、SillyTavern がブラウザにポップアップ表示されます。
ZIP ダウンロードによるインストール(推奨しない)
1. [NodeJS](https://nodejs.org/en) をインストールする(最新の LTS 版を推奨)
2. GitHub のリポジトリから zip をダウンロードする。(`ソースコード(zip)` は [Releases](https://github.com/SillyTavern/SillyTavern/releases/latest) から入手)
3. お好きなフォルダに解凍してください
4. `Start.bat` をダブルクリックまたはコマンドラインで実行する。
5. サーバーがあなたのためにすべてを準備したら、ブラウザのタブを開きます。
### Linux
1. `node -v` を実行して、Node.js v18 以上(最新の [LTS バージョン](https://nodejs.org/en/download/) を推奨)がインストールされていることを確認してください。
または、[Node Version Manager](https://github.com/nvm-sh/nvm#installing-and-updating) スクリプトを使用して、迅速かつ簡単に Node のインストールを管理します。
2. `start.sh` スクリプトを実行する。
3. お楽しみください。
## API キー管理
SillyTavern は API キーをサーバーディレクトリの `secrets.json` ファイルに保存します。
デフォルトでは、入力後にページをリロードしても、フロントエンドには表示されません。
API ブロックのボタンをクリックして、キーを閲覧できるようにする:
1. ファイル `config.yaml``allowKeysExposure` の値を `true` に設定する。
2. SillyTavern サーバを再起動します。
## リモート接続
SillyTavern をスマホで使用しながら、同じ Wifi ネットワーク上で ST サーバーを PC で実行したい場合に使用します。
しかし、これはどこからでもリモート接続を許可するために使用することができます。
**重要: SillyTavern はシングルユーザーのプログラムなので、ログインすれば誰でもすべてのキャラクターとチャットを見ることができ、UI 内で設定を変更することができます。**
### 1. ホワイトリスト IP の管理
* SillyTavern のベースインストールフォルダ内に `whitelist.txt` という新しいテキストファイルを作成します。
* テキストエディタでこのファイルを開き、接続を許可したい IP のリストを追加します。
*個々の IP とワイルドカード IP 範囲の両方が受け入れられる。例:*
```txt
192.168.0.1
192.168.0.20
```
または
```txt
192.168.0.*
```
(上記のワイルドカード IP 範囲は、ローカルネットワーク上のどのデバイスでも)
CIDR マスクも受け付ける10.0.0.0/24
* `whitelist.txt` ファイルを保存する。
* TAI サーバーを再起動する。
これでファイルに指定された IP を持つデバイスが接続できるようになる。
*注: `config.yaml` にも `whitelist` 配列があり、同じように使うことができるが、`whitelist.txt` が存在する場合、この配列は無視される。*
### 2. ST ホストマシンの IP の取得
ホワイトリストの設定後、ST ホストデバイスの IP が必要になります。
ST ホストデバイスが同じ無線 LAN ネットワーク上にある場合、ST ホストの内部無線 LAN IP を使用します:
* Windows の場合: ウィンドウズボタン > 検索バーに `cmd.exe` と入力 > コンソールに `ipconfig` と入力して Enter > `IPv4` のリストを探す。
同じネットワーク上にいない状態で、ホストしているSTに接続したい場合は、STホスト機器のパブリックIPが必要です。
* ST ホストデバイスを使用中に、[このページ](https://whatismyipaddress.com/)にアクセスし、`IPv4` を探してください。これはリモートデバイスからの接続に使用するものです。
### 3. リモートデバイスを ST ホストマシンに接続します。
最終的に使用する IP が何であれ、その IP アドレスとポート番号をリモートデバイスのウェブブラウザに入力します。
同じ無線 LAN ネットワーク上の ST ホストの典型的なアドレスは以下のようになります:
`http://192.168.0.5:8000`
http:// を使用し、https:// は使用しないでください
### ST をすべての IP に開放する
これはお勧めしませんが、`config.yaml` を開き、`whitelistMode` を `false` に変更してください。
SillyTavern のベースインストールフォルダにある `whitelist.txt` が存在する場合は削除(または名前の変更)する必要があります。
これは通常安全ではないので、これを行う際にはユーザー名とパスワードを設定する必要があります。
ユーザー名とパスワードは `config.yaml` で設定します。
ST サーバを再起動すると、ユーザ名とパスワードさえ知っていれば、IP に関係なくどのデバイスでも ST サーバに接続できるようになる。
### まだ接続できませんか?
* `config.yaml` で見つかったポートに対して、インバウンド/アウトバウンドのファイアウォールルールを作成します。これをルーターのポートフォワーディングと間違えないでください。そうしないと、誰かがあなたのチャットログを見つける可能性があり、それはマジで止めましょう。
* 設定 > ネットワークとインターネット > イーサネットで、プライベートネットワークのプロファイルタイプを有効にします。そうしないと、前述のファイアウォールルールを使っても接続できません。
## パフォーマンスに問題がありますか?
ユーザー設定パネルでブラー効果なし(高速 UIモードを有効にしてみてください。
## このプロジェクトが好きです!どうすればコントリビュートできますか?
### やるべきこと
1. プルリクエストを送る
2. 確立されたテンプレートを使って機能提案と課題レポートを送る
3. 何か質問する前に、readme ファイルや組み込みのドキュメントを読んでください
### やらないべきこと
1. 金銭の寄付を申し出る
2. 何の脈絡もなくバグ報告を送る
3. すでに何度も回答されている質問をする
## 古い背景画像はどこにありますか?
100 オリジナルコンテンツのみのポリシーに移行しているため、古い背景画像はこのリポジトリから削除されました。
アーカイブはこちら:
<https://files.catbox.moe/1xevnc.zip>
## スクリーンショット
<img width="400" alt="image" src="https://user-images.githubusercontent.com/18619528/228649245-8061c60f-63dc-488e-9325-f151b7a3ec2d.png">
<img width="400" alt="image" src="https://user-images.githubusercontent.com/18619528/228649856-fbdeef05-d727-4d5a-be80-266cbbc6b811.png">
## ライセンスとクレジット
**このプログラムは有用であることを願って配布されていますが、いかなる保証もありません;
また、商品性または特定目的への適合性についての黙示の保証もありません。
詳細は GNU Affero General Public License をご覧ください。**
* Humi によるTAI Base: 不明ライセンス
* Cohee の修正と派生コード: AGPL v3
* RossAscends の追加: AGPL v3
* CncAnon の TavernAITurbo 改造の一部: 不明ライセンス
* kingbri のさまざまなコミットと提案 (<https://github.com/bdashore3>)
* city_unit の拡張機能と様々な QoL 機能 (<https://github.com/city-unit>)
* StefanDanielSchwarz のさまざまなコミットとバグ報告 (<https://github.com/StefanDanielSchwarz>)
* PepperTaco の作品にインスパイアされた Waifu モード (<https:/fugithub.com/peppertaco/Tavern/>)
* ピグマリオン大学の皆さん、素晴らしいテスターとしてクールな機能を提案してくれてありがとう!
* TextGen のプリセットをコンパイルしてくれた obabooga に感謝
* KAI Lite の KoboldAI プリセット: <https://lite.koboldai.net/>
* Google による Noto Sans フォントOFLライセンス
* Font Awesome によるアイコンテーマ <https://fontawesome.com> (アイコン: CC BY 4.0、フォント: SIL OFL 1.1、コード: MIT License)
* ZeldaFan0225 による AI Horde クライアントライブラリ: <https://github.com/ZeldaFan0225/ai_horde>
* AlpinDale による Linux 起動スクリプト
* FAQ を提供してくれた paniphons に感謝
* 10K ディスコード・ユーザー記念背景 by @kallmeflocc
* デフォルトコンテンツ(キャラクターと伝承書)の提供: @OtisAlejandro、@RossAscends、@kallmeflocc
* @doloroushyeonse による韓国語翻訳
* k_euler_a による Horde のサポート <https://github.com/Teashrock>
* [@XXpE3](https://github.com/XXpE3) による中国語翻訳、中国語 ISSUES の連絡先は @XXpE3

View File

@ -1,4 +1,4 @@
[English](readme.md) | 中文
[English](readme.md) | 中文 | [日本語](readme-ja_jp.md)
![image](https://github.com/SillyTavern/SillyTavern/assets/18619528/c2be4c3f-aada-4f64-87a3-ae35a68b61a4)
@ -47,7 +47,7 @@ SillyTavern 本身并无用处,因为它只是一个用户聊天界面。你
获取支持,或分享喜爱的角色和 Prompt
### [加入 Discord 社区](https://discord.gg/RZdyAEUPvj)
### [加入 Discord 社区](https://discord.gg/sillytavern)
***

13
.github/readme.md vendored
View File

@ -1,4 +1,4 @@
English | [中文](readme-zh_cn.md)
English | [中文](readme-zh_cn.md) | [日本語](readme-ja_jp.md)
![SillyTavern-Banner](https://github.com/SillyTavern/SillyTavern/assets/18619528/c2be4c3f-aada-4f64-87a3-ae35a68b61a4)
@ -162,6 +162,17 @@ Installing via ZIP download (discouraged)
### Linux
#### Unofficial Debian/Ubuntu PKGBUILD
> **This installation method is unofficial and not supported by the project. Report any issues to the PKGBUILD maintainer.**
> The method is intended for Debian-based distributions (Ubuntu, Mint, etc).
1. Install [makedeb](https://www.makedeb.org/).
2. Ensure you have Node.js v18 or higher installed by running `node -v`. If you need to upgrade, you can install a [node.js repo](https://mpr.makedeb.org/packages/nodejs-repo) (you'll might need to edit the version inside the PKGBUILD). As an alternative, install and configure [nvm](https://mpr.makedeb.org/packages/nvm) to manage multiple node.js installations. Finally, you can [install node.js manually](https://nodejs.org/en/download), but you will need to update the PATH variable of your environment.
3. Now build the [sillytavern package](https://mpr.makedeb.org/packages/sillytavern). The build needs to run with the correct node.js version.
#### Manual
1. Ensure you have Node.js v18 or higher (the latest [LTS version](https://nodejs.org/en/download/) is recommended) installed by running `node -v`.
Alternatively, use the [Node Version Manager](https://github.com/nvm-sh/nvm#installing-and-updating) script to quickly and easily manage your Node installations.
2. Run the `start.sh` script.

View File

@ -1,5 +1,5 @@
pushd %~dp0
call npm install --no-audit
node server.js
node server.js %*
pause
popd

View File

@ -12,6 +12,6 @@ if %errorlevel% neq 0 (
)
)
call npm install
node server.js
node server.js %*
pause
popd

View File

@ -7,7 +7,7 @@
"source": [
"**Links**<br>\n",
"Extensions API GitHub: https://github.com/SillyTavern/SillyTavern-extras/<br>\n",
"SillyTavern community Discord (support and discussion): https://discord.gg/RZdyAEUPvj"
"SillyTavern community Discord (support and discussion): https://discord.gg/sillytavern"
]
},
{

View File

@ -51,7 +51,7 @@ extras:
# Extra models for plugins. Expects model IDs from HuggingFace model hub in ONNX format
classificationModel: Cohee/distilbert-base-uncased-go-emotions-onnx
captioningModel: Xenova/vit-gpt2-image-captioning
embeddingModel: Xenova/all-mpnet-base-v2
embeddingModel: Cohee/jina-embeddings-v2-base-en
promptExpansionModel: Cohee/fooocus_expansion-onnx
# -- OPENAI CONFIGURATION --
openai:

10
package-lock.json generated
View File

@ -1,12 +1,12 @@
{
"name": "sillytavern",
"version": "1.11.1",
"version": "1.11.2",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "sillytavern",
"version": "1.11.1",
"version": "1.11.2",
"hasInstallScript": true,
"license": "AGPL-3.0",
"dependencies": {
@ -2413,9 +2413,9 @@
"dev": true
},
"node_modules/follow-redirects": {
"version": "1.15.2",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz",
"integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==",
"version": "1.15.4",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.4.tgz",
"integrity": "sha512-Cr4D/5wlrb0z9dgERpUL3LrmPKVDsETIJhaCMeDfuFYcqa5bldGV6wBsAN6X/vxlXQtFBMrXdXxdL8CbDTGniw==",
"funding": [
{
"type": "individual",

View File

@ -42,6 +42,9 @@
},
"vectra": {
"openai": "^4.17.0"
},
"axios": {
"follow-redirects": "^1.15.4"
}
},
"name": "sillytavern",
@ -51,7 +54,7 @@
"type": "git",
"url": "https://github.com/SillyTavern/SillyTavern.git"
},
"version": "1.11.1",
"version": "1.11.2",
"scripts": {
"start": "node server.js",
"start-multi": "node server.js --disableCsrf",

View File

@ -0,0 +1,11 @@
{
"story_string": "Below is an instruction that describes a task. Write a response that appropriately completes the request.\n\n### Instruction:\n{{#if system}}{{system}}\n{{/if}}{{#if wiBefore}}{{wiBefore}}\n{{/if}}{{#if description}}{{description}}\n{{/if}}{{#if personality}}{{char}}'s personality: {{personality}}\n{{/if}}{{#if scenario}}Scenario: {{scenario}}\n{{/if}}{{#if wiAfter}}{{wiAfter}}\n{{/if}}{{#if persona}}{{persona}}\n{{/if}}",
"example_separator": "",
"chat_start": "",
"use_stop_strings": false,
"always_force_name2": false,
"trim_sentences": false,
"include_newline": false,
"single_line": false,
"name": "Alpaca-Single-Turn"
}

View File

@ -107,6 +107,12 @@
position: relative;
}
.select2-results .select2-results__option--group {
color: var(--SmartThemeBodyColor);
background-color: var(--SmartThemeBlurTintColor);
position: relative;
}
/* Customize the hovered option list item */
.select2-results .select2-results__option--highlighted.select2-results__option--selectable {
color: var(--SmartThemeBodyColor);
@ -114,12 +120,20 @@
opacity: 1;
}
.select2-results__option.select2-results__option--group::before {
display: none;
}
/* Customize the option list item */
.select2-results__option {
padding-left: 30px;
/* Add some padding to make room for the checkbox */
}
.select2-results .select2-results__option--group .select2-results__options--nested .select2-results__option {
padding-left: 2em;
}
/* Add the custom checkbox */
.select2-results__option::before {
content: '';

50
public/img/custom.svg Normal file
View File

@ -0,0 +1,50 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
version="1.0"
width="16.986956"
height="22.042189"
id="svg11382"
sodipodi:docname="custom.svg"
viewBox="0 0 679.47822 881.68754"
inkscape:version="1.1.2 (b8e25be833, 2022-02-05)"
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
inkscape:zoom="42.995"
inkscape:cx="11.082684"
inkscape:cy="8.9545296"
inkscape:window-width="1920"
inkscape:window-height="1009"
inkscape:window-x="-8"
inkscape:window-y="-8"
inkscape:window-maximized="1"
inkscape:current-layer="g11476"
id="namedview669"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0" />
<defs
id="defs11384" />
<g
id="layer1"
transform="translate(-48.907487,-55.625885)">
<g
id="g11476">
<path
d="m 465.24901,686.59683 c -51.1537,0 -102.3074,0 -153.4611,0 -1.31708,-52.18472 2.26923,-108.82047 37.00965,-150.43627 23.21285,-33.61844 63.37794,-47.61571 92.45252,-74.91276 37.85456,-29.39299 66.45422,-78.39712 58.32465,-128.1697 -2.77308,-43.29131 -38.24417,-77.99847 -79.30937,-85.0026 -50.42874,-11.82866 -111.67907,2.3265 -139.55528,50.25117 -16.71781,25.76665 -27.12589,57.26058 -22.82769,88.35614 -58.1841,0.51881 -116.3682,1.03762 -174.552298,1.55643 0.16527,-71.42695 3.86133,-151.28527 55.163188,-206.18091 46.46301,-56.01624 114.22212,-93.030055 186.31391,-98.273555 91.93492,-7.78137 193.46191,-4.40991 270.27577,54.209695 53.26315,37.16622 90.35652,97.5512 95.89082,163.52777 10.37716,64.92902 -13.64221,131.79055 -55.35059,180.60583 -35.39219,39.71396 -82.8918,64.29734 -123.59888,97.30398 -32.73006,25.67093 -48.60796,65.61157 -46.7753,107.16478 z"
style="stroke-width:0.905819"
id="path672" />
<path
d="m 483.48936,847.70143 a 94.042557,89.593537 0 1 1 -188.08511,0 94.042557,89.593537 0 1 1 188.08511,0 z"
style="stroke-width:1.10768"
id="path674" />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

60
public/img/scale.svg Normal file
View File

@ -0,0 +1,60 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="8.2832384mm"
height="9.4351206mm"
viewBox="0 0 8.2832384 9.4351206"
version="1.1"
id="svg1"
xml:space="preserve"
inkscape:version="1.3 (0e150ed, 2023-07-21)"
sodipodi:docname="scale.svg"
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="namedview1"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="mm"
showgrid="false"
inkscape:zoom="1.0289025"
inkscape:cx="8.7471843"
inkscape:cy="8.2612296"
inkscape:window-width="1280"
inkscape:window-height="688"
inkscape:window-x="0"
inkscape:window-y="25"
inkscape:window-maximized="1"
inkscape:current-layer="layer1" /><defs
id="defs1" /><g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(-88.746886,-143.1532)"><g
id="g1"
transform="matrix(0.26458333,0,0,0.26458333,88.077577,143.1532)"><path
d="m 14.3423,8.42148 0.0218,-0.00723 2.1725,-0.51502 c 0.4642,-0.10879 0.8631,-0.40619 1.1025,-0.81965 l 0.5041,-0.87044 0.0146,-0.02902 0.0145,0.02902 0.5041,0.87044 c 0.2394,0.41346 0.6383,0.71086 1.1025,0.81965 l 2.1725,0.51502 0.0254,0.01087 H 21.9514 L 19.7789,8.94737 C 19.3147,9.0562 18.9158,9.35359 18.6764,9.76705 l -0.5041,0.87045 -0.0145,0.029 -0.0146,-0.029 -0.5041,-0.87045 C 17.3997,9.35359 17.0008,9.0562 16.5366,8.94737 L 14.3641,8.43239 14.3423,8.42512 Z"
id="path10" /><path
d="M 14.3423,2.245 14.3641,2.23775 16.5366,1.72274 C 17.0008,1.61393 17.3997,1.31653 17.6391,0.903078 L 18.1432,0.0290146 18.1578,0 18.1723,0.0290146 18.6764,0.899453 C 18.9158,1.31291 19.3147,1.61031 19.7789,1.71911 L 21.9514,2.23412 21.9768,2.245 H 21.9514 L 19.7789,2.76726 C 19.3147,2.87607 18.9158,3.17347 18.6764,3.58692 L 18.1723,4.45738 18.1578,4.48639 18.1432,4.45738 17.6391,3.58692 C 17.3997,3.17347 17.0008,2.87607 16.5366,2.76726 L 14.3641,2.25225 Z"
id="path11" /><path
d="M 18.183,15.7998 C 17.9328,13.7832 16.754,11.9807 14.9697,10.9507 11.749,9.03938 8.96364,8.82176 6.23265,6.73999 L 2.52967,8.79638 18.183,17.8344 33.8364,8.79638 30.1334,6.74359 c -2.731,2.08181 -5.5164,2.29943 -8.737,4.21071 -1.7844,1.0301 -2.9631,2.8326 -3.2134,4.8491 z"
id="path12" /><path
d="M 18.183,35.6603 C 17.824,35.653 17.4649,35.5551 17.1458,35.3701 L 2.52967,26.9269 V 8.79646 L 17.1458,17.236 c 0.3227,0.185 0.6782,0.2829 1.0372,0.2902 0.3591,-0.0073 0.7181,-0.1052 1.0373,-0.2902 L 33.8364,8.79646 V 26.9306 l -14.6161,8.4395 c -0.3228,0.185 -0.6782,0.2829 -1.0373,0.2902 z"
id="path13" /><path
d="m 18.183,35.6603 c 0.3591,-0.0073 0.7181,-0.1052 1.0373,-0.2902 l 14.616,-8.4395 V 8.79646 L 19.2203,17.236 c -0.3228,0.185 -0.6782,0.2829 -1.0373,0.2902"
id="path14" /><path
d="M 18.183,17.5262 C 17.824,17.5189 17.4649,17.421 17.1458,17.236 L 2.52967,8.79646 V 26.9306 l 14.61613,8.4395 c 0.3227,0.185 0.6782,0.2829 1.0372,0.2902"
id="path15" /><path
d="M 18.1831,17.5262 C 17.824,17.5189 17.465,17.421 17.1458,17.236 l -1.4507,-0.8378 v 18.1341 l 1.4507,0.8378 c 0.3228,0.185 0.6782,0.2829 1.0373,0.2902 0.359,-0.0073 0.7181,-0.1052 1.0372,-0.2902 l 1.4508,-0.8378 V 16.3946 l -1.4508,0.8378 c -0.3227,0.1849 -0.6782,0.2829 -1.0372,0.2901 z"
id="path16" /><path
d="M 9,5.32055 9.02174,5.31328 11.1942,4.79826 c 0.4643,-0.10879 0.8632,-0.40618 1.1026,-0.81964 l 0.5041,-0.87044 0.0145,-0.02902 0.0145,0.02902 0.5041,0.87044 c 0.2394,0.41346 0.6384,0.71085 1.1026,0.81964 l 2.1725,0.51502 0.0253,0.01087 H 16.6091 L 14.4366,5.84644 C 13.9724,5.95523 13.5734,6.25263 13.334,6.66609 L 12.8299,7.53653 12.8154,7.56555 12.8009,7.53653 12.2968,6.66609 C 12.0574,6.25263 11.6585,5.95523 11.1942,5.84644 L 9.02174,5.33142 9,5.32415 Z"
id="path17" /><path
d="m 19.6593,5.32055 0.0218,-0.00727 2.1724,-0.51502 c 0.4642,-0.10879 0.8632,-0.40618 1.1026,-0.81964 l 0.5041,-0.87044 0.0145,-0.02902 0.0145,0.02902 0.5042,0.87044 c 0.2393,0.41346 0.6383,0.71085 1.1025,0.81964 l 2.1724,0.51502 0.0255,0.01087 H 27.2683 L 25.0959,5.84644 C 24.6317,5.95523 24.2327,6.25263 23.9934,6.66609 L 23.4892,7.53653 23.4747,7.56555 23.4602,7.53653 22.9561,6.66609 C 22.7167,6.25263 22.3177,5.95523 21.8535,5.84644 L 19.6811,5.33142 19.6593,5.32415 Z"
id="path18" /></g></g></svg>

After

Width:  |  Height:  |  Size: 4.7 KiB

View File

@ -523,6 +523,32 @@
</div>
</div>
</div>
<div data-newbie-hidden class="range-block" data-source="openrouter">
<div class="range-block-title" data-i18n="Min P">
Min P
</div>
<div class="range-block-range-and-counter">
<div class="range-block-range">
<input type="range" id="min_p_openai" name="volume" min="0" max="1" step="0.001">
</div>
<div class="range-block-counter">
<input type="number" min="0" max="1" step="0.001" data-for="min_p_openai" id="min_p_counter_openai">
</div>
</div>
</div>
<div data-newbie-hidden class="range-block" data-source="openrouter">
<div class="range-block-title" data-i18n="Top A">
Top A
</div>
<div class="range-block-range-and-counter">
<div class="range-block-range">
<input type="range" id="top_a_openai" name="volume" min="0" max="1" step="0.001">
</div>
<div class="range-block-counter">
<input type="number" min="0" max="1" step="0.001" data-for="top_a_openai" id="top_a_counter_openai">
</div>
</div>
</div>
<div class="inline-drawer m-t-1 wide100p">
<div class="inline-drawer-toggle inline-drawer-header">
<b data-i18n="Quick Edit">Quick Prompts Edit</b>
@ -1151,6 +1177,12 @@
</div>
</div><!-- end of novel settings-->
<div id="textgenerationwebui_api-settings">
<div data-newbie-hidden class="flex-container justifyCenter">
<small class="flex-container alignitemscenter">
<div id="samplerResetButton" class="menu_button whitespacenowrap">Neutralize Samplers</div>
<div class="fa-solid fa-circle-info opacity50p" title="Sets all samplers to their neutral/disabled state."></div>
</small>
</div>
<div data-newbie-hidden data-tg-type="aphrodite" class="flex-container flexFlowColumn alignitemscenter flexBasis100p flexGrow flexShrink gap0">
<small data-i18n="Multiple swipes per generation">Multiple swipes per generation</small>
<input type="number" id="n_textgenerationwebui" class="text_pole textAlignCenter" min="1" value="1" />
@ -1161,8 +1193,8 @@
<span data-i18n="temperature">Temperature</span>
<div class="fa-solid fa-circle-info opacity50p" title="Temperature controls the randomness in token selection:&#13;- low temperature (<1.0) leads to more predictable text, favoring higher probability tokens.&#13;- high temperature (>1.0) increases creativity and diversity in the output by giving lower probability tokens a better chance.&#13;Set to 1.0 for the original probabilities."></div>
</small>
<input class="neo-range-slider" type="range" id="temp_textgenerationwebui" name="volume" min="0.0" max="4.0" step="0.01" x-setting-id="temp">
<input class="neo-range-input" type="number" min="0.0" max="4.0" step="0.01" data-for="temp_textgenerationwebui" id="temp_counter_textgenerationwebui">
<input class="neo-range-slider" type="range" id="temp_textgenerationwebui" name="volume" min="0.0" max="5.0" step="0.01" x-setting-id="temp">
<input class="neo-range-input" type="number" min="0.0" max="5.0" step="0.01" data-for="temp_textgenerationwebui" id="temp_counter_textgenerationwebui">
</div>
<div data-newbie-hidden class="alignitemscenter flex-container flexFlowColumn flexBasis48p flexGrow flexShrink gap0">
<small>
@ -1190,8 +1222,8 @@
<span data-i18n="Min P">Min P</span>
<div class="fa-solid fa-circle-info opacity50p" title="Min P sets a base minimum probability.&#13;This is scaled according to the top token's probability.&#13;E.g If Top token is 80% probability, and Min P is 0.1, only tokens higher than 8% would be considered.&#13;Set to 0 to disable."></div>
</small>
<input class="neo-range-slider" type="range" id="min_p_textgenerationwebui" name="volume" min="0" max="1" step="0.01">
<input class="neo-range-input" type="number" min="0" max="1" step="0.05" data-for="min_p_textgenerationwebui" id="min_p_counter_textgenerationwebui">
<input class="neo-range-slider" type="range" id="min_p_textgenerationwebui" name="volume" min="0" max="1" step="0.001">
<input class="neo-range-input" type="number" min="0" max="1" step="0.001" data-for="min_p_textgenerationwebui" id="min_p_counter_textgenerationwebui">
</div>
<div data-newbie-hidden class="alignitemscenter flex-container flexFlowColumn flexBasis48p flexGrow flexShrink gap0">
<small data-i18n="Top A">Top A</small>
@ -1203,12 +1235,12 @@
<input class="neo-range-slider" type="range" id="tfs_textgenerationwebui" name="volume" min="0" max="1" step="0.01">
<input class="neo-range-input" type="number" min="0" max="1" step="0.01" data-for="tfs_textgenerationwebui" id="tfs_counter_textgenerationwebui">
</div>
<div data-newbie-hidden class="alignitemscenter flex-container flexFlowColumn flexBasis48p flexGrow flexShrink gap0">
<div data-newbie-hidden data-tg-type="ooba" class="alignitemscenter flex-container flexFlowColumn flexBasis48p flexGrow flexShrink gap0">
<small data-i18n="Epsilon Cutoff">Epsilon Cutoff</small>
<input class="neo-range-slider" type="range" id="epsilon_cutoff_textgenerationwebui" name="volume" min="0" max="9" step="0.01">
<input class="neo-range-input" type="number" min="0" max="9" step="0.01" data-for="epsilon_cutoff_textgenerationwebui" id="epsilon_cutoff_counter_textgenerationwebui">
</div>
<div data-newbie-hidden class="alignitemscenter flex-container flexFlowColumn flexBasis48p flexGrow flexShrink gap0">
<div data-newbie-hidden data-tg-type="ooba" class="alignitemscenter flex-container flexFlowColumn flexBasis48p flexGrow flexShrink gap0">
<small data-i18n="Eta Cutoff">Eta Cutoff</small>
<input class="neo-range-slider" type="range" id="eta_cutoff_textgenerationwebui" name="volume" min="0" max="20" step="0.01">
<input class="neo-range-input" type="number" min="0" max="20" step="0.01" data-for="eta_cutoff_textgenerationwebui" id="eta_cutoff_counter_textgenerationwebui">
@ -1218,12 +1250,12 @@
<input class="neo-range-slider" type="range" id="rep_pen_textgenerationwebui" name="volume" min="1" max="3" step="0.01">
<input class="neo-range-input" type="number" min="1" max="3" step="0.01" data-for="rep_pen_textgenerationwebui" id="rep_pen_counter_textgenerationwebui">
</div>
<div data-forAphro=False class="alignitemscenter flex-container flexFlowColumn flexBasis48p flexGrow flexShrink gap0">
<div data-forAphro="False" class="alignitemscenter flex-container flexFlowColumn flexBasis48p flexGrow flexShrink gap0">
<small data-i18n="rep.pen range">Repetition Penalty Range</small>
<input class="neo-range-slider" type="range" id="rep_pen_range_textgenerationwebui" name="volume" min="-1" max="8192" step="1">
<input class="neo-range-input" type="number" min="-1" max="8192" step="1" data-for="rep_pen_range_textgenerationwebui" id="rep_pen_range_counter_textgenerationwebui">
</div>
<div data-forAphro=False data-newbie-hidden class="alignitemscenter flex-container flexFlowColumn flexBasis48p flexGrow flexShrink gap0">
<div data-forAphro="False" data-tg-type="ooba" data-newbie-hidden class="alignitemscenter flex-container flexFlowColumn flexBasis48p flexGrow flexShrink gap0">
<small data-i18n="Encoder Rep. Pen.">Encoder Penalty</small>
<input class="neo-range-slider" type="range" id="encoder_rep_pen_textgenerationwebui" name="volume" min="0.8" max="1.5" step="0.01" />
<input class="neo-range-input" type="number" min="0.8" max="1.5" step="0.01" data-for="encoder_rep_pen_textgenerationwebui" id="encoder_rep_pen_counter_textgenerationwebui">
@ -1238,12 +1270,12 @@
<input class="neo-range-slider" type="range" id="presence_pen_textgenerationwebui" name="volume" min="-2" max="2" step="0.01" />
<input class="neo-range-input" type="number" min="-2" max="2" step="0.01" data-for="presence_pen_textgenerationwebui" id="presence_pen_counter_textgenerationwebui">
</div>
<div data-forAphro=False data-newbie-hidden class="alignitemscenter flex-container flexFlowColumn flexBasis48p flexGrow flexShrink gap0">
<div data-forAphro="False" data-tg-type="ooba" data-newbie-hidden class="alignitemscenter flex-container flexFlowColumn flexBasis48p flexGrow flexShrink gap0">
<small data-i18n="No Repeat Ngram Size">No Repeat Ngram Size</small>
<input class="neo-range-slider" type="range" id="no_repeat_ngram_size_textgenerationwebui" name="volume" min="0" max="20" step="1">
<input class="neo-range-input" type="number" min="0" max="20" step="1" data-for="no_repeat_ngram_size_textgenerationwebui" id="no_repeat_ngram_size_counter_textgenerationwebui">
</div>
<div data-newbie-hidden class="alignitemscenter flex-container flexFlowColumn flexBasis48p flexGrow flexShrink gap0">
<div data-newbie-hidden data-tg-type="ooba" class="alignitemscenter flex-container flexFlowColumn flexBasis48p flexGrow flexShrink gap0">
<small data-i18n="Min Length">Min Length</small>
<input class="neo-range-slider" type="range" id="min_length_textgenerationwebui" name="volume" min="0" max="2000" step="1" />
<input class="neo-range-input" type="number" min="0" max="2000" step="1" data-for="min_length_textgenerationwebui" id="min_length_counter_textgenerationwebui">
@ -1270,6 +1302,30 @@
<input class="neo-range-input" type="number" min="0" max="5" step="1" data-for="prompt_log_probs_aphrodite" id="prompt_log_probs_aphrodite_counter_textgenerationwebui">
</div>
-->
<div data-newbie-hidden name="dynaTempBlock" class="wide100p">
<h4 class="wide100p textAlignCenter" data-i18n="DynaTemp">
<div class="flex-container alignitemscenter" style="justify-content: center;">
<div class="checkbox_label" for="dynatemp_textgenerationwebui">
<input type="checkbox" id="dynatemp_textgenerationwebui" />
<small data-i18n="dynatemp"></small>
</div>
<span style="text-align: center;">Dynamic Temperature</span>
<div class="fa-solid fa-circle-info opacity50p" title="Scales Temperature dynamically per token (based on the variation of probabilities.)"></div>
</div>
</h4>
<div class="flex-container flexFlowRow alignitemscenter gap10px flexShrink">
<div class="alignitemscenter flex-container marginBot5 flexFlowColumn flexGrow flexShrink gap0">
<small data-i18n="Minimum Temp">Minimum Temp</small>
<input class="neo-range-slider" type="range" id="min_temp_textgenerationwebui" name="volume" min="0" max="5" step="0.01" />
<input class="neo-range-input" type="number" min="0" max="5" step="0.01" data-for="min_temp_textgenerationwebui" id="min_temp_counter_textgenerationwebui">
</div>
<div class="alignitemscenter flex-container marginBot5 flexFlowColumn flexGrow flexShrink gap0">
<small data-i18n="Maximum Temp">Maximum Temp</small>
<input class="neo-range-slider" type="range" id="max_temp_textgenerationwebui" name="volume" min="0" max="5" step="0.01" />
<input class="neo-range-input" type="number" min="0" max="5" step="0.01" data-for="max_temp_textgenerationwebui" id="max_temp_counter_textgenerationwebui">
</div>
</div>
</div>
<div data-newbie-hidden name="miroStatBlock" class="wide100p">
<h4 class="wide100p textAlignCenter" data-i18n="Mirostat (mode=1 is only for llama.cpp)">Mirostat
<div class=" fa-solid fa-circle-info opacity50p " title="Mode=1 is only for llama.cpp&#13;More helpful tips coming soon."></div>
@ -1292,7 +1348,7 @@
</div>
</div>
</div>
<div data-newbie-hidden name="beamSearchBlock" class="wide100p">
<div data-newbie-hidden data-tg-type="ooba" name="beamSearchBlock" class="wide100p">
<h4 class="wide100p textAlignCenter" span data-i18n="Beam search">Beam Search
<div class=" fa-solid fa-circle-info opacity50p " title="Helpful tip coming soon."></div>
</h4>
@ -1315,7 +1371,7 @@
</div>
</div>
</div>
<div data-forAphro=False data-newbie-hidden name="contrastiveSearchBlock" class="alignitemscenter flex-container flexFlowColumn flexBasis48p flexGrow flexShrink gap0">
<div data-forAphro="False" data-tg-type="ooba" data-newbie-hidden name="contrastiveSearchBlock" class="alignitemscenter flex-container flexFlowColumn flexBasis48p flexGrow flexShrink gap0">
<h4 class="textAlignCenter" data-i18n="Contrastive search">Contrast Search
<div class=" fa-solid fa-circle-info opacity50p " title="Helpful tip coming soon."></div>
</h4>
@ -1327,17 +1383,17 @@
</div>
<div data-newbie-hidden name="checkboxes" class="flex-container flexBasis48p justifyCenter flexGrow flexShrink ">
<div class="flex-container flexFlowColumn marginTop5">
<label data-forAphro=False class="checkbox_label flexGrow flexShrink" for="do_sample_textgenerationwebui">
<label data-forAphro="False" data-tg-type="ooba" class="checkbox_label flexGrow flexShrink" for="do_sample_textgenerationwebui">
<input type="checkbox" id="do_sample_textgenerationwebui" />
<small data-i18n="Do Sample">Do Sample</small>
</label>
<label data-forAphro=False class="checkbox_label flexGrow flexShrink" for="add_bos_token_textgenerationwebui">
<label data-forAphro="False" data-tg-type="ooba" class="checkbox_label flexGrow flexShrink" for="add_bos_token_textgenerationwebui">
<input type="checkbox" id="add_bos_token_textgenerationwebui" />
<small data-i18n="Add BOS Token">Add BOS Token
<div class="fa-solid fa-circle-info opacity50p " data-i18n="Add the bos_token to the beginning of prompts. Disabling this can make the replies more creative." title="Add the bos_token to the beginning of prompts. Disabling this can make the replies more creative."></div>
</small>
</label>
<label data-forAphro=False class="checkbox_label flexGrow flexShrink" for="ban_eos_token_textgenerationwebui">
<label data-forAphro="False" class="checkbox_label flexGrow flexShrink" for="ban_eos_token_textgenerationwebui">
<input type="checkbox" id="ban_eos_token_textgenerationwebui" />
<small data-i18n="Ban EOS Token">Ban EOS Token
<div class="fa-solid fa-circle-info opacity50p " data-i18n="Ban the eos_token. This forces the model to never end the generation prematurely" title="Ban the eos_token. This forces the model to never end the generation prematurely."></div>
@ -1351,11 +1407,11 @@
</small>
</label>
<label class="checkbox_label flexGrow flexShrink" for="skip_special_tokens_textgenerationwebui">
<label class="checkbox_label flexGrow flexShrink" for="skip_special_tokens_textgenerationwebui">
<input type="checkbox" id="skip_special_tokens_textgenerationwebui" />
<small data-i18n="Skip Special Tokens">Skip Special Tokens</small>
</label>
<label data-forAphro=False class="checkbox_label flexGrow flexShrink" for="temperature_last_textgenerationwebui">
<label data-forAphro="False" data-tg-type="ooba, aphrodite" class="checkbox_label flexGrow flexShrink" for="temperature_last_textgenerationwebui">
<input type="checkbox" id="temperature_last_textgenerationwebui" />
<small data-i18n="Temperature Last">Temperature Last
<div class="fa-solid fa-circle-info opacity50p " data-i18n="Use the temperature sampler last." title="Use the temperature sampler last."></div>
@ -1366,10 +1422,9 @@
<input type="checkbox" id="spaces_between_special_tokens_aphrodite_textgenerationwebui" />
<small data-i18n="Spaces Between Special Tokens">Spaces Between Special Tokens</small>
</label>
</div>
</div>
<div data-forAphro=False data-newbie-hidden class="flex-container flexFlowColumn alignitemscenter flexBasis48p flexGrow flexShrink gap0">
<div data-forAphro="False" data-newbie-hidden class="flex-container flexFlowColumn alignitemscenter flexBasis48p flexGrow flexShrink gap0">
<small data-i18n="Seed" class="textAlignCenter">Seed</small>
<input type="number" id="seed_textgenerationwebui" class="text_pole textAlignCenter" min="-1" value="-1" maxlength="100" />
</div>
@ -1398,8 +1453,8 @@
<div class="logit_bias_list"></div>
</div>
</div>
<div data-newbie-hidden data-forAphro=False class="wide100p">
<hr class="width100p">
<div data-newbie-hidden data-forAphro="False" class="wide100p">
<hr class="width100p">
<h4 data-i18n="CFG" class="textAlignCenter">CFG
<div class="margin5 fa-solid fa-circle-info opacity50p " title="Helpful tip coming soon."></div>
</h4>
@ -1420,7 +1475,7 @@
</div>
</div>
</div>
<div data-newbie-hidden data-forAphro=False id="grammar_block_ooba" class="wide100p">
<div data-newbie-hidden data-forAphro="False" id="grammar_block_ooba" class="wide100p">
<hr class="wide100p">
<h4 class="wide100p textAlignCenter" data-i18n="GBNF Grammar">GBNF Grammar
<a href="https://github.com/ggerganov/llama.cpp/blob/master/grammars/README.md" target="_blank">
@ -1551,13 +1606,23 @@
</div>
</div>
<div data-newbie-hidden class="range-block" data-source="claude">
<label for="claude_exclude_prefixes" title="Exclude Human/Assistant prefixes" class="checkbox_label widthFreeExpand">
<input id="claude_exclude_prefixes" type="checkbox" />
<span data-i18n="Exclude Human/Assistant prefixes">Exclude Human/Assistant prefixes</span>
</label>
<div class="toggle-description justifyLeft marginBot5">
<span data-i18n="Exclude Human/Assistant prefixes from being added to the prompt.">
Exclude Human/Assistant prefixes from being added to the prompt, except very first/last message, system prompt Human message and Assistant suffix.
Requires 'Add character names' checked.
</span>
</div>
<label for="exclude_assistant" title="Exclude Assistant suffix" class="checkbox_label widthFreeExpand">
<input id="exclude_assistant" type="checkbox" />
<span data-i18n="Exclude Assistant suffix">Exclude Assistant suffix</span>
</label>
<div class="toggle-description justifyLeft">
<div class="toggle-description justifyLeft marginBot5">
<span data-i18n="Exclude the assistant suffix from being added to the end of prompt.">
Exclude the assistant suffix from being added to the end of prompt (Requires jailbreak with 'Assistant:' in it).
Exclude the assistant suffix from being added to the end of prompt. Requires jailbreak with 'Assistant:' in it.
</span>
</div>
<div id="claude_assistant_prefill_block" class="wide100p">
@ -1570,7 +1635,7 @@
Use system prompt (Claude 2.1+ only)
</span>
</label>
<div class="toggle-description justifyLeft">
<div class="toggle-description justifyLeft marginBot5">
<span data-i18n="Exclude the 'Human: ' prefix from being added to the beginning of the prompt.">
Exclude the 'Human: ' prefix from being added to the beginning of the prompt.
Instead, place it between the system prompt and the first message with the role 'assistant' (right before 'Chat History' by default).
@ -1643,11 +1708,11 @@
<div class="flex-container flexFlowColumn">
<div id="main-API-selector-block">
<select id="main_api">
<option value="kobold"><span data-i18n="KoboldAI">KoboldAI Classic</span></option>
<option value="koboldhorde"><span data-i18n="KoboldAI Horde">KoboldAI Horde</span></option>
<option value="novel"><span data-i18n="NovelAI">NovelAI</span></option>
<option value="textgenerationwebui"><span data-i18n="Text Completion">Text Completion</span></option>
<option value="openai"><span data-i18n="Chat Completion">Chat Completion</span></option>
<option value="novel"><span data-i18n="NovelAI">NovelAI</span></option>
<option value="koboldhorde"><span data-i18n="KoboldAI Horde">KoboldAI Horde</span></option>
<option value="kobold"><span data-i18n="KoboldAI">KoboldAI Classic</span></option>
</select>
</div>
<div id="kobold_horde" style="position: relative;"> <!-- shows the kobold settings -->
@ -1717,6 +1782,9 @@
<h4 data-i18n="API url">API url</h4>
<small data-i18n="Example: http://127.0.0.1:5000/api ">Example: http://127.0.0.1:5000/api </small>
<input id="api_url_text" name="api_url" class="text_pole" placeholder="http://127.0.0.1:5000/api" maxlength="500" value="" autocomplete="off" data-server-history="kobold">
<div id="koboldcpp_hint" class="neutral_warning displayNone">
We have a dedicated KoboldCpp support under Text Completion ⇒ KoboldCpp.
</div>
<div class="flex-container">
<div id="api_button" class="api_button menu_button" type="submit" data-i18n="Connect" data-server-connect="kobold">Connect</div>
<div class="api_loading menu_button" data-i18n="Cancel">Cancel</div>
@ -1840,6 +1908,7 @@
<small data-i18n="Example: http://127.0.0.1:5000 ">Example: http://127.0.0.1:5000</small>
<input id="textgenerationwebui_api_url_text" name="textgenerationwebui_api_url" class="text_pole wide100p" maxlength="500" value="" autocomplete="off" data-server-history="ooba_blocking">
</div>
<input id="custom_model_textgenerationwebui" class="text_pole wide100p" maxlength="500" placeholder="Custom model (optional)" type="text">
</div>
<div data-tg-type="aphrodite">
<div class="flex-container flexFlowColumn">
@ -1887,7 +1956,8 @@
</div>
<div class="flex1">
<h4>
<span data-i18n="Ollama Model">Ollama Model</h4>
<span data-i18n="Ollama Model">Ollama Model
</h4>
</h4>
<select id="ollama_model">
<option>
@ -1941,6 +2011,10 @@
<input type="checkbox" id="legacy_api_textgenerationwebui" />
<span data-i18n="Legacy API (pre-OAI, no streaming)">Legacy API (pre-OAI, no streaming)</span>
</label>
<label data-tg-type="ooba" class="checkbox_label margin-bot-10px" for="bypass_status_check_textgenerationwebui">
<input type="checkbox" id="bypass_status_check_textgenerationwebui" />
<span data-i18n="Bypass status check">Bypass status check</span>
</label>
</form>
<div class="online_status">
<div class="online_status_indicator"></div>
@ -2290,10 +2364,10 @@
<div class="flex-container">
<input id="custom_model_id" class="text_pole wide100p" maxlength="500" value="" autocomplete="off" placeholder="Example: gpt-3.5-turbo">
</div>
<h4 data-i18n="Available Models">Available Models</h4>
<div class="flex-container">
<select id="model_custom_select" class="text_pole"></select>
</div>
<h4 data-i18n="Available Models">Available Models</h4>
<div class="flex-container">
<select id="model_custom_select" class="text_pole"></select>
</div>
</form>
<div class="flex-container flex">
<div id="api_button_openai" class="api_button menu_button menu_button_icon" type="submit" data-i18n="Connect">Connect</div>
@ -2770,10 +2844,10 @@
</div>
<div class="range-block-range-and-counter">
<div class="range-block-range paddingLeftRight5">
<input type="range" id="world_info_depth" name="volume" min="0" max="100" step="1">
<input class="neo-range-slider" type="range" id="world_info_depth" name="volume" min="0" max="100" step="1">
</div>
<div class="range-block-counter margin0">
<input type="number" data-for="world_info_depth" id="world_info_depth_counter">
<input class="neo-range-input" type="number" min="0" max="100" step="1" data-for="world_info_depth" id="world_info_depth_counter">
</div>
</div>
</div>
@ -2783,10 +2857,10 @@
</div>
<div class="range-block-range-and-counter ">
<div class="range-block-range paddingLeftRight5">
<input type="range" id="world_info_budget" name="volume" min="1" max="100" step="1">
<input class="neo-range-slider" type="range" id="world_info_budget" name="volume" min="1" max="100" step="1">
</div>
<div class="range-block-counter margin0">
<input type="number" min="1" max="100" step="1" data-for="world_info_budget" id="world_info_budget_counter">
<input class="neo-range-input" type="number" min="1" max="100" step="1" data-for="world_info_budget" id="world_info_budget_counter">
</div>
</div>
</div>
@ -2796,10 +2870,10 @@
</div>
<div class="range-block-range-and-counter ">
<div class="range-block-range paddingLeftRight5">
<input type="range" id="world_info_budget_cap" name="volume" min="0" max="8192" step="1">
<input class="neo-range-slider" type="range" id="world_info_budget_cap" name="volume" min="0" max="8192" step="1">
</div>
<div class="range-block-counter margin0">
<input type="number" min="0" max="8192" step="1" data-for="world_info_budget_cap" id="world_info_budget_cap_counter">
<input class="neo-range-input" type="number" min="0" max="8192" step="1" data-for="world_info_budget_cap" id="world_info_budget_cap_counter">
</div>
</div>
<div class="budget_cap_note">
@ -2812,10 +2886,10 @@
</div>
<div class="range-block-range-and-counter">
<div class="range-block-range paddingLeftRight5">
<input type="range" id="world_info_min_activations" name="volume" min="0" max="100" step="1">
<input class="neo-range-slider" type="range" id="world_info_min_activations" name="volume" min="0" max="100" step="1">
</div>
<div class="range-block-counter margin0">
<input type="number" data-for="world_info_min_activations" id="world_info_min_activations_counter">
<input class="neo-range-input" type="number" min="0" max="100" step="1" data-for="world_info_min_activations" id="world_info_min_activations_counter">
</div>
</div>
</div>
@ -2825,10 +2899,10 @@
</div>
<div class="range-block-range-and-counter">
<div class="range-block-range paddingLeftRight5">
<input type="range" id="world_info_min_activations_depth_max" name="volume" min="0" max="100" step="1">
<input class="neo-range-slider" type="range" id="world_info_min_activations_depth_max" name="volume" min="0" max="100" step="1">
</div>
<div class="range-block-counter margin0">
<input type="number" data-for="world_info_min_activations_depth_max" id="world_info_min_activations_depth_max_counter">
<input class="neo-range-input" type="number" min="0" max="100" step="1" data-for="world_info_min_activations_depth_max" id="world_info_min_activations_depth_max_counter">
</div>
</div>
<div class="budget_cap_note">
@ -2885,6 +2959,7 @@
<div id="world_backfill_memos" class="menu_button fa-solid fa-notes-medical" title="Fill empty Memo/Titles with Keywords" data-i18n="[title]Fill empty Memo/Titles with Keywords"></div>
<div id="world_import_button" class="menu_button fa-solid fa-file-import" title="Import World Info" data-i18n="[title]Import World Info"></div>
<div id="world_popup_export" class="menu_button fa-solid fa-file-export" title="Export World Info" data-i18n="[title]Export World Info"></div>
<div id="world_duplicate" class="menu_button fa-solid fa-paste" title="Duplicate World Info" data-i18n="[title]Duplicate World Info"></div>
<div id="world_popup_delete" class="menu_button fa-solid fa-trash-can redWarningBG" title="Delete World Info" data-i18n="[title]Delete World Info"></div>
<input type="search" class="text_pole textarea_compact" data-i18n="[placeholder]Search..." id="world_info_search" placeholder="Search...">
<select id="world_info_sort_order" class="margin0">
@ -4274,6 +4349,7 @@
<small class="textAlignCenter">Logic</small>
<select name="entryLogicType" class="widthFitContent margin0">
<option value="0">AND ANY</option>
<option value="3">AND ALL</option>
<option value="1">NOT ALL</option>
<option value="2">NOT ANY</option>
</select>
@ -4306,7 +4382,13 @@
<label class="checkbox flex-container alignitemscenter flexNoGap">
<input type="checkbox" name="exclude_recursion" />
<span data-i18n="Exclude from recursion">
Non-recursable
Non-recursable (this entry will not be activated by another)
</span>
</label>
<label class="checkbox flex-container alignitemscenter flexNoGap">
<input type="checkbox" name="prevent_recursion" />
<span data-i18n="Prevent further recursion (this entry will not activate others)">
Prevent further recursion (this entry will not activate others)
</span>
</label>
</div>
@ -4330,7 +4412,7 @@
<label for="characterFilter" class="">
<small data-i18n="Filter to Character(s)">Filter to Character(s)</small>
</label>
<label class="checkbox flex-container alignitemscenter">
<label class="checkbox_label flexNoGap">
<input type="checkbox" name="character_exclusion" />
<span data-i18n="Character Exclusion">
<small>Character Exclusion</small>
@ -4409,7 +4491,7 @@
<div id="logit_bias_template" class="template_element">
<div class="logit_bias_form">
<input class="logit_bias_text text_pole" data-i18n="[placeholder]Type here..." placeholder="type here..." />
<input class="logit_bias_value text_pole" type="number" min="-2" value="0" max="2" step="0.01" />
<input class="logit_bias_value text_pole" type="number" min="-100" value="0" max="100" step="0.01" />
<i class="menu_button fa-solid fa-xmark logit_bias_remove"></i>
</div>
</div>
@ -4676,10 +4758,10 @@
<h3><span data-i18n="Alternate Greetings">Alternate Greetings</span></h3>
<div title="Add" class="menu_button fa-solid fa-plus add_alternate_greeting" data-i18n="[title]Add"></div>
</div>
<div class="justifyLeft" data-i18n="Alternate Greetings Subtitle">
<small class="justifyLeft" data-i18n="Alternate Greetings Subtitle">
These will be displayed as swipes on the first message when starting a new chat.
Group members can select one of them to initiate the conversation.
</div>
</small>
<hr>
<div class="alternate_greetings_list flexFlowColumn flex-container wide100p">
<strong class="alternate_grettings_hint margin-bot-10px" data-i18n="Alternate Greetings Hint">
@ -4694,7 +4776,7 @@
<strong>Alternate Greeting #<span class="greeting_index"></span></strong>
<div class="menu_button fa-solid fa-trash-alt delete_alternate_greeting"></div>
</div>
<textarea name="alternate_greetings" data-i18n="[placeholder](This will be the first message from the character that starts every chat)" placeholder="(This will be the first message from the character that starts every chat)" class="text_pole textarea_compact alternate_greeting_text" maxlength="50000" value="" autocomplete="off" rows="12"></textarea>
<textarea name="alternate_greetings" data-i18n="[placeholder](This will be the first message from the character that starts every chat)" placeholder="(This will be the first message from the character that starts every chat)" class="text_pole textarea_compact alternate_greeting_text" maxlength="50000" value="" autocomplete="off" rows="16"></textarea>
</div>
</div>
<!-- chat and input bar -->
@ -5153,4 +5235,4 @@
</script>
</body>
</html>
</html>

View File

@ -0,0 +1,17 @@
{
"system_prompt": "Write {{char}}'s next reply in a fictional roleplay chat between {{user}} and {{char}}.\nWrite 1 reply only, italicize actions, and avoid quotation marks. Use markdown. Be proactive, creative, and drive the plot and conversation forward. Include dialog as well as narration.",
"input_sequence": "",
"output_sequence": "",
"first_output_sequence": "<START OF ROLEPLAY>",
"last_output_sequence": "\n### Response:",
"system_sequence_prefix": "",
"system_sequence_suffix": "",
"stop_sequence": "",
"separator_sequence": "",
"wrap": true,
"macro": true,
"names": false,
"names_force_groups": true,
"activation_regex": "",
"name": "Alpaca-Single-Turn"
}

File diff suppressed because it is too large Load Diff

View File

@ -1293,34 +1293,6 @@ class PromptManager {
this.log('Updated token usage with ' + this.tokenUsage);
}
/**
* Populates legacy token counts
*
* @deprecated This might serve no purpose and should be evaluated for removal
*
* @param {MessageCollection} messages
*/
populateLegacyTokenCounts(messages) {
// Update general token counts
const chatHistory = messages.getItemByIdentifier('chatHistory');
const startChat = chatHistory?.getCollection()[0]?.getTokens() || 0;
const continueNudge = chatHistory?.getCollection().find(message => message.identifier === 'continueNudge')?.getTokens() || 0;
this.tokenHandler.counts = {
...this.tokenHandler.counts,
...{
'start_chat': startChat,
'prompt': 0,
'bias': this.tokenHandler.counts.bias ?? 0,
'nudge': continueNudge,
'jailbreak': this.tokenHandler.counts.jailbreak ?? 0,
'impersonate': 0,
'examples': this.tokenHandler.counts.dialogueExamples ?? 0,
'conversation': this.tokenHandler.counts.chatHistory ?? 0,
},
};
}
/**
* Empties, then re-assembles the container containing the prompt list.
*/
@ -1381,7 +1353,7 @@ class PromptManager {
footerDiv.querySelector('.menu_button:last-child').addEventListener('click', this.handleNewPrompt);
// Add prompt export dialogue and options
const exportForCharacter =`
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>

View File

@ -1,5 +1,4 @@
import {
Generate,
characters,
online_status,
main_api,
@ -18,6 +17,7 @@ import {
menu_type,
substituteParams,
callPopup,
sendTextareaMessage,
} from '../script.js';
import {
@ -47,8 +47,6 @@ var LeftNavPanel = document.getElementById('left-nav-panel');
var WorldInfo = document.getElementById('WorldInfo');
var SelectedCharacterTab = document.getElementById('rm_button_selected_ch');
var AutoConnectCheckbox = document.getElementById('auto-connect-checkbox');
var AutoLoadChatCheckbox = document.getElementById('auto-load-chat-checkbox');
var connection_made = false;
var retry_delay = 500;
@ -368,7 +366,7 @@ function RA_autoconnect(PrevApi) {
setTimeout(RA_autoconnect, 100);
return;
}
if (online_status === 'no_connection' && LoadLocalBool('AutoConnectEnabled')) {
if (online_status === 'no_connection' && power_user.auto_connect) {
switch (main_api) {
case 'kobold':
if (api_server && isValidUrl(api_server)) {
@ -719,21 +717,19 @@ export function initRossMods() {
RA_checkOnlineStatus();
}, 100);
// read the state of AutoConnect and AutoLoadChat.
$(AutoConnectCheckbox).prop('checked', LoadLocalBool('AutoConnectEnabled'));
$(AutoLoadChatCheckbox).prop('checked', LoadLocalBool('AutoLoadChatEnabled'));
if (power_user.auto_load_chat) {
RA_autoloadchat();
}
setTimeout(function () {
if (LoadLocalBool('AutoLoadChatEnabled') == true) { RA_autoloadchat(); }
}, 200);
if (power_user.auto_connect) {
RA_autoconnect();
}
//Autoconnect on page load if enabled, or when api type is changed
if (LoadLocalBool('AutoConnectEnabled') == true) { RA_autoconnect(); }
$('#main_api').change(function () {
var PrevAPI = main_api;
setTimeout(() => RA_autoconnect(PrevAPI), 100);
});
$('#api_button').click(function () { setTimeout(RA_checkOnlineStatus, 100); });
//toggle pin class when lock toggle clicked
@ -855,10 +851,6 @@ export function initRossMods() {
OpenNavPanels();
}, 300);
//save AutoConnect and AutoLoadChat prefs
$(AutoConnectCheckbox).on('change', function () { SaveLocal('AutoConnectEnabled', $(AutoConnectCheckbox).prop('checked')); });
$(AutoLoadChatCheckbox).on('change', function () { SaveLocal('AutoLoadChatEnabled', $(AutoLoadChatCheckbox).prop('checked')); });
$(SelectedCharacterTab).click(function () { SaveLocal('SelectedNavTab', 'rm_button_selected_ch'); });
$('#rm_button_characters').click(function () { SaveLocal('SelectedNavTab', 'rm_button_characters'); });
@ -954,9 +946,9 @@ export function initRossMods() {
//Enter to send when send_textarea in focus
if ($(':focus').attr('id') === 'send_textarea') {
const sendOnEnter = shouldSendOnEnter();
if (!event.shiftKey && !event.ctrlKey && !event.altKey && event.key == 'Enter' && is_send_press == false && sendOnEnter) {
if (!event.shiftKey && !event.ctrlKey && !event.altKey && event.key == 'Enter' && sendOnEnter) {
event.preventDefault();
Generate();
sendTextareaMessage();
}
}
if ($(':focus').attr('id') === 'dialogue_popup_input' && !isMobile()) {

View File

@ -18,6 +18,8 @@ const defaultUrl = 'http://localhost:5100';
let saveMetadataTimeout = null;
let requiresReload = false;
export function saveMetadataDebounced() {
const context = getContext();
const groupId = context.groupId;
@ -193,24 +195,32 @@ async function discoverExtensions() {
function onDisableExtensionClick() {
const name = $(this).data('name');
disableExtension(name);
disableExtension(name, false);
}
function onEnableExtensionClick() {
const name = $(this).data('name');
enableExtension(name);
enableExtension(name, false);
}
async function enableExtension(name) {
async function enableExtension(name, reload = true) {
extension_settings.disabledExtensions = extension_settings.disabledExtensions.filter(x => x !== name);
await saveSettings();
location.reload();
if (reload) {
location.reload();
} else {
requiresReload = true;
}
}
async function disableExtension(name) {
async function disableExtension(name, reload = true) {
extension_settings.disabledExtensions.push(name);
await saveSettings();
location.reload();
if (reload) {
location.reload();
} else {
requiresReload = true;
}
}
async function getManifests(names) {
@ -560,6 +570,7 @@ function getModuleInformation() {
* Generates the HTML strings for all extensions and displays them in a popup.
*/
async function showExtensionsDetails() {
let popupPromise;
try {
showLoader();
let htmlDefault = '<h3>Built-in Extensions:</h3>';
@ -590,13 +601,20 @@ async function showExtensionsDetails() {
${htmlDefault}
${htmlExternal}
`;
callPopup(`<div class="extensions_info">${html}</div>`, 'text');
popupPromise = callPopup(`<div class="extensions_info">${html}</div>`, 'text');
} catch (error) {
toastr.error('Error loading extensions. See browser console for details.');
console.error(error);
} finally {
hideLoader();
}
if (popupPromise) {
await popupPromise;
}
if (requiresReload) {
showLoader();
location.reload();
}
}
@ -636,7 +654,7 @@ async function updateExtension(extensionName, quiet) {
toastr.success('Extension is already up to date');
}
} else {
toastr.success(`Extension ${extensionName} updated to ${data.shortCommitHash}`);
toastr.success(`Extension ${extensionName} updated to ${data.shortCommitHash}`, 'Reload the page to apply updates');
}
} catch (error) {
console.error('Error:', error);

View File

@ -286,6 +286,7 @@ jQuery(function () {
(extension_settings.caption.source === 'multimodal' && extension_settings.caption.multimodal_api === 'google' && secret_state[SECRET_KEYS.MAKERSUITE]) ||
(extension_settings.caption.source === 'multimodal' && extension_settings.caption.multimodal_api === 'ollama' && textgenerationwebui_settings.server_urls[textgen_types.OLLAMA]) ||
(extension_settings.caption.source === 'multimodal' && extension_settings.caption.multimodal_api === 'llamacpp' && textgenerationwebui_settings.server_urls[textgen_types.LLAMACPP]) ||
(extension_settings.caption.source === 'multimodal' && extension_settings.caption.multimodal_api === 'ooba' && textgenerationwebui_settings.server_urls[textgen_types.OOBA]) ||
(extension_settings.caption.source === 'multimodal' && extension_settings.caption.multimodal_api === 'custom') ||
extension_settings.caption.source === 'local' ||
extension_settings.caption.source === 'horde';
@ -351,6 +352,7 @@ jQuery(function () {
<label for="caption_multimodal_api">API</label>
<select id="caption_multimodal_api" class="flex1 text_pole">
<option value="llamacpp">llama.cpp</option>
<option value="ooba">Text Generation WebUI (oobabooga)</option>
<option value="ollama">Ollama</option>
<option value="openai">OpenAI</option>
<option value="openrouter">OpenRouter</option>
@ -369,6 +371,7 @@ jQuery(function () {
<option data-type="ollama" value="bakllava:latest">bakllava:latest</option>
<option data-type="ollama" value="llava:latest">llava:latest</option>
<option data-type="llamacpp" value="llamacpp_current">[Currently loaded]</option>
<option data-type="ooba" value="ooba_current">[Currently loaded]</option>
<option data-type="custom" value="custom_current">[Currently selected]</option>
</select>
</div>

View File

@ -10,6 +10,7 @@ export { MODULE_NAME };
const MODULE_NAME = 'expressions';
const UPDATE_INTERVAL = 2000;
const STREAMING_UPDATE_INTERVAL = 6000;
const TALKINGCHECK_UPDATE_INTERVAL = 500;
const FALLBACK_EXPRESSION = 'joy';
const DEFAULT_EXPRESSIONS = [
'talkinghead',
@ -46,9 +47,16 @@ const DEFAULT_EXPRESSIONS = [
let expressionsList = null;
let lastCharacter = undefined;
let lastMessage = null;
let lastTalkingState = false;
let lastTalkingStateMessage = null; // last message as seen by `updateTalkingState` (tracked separately, different timer)
let spriteCache = {};
let inApiCall = false;
let lastServerResponseTime = 0;
export let lastExpression = {};
function isTalkingHeadEnabled() {
return extension_settings.expressions.talkinghead && !extension_settings.expressions.local;
}
function isVisualNovelMode() {
return Boolean(!isMobile() && power_user.waifuMode && getContext().groupId);
@ -380,7 +388,10 @@ function onExpressionsShowDefaultInput() {
}
}
async function unloadLiveChar() {
/**
* Stops animating a talkinghead.
*/
async function unloadTalkingHead() {
if (!modules.includes('talkinghead')) {
console.debug('talkinghead module is disabled');
return;
@ -399,7 +410,10 @@ async function unloadLiveChar() {
}
}
async function loadLiveChar() {
/**
* Posts `talkinghead.png` of the current character to the talkinghead module in SillyTavern-extras, to start animating it.
*/
async function loadTalkingHead() {
if (!modules.includes('talkinghead')) {
console.debug('talkinghead module is disabled');
return;
@ -408,6 +422,8 @@ async function loadLiveChar() {
const spriteFolderName = getSpriteFolderName();
const talkingheadPath = `/characters/${encodeURIComponent(spriteFolderName)}/talkinghead.png`;
const emotionsSettingsPath = `/characters/${encodeURIComponent(spriteFolderName)}/_emotions.json`;
const animatorSettingsPath = `/characters/${encodeURIComponent(spriteFolderName)}/_animator.json`;
try {
const spriteResponse = await fetch(talkingheadPath);
@ -436,6 +452,69 @@ async function loadLiveChar() {
const loadResponseText = await loadResponse.text();
console.log(`Load talkinghead response: ${loadResponseText}`);
// Optional: per-character emotion templates
let emotionsSettings;
try {
const emotionsResponse = await fetch(emotionsSettingsPath);
if (emotionsResponse.ok) {
emotionsSettings = await emotionsResponse.json();
console.log(`Loaded ${emotionsSettingsPath}`);
} else {
throw new Error();
}
}
catch (error) {
emotionsSettings = {}; // blank -> use server defaults (to unload the previous character's customizations)
console.log(`No valid config at ${emotionsSettingsPath}, using server defaults`);
}
try {
const url = new URL(getApiUrl());
url.pathname = '/api/talkinghead/load_emotion_templates';
const apiResult = await doExtrasFetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Bypass-Tunnel-Reminder': 'bypass',
},
body: JSON.stringify(emotionsSettings),
});
}
catch (error) {
// it's ok if not supported
console.log('Failed to send _emotions.json (backend too old?), ignoring');
}
// Optional: per-character animator and postprocessor config
let animatorSettings;
try {
const animatorResponse = await fetch(animatorSettingsPath);
if (animatorResponse.ok) {
animatorSettings = await animatorResponse.json();
console.log(`Loaded ${animatorSettingsPath}`);
} else {
throw new Error();
}
}
catch (error) {
animatorSettings = {}; // blank -> use server defaults (to unload the previous character's customizations)
console.log(`No valid config at ${animatorSettingsPath}, using server defaults`);
}
try {
const url = new URL(getApiUrl());
url.pathname = '/api/talkinghead/load_animator_settings';
const apiResult = await doExtrasFetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Bypass-Tunnel-Reminder': 'bypass',
},
body: JSON.stringify(animatorSettings),
});
}
catch (error) {
// it's ok if not supported
console.log('Failed to send _animator.json (backend too old?), ignoring');
}
} catch (error) {
console.error(`Error loading talkinghead image: ${talkingheadPath} - ${error}`);
}
@ -449,7 +528,7 @@ function handleImageChange() {
return;
}
if (extension_settings.expressions.talkinghead && !extension_settings.expressions.local) {
if (isTalkingHeadEnabled()) {
// Method get IP of endpoint
const talkingheadResultFeedSrc = `${getApiUrl()}/api/talkinghead/result_feed`;
$('#expression-holder').css({ display: '' });
@ -558,9 +637,10 @@ async function moduleWorker() {
return;
}
const lastMessageChanged = !((lastCharacter === context.characterId || lastCharacter === context.groupId) && lastMessage === currentLastMessage.mes);
// check if last message changed
if ((lastCharacter === context.characterId || lastCharacter === context.groupId)
&& lastMessage === currentLastMessage.mes) {
if (!lastMessageChanged) {
return;
}
@ -610,21 +690,81 @@ async function moduleWorker() {
}
}
async function talkingHeadCheck() {
/**
* Starts/stops talkinghead talking animation.
*
* Talking starts only when all the following conditions are met:
* - The LLM is currently streaming its output.
* - The AI's current last message is non-empty, and also not just '...' (as produced by a swipe).
* - The AI's current last message has changed from what we saw during the previous call.
*
* In all other cases, talking stops.
*
* A talkinghead API call is made only when the talking state changes.
*/
async function updateTalkingState() {
// Don't bother if talkinghead is disabled or not loaded.
if (!isTalkingHeadEnabled() || !modules.includes('talkinghead')) {
return;
}
const context = getContext();
const currentLastMessage = getLastCharacterMessage();
try {
// TODO: Not sure if we need also "&& !context.groupId" here - the classify check in `moduleWorker`
// (that similarly checks the streaming processor state) does that for some reason.
// Talkinghead isn't currently designed to work with groups.
const lastMessageChanged = !((lastCharacter === context.characterId || lastCharacter === context.groupId) && lastTalkingStateMessage === currentLastMessage.mes);
const url = new URL(getApiUrl());
let newTalkingState;
if (context.streamingProcessor && !context.streamingProcessor.isFinished &&
currentLastMessage.mes.length !== 0 && currentLastMessage.mes !== '...' && lastMessageChanged) {
url.pathname = '/api/talkinghead/start_talking';
newTalkingState = true;
} else {
url.pathname = '/api/talkinghead/stop_talking';
newTalkingState = false;
}
try {
// Call the talkinghead API only if the talking state changed.
if (newTalkingState !== lastTalkingState) {
console.debug(`updateTalkingState: calling ${url.pathname}`);
await doExtrasFetch(url);
}
}
catch (error) {
// it's ok if not supported
}
finally {
lastTalkingState = newTalkingState;
}
}
catch (error) {
// console.log(error);
}
finally {
lastTalkingStateMessage = currentLastMessage.mes;
}
}
/**
* Checks whether the current character has a talkinghead image available.
* @returns {Promise<boolean>} True if the character has a talkinghead image available, false otherwise.
*/
async function isTalkingHeadAvailable() {
let spriteFolderName = getSpriteFolderName();
try {
await validateImages(spriteFolderName);
let talkingheadObj = spriteCache[spriteFolderName].find(obj => obj.label === 'talkinghead');
let talkingheadPath_f = talkingheadObj ? talkingheadObj.path : null;
let talkingheadPath = talkingheadObj ? talkingheadObj.path : null;
if (talkingheadPath_f != null) {
//console.log("talkingheadPath_f " + talkingheadPath_f);
if (talkingheadPath != null) {
return true;
} else {
//console.log("talkingheadPath_f is null");
unloadLiveChar();
await unloadTalkingHead();
return false;
}
} catch (err) {
@ -646,22 +786,22 @@ function getSpriteFolderName(characterMessage = null, characterName = null) {
return spriteFolderName;
}
function setTalkingHeadState(switch_var) {
extension_settings.expressions.talkinghead = switch_var; // Store setting
function setTalkingHeadState(newState) {
extension_settings.expressions.talkinghead = newState; // Store setting
saveSettingsDebounced();
if (extension_settings.expressions.local) {
return;
}
talkingHeadCheck().then(result => {
isTalkingHeadAvailable().then(result => {
if (result) {
//console.log("talkinghead exists!");
if (extension_settings.expressions.talkinghead) {
loadLiveChar();
loadTalkingHead();
} else {
unloadLiveChar();
unloadTalkingHead();
}
handleImageChange(); // Change image as needed
@ -692,6 +832,7 @@ function getFolderNameByMessage(message) {
}
async function sendExpressionCall(name, expression, force, vnMode) {
lastExpression[name.split('/')[0]] = expression;
if (!vnMode) {
vnMode = isVisualNovelMode();
}
@ -730,22 +871,29 @@ async function setSpriteSlashCommand(_, spriteId) {
spriteId = spriteId.trim().toLowerCase();
// In talkinghead mode, don't check for the existence of the sprite
// (emotion names are the same as for sprites, but it only needs "talkinghead.png").
const currentLastMessage = getLastCharacterMessage();
const spriteFolderName = getSpriteFolderName(currentLastMessage, currentLastMessage.name);
await validateImages(spriteFolderName);
let label = spriteId;
if (!isTalkingHeadEnabled()) {
await validateImages(spriteFolderName);
// Fuzzy search for sprite
const fuse = new Fuse(spriteCache[spriteFolderName], { keys: ['label'] });
const results = fuse.search(spriteId);
const spriteItem = results[0]?.item;
// Fuzzy search for sprite
const fuse = new Fuse(spriteCache[spriteFolderName], { keys: ['label'] });
const results = fuse.search(spriteId);
const spriteItem = results[0]?.item;
if (!spriteItem) {
console.log('No sprite found for search term ' + spriteId);
return;
if (!spriteItem) {
console.log('No sprite found for search term ' + spriteId);
return;
}
label = spriteItem.label;
}
const vnMode = isVisualNovelMode();
await sendExpressionCall(spriteFolderName, spriteItem.label, true, vnMode);
await sendExpressionCall(spriteFolderName, label, true, vnMode);
}
/**
@ -996,7 +1144,7 @@ async function getExpressionsList() {
}
async function setExpression(character, expression, force) {
if (extension_settings.expressions.local || !extension_settings.expressions.talkinghead) {
if (!isTalkingHeadEnabled()) {
console.debug('entered setExpressions');
await validateImages(character);
const img = $('img.expression');
@ -1107,24 +1255,38 @@ async function setExpression(character, expression, force) {
document.getElementById('expression-holder').style.display = '';
} else {
talkingHeadCheck().then(result => {
// Set the talkinghead emotion to the specified expression
// TODO: For now, talkinghead emote only supported when VN mode is off; see also updateVisualNovelMode.
try {
let result = await isTalkingHeadAvailable();
if (result) {
// Find the <img> element with id="expression-image" and class="expression"
const imgElement = document.querySelector('img#expression-image.expression');
//console.log("searching");
if (imgElement && imgElement instanceof HTMLImageElement) {
//console.log("setting value");
imgElement.src = getApiUrl() + '/api/talkinghead/result_feed';
}
} else {
//console.log("The fetch failed!");
const url = new URL(getApiUrl());
url.pathname = '/api/talkinghead/set_emotion';
await doExtrasFetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ emotion_name: expression }),
});
}
});
}
catch (error) {
// `set_emotion` is not present in old versions, so let it 404.
}
try {
// Find the <img> element with id="expression-image" and class="expression"
const imgElement = document.querySelector('img#expression-image.expression');
//console.log("searching");
if (imgElement && imgElement instanceof HTMLImageElement) {
//console.log("setting value");
imgElement.src = getApiUrl() + '/api/talkinghead/result_feed';
}
}
catch (error) {
//console.log("The fetch failed!");
}
}
}
@ -1245,6 +1407,11 @@ async function onClickExpressionUpload(event) {
// Reset the input
e.target.form.reset();
// In talkinghead mode, when a new talkinghead image is uploaded, refresh the live char.
if (isTalkingHeadEnabled() && id === 'talkinghead') {
await loadTalkingHead();
}
};
$('#expression_upload')
@ -1471,11 +1638,17 @@ function setExpressionOverrideHtml(forceClear = false) {
const updateFunction = wrapper.update.bind(wrapper);
setInterval(updateFunction, UPDATE_INTERVAL);
moduleWorker();
// For setting the talkinghead talking animation on/off quickly enough for realtime use, we need another timer on a shorter schedule.
const wrapperTalkingState = new ModuleWorkerWrapper(updateTalkingState);
const updateTalkingStateFunction = wrapperTalkingState.update.bind(wrapperTalkingState);
setInterval(updateTalkingStateFunction, TALKINGCHECK_UPDATE_INTERVAL);
updateTalkingState();
dragElement($('#expression-holder'));
eventSource.on(event_types.CHAT_CHANGED, () => {
// character changed
removeExpression();
spriteCache = {};
lastExpression = {};
//clear expression
let imgElement = document.getElementById('expression-image');
@ -1501,4 +1674,5 @@ function setExpressionOverrideHtml(forceClear = false) {
eventSource.on(event_types.GROUP_UPDATED, updateVisualNovelModeDebounced);
registerSlashCommand('sprite', setSpriteSlashCommand, ['emote'], '<span class="monospace">(spriteId)</span> force sets the sprite for the current character', true, true);
registerSlashCommand('spriteoverride', setSpriteSetCommand, ['costume'], '<span class="monospace">(optional folder)</span> sets an override sprite folder for the current character. If the name starts with a slash or a backslash, selects a sub-folder in the character-named folder. Empty value to reset to default.', true, true);
registerSlashCommand('lastsprite', (_, value) => lastExpression[value.trim()] ?? '', [], '<span class="monospace">(charName)</span> Returns the last set sprite / expression for the named character.', true, true);
})();

View File

@ -4,7 +4,7 @@ import {
characters,
getRequestHeaders,
} from '../../../script.js';
import { selected_group } from '../../group-chats.js';
import { groups, selected_group } from '../../group-chats.js';
import { loadFileToDocument, delay } from '../../utils.js';
import { loadMovingUIState } from '../../power-user.js';
import { dragElement } from '../../RossAscends-mods.js';
@ -416,7 +416,26 @@ function viewWithDragbox(items) {
// Registers a simple command for opening the char gallery.
registerSlashCommand('show-gallery', showGalleryCommand, ['sg'], ' shows the gallery', true, true);
registerSlashCommand('list-gallery', listGalleryCommand, ['lg'], '<span class="monospace">[optional char=charName] [optional group=groupName]</span> list images in the gallery of the current char / group or a specified char / group', true, true);
function showGalleryCommand(args) {
showCharGallery();
}
async function listGalleryCommand(args) {
try {
let url = args.char ?? (args.group ? groups.find(it=>it.name == args.group)?.id : null) ?? (selected_group || this_chid);
if (!args.char && !args.group && !selected_group && this_chid) {
const char = characters[this_chid];
url = char.avatar.replace('.png', '');
}
const items = await getGalleryItems(url);
return JSON.stringify(items.map(it=>it.src));
} catch (err) {
console.trace();
console.error(err);
}
return JSON.stringify([]);
}

View File

@ -0,0 +1,451 @@
// eslint-disable-next-line no-unused-vars
import { QuickReply } from '../src/QuickReply.js';
import { QuickReplyContextLink } from '../src/QuickReplyContextLink.js';
import { QuickReplySet } from '../src/QuickReplySet.js';
// eslint-disable-next-line no-unused-vars
import { QuickReplySettings } from '../src/QuickReplySettings.js';
// eslint-disable-next-line no-unused-vars
import { SettingsUi } from '../src/ui/SettingsUi.js';
export class QuickReplyApi {
/**@type {QuickReplySettings}*/ settings;
/**@type {SettingsUi}*/ settingsUi;
constructor(/**@type {QuickReplySettings}*/settings, /**@type {SettingsUi}*/settingsUi) {
this.settings = settings;
this.settingsUi = settingsUi;
}
/**
* Finds and returns an existing Quick Reply Set by its name.
*
* @param {String} name name of the quick reply set
* @returns the quick reply set, or undefined if not found
*/
getSetByName(name) {
return QuickReplySet.get(name);
}
/**
* Finds and returns an existing Quick Reply by its set's name and its label.
*
* @param {String} setName name of the quick reply set
* @param {String} label label of the quick reply
* @returns the quick reply, or undefined if not found
*/
getQrByLabel(setName, label) {
const set = this.getSetByName(setName);
if (!set) return;
return set.qrList.find(it=>it.label == label);
}
/**
* Executes a quick reply by its index and returns the result.
*
* @param {Number} idx the index (zero-based) of the quick reply to execute
* @returns the return value of the quick reply, or undefined if not found
*/
async executeQuickReplyByIndex(idx) {
const qr = [...this.settings.config.setList, ...(this.settings.chatConfig?.setList ?? [])]
.map(it=>it.set.qrList)
.flat()[idx]
;
if (qr) {
return await qr.onExecute();
} else {
throw new Error(`No quick reply at index "${idx}"`);
}
}
/**
* Executes an existing quick reply.
*
* @param {String} setName name of the existing quick reply set
* @param {String} label label of the existing quick reply (text on the button)
* @param {Object} [args] optional arguments
*/
async executeQuickReply(setName, label, args = {}) {
const qr = this.getQrByLabel(setName, label);
if (!qr) {
throw new Error(`No quick reply with label "${label}" in set "${setName}" found.`);
}
return await qr.execute(args);
}
/**
* Adds or removes a quick reply set to the list of globally active quick reply sets.
*
* @param {String} name the name of the set
* @param {Boolean} isVisible whether to show the set's buttons or not
*/
toggleGlobalSet(name, isVisible = true) {
const set = this.getSetByName(name);
if (!set) {
throw new Error(`No quick reply set with name "${name}" found.`);
}
if (this.settings.config.hasSet(set)) {
this.settings.config.removeSet(set);
} else {
this.settings.config.addSet(set, isVisible);
}
}
/**
* Adds a quick reply set to the list of globally active quick reply sets.
*
* @param {String} name the name of the set
* @param {Boolean} isVisible whether to show the set's buttons or not
*/
addGlobalSet(name, isVisible = true) {
const set = this.getSetByName(name);
if (!set) {
throw new Error(`No quick reply set with name "${name}" found.`);
}
this.settings.config.addSet(set, isVisible);
}
/**
* Removes a quick reply set from the list of globally active quick reply sets.
*
* @param {String} name the name of the set
*/
removeGlobalSet(name) {
const set = this.getSetByName(name);
if (!set) {
throw new Error(`No quick reply set with name "${name}" found.`);
}
this.settings.config.removeSet(set);
}
/**
* Adds or removes a quick reply set to the list of the current chat's active quick reply sets.
*
* @param {String} name the name of the set
* @param {Boolean} isVisible whether to show the set's buttons or not
*/
toggleChatSet(name, isVisible = true) {
if (!this.settings.chatConfig) return;
const set = this.getSetByName(name);
if (!set) {
throw new Error(`No quick reply set with name "${name}" found.`);
}
if (this.settings.chatConfig.hasSet(set)) {
this.settings.chatConfig.removeSet(set);
} else {
this.settings.chatConfig.addSet(set, isVisible);
}
}
/**
* Adds a quick reply set to the list of the current chat's active quick reply sets.
*
* @param {String} name the name of the set
* @param {Boolean} isVisible whether to show the set's buttons or not
*/
addChatSet(name, isVisible = true) {
if (!this.settings.chatConfig) return;
const set = this.getSetByName(name);
if (!set) {
throw new Error(`No quick reply set with name "${name}" found.`);
}
this.settings.chatConfig.addSet(set, isVisible);
}
/**
* Removes a quick reply set from the list of the current chat's active quick reply sets.
*
* @param {String} name the name of the set
*/
removeChatSet(name) {
if (!this.settings.chatConfig) return;
const set = this.getSetByName(name);
if (!set) {
throw new Error(`No quick reply set with name "${name}" found.`);
}
this.settings.chatConfig.removeSet(set);
}
/**
* Creates a new quick reply in an existing quick reply set.
*
* @param {String} setName name of the quick reply set to insert the new quick reply into
* @param {String} label label for the new quick reply (text on the button)
* @param {Object} [props]
* @param {String} [props.message] the message to be sent or slash command to be executed by the new quick reply
* @param {String} [props.title] the title / tooltip to be shown on the quick reply button
* @param {Boolean} [props.isHidden] whether to hide or show the button
* @param {Boolean} [props.executeOnStartup] whether to execute the quick reply when SillyTavern starts
* @param {Boolean} [props.executeOnUser] whether to execute the quick reply after a user has sent a message
* @param {Boolean} [props.executeOnAi] whether to execute the quick reply after the AI has sent a message
* @param {Boolean} [props.executeOnChatChange] whether to execute the quick reply when a new chat is loaded
* @returns {QuickReply} the new quick reply
*/
createQuickReply(setName, label, {
message,
title,
isHidden,
executeOnStartup,
executeOnUser,
executeOnAi,
executeOnChatChange,
} = {}) {
const set = this.getSetByName(setName);
if (!set) {
throw new Error(`No quick reply set with named "${setName}" found.`);
}
const qr = set.addQuickReply();
qr.label = label ?? '';
qr.message = message ?? '';
qr.title = title ?? '';
qr.isHidden = isHidden ?? false;
qr.executeOnStartup = executeOnStartup ?? false;
qr.executeOnUser = executeOnUser ?? false;
qr.executeOnAi = executeOnAi ?? false;
qr.executeOnChatChange = executeOnChatChange ?? false;
qr.onUpdate();
return qr;
}
/**
* Updates an existing quick reply.
*
* @param {String} setName name of the existing quick reply set
* @param {String} label label of the existing quick reply (text on the button)
* @param {Object} [props]
* @param {String} [props.newLabel] new label for quick reply (text on the button)
* @param {String} [props.message] the message to be sent or slash command to be executed by the quick reply
* @param {String} [props.title] the title / tooltip to be shown on the quick reply button
* @param {Boolean} [props.isHidden] whether to hide or show the button
* @param {Boolean} [props.executeOnStartup] whether to execute the quick reply when SillyTavern starts
* @param {Boolean} [props.executeOnUser] whether to execute the quick reply after a user has sent a message
* @param {Boolean} [props.executeOnAi] whether to execute the quick reply after the AI has sent a message
* @param {Boolean} [props.executeOnChatChange] whether to execute the quick reply when a new chat is loaded
* @returns {QuickReply} the altered quick reply
*/
updateQuickReply(setName, label, {
newLabel,
message,
title,
isHidden,
executeOnStartup,
executeOnUser,
executeOnAi,
executeOnChatChange,
} = {}) {
const qr = this.getQrByLabel(setName, label);
if (!qr) {
throw new Error(`No quick reply with label "${label}" in set "${setName}" found.`);
}
qr.label = newLabel ?? qr.label;
qr.message = message ?? qr.message;
qr.title = title ?? qr.title;
qr.isHidden = isHidden ?? qr.isHidden;
qr.executeOnStartup = executeOnStartup ?? qr.executeOnStartup;
qr.executeOnUser = executeOnUser ?? qr.executeOnUser;
qr.executeOnAi = executeOnAi ?? qr.executeOnAi;
qr.executeOnChatChange = executeOnChatChange ?? qr.executeOnChatChange;
qr.onUpdate();
return qr;
}
/**
* Deletes an existing quick reply.
*
* @param {String} setName name of the existing quick reply set
* @param {String} label label of the existing quick reply (text on the button)
*/
deleteQuickReply(setName, label) {
const qr = this.getQrByLabel(setName, label);
if (!qr) {
throw new Error(`No quick reply with label "${label}" in set "${setName}" found.`);
}
qr.delete();
}
/**
* Adds an existing quick reply set as a context menu to an existing quick reply.
*
* @param {String} setName name of the existing quick reply set containing the quick reply
* @param {String} label label of the existing quick reply
* @param {String} contextSetName name of the existing quick reply set to be used as a context menu
* @param {Boolean} isChained whether or not to chain the context menu quick replies
*/
createContextItem(setName, label, contextSetName, isChained = false) {
const qr = this.getQrByLabel(setName, label);
const set = this.getSetByName(contextSetName);
if (!qr) {
throw new Error(`No quick reply with label "${label}" in set "${setName}" found.`);
}
if (!set) {
throw new Error(`No quick reply set with name "${contextSetName}" found.`);
}
const cl = new QuickReplyContextLink();
cl.set = set;
cl.isChained = isChained;
qr.addContextLink(cl);
}
/**
* Removes a quick reply set from a quick reply's context menu.
*
* @param {String} setName name of the existing quick reply set containing the quick reply
* @param {String} label label of the existing quick reply
* @param {String} contextSetName name of the existing quick reply set to be used as a context menu
*/
deleteContextItem(setName, label, contextSetName) {
const qr = this.getQrByLabel(setName, label);
const set = this.getSetByName(contextSetName);
if (!qr) {
throw new Error(`No quick reply with label "${label}" in set "${setName}" found.`);
}
if (!set) {
throw new Error(`No quick reply set with name "${contextSetName}" found.`);
}
qr.removeContextLink(set.name);
}
/**
* Removes all entries from a quick reply's context menu.
*
* @param {String} setName name of the existing quick reply set containing the quick reply
* @param {String} label label of the existing quick reply
*/
clearContextMenu(setName, label) {
const qr = this.getQrByLabel(setName, label);
if (!qr) {
throw new Error(`No quick reply with label "${label}" in set "${setName}" found.`);
}
qr.clearContextLinks();
}
/**
* Create a new quick reply set.
*
* @param {String} name name of the new quick reply set
* @param {Object} [props]
* @param {Boolean} [props.disableSend] whether or not to send the quick replies or put the message or slash command into the char input box
* @param {Boolean} [props.placeBeforeInput] whether or not to place the quick reply contents before the existing user input
* @param {Boolean} [props.injectInput] whether or not to automatically inject the user input at the end of the quick reply
* @returns {Promise<QuickReplySet>} the new quick reply set
*/
async createSet(name, {
disableSend,
placeBeforeInput,
injectInput,
} = {}) {
const set = new QuickReplySet();
set.name = name;
set.disableSend = disableSend ?? false;
set.placeBeforeInput = placeBeforeInput ?? false;
set.injectInput = injectInput ?? false;
const oldSet = this.getSetByName(name);
if (oldSet) {
QuickReplySet.list.splice(QuickReplySet.list.indexOf(oldSet), 1, set);
} else {
const idx = QuickReplySet.list.findIndex(it=>it.name.localeCompare(name) == 1);
if (idx > -1) {
QuickReplySet.list.splice(idx, 0, set);
} else {
QuickReplySet.list.push(set);
}
}
await set.save();
this.settingsUi.rerender();
return set;
}
/**
* Update an existing quick reply set.
*
* @param {String} name name of the existing quick reply set
* @param {Object} [props]
* @param {Boolean} [props.disableSend] whether or not to send the quick replies or put the message or slash command into the char input box
* @param {Boolean} [props.placeBeforeInput] whether or not to place the quick reply contents before the existing user input
* @param {Boolean} [props.injectInput] whether or not to automatically inject the user input at the end of the quick reply
* @returns {Promise<QuickReplySet>} the altered quick reply set
*/
async updateSet(name, {
disableSend,
placeBeforeInput,
injectInput,
} = {}) {
const set = this.getSetByName(name);
if (!set) {
throw new Error(`No quick reply set with name "${name}" found.`);
}
set.disableSend = disableSend ?? false;
set.placeBeforeInput = placeBeforeInput ?? false;
set.injectInput = injectInput ?? false;
await set.save();
this.settingsUi.rerender();
return set;
}
/**
* Delete an existing quick reply set.
*
* @param {String} name name of the existing quick reply set
*/
async deleteSet(name) {
const set = this.getSetByName(name);
if (!set) {
throw new Error(`No quick reply set with name "${name}" found.`);
}
await set.delete();
this.settingsUi.rerender();
}
/**
* Gets a list of all quick reply sets.
*
* @returns array with the names of all quick reply sets
*/
listSets() {
return QuickReplySet.list.map(it=>it.name);
}
/**
* Gets a list of all globally active quick reply sets.
*
* @returns array with the names of all quick reply sets
*/
listGlobalSets() {
return this.settings.config.setList.map(it=>it.set.name);
}
/**
* Gets a list of all quick reply sets activated by the current chat.
*
* @returns array with the names of all quick reply sets
*/
listChatSets() {
return this.settings.chatConfig?.setList?.flatMap(it=>it.set.name) ?? [];
}
/**
* Gets a list of all quick replies in the quick reply set.
*
* @param {String} setName name of the existing quick reply set
* @returns array with the labels of this set's quick replies
*/
listQuickReplies(setName) {
const set = this.getSetByName(setName);
if (!set) {
throw new Error(`No quick reply set with name "${name}" found.`);
}
return set.qrList.map(it=>it.label);
}
}

View File

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

View File

@ -0,0 +1,83 @@
<div id="qr--modalEditor">
<div id="qr--main">
<h3>Labels and Message</h3>
<div class="qr--labels">
<label>
<span class="qr--labelText">Label</span>
<input type="text" class="text_pole" id="qr--modal-label">
</label>
<label>
<span class="qr--labelText">Title</span>
<small class="qr--labelHint">(tooltip, leave empty to show message or /command)</small>
<input type="text" class="text_pole" id="qr--modal-title">
</label>
</div>
<div class="qr--modal-messageContainer">
<label for="qr--modal-message">Message / Command:</label>
<textarea class="monospace" id="qr--modal-message"></textarea>
</div>
</div>
<div id="qr--qrOptions">
<h3>Context Menu</h3>
<div id="qr--ctxEditor">
<template id="qr--ctxItem">
<div class="qr--ctxItem" data-order="0">
<div class="drag-handle ui-sortable-handle"></div>
<select class="qr--set"></select>
<label class="qr--isChainedLabel checkbox_label" title="When enabled, the current Quick Reply will be sent together with (before) the clicked QR from the context menu.">
Chaining:
<input type="checkbox" class="qr--isChained">
</label>
<div class="qr--delete menu_button menu_button_icon fa-solid fa-trash-can" title="Remove entry"></div>
</div>
</template>
</div>
<div class="qr--ctxEditorActions">
<span id="qr--ctxAdd" class="menu_button menu_button_icon fa-solid fa-plus" title="Add quick reply set to context menu"></span>
</div>
<h3>Auto-Execute</h3>
<div class="flex-container flexFlowColumn">
<label class="checkbox_label" title="Prevent this quick reply from triggering other auto-executed quick replies while auto-executing (i.e., prevent recursive auto-execution)">
<input type="checkbox" id="qr--preventAutoExecute" >
<span><i class="fa-solid fa-fw fa-plane-slash"></i> Don't trigger auto-execute</span>
</label>
<label class="checkbox_label">
<input type="checkbox" id="qr--isHidden" >
<span><i class="fa-solid fa-fw fa-eye-slash"></i> Invisible (auto-execute only)</span>
</label>
<label class="checkbox_label">
<input type="checkbox" id="qr--executeOnStartup" >
<span><i class="fa-solid fa-fw fa-rocket"></i> Execute on app startup</span>
</label>
<label class="checkbox_label">
<input type="checkbox" id="qr--executeOnUser" >
<span><i class="fa-solid fa-fw fa-user"></i> Execute on user message</span>
</label>
<label class="checkbox_label">
<input type="checkbox" id="qr--executeOnAi" >
<span><i class="fa-solid fa-fw fa-robot"></i> Execute on AI message</span>
</label>
<label class="checkbox_label">
<input type="checkbox" id="qr--executeOnChatChange" >
<span><i class="fa-solid fa-fw fa-message"></i> Execute on opening chat</span>
</label>
</div>
<h3>Testing</h3>
<div id="qr--modal-execute" class="menu_button" title="Execute the quick reply now">
<i class="fa-solid fa-play"></i>
Execute
</div>
<label class="checkbox_label">
<input type="checkbox" id="qr--modal-executeHide">
<span> Hide editor while executing</span>
</label>
<div id="qr--modal-executeErrors"></div>
</div>
</div>

View File

@ -0,0 +1,71 @@
<div id="qr--settings">
<div class="inline-drawer">
<div class="inline-drawer-toggle inline-drawer-header">
<strong>Quick Reply</strong>
<div class="inline-drawer-icon fa-solid fa-circle-chevron-down down"></div>
</div>
<div class="inline-drawer-content">
<label class="flex-container">
<input type="checkbox" id="qr--isEnabled"> Enable Quick Replies
</label>
<label class="flex-container">
<input type="checkbox" id="qr--isCombined"> Combine buttons from all active sets
</label>
<hr>
<div id="qr--global">
<div class="qr--head">
<div class="qr--title">Global Quick Reply Sets</div>
<div class="qr--actions">
<div class="qr--setListAdd menu_button menu_button_icon fa-solid fa-plus" id="qr--global-setListAdd" title="Add quick reply set"></div>
</div>
</div>
<div id="qr--global-setList" class="qr--setList"></div>
</div>
<hr>
<div id="qr--chat">
<div class="qr--head">
<div class="qr--title">Chat Quick Reply Sets</div>
<div class="qr--actions">
<div class="qr--setListAdd menu_button menu_button_icon fa-solid fa-plus" id="qr--chat-setListAdd" title="Add quick reply set"></div>
</div>
</div>
<div id="qr--chat-setList" class="qr--setList"></div>
</div>
<hr>
<div id="qr--editor">
<div class="qr--head">
<div class="qr--title">Edit Quick Replies</div>
<div class="qr--actions">
<select id="qr--set" class="text_pole"></select>
<div class="qr--add menu_button menu_button_icon fa-solid fa-plus" id="qr--set-new" title="Create new quick reply set"></div>
<div class="qr--add menu_button menu_button_icon fa-solid fa-file-import" id="qr--set-import" title="Import quick reply set"></div>
<input type="file" id="qr--set-importFile" accept=".json" hidden>
<div class="qr--add menu_button menu_button_icon fa-solid fa-file-export" id="qr--set-export" title="Export quick reply set"></div>
<div class="qr--del menu_button menu_button_icon fa-solid fa-trash redWarningBG" id="qr--set-delete" title="Delete quick reply set"></div>
</div>
</div>
<div id="qr--set-settings">
<label class="flex-container">
<input type="checkbox" id="qr--disableSend"> <span>Disable send (insert into input field)</span>
</label>
<label class="flex-container">
<input type="checkbox" id="qr--placeBeforeInput"> <span>Place quick reply before input</span>
</label>
<label class="flex-container" id="qr--injectInputContainer">
<input type="checkbox" id="qr--injectInput"> <span>Inject user input automatically <small>(if disabled, use <code>{{input}}</code> macro for manual injection)</small></span>
</label>
</div>
<div id="qr--set-qrList" class="qr--qrList"></div>
<div class="qr--set-qrListActions">
<div class="qr--add menu_button menu_button_icon fa-solid fa-plus" id="qr--set-add" title="Add quick reply"></div>
</div>
</div>
</div>
</div>
</div>

File diff suppressed because it is too large Load Diff

View File

@ -6,6 +6,6 @@
"js": "index.js",
"css": "style.css",
"author": "RossAscends#1779",
"version": "1.0.0",
"version": "2.0.0",
"homePage": "https://github.com/SillyTavern/SillyTavern"
}
}

View File

@ -0,0 +1,76 @@
import { warn } from '../index.js';
// eslint-disable-next-line no-unused-vars
import { QuickReply } from './QuickReply.js';
// eslint-disable-next-line no-unused-vars
import { QuickReplySettings } from './QuickReplySettings.js';
export class AutoExecuteHandler {
/**@type {QuickReplySettings}*/ settings;
/**@type {Boolean[]}*/ preventAutoExecuteStack = [];
constructor(/**@type {QuickReplySettings}*/settings) {
this.settings = settings;
}
checkExecute() {
return this.settings.isEnabled && !this.preventAutoExecuteStack.slice(-1)[0];
}
async performAutoExecute(/**@type {QuickReply[]}*/qrList) {
for (const qr of qrList) {
this.preventAutoExecuteStack.push(qr.preventAutoExecute);
try {
await qr.execute({ isAutoExecute:true });
} catch (ex) {
warn(ex);
} finally {
this.preventAutoExecuteStack.pop();
}
}
}
async handleStartup() {
if (!this.checkExecute()) return;
const qrList = [
...this.settings.config.setList.map(link=>link.set.qrList.filter(qr=>qr.executeOnStartup)).flat(),
...(this.settings.chatConfig?.setList?.map(link=>link.set.qrList.filter(qr=>qr.executeOnStartup))?.flat() ?? []),
];
await this.performAutoExecute(qrList);
}
async handleUser() {
if (!this.checkExecute()) return;
const qrList = [
...this.settings.config.setList.map(link=>link.set.qrList.filter(qr=>qr.executeOnUser)).flat(),
...(this.settings.chatConfig?.setList?.map(link=>link.set.qrList.filter(qr=>qr.executeOnUser))?.flat() ?? []),
];
await this.performAutoExecute(qrList);
}
async handleAi() {
if (!this.checkExecute()) return;
const qrList = [
...this.settings.config.setList.map(link=>link.set.qrList.filter(qr=>qr.executeOnAi)).flat(),
...(this.settings.chatConfig?.setList?.map(link=>link.set.qrList.filter(qr=>qr.executeOnAi))?.flat() ?? []),
];
await this.performAutoExecute(qrList);
}
async handleChatChanged() {
if (!this.checkExecute()) return;
const qrList = [
...this.settings.config.setList.map(link=>link.set.qrList.filter(qr=>qr.executeOnChatChange)).flat(),
...(this.settings.chatConfig?.setList?.map(link=>link.set.qrList.filter(qr=>qr.executeOnChatChange))?.flat() ?? []),
];
await this.performAutoExecute(qrList);
}
}

View File

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

View File

@ -0,0 +1,489 @@
import { callPopup } from '../../../../script.js';
import { getSortableDelay } from '../../../utils.js';
import { log, warn } from '../index.js';
import { QuickReplyContextLink } from './QuickReplyContextLink.js';
import { QuickReplySet } from './QuickReplySet.js';
import { ContextMenu } from './ui/ctx/ContextMenu.js';
export class QuickReply {
/**
* @param {{ id?: number; contextList?: any; }} props
*/
static from(props) {
props.contextList = (props.contextList ?? []).map((/** @type {any} */ it)=>QuickReplyContextLink.from(it));
return Object.assign(new this(), props);
}
/**@type {Number}*/ id;
/**@type {String}*/ label = '';
/**@type {String}*/ title = '';
/**@type {String}*/ message = '';
/**@type {QuickReplyContextLink[]}*/ contextList;
/**@type {Boolean}*/ preventAutoExecute = true;
/**@type {Boolean}*/ isHidden = false;
/**@type {Boolean}*/ executeOnStartup = false;
/**@type {Boolean}*/ executeOnUser = false;
/**@type {Boolean}*/ executeOnAi = false;
/**@type {Boolean}*/ executeOnChatChange = false;
/**@type {Function}*/ onExecute;
/**@type {Function}*/ onDelete;
/**@type {Function}*/ onUpdate;
/**@type {HTMLElement}*/ dom;
/**@type {HTMLElement}*/ domLabel;
/**@type {HTMLElement}*/ settingsDom;
/**@type {HTMLInputElement}*/ settingsDomLabel;
/**@type {HTMLTextAreaElement}*/ settingsDomMessage;
get hasContext() {
return this.contextList && this.contextList.length > 0;
}
unrender() {
this.dom?.remove();
this.dom = null;
}
updateRender() {
if (!this.dom) return;
this.dom.title = this.title || this.message;
this.domLabel.textContent = this.label;
this.dom.classList[this.hasContext ? 'add' : 'remove']('qr--hasCtx');
}
render() {
this.unrender();
if (!this.dom) {
const root = document.createElement('div'); {
this.dom = root;
root.classList.add('qr--button');
root.classList.add('menu_button');
if (this.hasContext) {
root.classList.add('qr--hasCtx');
}
root.title = this.title || this.message;
root.addEventListener('contextmenu', (evt) => {
log('contextmenu', this, this.hasContext);
if (this.hasContext) {
evt.preventDefault();
evt.stopPropagation();
const menu = new ContextMenu(this);
menu.show(evt);
}
});
root.addEventListener('click', (evt)=>{
if (evt.ctrlKey) {
this.showEditor();
return;
}
this.execute();
});
const lbl = document.createElement('div'); {
this.domLabel = lbl;
lbl.classList.add('qr--button-label');
lbl.textContent = this.label;
root.append(lbl);
}
const expander = document.createElement('div'); {
expander.classList.add('qr--button-expander');
expander.textContent = '⋮';
expander.title = 'Open context menu';
expander.addEventListener('click', (evt) => {
evt.stopPropagation();
evt.preventDefault();
const menu = new ContextMenu(this);
menu.show(evt);
});
root.append(expander);
}
}
}
return this.dom;
}
renderSettings(idx) {
if (!this.settingsDom) {
const item = document.createElement('div'); {
this.settingsDom = item;
item.classList.add('qr--set-item');
item.setAttribute('data-order', String(idx));
item.setAttribute('data-id', String(this.id));
const drag = document.createElement('div'); {
drag.classList.add('drag-handle');
drag.classList.add('ui-sortable-handle');
drag.textContent = '☰';
item.append(drag);
}
const lblContainer = document.createElement('div'); {
lblContainer.classList.add('qr--set-itemLabelContainer');
const lbl = document.createElement('input'); {
this.settingsDomLabel = lbl;
lbl.classList.add('qr--set-itemLabel');
lbl.classList.add('text_pole');
lbl.value = this.label;
lbl.addEventListener('input', ()=>this.updateLabel(lbl.value));
lblContainer.append(lbl);
}
item.append(lblContainer);
}
const optContainer = document.createElement('div'); {
optContainer.classList.add('qr--set-optionsContainer');
const opt = document.createElement('div'); {
opt.classList.add('qr--action');
opt.classList.add('menu_button');
opt.classList.add('fa-solid');
opt.textContent = '⁝';
opt.title = 'Additional options:\n - large editor\n - context menu\n - auto-execution\n - tooltip';
opt.addEventListener('click', ()=>this.showEditor());
optContainer.append(opt);
}
item.append(optContainer);
}
const mes = document.createElement('textarea'); {
this.settingsDomMessage = mes;
mes.id = `qr--set--item${this.id}`;
mes.classList.add('qr--set-itemMessage');
mes.value = this.message;
//HACK need to use jQuery to catch the triggered event from the expanded editor
$(mes).on('input', ()=>this.updateMessage(mes.value));
item.append(mes);
}
const actions = document.createElement('div'); {
actions.classList.add('qr--actions');
const del = document.createElement('div'); {
del.classList.add('qr--action');
del.classList.add('menu_button');
del.classList.add('menu_button_icon');
del.classList.add('fa-solid');
del.classList.add('fa-trash-can');
del.classList.add('redWarningBG');
del.title = 'Remove quick reply';
del.addEventListener('click', ()=>this.delete());
actions.append(del);
}
item.append(actions);
}
}
}
return this.settingsDom;
}
unrenderSettings() {
this.settingsDom?.remove();
}
async showEditor() {
const response = await fetch('/scripts/extensions/quick-reply/html/qrEditor.html', { cache: 'no-store' });
if (response.ok) {
this.template = document.createRange().createContextualFragment(await response.text()).querySelector('#qr--modalEditor');
/**@type {HTMLElement} */
// @ts-ignore
const dom = this.template.cloneNode(true);
const popupResult = callPopup(dom, 'text', undefined, { okButton: 'OK', wide: true, large: true, rows: 1 });
// basics
/**@type {HTMLInputElement}*/
const label = dom.querySelector('#qr--modal-label');
label.value = this.label;
label.addEventListener('input', ()=>{
this.updateLabel(label.value);
});
/**@type {HTMLInputElement}*/
const title = dom.querySelector('#qr--modal-title');
title.value = this.title;
title.addEventListener('input', () => {
this.updateTitle(title.value);
});
/**@type {HTMLTextAreaElement}*/
const message = dom.querySelector('#qr--modal-message');
message.value = this.message;
message.addEventListener('input', () => {
this.updateMessage(message.value);
});
//TODO move tab support for textarea into its own helper(?) and use for both this and .editor_maximize
message.addEventListener('keydown', (evt) => {
if (evt.key == 'Tab' && !evt.shiftKey && !evt.ctrlKey && !evt.altKey) {
evt.preventDefault();
const start = message.selectionStart;
const end = message.selectionEnd;
if (end - start > 0 && message.value.substring(start, end).includes('\n')) {
const lineStart = message.value.lastIndexOf('\n', start);
const count = message.value.substring(lineStart, end).split('\n').length - 1;
message.value = `${message.value.substring(0, lineStart)}${message.value.substring(lineStart, end).replace(/\n/g, '\n\t')}${message.value.substring(end)}`;
message.selectionStart = start + 1;
message.selectionEnd = end + count;
this.updateMessage(message.value);
} else {
message.value = `${message.value.substring(0, start)}\t${message.value.substring(end)}`;
message.selectionStart = start + 1;
message.selectionEnd = end + 1;
this.updateMessage(message.value);
}
} else if (evt.key == 'Tab' && evt.shiftKey && !evt.ctrlKey && !evt.altKey) {
evt.preventDefault();
const start = message.selectionStart;
const end = message.selectionEnd;
const lineStart = message.value.lastIndexOf('\n', start);
const count = message.value.substring(lineStart, end).split('\n\t').length - 1;
message.value = `${message.value.substring(0, lineStart)}${message.value.substring(lineStart, end).replace(/\n\t/g, '\n')}${message.value.substring(end)}`;
message.selectionStart = start - 1;
message.selectionEnd = end - count;
this.updateMessage(message.value);
}
});
// context menu
/**@type {HTMLTemplateElement}*/
const tpl = dom.querySelector('#qr--ctxItem');
const linkList = dom.querySelector('#qr--ctxEditor');
const fillQrSetSelect = (/**@type {HTMLSelectElement}*/select, /**@type {QuickReplyContextLink}*/ link) => {
[{ name: 'Select a QR set' }, ...QuickReplySet.list].forEach(qrs => {
const opt = document.createElement('option'); {
opt.value = qrs.name;
opt.textContent = qrs.name;
opt.selected = qrs.name == link.set?.name;
select.append(opt);
}
});
};
const addCtxItem = (/**@type {QuickReplyContextLink}*/link, /**@type {Number}*/idx) => {
/**@type {HTMLElement} */
// @ts-ignore
const itemDom = tpl.content.querySelector('.qr--ctxItem').cloneNode(true); {
itemDom.setAttribute('data-order', String(idx));
/**@type {HTMLSelectElement} */
const select = itemDom.querySelector('.qr--set');
fillQrSetSelect(select, link);
select.addEventListener('change', () => {
link.set = QuickReplySet.get(select.value);
this.updateContext();
});
/**@type {HTMLInputElement} */
const chain = itemDom.querySelector('.qr--isChained');
chain.checked = link.isChained;
chain.addEventListener('click', () => {
link.isChained = chain.checked;
this.updateContext();
});
itemDom.querySelector('.qr--delete').addEventListener('click', () => {
itemDom.remove();
this.contextList.splice(this.contextList.indexOf(link), 1);
this.updateContext();
});
linkList.append(itemDom);
}
};
[...this.contextList].forEach((link, idx) => addCtxItem(link, idx));
dom.querySelector('#qr--ctxAdd').addEventListener('click', () => {
const link = new QuickReplyContextLink();
this.contextList.push(link);
addCtxItem(link, this.contextList.length - 1);
});
const onContextSort = () => {
this.contextList = Array.from(linkList.querySelectorAll('.qr--ctxItem')).map((it,idx) => {
const link = this.contextList[Number(it.getAttribute('data-order'))];
it.setAttribute('data-order', String(idx));
return link;
});
this.updateContext();
};
// @ts-ignore
$(linkList).sortable({
delay: getSortableDelay(),
stop: () => onContextSort(),
});
// auto-exec
/**@type {HTMLInputElement}*/
const preventAutoExecute = dom.querySelector('#qr--preventAutoExecute');
preventAutoExecute.checked = this.preventAutoExecute;
preventAutoExecute.addEventListener('click', ()=>{
this.preventAutoExecute = preventAutoExecute.checked;
this.updateContext();
});
/**@type {HTMLInputElement}*/
const isHidden = dom.querySelector('#qr--isHidden');
isHidden.checked = this.isHidden;
isHidden.addEventListener('click', ()=>{
this.isHidden = isHidden.checked;
this.updateContext();
});
/**@type {HTMLInputElement}*/
const executeOnStartup = dom.querySelector('#qr--executeOnStartup');
executeOnStartup.checked = this.executeOnStartup;
executeOnStartup.addEventListener('click', ()=>{
this.executeOnStartup = executeOnStartup.checked;
this.updateContext();
});
/**@type {HTMLInputElement}*/
const executeOnUser = dom.querySelector('#qr--executeOnUser');
executeOnUser.checked = this.executeOnUser;
executeOnUser.addEventListener('click', ()=>{
this.executeOnUser = executeOnUser.checked;
this.updateContext();
});
/**@type {HTMLInputElement}*/
const executeOnAi = dom.querySelector('#qr--executeOnAi');
executeOnAi.checked = this.executeOnAi;
executeOnAi.addEventListener('click', ()=>{
this.executeOnAi = executeOnAi.checked;
this.updateContext();
});
/**@type {HTMLInputElement}*/
const executeOnChatChange = dom.querySelector('#qr--executeOnChatChange');
executeOnChatChange.checked = this.executeOnChatChange;
executeOnChatChange.addEventListener('click', ()=>{
this.executeOnChatChange = executeOnChatChange.checked;
this.updateContext();
});
/**@type {HTMLElement}*/
const executeErrors = dom.querySelector('#qr--modal-executeErrors');
/**@type {HTMLInputElement}*/
const executeHide = dom.querySelector('#qr--modal-executeHide');
let executePromise;
/**@type {HTMLElement}*/
const executeBtn = dom.querySelector('#qr--modal-execute');
executeBtn.addEventListener('click', async()=>{
if (executePromise) return;
executeBtn.classList.add('qr--busy');
executeErrors.innerHTML = '';
if (executeHide.checked) {
document.querySelector('#shadow_popup').classList.add('qr--hide');
}
try {
executePromise = this.execute();
await executePromise;
} catch (ex) {
executeErrors.textContent = ex.message;
}
executePromise = null;
executeBtn.classList.remove('qr--busy');
document.querySelector('#shadow_popup').classList.remove('qr--hide');
});
await popupResult;
} else {
warn('failed to fetch qrEditor template');
}
}
delete() {
if (this.onDelete) {
this.unrender();
this.unrenderSettings();
this.onDelete(this);
}
}
/**
* @param {string} value
*/
updateMessage(value) {
if (this.onUpdate) {
if (this.settingsDomMessage && this.settingsDomMessage.value != value) {
this.settingsDomMessage.value = value;
}
this.message = value;
this.updateRender();
this.onUpdate(this);
}
}
/**
* @param {string} value
*/
updateLabel(value) {
if (this.onUpdate) {
if (this.settingsDomLabel && this.settingsDomLabel.value != value) {
this.settingsDomLabel.value = value;
}
this.label = value;
this.updateRender();
this.onUpdate(this);
}
}
/**
* @param {string} value
*/
updateTitle(value) {
if (this.onUpdate) {
this.title = value;
this.updateRender();
this.onUpdate(this);
}
}
updateContext() {
if (this.onUpdate) {
this.updateRender();
this.onUpdate(this);
}
}
addContextLink(cl) {
this.contextList.push(cl);
this.updateContext();
}
removeContextLink(setName) {
const idx = this.contextList.findIndex(it=>it.set.name == setName);
if (idx > -1) {
this.contextList.splice(idx, 1);
this.updateContext();
}
}
clearContextLinks() {
if (this.contextList.length) {
this.contextList.splice(0, this.contextList.length);
this.updateContext();
}
}
async execute(args = {}) {
if (this.message?.length > 0 && this.onExecute) {
const message = this.message.replace(/\{\{arg::([^}]+)\}\}/g, (_, key) => {
return args[key] ?? '';
});
return await this.onExecute(this, message, args.isAutoExecute ?? false);
}
}
toJSON() {
return {
id: this.id,
label: this.label,
title: this.title,
message: this.message,
contextList: this.contextList,
preventAutoExecute: this.preventAutoExecute,
isHidden: this.isHidden,
executeOnStartup: this.executeOnStartup,
executeOnUser: this.executeOnUser,
executeOnAi: this.executeOnAi,
executeOnChatChange: this.executeOnChatChange,
};
}
}

View File

@ -0,0 +1,122 @@
import { getSortableDelay } from '../../../utils.js';
import { QuickReplySetLink } from './QuickReplySetLink.js';
import { QuickReplySet } from './QuickReplySet.js';
export class QuickReplyConfig {
/**@type {QuickReplySetLink[]}*/ setList = [];
/**@type {Boolean}*/ isGlobal;
/**@type {Function}*/ onUpdate;
/**@type {Function}*/ onRequestEditSet;
/**@type {HTMLElement}*/ dom;
/**@type {HTMLElement}*/ setListDom;
static from(props) {
props.setList = props.setList?.map(it=>QuickReplySetLink.from(it))?.filter(it=>it.set) ?? [];
const instance = Object.assign(new this(), props);
instance.init();
return instance;
}
init() {
this.setList.forEach(it=>this.hookQuickReplyLink(it));
}
hasSet(qrs) {
return this.setList.find(it=>it.set == qrs) != null;
}
addSet(qrs, isVisible = true) {
if (!this.hasSet(qrs)) {
const qrl = new QuickReplySetLink();
qrl.set = qrs;
qrl.isVisible = isVisible;
this.hookQuickReplyLink(qrl);
this.setList.push(qrl);
this.setListDom.append(qrl.renderSettings(this.setList.length - 1));
this.update();
}
}
removeSet(qrs) {
const idx = this.setList.findIndex(it=>it.set == qrs);
if (idx > -1) {
this.setList.splice(idx, 1);
this.update();
this.updateSetListDom();
}
}
renderSettingsInto(/**@type {HTMLElement}*/root) {
/**@type {HTMLElement}*/
this.setListDom = root.querySelector('.qr--setList');
root.querySelector('.qr--setListAdd').addEventListener('click', ()=>{
this.addSet(QuickReplySet.list[0]);
});
this.updateSetListDom();
}
updateSetListDom() {
this.setListDom.innerHTML = '';
// @ts-ignore
$(this.setListDom).sortable({
delay: getSortableDelay(),
stop: ()=>this.onSetListSort(),
});
this.setList.filter(it=>!it.set.isDeleted).forEach((qrl,idx)=>this.setListDom.append(qrl.renderSettings(idx)));
}
onSetListSort() {
this.setList = Array.from(this.setListDom.children).map((it,idx)=>{
const qrl = this.setList[Number(it.getAttribute('data-order'))];
qrl.index = idx;
it.setAttribute('data-order', String(idx));
return qrl;
});
this.update();
}
/**
* @param {QuickReplySetLink} qrl
*/
hookQuickReplyLink(qrl) {
qrl.onDelete = ()=>this.deleteQuickReplyLink(qrl);
qrl.onUpdate = ()=>this.update();
qrl.onRequestEditSet = ()=>this.requestEditSet(qrl.set);
}
deleteQuickReplyLink(qrl) {
this.setList.splice(this.setList.indexOf(qrl), 1);
this.update();
}
update() {
if (this.onUpdate) {
this.onUpdate(this);
}
}
requestEditSet(qrs) {
if (this.onRequestEditSet) {
this.onRequestEditSet(qrs);
}
}
toJSON() {
return {
setList: this.setList,
};
}
}

View File

@ -0,0 +1,22 @@
import { QuickReplySet } from './QuickReplySet.js';
export class QuickReplyContextLink {
static from(props) {
props.set = QuickReplySet.get(props.set);
const x = Object.assign(new this(), props);
return x;
}
/**@type {QuickReplySet}*/ set;
/**@type {Boolean}*/ isChained = false;
toJSON() {
return {
set: this.set?.name,
isChained: this.isChained,
};
}
}

View File

@ -0,0 +1,209 @@
import { getRequestHeaders, substituteParams } from '../../../../script.js';
import { executeSlashCommands } from '../../../slash-commands.js';
import { debounceAsync, warn } from '../index.js';
import { QuickReply } from './QuickReply.js';
export class QuickReplySet {
/**@type {QuickReplySet[]}*/ static list = [];
static from(props) {
props.qrList = []; //props.qrList?.map(it=>QuickReply.from(it));
const instance = Object.assign(new this(), props);
// instance.init();
return instance;
}
/**
* @param {String} name - name of the QuickReplySet
*/
static get(name) {
return this.list.find(it=>it.name == name);
}
/**@type {String}*/ name;
/**@type {Boolean}*/ disableSend = false;
/**@type {Boolean}*/ placeBeforeInput = false;
/**@type {Boolean}*/ injectInput = false;
/**@type {QuickReply[]}*/ qrList = [];
/**@type {Number}*/ idIndex = 0;
/**@type {Boolean}*/ isDeleted = false;
/**@type {Function}*/ save;
/**@type {HTMLElement}*/ dom;
/**@type {HTMLElement}*/ settingsDom;
constructor() {
this.save = debounceAsync(()=>this.performSave(), 200);
}
init() {
this.qrList.forEach(qr=>this.hookQuickReply(qr));
}
unrender() {
this.dom?.remove();
this.dom = null;
}
render() {
this.unrender();
if (!this.dom) {
const root = document.createElement('div'); {
this.dom = root;
root.classList.add('qr--buttons');
this.qrList.filter(qr=>!qr.isHidden).forEach(qr=>{
root.append(qr.render());
});
}
}
return this.dom;
}
rerender() {
if (!this.dom) return;
this.dom.innerHTML = '';
this.qrList.filter(qr=>!qr.isHidden).forEach(qr=>{
this.dom.append(qr.render());
});
}
renderSettings() {
if (!this.settingsDom) {
this.settingsDom = document.createElement('div'); {
this.settingsDom.classList.add('qr--set-qrListContents');
this.qrList.forEach((qr,idx)=>{
this.renderSettingsItem(qr, idx);
});
}
}
return this.settingsDom;
}
renderSettingsItem(qr, idx) {
this.settingsDom.append(qr.renderSettings(idx));
}
/**
* @param {QuickReply} qr
* @param {String} [message] - optional altered message to be used
*/
async execute(qr, message = null, isAutoExecute = false) {
/**@type {HTMLTextAreaElement}*/
const ta = document.querySelector('#send_textarea');
const finalMessage = message ?? qr.message;
let input = ta.value;
if (!isAutoExecute && this.injectInput && input.length > 0) {
if (this.placeBeforeInput) {
input = `${finalMessage} ${input}`;
} else {
input = `${input} ${finalMessage}`;
}
} else {
input = `${finalMessage} `;
}
if (input[0] == '/' && !this.disableSend) {
const result = await executeSlashCommands(input);
return typeof result === 'object' ? result?.pipe : '';
}
ta.value = substituteParams(input);
ta.focus();
if (!this.disableSend) {
// @ts-ignore
document.querySelector('#send_but').click();
}
}
addQuickReply() {
const id = Math.max(this.idIndex, this.qrList.reduce((max,qr)=>Math.max(max,qr.id),0)) + 1;
this.idIndex = id + 1;
const qr = QuickReply.from({ id });
this.qrList.push(qr);
this.hookQuickReply(qr);
if (this.settingsDom) {
this.renderSettingsItem(qr, this.qrList.length - 1);
}
if (this.dom) {
this.dom.append(qr.render());
}
this.save();
return qr;
}
hookQuickReply(qr) {
qr.onExecute = (_, message, isAutoExecute)=>this.execute(qr, message, isAutoExecute);
qr.onDelete = ()=>this.removeQuickReply(qr);
qr.onUpdate = ()=>this.save();
}
removeQuickReply(qr) {
this.qrList.splice(this.qrList.indexOf(qr), 1);
this.save();
}
toJSON() {
return {
version: 2,
name: this.name,
disableSend: this.disableSend,
placeBeforeInput: this.placeBeforeInput,
injectInput: this.injectInput,
qrList: this.qrList,
idIndex: this.idIndex,
};
}
async performSave() {
const response = await fetch('/savequickreply', {
method: 'POST',
headers: getRequestHeaders(),
body: JSON.stringify(this),
});
if (response.ok) {
this.rerender();
} else {
warn(`Failed to save Quick Reply Set: ${this.name}`);
}
}
async delete() {
const response = await fetch('/deletequickreply', {
method: 'POST',
headers: getRequestHeaders(),
body: JSON.stringify(this),
});
if (response.ok) {
this.unrender();
const idx = QuickReplySet.list.indexOf(this);
QuickReplySet.list.splice(idx, 1);
this.isDeleted = true;
} else {
warn(`Failed to delete Quick Reply Set: ${this.name}`);
}
}
}

View File

@ -0,0 +1,129 @@
import { QuickReplySet } from './QuickReplySet.js';
export class QuickReplySetLink {
static from(props) {
props.set = QuickReplySet.get(props.set);
/**@type {QuickReplySetLink}*/
const instance = Object.assign(new this(), props);
return instance;
}
/**@type {QuickReplySet}*/ set;
/**@type {Boolean}*/ isVisible = true;
/**@type {Number}*/ index;
/**@type {Function}*/ onUpdate;
/**@type {Function}*/ onRequestEditSet;
/**@type {Function}*/ onDelete;
/**@type {HTMLElement}*/ settingsDom;
renderSettings(idx) {
this.index = idx;
const item = document.createElement('div'); {
this.settingsDom = item;
item.classList.add('qr--item');
item.setAttribute('data-order', String(this.index));
const drag = document.createElement('div'); {
drag.classList.add('drag-handle');
drag.classList.add('ui-sortable-handle');
drag.textContent = '☰';
item.append(drag);
}
const set = document.createElement('select'); {
set.classList.add('qr--set');
// fix for jQuery sortable breaking childrens' touch events
set.addEventListener('touchstart', (evt)=>evt.stopPropagation());
set.addEventListener('change', ()=>{
this.set = QuickReplySet.get(set.value);
this.update();
});
QuickReplySet.list.forEach(qrs=>{
const opt = document.createElement('option'); {
opt.value = qrs.name;
opt.textContent = qrs.name;
opt.selected = qrs == this.set;
set.append(opt);
}
});
item.append(set);
}
const visible = document.createElement('label'); {
visible.classList.add('qr--visible');
visible.title = 'Show buttons';
const cb = document.createElement('input'); {
cb.type = 'checkbox';
cb.checked = this.isVisible;
cb.addEventListener('click', ()=>{
this.isVisible = cb.checked;
this.update();
});
visible.append(cb);
}
visible.append('Buttons');
item.append(visible);
}
const edit = document.createElement('div'); {
edit.classList.add('menu_button');
edit.classList.add('menu_button_icon');
edit.classList.add('fa-solid');
edit.classList.add('fa-pencil');
edit.title = 'Edit quick reply set';
edit.addEventListener('click', ()=>this.requestEditSet());
item.append(edit);
}
const del = document.createElement('div'); {
del.classList.add('qr--del');
del.classList.add('menu_button');
del.classList.add('menu_button_icon');
del.classList.add('fa-solid');
del.classList.add('fa-trash-can');
del.title = 'Remove quick reply set';
del.addEventListener('click', ()=>this.delete());
item.append(del);
}
}
return this.settingsDom;
}
unrenderSettings() {
this.settingsDom?.remove();
this.settingsDom = null;
}
update() {
if (this.onUpdate) {
this.onUpdate(this);
}
}
requestEditSet() {
if (this.onRequestEditSet) {
this.onRequestEditSet(this.set);
}
}
delete() {
this.unrenderSettings();
if (this.onDelete) {
this.onDelete();
}
}
toJSON() {
return {
set: this.set.name,
isVisible: this.isVisible,
};
}
}

View File

@ -0,0 +1,85 @@
import { chat_metadata, saveChatDebounced, saveSettingsDebounced } from '../../../../script.js';
import { extension_settings } from '../../../extensions.js';
import { QuickReplyConfig } from './QuickReplyConfig.js';
export class QuickReplySettings {
static from(props) {
props.config = QuickReplyConfig.from(props.config);
const instance = Object.assign(new this(), props);
instance.init();
return instance;
}
/**@type {Boolean}*/ isEnabled = false;
/**@type {Boolean}*/ isCombined = false;
/**@type {Boolean}*/ isPopout = false;
/**@type {QuickReplyConfig}*/ config;
/**@type {QuickReplyConfig}*/ _chatConfig;
get chatConfig() {
return this._chatConfig;
}
set chatConfig(value) {
if (this._chatConfig != value) {
this.unhookConfig(this._chatConfig);
this._chatConfig = value;
this.hookConfig(this._chatConfig);
}
}
/**@type {Function}*/ onSave;
/**@type {Function}*/ onRequestEditSet;
init() {
this.hookConfig(this.config);
this.hookConfig(this.chatConfig);
}
hookConfig(config) {
if (config) {
config.onUpdate = ()=>this.save();
config.onRequestEditSet = (qrs)=>this.requestEditSet(qrs);
}
}
unhookConfig(config) {
if (config) {
config.onUpdate = null;
config.onRequestEditSet = null;
}
}
save() {
extension_settings.quickReplyV2 = this.toJSON();
saveSettingsDebounced();
if (this.chatConfig) {
chat_metadata.quickReply = this.chatConfig.toJSON();
saveChatDebounced();
}
if (this.onSave) {
this.onSave();
}
}
requestEditSet(qrs) {
if (this.onRequestEditSet) {
this.onRequestEditSet(qrs);
}
}
toJSON() {
return {
isEnabled: this.isEnabled,
isCombined: this.isCombined,
isPopout: this.isPopout,
config: this.config,
};
}
}

View File

@ -0,0 +1,270 @@
import { registerSlashCommand } from '../../../slash-commands.js';
// eslint-disable-next-line no-unused-vars
import { QuickReplyApi } from '../api/QuickReplyApi.js';
export class SlashCommandHandler {
/**@type {QuickReplyApi}*/ api;
constructor(/**@type {QuickReplyApi}*/api) {
this.api = api;
}
init() {
registerSlashCommand('qr', (_, value) => this.executeQuickReplyByIndex(Number(value)), [], '<span class="monospace">(number)</span> activates the specified Quick Reply', true, true);
registerSlashCommand('qrset', ()=>toastr.warning('The command /qrset has been deprecated. Use /qr-set, /qr-set-on, and /qr-set-off instead.'), [], '<strong>DEPRECATED</strong> The command /qrset has been deprecated. Use /qr-set, /qr-set-on, and /qr-set-off instead.', true, true);
registerSlashCommand('qr-set', (args, value)=>this.toggleGlobalSet(value, args), [], '<span class="monospace">[visible=true] (number)</span> toggle global QR set', true, true);
registerSlashCommand('qr-set-on', (args, value)=>this.addGlobalSet(value, args), [], '<span class="monospace">[visible=true] (number)</span> activate global QR set', true, true);
registerSlashCommand('qr-set-off', (_, value)=>this.removeGlobalSet(value), [], '<span class="monospace">(number)</span> deactivate global QR set', true, true);
registerSlashCommand('qr-chat-set', (args, value)=>this.toggleChatSet(value, args), [], '<span class="monospace">[visible=true] (number)</span> toggle chat QR set', true, true);
registerSlashCommand('qr-chat-set-on', (args, value)=>this.addChatSet(value, args), [], '<span class="monospace">[visible=true] (number)</span> activate chat QR set', true, true);
registerSlashCommand('qr-chat-set-off', (_, value)=>this.removeChatSet(value), [], '<span class="monospace">(number)</span> deactivate chat QR set', true, true);
registerSlashCommand('qr-set-list', (_, value)=>this.listSets(value ?? 'all'), [], '(all|global|chat) gets a list of the names of all quick reply sets', true, true);
registerSlashCommand('qr-list', (_, value)=>this.listQuickReplies(value), [], '(set name) gets a list of the names of all quick replies in this quick reply set', true, true);
const qrArgs = `
label - string - text on the button, e.g., label=MyButton
set - string - name of the QR set, e.g., set=PresetName1
hidden - bool - whether the button should be hidden, e.g., hidden=true
startup - bool - auto execute on app startup, e.g., startup=true
user - bool - auto execute on user message, e.g., user=true
bot - bool - auto execute on AI message, e.g., bot=true
load - bool - auto execute on chat load, e.g., load=true
title - bool - title / tooltip to be shown on button, e.g., title="My Fancy Button"
`.trim();
const qrUpdateArgs = `
newlabel - string - new text for the button, e.g. newlabel=MyRenamedButton
${qrArgs}
`.trim();
registerSlashCommand('qr-create', (args, message)=>this.createQuickReply(args, message), [], `<span class="monospace" style="white-space:pre-line;">[arguments] (message)\n arguments:\n ${qrArgs}</span> creates a new Quick Reply, example: <tt>/qr-create set=MyPreset label=MyButton /echo 123</tt>`, true, true);
registerSlashCommand('qr-update', (args, message)=>this.updateQuickReply(args, message), [], `<span class="monospace" style="white-space:pre-line;">[arguments] (message)\n arguments:\n ${qrUpdateArgs}</span> updates Quick Reply, example: <tt>/qr-update set=MyPreset label=MyButton newlabel=MyRenamedButton /echo 123</tt>`, true, true);
registerSlashCommand('qr-delete', (args, name)=>this.deleteQuickReply(args, name), [], '<span class="monospace">set=string [label]</span> deletes Quick Reply', true, true);
registerSlashCommand('qr-contextadd', (args, name)=>this.createContextItem(args, name), [], '<span class="monospace">set=string label=string [chain=false] (preset name)</span> add context menu preset to a QR, example: <tt>/qr-contextadd set=MyPreset label=MyButton chain=true MyOtherPreset</tt>', true, true);
registerSlashCommand('qr-contextdel', (args, name)=>this.deleteContextItem(args, name), [], '<span class="monospace">set=string label=string (preset name)</span> remove context menu preset from a QR, example: <tt>/qr-contextdel set=MyPreset label=MyButton MyOtherPreset</tt>', true, true);
registerSlashCommand('qr-contextclear', (args, label)=>this.clearContextMenu(args, label), [], '<span class="monospace">set=string (label)</span> remove all context menu presets from a QR, example: <tt>/qr-contextclear set=MyPreset MyButton</tt>', true, true);
const presetArgs = `
nosend - bool - disable send / insert in user input (invalid for slash commands)
before - bool - place QR before user input
inject - bool - inject user input automatically (if disabled use {{input}})
`.trim();
registerSlashCommand('qr-set-create', (args, name)=>this.createSet(name, args), ['qr-presetadd'], `<span class="monospace" style="white-space:pre-line;">[arguments] (name)\n arguments:\n ${presetArgs}</span> create a new preset (overrides existing ones), example: <tt>/qr-set-add MyNewPreset</tt>`, true, true);
registerSlashCommand('qr-set-update', (args, name)=>this.updateSet(name, args), ['qr-presetupdate'], `<span class="monospace" style="white-space:pre-line;">[arguments] (name)\n arguments:\n ${presetArgs}</span> update an existing preset, example: <tt>/qr-set-update enabled=false MyPreset</tt>`, true, true);
registerSlashCommand('qr-set-delete', (args, name)=>this.deleteSet(name), ['qr-presetdelete'], `<span class="monospace" style="white-space:pre-line;">(name)\n arguments:\n ${presetArgs}</span> delete an existing preset, example: <tt>/qr-set-delete MyPreset</tt>`, true, true);
}
getSetByName(name) {
const set = this.api.getSetByName(name);
if (!set) {
toastr.error(`No Quick Reply Set with the name "${name}" could be found.`);
}
return set;
}
getQrByLabel(setName, label) {
const qr = this.api.getQrByLabel(setName, label);
if (!qr) {
toastr.error(`No Quick Reply with the label "${label}" could be found in the set "${setName}"`);
}
return qr;
}
async executeQuickReplyByIndex(idx) {
try {
return await this.api.executeQuickReplyByIndex(idx);
} catch (ex) {
toastr.error(ex.message);
}
}
toggleGlobalSet(name, args = {}) {
try {
this.api.toggleGlobalSet(name, JSON.parse(args.visible ?? 'true') === true);
} catch (ex) {
toastr.error(ex.message);
}
}
addGlobalSet(name, args = {}) {
try {
this.api.addGlobalSet(name, JSON.parse(args.visible ?? 'true') === true);
} catch (ex) {
toastr.error(ex.message);
}
}
removeGlobalSet(name) {
try {
this.api.removeGlobalSet(name);
} catch (ex) {
toastr.error(ex.message);
}
}
toggleChatSet(name, args = {}) {
try {
this.api.toggleChatSet(name, JSON.parse(args.visible ?? 'true') === true);
} catch (ex) {
toastr.error(ex.message);
}
}
addChatSet(name, args = {}) {
try {
this.api.addChatSet(name, JSON.parse(args.visible ?? 'true') === true);
} catch (ex) {
toastr.error(ex.message);
}
}
removeChatSet(name) {
try {
this.api.removeChatSet(name);
} catch (ex) {
toastr.error(ex.message);
}
}
createQuickReply(args, message) {
try {
this.api.createQuickReply(
args.set ?? '',
args.label ?? '',
{
message: message ?? '',
title: args.title,
isHidden: JSON.parse(args.hidden ?? 'false') === true,
executeOnStartup: JSON.parse(args.startup ?? 'false') === true,
executeOnUser: JSON.parse(args.user ?? 'false') === true,
executeOnAi: JSON.parse(args.bot ?? 'false') === true,
executeOnChatChange: JSON.parse(args.load ?? 'false') === true,
},
);
} catch (ex) {
toastr.error(ex.message);
}
}
updateQuickReply(args, message) {
try {
this.api.updateQuickReply(
args.set ?? '',
args.label ?? '',
{
newLabel: args.newlabel,
message: (message ?? '').trim().length > 0 ? message : undefined,
title: args.title,
isHidden: args.hidden,
executeOnStartup: args.startup,
executeOnUser: args.user,
executeOnAi: args.bot,
executeOnChatChange: args.load,
},
);
} catch (ex) {
toastr.error(ex.message);
}
}
deleteQuickReply(args, label) {
try {
this.api.deleteQuickReply(args.set, label);
} catch (ex) {
toastr.error(ex.message);
}
}
createContextItem(args, name) {
try {
this.api.createContextItem(
args.set,
args.label,
name,
JSON.parse(args.chain ?? 'false') === true,
);
} catch (ex) {
toastr.error(ex.message);
}
}
deleteContextItem(args, name) {
try {
this.api.deleteContextItem(args.set, args.label, name);
} catch (ex) {
toastr.error(ex.message);
}
}
clearContextMenu(args, label) {
try {
this.api.clearContextMenu(args.set, args.label ?? label);
} catch (ex) {
toastr.error(ex.message);
}
}
createSet(name, args) {
try {
this.api.createSet(
args.name ?? name ?? '',
{
disableSend: JSON.parse(args.nosend ?? 'false') === true,
placeBeforeInput: JSON.parse(args.before ?? 'false') === true,
injectInput: JSON.parse(args.inject ?? 'false') === true,
},
);
} catch (ex) {
toastr.error(ex.message);
}
}
updateSet(name, args) {
try {
this.api.updateSet(
args.name ?? name ?? '',
{
disableSend: args.nosend !== undefined ? JSON.parse(args.nosend ?? 'false') === true : undefined,
placeBeforeInput: args.before !== undefined ? JSON.parse(args.before ?? 'false') === true : undefined,
injectInput: args.inject !== undefined ? JSON.parse(args.inject ?? 'false') === true : undefined,
},
);
} catch (ex) {
toastr.error(ex.message);
}
}
deleteSet(name) {
try {
this.api.deleteSet(name ?? '');
} catch (ex) {
toastr.error(ex.message);
}
}
listSets(source) {
try {
switch (source) {
case 'global':
return this.api.listGlobalSets();
case 'chat':
return this.api.listChatSets();
default:
return this.api.listSets();
}
} catch (ex) {
toastr.error(ex.message);
}
}
listQuickReplies(name) {
try {
return this.api.listQuickReplies(name);
} catch (ex) {
toastr.error(ex.message);
}
}
}

View File

@ -0,0 +1,161 @@
import { animation_duration } from '../../../../../script.js';
import { dragElement } from '../../../../RossAscends-mods.js';
import { loadMovingUIState } from '../../../../power-user.js';
// eslint-disable-next-line no-unused-vars
import { QuickReplySettings } from '../QuickReplySettings.js';
export class ButtonUi {
/**@type {QuickReplySettings}*/ settings;
/**@type {HTMLElement}*/ dom;
/**@type {HTMLElement}*/ popoutDom;
constructor(/**@type {QuickReplySettings}*/settings) {
this.settings = settings;
}
render() {
if (this.settings.isPopout) {
return this.renderPopout();
}
return this.renderBar();
}
unrender() {
this.dom?.remove();
this.dom = null;
this.popoutDom?.remove();
this.popoutDom = null;
}
show() {
if (!this.settings.isEnabled) return;
if (this.settings.isPopout) {
document.body.append(this.render());
loadMovingUIState();
$(this.render()).fadeIn(animation_duration);
dragElement($(this.render()));
} else {
const sendForm = document.querySelector('#send_form');
if (sendForm.children.length > 0) {
sendForm.children[0].insertAdjacentElement('beforebegin', this.render());
} else {
sendForm.append(this.render());
}
}
}
hide() {
this.unrender();
}
refresh() {
this.hide();
this.show();
}
renderBar() {
if (!this.dom) {
let buttonHolder;
const root = document.createElement('div'); {
this.dom = root;
buttonHolder = root;
root.id = 'qr--bar';
root.classList.add('flex-container');
root.classList.add('flexGap5');
const popout = document.createElement('div'); {
popout.id = 'qr--popoutTrigger';
popout.classList.add('menu_button');
popout.classList.add('fa-solid');
popout.classList.add('fa-window-restore');
popout.addEventListener('click', ()=>{
this.settings.isPopout = true;
this.refresh();
this.settings.save();
});
root.append(popout);
}
if (this.settings.isCombined) {
const buttons = document.createElement('div'); {
buttonHolder = buttons;
buttons.classList.add('qr--buttons');
root.append(buttons);
}
}
[...this.settings.config.setList, ...(this.settings.chatConfig?.setList ?? [])]
.filter(link=>link.isVisible)
.forEach(link=>buttonHolder.append(link.set.render()))
;
}
}
return this.dom;
}
renderPopout() {
if (!this.popoutDom) {
let buttonHolder;
const root = document.createElement('div'); {
this.popoutDom = root;
root.id = 'qr--popout';
root.classList.add('qr--popout');
root.classList.add('draggable');
const head = document.createElement('div'); {
head.classList.add('qr--header');
root.append(head);
const controls = document.createElement('div'); {
controls.classList.add('qr--controls');
controls.classList.add('panelControlBar');
controls.classList.add('flex-container');
const drag = document.createElement('div'); {
drag.id = 'qr--popoutheader';
drag.classList.add('fa-solid');
drag.classList.add('fa-grip');
drag.classList.add('drag-grabber');
drag.classList.add('hoverglow');
controls.append(drag);
}
const close = document.createElement('div'); {
close.classList.add('qr--close');
close.classList.add('fa-solid');
close.classList.add('fa-circle-xmark');
close.classList.add('hoverglow');
close.addEventListener('click', ()=>{
this.settings.isPopout = false;
this.refresh();
this.settings.save();
});
controls.append(close);
}
head.append(controls);
}
}
const body = document.createElement('div'); {
buttonHolder = body;
body.classList.add('qr--body');
if (this.settings.isCombined) {
const buttons = document.createElement('div'); {
buttonHolder = buttons;
buttons.classList.add('qr--buttons');
body.append(buttons);
}
}
[...this.settings.config.setList, ...(this.settings.chatConfig?.setList ?? [])]
.filter(link=>link.isVisible)
.forEach(link=>buttonHolder.append(link.set.render()))
;
root.append(body);
}
}
}
return this.popoutDom;
}
}

View File

@ -0,0 +1,366 @@
import { callPopup } from '../../../../../script.js';
import { getSortableDelay } from '../../../../utils.js';
import { log, warn } from '../../index.js';
import { QuickReply } from '../QuickReply.js';
import { QuickReplySet } from '../QuickReplySet.js';
// eslint-disable-next-line no-unused-vars
import { QuickReplySettings } from '../QuickReplySettings.js';
export class SettingsUi {
/**@type {QuickReplySettings}*/ settings;
/**@type {HTMLElement}*/ template;
/**@type {HTMLElement}*/ dom;
/**@type {HTMLInputElement}*/ isEnabled;
/**@type {HTMLInputElement}*/ isCombined;
/**@type {HTMLElement}*/ globalSetList;
/**@type {HTMLElement}*/ chatSetList;
/**@type {QuickReplySet}*/ currentQrSet;
/**@type {HTMLInputElement}*/ disableSend;
/**@type {HTMLInputElement}*/ placeBeforeInput;
/**@type {HTMLInputElement}*/ injectInput;
/**@type {HTMLSelectElement}*/ currentSet;
constructor(/**@type {QuickReplySettings}*/settings) {
this.settings = settings;
settings.onRequestEditSet = (qrs) => this.selectQrSet(qrs);
}
rerender() {
if (!this.dom) return;
const content = this.dom.querySelector('.inline-drawer-content');
content.innerHTML = '';
// @ts-ignore
Array.from(this.template.querySelector('.inline-drawer-content').cloneNode(true).children).forEach(el=>{
content.append(el);
});
this.prepareDom();
}
unrender() {
this.dom?.remove();
this.dom = null;
}
async render() {
if (!this.dom) {
const response = await fetch('/scripts/extensions/quick-reply/html/settings.html', { cache: 'no-store' });
if (response.ok) {
this.template = document.createRange().createContextualFragment(await response.text()).querySelector('#qr--settings');
// @ts-ignore
this.dom = this.template.cloneNode(true);
this.prepareDom();
} else {
warn('failed to fetch settings template');
}
}
return this.dom;
}
prepareGeneralSettings() {
// general settings
this.isEnabled = this.dom.querySelector('#qr--isEnabled');
this.isEnabled.checked = this.settings.isEnabled;
this.isEnabled.addEventListener('click', ()=>this.onIsEnabled());
this.isCombined = this.dom.querySelector('#qr--isCombined');
this.isCombined.checked = this.settings.isCombined;
this.isCombined.addEventListener('click', ()=>this.onIsCombined());
}
prepareGlobalSetList() {
const dom = this.template.querySelector('#qr--global');
const clone = dom.cloneNode(true);
// @ts-ignore
this.settings.config.renderSettingsInto(clone);
this.dom.querySelector('#qr--global').replaceWith(clone);
}
prepareChatSetList() {
const dom = this.template.querySelector('#qr--chat');
const clone = dom.cloneNode(true);
if (this.settings.chatConfig) {
// @ts-ignore
this.settings.chatConfig.renderSettingsInto(clone);
} else {
const info = document.createElement('div'); {
info.textContent = 'No active chat.';
// @ts-ignore
clone.append(info);
}
}
this.dom.querySelector('#qr--chat').replaceWith(clone);
}
prepareQrEditor() {
// qr editor
this.dom.querySelector('#qr--set-new').addEventListener('click', async()=>this.addQrSet());
/**@type {HTMLInputElement}*/
const importFile = this.dom.querySelector('#qr--set-importFile');
importFile.addEventListener('change', async()=>{
await this.importQrSet(importFile.files);
importFile.value = null;
});
this.dom.querySelector('#qr--set-import').addEventListener('click', ()=>importFile.click());
this.dom.querySelector('#qr--set-export').addEventListener('click', async()=>this.exportQrSet());
this.dom.querySelector('#qr--set-delete').addEventListener('click', async()=>this.deleteQrSet());
this.dom.querySelector('#qr--set-add').addEventListener('click', async()=>{
this.currentQrSet.addQuickReply();
});
this.qrList = this.dom.querySelector('#qr--set-qrList');
this.currentSet = this.dom.querySelector('#qr--set');
this.currentSet.addEventListener('change', ()=>this.onQrSetChange());
QuickReplySet.list.forEach(qrs=>{
const opt = document.createElement('option'); {
opt.value = qrs.name;
opt.textContent = qrs.name;
this.currentSet.append(opt);
}
});
this.disableSend = this.dom.querySelector('#qr--disableSend');
this.disableSend.addEventListener('click', ()=>{
const qrs = this.currentQrSet;
qrs.disableSend = this.disableSend.checked;
qrs.save();
});
this.placeBeforeInput = this.dom.querySelector('#qr--placeBeforeInput');
this.placeBeforeInput.addEventListener('click', ()=>{
const qrs = this.currentQrSet;
qrs.placeBeforeInput = this.placeBeforeInput.checked;
qrs.save();
});
this.injectInput = this.dom.querySelector('#qr--injectInput');
this.injectInput.addEventListener('click', ()=>{
const qrs = this.currentQrSet;
qrs.injectInput = this.injectInput.checked;
qrs.save();
});
this.onQrSetChange();
}
onQrSetChange() {
this.currentQrSet = QuickReplySet.get(this.currentSet.value);
this.disableSend.checked = this.currentQrSet.disableSend;
this.placeBeforeInput.checked = this.currentQrSet.placeBeforeInput;
this.injectInput.checked = this.currentQrSet.injectInput;
this.qrList.innerHTML = '';
const qrsDom = this.currentQrSet.renderSettings();
this.qrList.append(qrsDom);
// @ts-ignore
$(qrsDom).sortable({
delay: getSortableDelay(),
handle: '.drag-handle',
stop: ()=>this.onQrListSort(),
});
}
prepareDom() {
this.prepareGeneralSettings();
this.prepareGlobalSetList();
this.prepareChatSetList();
this.prepareQrEditor();
}
async onIsEnabled() {
this.settings.isEnabled = this.isEnabled.checked;
this.settings.save();
}
async onIsCombined() {
this.settings.isCombined = this.isCombined.checked;
this.settings.save();
}
async onGlobalSetListSort() {
this.settings.config.setList = Array.from(this.globalSetList.children).map((it,idx)=>{
const set = this.settings.config.setList[Number(it.getAttribute('data-order'))];
it.setAttribute('data-order', String(idx));
return set;
});
this.settings.save();
}
async onChatSetListSort() {
this.settings.chatConfig.setList = Array.from(this.chatSetList.children).map((it,idx)=>{
const set = this.settings.chatConfig.setList[Number(it.getAttribute('data-order'))];
it.setAttribute('data-order', String(idx));
return set;
});
this.settings.save();
}
updateOrder(list) {
Array.from(list.children).forEach((it,idx)=>{
it.setAttribute('data-order', idx);
});
}
async onQrListSort() {
this.currentQrSet.qrList = Array.from(this.qrList.querySelectorAll('.qr--set-item')).map((it,idx)=>{
const qr = this.currentQrSet.qrList.find(qr=>qr.id == Number(it.getAttribute('data-id')));
it.setAttribute('data-order', String(idx));
return qr;
});
this.currentQrSet.save();
}
async deleteQrSet() {
const confirmed = await callPopup(`Are you sure you want to delete the Quick Reply Set "${this.currentQrSet.name}"?<br>This cannot be undone.`, 'confirm');
if (confirmed) {
await this.doDeleteQrSet(this.currentQrSet);
this.rerender();
}
}
async doDeleteQrSet(qrs) {
await qrs.delete();
//TODO (HACK) should just bubble up from QuickReplySet.delete() but that would require proper or at least more comples onDelete listeners
for (let i = this.settings.config.setList.length - 1; i >= 0; i--) {
if (this.settings.config.setList[i].set == qrs) {
this.settings.config.setList.splice(i, 1);
}
}
if (this.settings.chatConfig) {
for (let i = this.settings.chatConfig.setList.length - 1; i >= 0; i--) {
if (this.settings.chatConfig.setList[i].set == qrs) {
this.settings.chatConfig.setList.splice(i, 1);
}
}
}
this.settings.save();
}
async addQrSet() {
const name = await callPopup('Quick Reply Set Name:', 'input');
if (name && name.length > 0) {
const oldQrs = QuickReplySet.get(name);
if (oldQrs) {
const replace = await callPopup(`A Quick Reply Set named "${name}" already exists.<br>Do you want to overwrite the existing Quick Reply Set?<br>The existing set will be deleted. This cannot be undone.`, 'confirm');
if (replace) {
const idx = QuickReplySet.list.indexOf(oldQrs);
await this.doDeleteQrSet(oldQrs);
const qrs = new QuickReplySet();
qrs.name = name;
qrs.addQuickReply();
QuickReplySet.list.splice(idx, 0, qrs);
this.rerender();
this.currentSet.value = name;
this.onQrSetChange();
this.prepareGlobalSetList();
this.prepareChatSetList();
}
} else {
const qrs = new QuickReplySet();
qrs.name = name;
qrs.addQuickReply();
const idx = QuickReplySet.list.findIndex(it=>it.name.localeCompare(name) == 1);
if (idx > -1) {
QuickReplySet.list.splice(idx, 0, qrs);
} else {
QuickReplySet.list.push(qrs);
}
const opt = document.createElement('option'); {
opt.value = qrs.name;
opt.textContent = qrs.name;
if (idx > -1) {
this.currentSet.children[idx].insertAdjacentElement('beforebegin', opt);
} else {
this.currentSet.append(opt);
}
}
this.currentSet.value = name;
this.onQrSetChange();
this.prepareGlobalSetList();
this.prepareChatSetList();
}
}
}
async importQrSet(/**@type {FileList}*/files) {
for (let i = 0; i < files.length; i++) {
await this.importSingleQrSet(files.item(i));
}
}
async importSingleQrSet(/**@type {File}*/file) {
log('FILE', file);
try {
const text = await file.text();
const props = JSON.parse(text);
if (!Number.isInteger(props.version) || typeof props.name != 'string') {
toastr.error(`The file "${file.name}" does not appear to be a valid quick reply set.`);
warn(`The file "${file.name}" does not appear to be a valid quick reply set.`);
} else {
/**@type {QuickReplySet}*/
const qrs = QuickReplySet.from(JSON.parse(JSON.stringify(props)));
qrs.qrList = props.qrList.map(it=>QuickReply.from(it));
qrs.init();
const oldQrs = QuickReplySet.get(props.name);
if (oldQrs) {
const replace = await callPopup(`A Quick Reply Set named "${qrs.name}" already exists.<br>Do you want to overwrite the existing Quick Reply Set?<br>The existing set will be deleted. This cannot be undone.`, 'confirm');
if (replace) {
const idx = QuickReplySet.list.indexOf(oldQrs);
await this.doDeleteQrSet(oldQrs);
QuickReplySet.list.splice(idx, 0, qrs);
await qrs.save();
this.rerender();
this.currentSet.value = qrs.name;
this.onQrSetChange();
this.prepareGlobalSetList();
this.prepareChatSetList();
}
} else {
const idx = QuickReplySet.list.findIndex(it=>it.name.localeCompare(qrs.name) == 1);
if (idx > -1) {
QuickReplySet.list.splice(idx, 0, qrs);
} else {
QuickReplySet.list.push(qrs);
}
await qrs.save();
const opt = document.createElement('option'); {
opt.value = qrs.name;
opt.textContent = qrs.name;
if (idx > -1) {
this.currentSet.children[idx].insertAdjacentElement('beforebegin', opt);
} else {
this.currentSet.append(opt);
}
}
this.currentSet.value = qrs.name;
this.onQrSetChange();
this.prepareGlobalSetList();
this.prepareChatSetList();
}
}
} catch (ex) {
warn(ex);
toastr.error(`Failed to import "${file.name}":\n\n${ex.message}`);
}
}
exportQrSet() {
const blob = new Blob([JSON.stringify(this.currentQrSet)], { type:'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a'); {
a.href = url;
a.download = `${this.currentQrSet.name}.json`;
a.click();
}
}
selectQrSet(qrs) {
this.currentSet.value = qrs.name;
this.onQrSetChange();
}
}

View File

@ -0,0 +1,108 @@
import { QuickReply } from '../../QuickReply.js';
// eslint-disable-next-line no-unused-vars
import { QuickReplySet } from '../../QuickReplySet.js';
import { MenuHeader } from './MenuHeader.js';
import { MenuItem } from './MenuItem.js';
export class ContextMenu {
/**@type {MenuItem[]}*/ itemList = [];
/**@type {Boolean}*/ isActive = false;
/**@type {HTMLElement}*/ root;
/**@type {HTMLElement}*/ menu;
constructor(/**@type {QuickReply}*/qr) {
// this.itemList = items;
this.itemList = this.build(qr).children;
this.itemList.forEach(item => {
item.onExpand = () => {
this.itemList.filter(it => it != item)
.forEach(it => it.collapse());
};
});
}
/**
* @param {QuickReply} qr
* @param {String} chainedMessage
* @param {QuickReplySet[]} hierarchy
* @param {String[]} labelHierarchy
*/
build(qr, chainedMessage = null, hierarchy = [], labelHierarchy = []) {
const tree = {
label: qr.label,
message: (chainedMessage && qr.message ? `${chainedMessage} | ` : '') + qr.message,
children: [],
};
qr.contextList.forEach((cl) => {
if (!hierarchy.includes(cl.set)) {
const nextHierarchy = [...hierarchy, cl.set];
const nextLabelHierarchy = [...labelHierarchy, tree.label];
tree.children.push(new MenuHeader(cl.set.name));
cl.set.qrList.forEach(subQr => {
const subTree = this.build(subQr, cl.isChained ? tree.message : null, nextHierarchy, nextLabelHierarchy);
tree.children.push(new MenuItem(
subTree.label,
subTree.message,
(evt) => {
evt.stopPropagation();
const finalQr = Object.assign(new QuickReply(), subQr);
finalQr.message = subTree.message.replace(/%%parent(-\d+)?%%/g, (_, index) => {
return nextLabelHierarchy.slice(parseInt(index ?? '-1'))[0];
});
cl.set.execute(finalQr);
},
subTree.children,
));
});
}
});
return tree;
}
render() {
if (!this.root) {
const blocker = document.createElement('div'); {
this.root = blocker;
blocker.classList.add('ctx-blocker');
blocker.addEventListener('click', () => this.hide());
const menu = document.createElement('ul'); {
this.menu = menu;
menu.classList.add('list-group');
menu.classList.add('ctx-menu');
this.itemList.forEach(it => menu.append(it.render()));
blocker.append(menu);
}
}
}
return this.root;
}
show({ clientX, clientY }) {
if (this.isActive) return;
this.isActive = true;
this.render();
this.menu.style.bottom = `${window.innerHeight - clientY}px`;
this.menu.style.left = `${clientX}px`;
document.body.append(this.root);
}
hide() {
if (this.root) {
this.root.remove();
}
this.isActive = false;
}
toggle(/**@type {PointerEvent}*/evt) {
if (this.isActive) {
this.hide();
} else {
this.show(evt);
}
}
}

View File

@ -1,114 +1,286 @@
#quickReplyBar {
outline: none;
/*
padding: 5px 0;
border-bottom: 1px solid var(--SmartThemeBorderColor);
*/
margin: 0;
transition: 0.3s;
opacity: 0.7;
display: flex;
align-items: center;
justify-content: center;
width: 100%;
display: none;
max-width: 100%;
overflow-x: auto;
order: 1;
position: relative;
#qr--bar {
outline: none;
margin: 0;
transition: 0.3s;
opacity: 0.7;
display: flex;
align-items: center;
justify-content: center;
width: 100%;
max-width: 100%;
overflow-x: auto;
order: 1;
padding-right: 2.5em;
position: relative;
}
#quickReplies {
margin: 0;
padding: 0;
display: flex;
justify-content: center;
flex-wrap: wrap;
gap: 5px;
width: 100%;
#qr--bar > #qr--popoutTrigger {
position: absolute;
right: 0.25em;
top: 0;
}
#quickReplyPopoutButton {
position: absolute;
right: 5px;
top: 0px;
#qr--popout {
display: flex;
flex-direction: column;
padding: 0;
z-index: 31;
}
#quickReplies div {
color: var(--SmartThemeBodyColor);
background-color: var(--black50a);
border: 1px solid var(--SmartThemeBorderColor);
border-radius: 10px;
padding: 3px 5px;
margin: 3px 0;
/* width: min-content; */
cursor: pointer;
transition: 0.3s;
display: flex;
align-items: center;
justify-content: center;
text-align: center;
#qr--popout > .qr--header {
flex: 0 0 auto;
height: 2em;
position: relative;
}
#quickReplies div:hover {
opacity: 1;
filter: brightness(1.2);
cursor: pointer;
#qr--popout > .qr--header > .qr--controls > .qr--close {
height: 15px;
aspect-ratio: 1 / 1;
font-size: 20px;
opacity: 0.5;
transition: all 250ms;
}
#qr--popout > .qr--body {
overflow-y: auto;
}
#qr--bar > .qr--buttons,
#qr--popout > .qr--body > .qr--buttons {
margin: 0;
padding: 0;
display: flex;
justify-content: center;
flex-wrap: wrap;
gap: 5px;
width: 100%;
}
#qr--bar > .qr--buttons > .qr--buttons,
#qr--popout > .qr--body > .qr--buttons > .qr--buttons {
display: contents;
}
#qr--bar > .qr--buttons .qr--button,
#qr--popout > .qr--body > .qr--buttons .qr--button {
color: var(--SmartThemeBodyColor);
border: 1px solid var(--SmartThemeBorderColor);
border-radius: 10px;
padding: 3px 5px;
margin: 3px 0;
cursor: pointer;
transition: 0.3s;
display: flex;
align-items: center;
justify-content: center;
text-align: center;
}
#qr--bar > .qr--buttons .qr--button:hover,
#qr--popout > .qr--body > .qr--buttons .qr--button:hover {
opacity: 1;
filter: brightness(1.2);
}
#qr--bar > .qr--buttons .qr--button > .qr--button-expander,
#qr--popout > .qr--body > .qr--buttons .qr--button > .qr--button-expander {
display: none;
}
#qr--bar > .qr--buttons .qr--button.qr--hasCtx > .qr--button-expander,
#qr--popout > .qr--body > .qr--buttons .qr--button.qr--hasCtx > .qr--button-expander {
display: block;
}
.qr--button-expander {
border-left: 1px solid;
margin-left: 1em;
text-align: center;
width: 2em;
}
.qr--button-expander:hover {
font-weight: bold;
}
.ctx-blocker {
/* backdrop-filter: blur(1px); */
/* background-color: rgba(0 0 0 / 10%); */
bottom: 0;
left: 0;
position: fixed;
right: 0;
top: 0;
z-index: 999;
/* backdrop-filter: blur(1px); */
/* background-color: rgba(0 0 0 / 10%); */
bottom: 0;
left: 0;
position: fixed;
right: 0;
top: 0;
z-index: 999;
}
.ctx-menu {
position: absolute;
overflow: visible;
position: absolute;
overflow: visible;
}
.list-group .list-group-item.ctx-header {
font-weight: bold;
cursor: default;
font-weight: bold;
cursor: default;
}
.ctx-item+.ctx-header {
border-top: 1px solid;
.ctx-item + .ctx-header {
border-top: 1px solid;
}
.ctx-item {
position: relative;
position: relative;
}
.ctx-expander {
border-left: 1px solid;
margin-left: 1em;
text-align: center;
width: 2em;
border-left: 1px solid;
margin-left: 1em;
text-align: center;
width: 2em;
}
.ctx-expander:hover {
font-weight: bold;
font-weight: bold;
}
.ctx-sub-menu {
position: absolute;
top: 0;
left: 100%;
position: absolute;
top: 0;
left: 100%;
}
@media screen and (max-width: 1000px) {
.ctx-blocker {
position: absolute;
}
.list-group .list-group-item.ctx-item {
padding: 1em;
}
.ctx-blocker {
position: absolute;
}
.list-group .list-group-item.ctx-item {
padding: 1em;
}
}
#qr--settings .qr--head {
display: flex;
align-items: baseline;
gap: 1em;
}
#qr--settings .qr--head > .qr--title {
font-weight: bold;
}
#qr--settings .qr--head > .qr--actions {
display: flex;
flex-direction: row;
align-items: baseline;
gap: 0.5em;
}
#qr--settings .qr--setList > .qr--item {
display: flex;
flex-direction: row;
gap: 0.5em;
align-items: baseline;
padding: 0 0.5em;
}
#qr--settings .qr--setList > .qr--item > .drag-handle {
padding: 0.75em;
}
#qr--settings .qr--setList > .qr--item > .qr--visible {
flex: 0 0 auto;
display: flex;
flex-direction: row;
}
#qr--settings #qr--set-settings #qr--injectInputContainer {
flex-wrap: nowrap;
}
#qr--settings #qr--set-qrList .qr--set-qrListContents {
padding: 0 0.5em;
}
#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item {
display: flex;
flex-direction: row;
gap: 0.5em;
align-items: baseline;
padding: 0.25em 0;
}
#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item > :nth-child(1) {
flex: 0 0 auto;
}
#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item > :nth-child(2) {
flex: 1 1 25%;
}
#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item > :nth-child(3) {
flex: 0 0 auto;
}
#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item > :nth-child(4) {
flex: 1 1 75%;
}
#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item > :nth-child(5) {
flex: 0 0 auto;
}
#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item > .drag-handle {
padding: 0.75em;
}
#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item .qr--set-itemLabel,
#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item .qr--action {
margin: 0;
}
#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item .qr--set-itemMessage {
font-size: smaller;
}
#qr--settings .qr--set-qrListActions {
display: flex;
flex-direction: row;
gap: 0.5em;
justify-content: center;
padding-bottom: 0.5em;
}
#qr--qrOptions > #qr--ctxEditor .qr--ctxItem {
display: flex;
flex-direction: row;
gap: 0.5em;
align-items: baseline;
}
@media screen and (max-width: 750px) {
body #dialogue_popup:has(#qr--modalEditor) #dialogue_popup_text > #qr--modalEditor {
flex-direction: column;
}
body #dialogue_popup:has(#qr--modalEditor) #dialogue_popup_text > #qr--modalEditor > #qr--main > .qr--labels {
flex-direction: column;
}
body #dialogue_popup:has(#qr--modalEditor) #dialogue_popup_text > #qr--modalEditor > #qr--main > .qr--modal-messageContainer > #qr--modal-message {
min-height: 90svh;
}
}
#dialogue_popup:has(#qr--modalEditor) {
aspect-ratio: unset;
}
#dialogue_popup:has(#qr--modalEditor) #dialogue_popup_text {
display: flex;
flex-direction: column;
}
#dialogue_popup:has(#qr--modalEditor) #dialogue_popup_text > #qr--modalEditor {
flex: 1 1 auto;
display: flex;
flex-direction: row;
gap: 1em;
}
#dialogue_popup:has(#qr--modalEditor) #dialogue_popup_text > #qr--modalEditor > #qr--main {
flex: 1 1 auto;
display: flex;
flex-direction: column;
}
#dialogue_popup:has(#qr--modalEditor) #dialogue_popup_text > #qr--modalEditor > #qr--main > .qr--labels {
flex: 0 0 auto;
display: flex;
flex-direction: row;
gap: 0.5em;
}
#dialogue_popup:has(#qr--modalEditor) #dialogue_popup_text > #qr--modalEditor > #qr--main > .qr--labels > label {
flex: 1 1 1px;
display: flex;
flex-direction: column;
}
#dialogue_popup:has(#qr--modalEditor) #dialogue_popup_text > #qr--modalEditor > #qr--main > .qr--labels > label > .qr--labelText {
flex: 1 1 auto;
}
#dialogue_popup:has(#qr--modalEditor) #dialogue_popup_text > #qr--modalEditor > #qr--main > .qr--labels > label > .qr--labelHint {
flex: 1 1 auto;
}
#dialogue_popup:has(#qr--modalEditor) #dialogue_popup_text > #qr--modalEditor > #qr--main > .qr--labels > label > input {
flex: 0 0 auto;
}
#dialogue_popup:has(#qr--modalEditor) #dialogue_popup_text > #qr--modalEditor > #qr--main > .qr--modal-messageContainer {
flex: 1 1 auto;
display: flex;
flex-direction: column;
}
#dialogue_popup:has(#qr--modalEditor) #dialogue_popup_text > #qr--modalEditor > #qr--main > .qr--modal-messageContainer > #qr--modal-message {
flex: 1 1 auto;
}
#dialogue_popup:has(#qr--modalEditor) #dialogue_popup_text > #qr--modalEditor #qr--modal-execute {
display: flex;
flex-direction: row;
gap: 0.5em;
}
#dialogue_popup:has(#qr--modalEditor) #dialogue_popup_text > #qr--modalEditor #qr--modal-execute.qr--busy {
opacity: 0.5;
cursor: wait;
}
#shadow_popup.qr--hide {
opacity: 0 !important;
}

View File

@ -0,0 +1,317 @@
#qr--bar {
outline: none;
margin: 0;
transition: 0.3s;
opacity: 0.7;
display: flex;
align-items: center;
justify-content: center;
width: 100%;
max-width: 100%;
overflow-x: auto;
order: 1;
padding-right: 2.5em;
position: relative;
> #qr--popoutTrigger {
position: absolute;
right: 0.25em;
top: 0;
}
}
#qr--popout {
display: flex;
flex-direction: column;
padding: 0;
z-index: 31;
> .qr--header {
flex: 0 0 auto;
height: 2em;
position: relative;
> .qr--controls {
> .qr--close {
height: 15px;
aspect-ratio: 1 / 1;
font-size: 20px;
opacity: 0.5;
transition: all 250ms;
}
}
}
> .qr--body {
overflow-y: auto;
}
}
#qr--bar, #qr--popout > .qr--body {
> .qr--buttons {
margin: 0;
padding: 0;
display: flex;
justify-content: center;
flex-wrap: wrap;
gap: 5px;
width: 100%;
> .qr--buttons {
display: contents;
}
.qr--button {
color: var(--SmartThemeBodyColor);
// background-color: var(--black50a);
border: 1px solid var(--SmartThemeBorderColor);
border-radius: 10px;
padding: 3px 5px;
margin: 3px 0;
cursor: pointer;
transition: 0.3s;
display: flex;
align-items: center;
justify-content: center;
text-align: center;
&:hover {
opacity: 1;
filter: brightness(1.2);
}
> .qr--button-expander {
display: none;
}
&.qr--hasCtx {
> .qr--button-expander {
display: block;
}
}
}
}
}
.qr--button-expander {
border-left: 1px solid;
margin-left: 1em;
text-align: center;
width: 2em;
&:hover {
font-weight: bold;
}
}
.ctx-blocker {
/* backdrop-filter: blur(1px); */
/* background-color: rgba(0 0 0 / 10%); */
bottom: 0;
left: 0;
position: fixed;
right: 0;
top: 0;
z-index: 999;
}
.ctx-menu {
position: absolute;
overflow: visible;
}
.list-group .list-group-item.ctx-header {
font-weight: bold;
cursor: default;
}
.ctx-item+.ctx-header {
border-top: 1px solid;
}
.ctx-item {
position: relative;
}
.ctx-expander {
border-left: 1px solid;
margin-left: 1em;
text-align: center;
width: 2em;
}
.ctx-expander:hover {
font-weight: bold;
}
.ctx-sub-menu {
position: absolute;
top: 0;
left: 100%;
}
@media screen and (max-width: 1000px) {
.ctx-blocker {
position: absolute;
}
.list-group .list-group-item.ctx-item {
padding: 1em;
}
}
#qr--settings {
.qr--head {
display: flex;
align-items: baseline;
gap: 1em;
> .qr--title {
font-weight: bold;
}
> .qr--actions {
display: flex;
flex-direction: row;
align-items: baseline;
gap: 0.5em;
}
}
.qr--setList {
> .qr--item {
display: flex;
flex-direction: row;
gap: 0.5em;
align-items: baseline;
padding: 0 0.5em;
> .drag-handle {
padding: 0.75em;
}
> .qr--visible {
flex: 0 0 auto;
display: flex;
flex-direction: row;
}
}
}
#qr--set-settings {
#qr--injectInputContainer {
flex-wrap: nowrap;
}
}
#qr--set-qrList {
.qr--set-qrListContents > {
padding: 0 0.5em;
> .qr--set-item {
display: flex;
flex-direction: row;
gap: 0.5em;
align-items: baseline;
padding: 0.25em 0;
> :nth-child(1) { flex: 0 0 auto; }
> :nth-child(2) { flex: 1 1 25%; }
> :nth-child(3) { flex: 0 0 auto; }
> :nth-child(4) { flex: 1 1 75%; }
> :nth-child(5) { flex: 0 0 auto; }
> .drag-handle {
padding: 0.75em;
}
.qr--set-itemLabel, .qr--action {
margin: 0;
}
.qr--set-itemMessage {
font-size: smaller;
}
}
}
}
.qr--set-qrListActions {
display: flex;
flex-direction: row;
gap: 0.5em;
justify-content: center;
padding-bottom: 0.5em;
}
}
#qr--qrOptions {
> #qr--ctxEditor {
.qr--ctxItem {
display: flex;
flex-direction: row;
gap: 0.5em;
align-items: baseline;
}
}
}
@media screen and (max-width: 750px) {
body #dialogue_popup:has(#qr--modalEditor) #dialogue_popup_text > #qr--modalEditor {
flex-direction: column;
> #qr--main > .qr--labels {
flex-direction: column;
}
> #qr--main > .qr--modal-messageContainer > #qr--modal-message {
min-height: 90svh;
}
}
}
#dialogue_popup:has(#qr--modalEditor) {
aspect-ratio: unset;
#dialogue_popup_text {
display: flex;
flex-direction: column;
> #qr--modalEditor {
flex: 1 1 auto;
display: flex;
flex-direction: row;
gap: 1em;
> #qr--main {
flex: 1 1 auto;
display: flex;
flex-direction: column;
> .qr--labels {
flex: 0 0 auto;
display: flex;
flex-direction: row;
gap: 0.5em;
> label {
flex: 1 1 1px;
display: flex;
flex-direction: column;
> .qr--labelText {
flex: 1 1 auto;
}
> .qr--labelHint {
flex: 1 1 auto;
}
> input {
flex: 0 0 auto;
}
}
}
> .qr--modal-messageContainer {
flex: 1 1 auto;
display: flex;
flex-direction: column;
> #qr--modal-message {
flex: 1 1 auto;
}
}
}
#qr--modal-execute {
display: flex;
flex-direction: row;
gap: 0.5em;
&.qr--busy {
opacity: 0.5;
cursor: wait;
}
}
}
}
}
#shadow_popup.qr--hide {
opacity: 0 !important;
}

View File

@ -1,9 +1,14 @@
<div id="regex_editor_template">
<div class="regex_editor">
<h3><strong data-i18n="Regex Editor">Regex Editor</strong>
<h3 class="flex-container justifyCenter alignItemsBaseline">
<strong data-i18n="Regex Editor">Regex Editor</strong>
<a href="https://regexr.com/" class="notes-link" target="_blank">
<span class="note-link-span">?</span>
</a>
<div id="regex_test_mode_toggle" class="menu_button menu_button_icon">
<i class="fa-solid fa-bug fa-sm"></i>
<span class="menu_button_text" data-i18n="Test Mode">Test Mode</span>
</div>
</h3>
<small class="flex-container extensions_info">
@ -11,6 +16,22 @@
</small>
<hr />
<div id="regex_test_mode" class="flex1 flex-container displayNone">
<div class="flex1">
<label class="title_restorable" for="regex_test_input">
<small data-i18n="Input">Input</small>
</label>
<textarea id="regex_test_input" class="text_pole textarea_compact" rows="4" placeholder="Type here..."></textarea>
</div>
<div class="flex1">
<label class="title_restorable" for="regex_test_output">
<small data-i18n="Output">Output</small>
</label>
<textarea id="regex_test_output" class="text_pole textarea_compact" rows="4" placeholder="Empty" readonly></textarea>
</div>
<hr>
</div>
<div class="flex-container flexFlowColumn">
<div class="flex1">
<label for="regex_script_name" class="title_restorable">
@ -35,7 +56,7 @@
<div>
<textarea
class="regex_replace_string text_pole wide100p textarea_compact"
placeholder="Use {{match}} to include the matched text from the Find Regex"
placeholder="Use {{match}} to include the matched text from the Find Regex or $1, $2, etc. for capture groups."
rows="2"
></textarea>
</div>
@ -94,16 +115,16 @@
<input type="checkbox" name="run_on_edit" />
<span data-i18n="Run On Edit">Run On Edit</span>
</label>
<label class="checkbox flex-container">
<label class="checkbox flex-container" title="Substitute {{macros}} in Find Regex before running it">
<input type="checkbox" name="substitute_regex" />
<span data-i18n="Substitute Regex">Substitute Regex</span>
<span data-i18n="Substitute Regex">Substitute Regex (?)</span>
</label>
</div>
<div class="flex-container flexFlowColumn alignitemsstart">
<small>Replacement Strategy</small>
<select name="replace_strategy_select" class="margin0">
<option value="0">Replace</option>
<option value="1">Overlay</option>
<option value="1">Overlay (currently broken)</option>
</select>
</div>
</div>

View File

@ -6,20 +6,33 @@ export {
runRegexScript,
};
/**
* @enum {number} Where the regex script should be applied
*/
const regex_placement = {
// MD Display is deprecated. Do not use.
/**
* @deprecated MD Display is deprecated. Do not use.
*/
MD_DISPLAY: 0,
USER_INPUT: 1,
AI_OUTPUT: 2,
SLASH_COMMAND: 3,
};
/**
* @enum {number} How the regex script should replace the matched string
*/
const regex_replace_strategy = {
REPLACE: 0,
OVERLAY: 1,
};
// Originally from: https://github.com/IonicaBizau/regex-parser.js/blob/master/lib/index.js
/**
* Instantiates a regular expression from a string.
* @param {string} input The input string.
* @returns {RegExp} The regular expression instance.
* @copyright Originally from: https://github.com/IonicaBizau/regex-parser.js/blob/master/lib/index.js
*/
function regexFromString(input) {
try {
// Parse input
@ -37,8 +50,21 @@ function regexFromString(input) {
}
}
// Parent function to fetch a regexed version of a raw string
/**
* Parent function to fetch a regexed version of a raw string
* @param {string} rawString The raw string to be regexed
* @param {regex_placement} placement The placement of the string
* @param {RegexParams} params The parameters to use for the regex script
* @returns {string} The regexed string
* @typedef {{characterOverride?: string, isMarkdown?: boolean, isPrompt?: boolean }} RegexParams The parameters to use for the regex script
*/
function getRegexedString(rawString, placement, { characterOverride, isMarkdown, isPrompt } = {}) {
// WTF have you passed me?
if (typeof rawString !== 'string') {
console.warn('getRegexedString: rawString is not a string. Returning empty string.');
return '';
}
let finalString = rawString;
if (extension_settings.disabledExtensions.includes('regex') || !rawString || placement === undefined) {
return finalString;
@ -62,14 +88,20 @@ function getRegexedString(rawString, placement, { characterOverride, isMarkdown,
return finalString;
}
// Runs the provided regex script on the given string
/**
* Runs the provided regex script on the given string
* @param {object} regexScript The regex script to run
* @param {string} rawString The string to run the regex script on
* @param {RegexScriptParams} params The parameters to use for the regex script
* @returns {string} The new string
* @typedef {{characterOverride?: string}} RegexScriptParams The parameters to use for the regex script
*/
function runRegexScript(regexScript, rawString, { characterOverride } = {}) {
let newString = rawString;
if (!regexScript || !!(regexScript.disabled) || !regexScript?.findRegex || !rawString) {
return newString;
}
let match;
const findRegex = regexFromString(regexScript.substituteRegex ? substituteParams(regexScript.findRegex) : regexScript.findRegex);
// The user skill issued. Return with nothing.
@ -77,46 +109,41 @@ function runRegexScript(regexScript, rawString, { characterOverride } = {}) {
return newString;
}
while ((match = findRegex.exec(rawString)) !== null) {
const fencedMatch = match[0];
const capturedMatch = match[1];
// Run replacement. Currently does not support the Overlay strategy
newString = rawString.replace(findRegex, function(match) {
const args = [...arguments];
const replaceString = regexScript.replaceString.replace(/{{match}}/gi, '$0');
const replaceWithGroups = replaceString.replaceAll(/\$(\d)+/g, (_, num) => {
// Get a full match or a capture group
const match = args[Number(num)];
let trimCapturedMatch;
let trimFencedMatch;
if (capturedMatch) {
const tempTrimCapture = filterString(capturedMatch, regexScript.trimStrings, { characterOverride });
trimFencedMatch = fencedMatch.replaceAll(capturedMatch, tempTrimCapture);
trimCapturedMatch = tempTrimCapture;
} else {
trimFencedMatch = filterString(fencedMatch, regexScript.trimStrings, { characterOverride });
}
// No match found - return the empty string
if (!match) {
return '';
}
// TODO: Use substrings for replacement. But not necessary at this time.
// A substring is from match.index to match.index + match[0].length or fencedMatch.length
const subReplaceString = substituteRegexParams(
regexScript.replaceString,
trimCapturedMatch ?? trimFencedMatch,
{
characterOverride,
replaceStrategy: regexScript.replaceStrategy ?? regex_replace_strategy.REPLACE,
},
);
if (!newString) {
newString = rawString.replace(fencedMatch, subReplaceString);
} else {
newString = newString.replace(fencedMatch, subReplaceString);
}
// Remove trim strings from the match
const filteredMatch = filterString(match, regexScript.trimStrings, { characterOverride });
// If the regex isn't global, break out of the loop
if (!findRegex.flags.includes('g')) {
break;
}
}
// TODO: Handle overlay here
return filteredMatch;
});
// Substitute at the end
return substituteParams(replaceWithGroups);
});
return newString;
}
// Filters anything to trim from the regex match
/**
* Filters anything to trim from the regex match
* @param {string} rawString The raw string to filter
* @param {string[]} trimStrings The strings to trim
* @param {RegexScriptParams} params The parameters to use for the regex filter
* @returns {string} The filtered string
*/
function filterString(rawString, trimStrings, { characterOverride } = {}) {
let finalString = rawString;
trimStrings.forEach((trimString) => {
@ -127,7 +154,14 @@ function filterString(rawString, trimStrings, { characterOverride } = {}) {
return finalString;
}
// Substitutes regex-specific and normal parameters
/**
* Substitutes regex-specific and normal parameters
* @param {string} rawString
* @param {string} regexMatch
* @param {RegexSubstituteParams} params The parameters to use for the regex substitution
* @returns {string} The substituted string
* @typedef {{characterOverride?: string, replaceStrategy?: number}} RegexSubstituteParams The parameters to use for the regex substitution
*/
function substituteRegexParams(rawString, regexMatch, { characterOverride, replaceStrategy } = {}) {
let finalString = rawString;
finalString = substituteParams(finalString, undefined, characterOverride);
@ -182,8 +216,13 @@ function substituteRegexParams(rawString, regexMatch, { characterOverride, repla
return finalString;
}
// Splices common sentence symbols and whitespace from the beginning and end of a string
// Using a for loop due to sequential ordering
/**
* Splices common sentence symbols and whitespace from the beginning and end of a string.
* Using a for loop due to sequential ordering.
* @param {string} rawString The raw string to splice
* @param {boolean} isSuffix String is a suffix
* @returns {string} The spliced string
*/
function spliceSymbols(rawString, isSuffix) {
let offset = 0;

View File

@ -165,6 +165,31 @@ async function onRegexEditorOpenClick(existingId) {
.prop('checked', true);
}
editorHtml.find('#regex_test_mode_toggle').on('click', function () {
editorHtml.find('#regex_test_mode').toggleClass('displayNone');
updateTestResult();
});
function updateTestResult() {
if (!editorHtml.find('#regex_test_mode').is(':visible')) {
return;
}
const testScript = {
scriptName: editorHtml.find('.regex_script_name').val(),
findRegex: editorHtml.find('.find_regex').val(),
replaceString: editorHtml.find('.regex_replace_string').val(),
trimStrings: String(editorHtml.find('.regex_trim_strings').val()).split('\n').filter((e) => e.length !== 0) || [],
substituteRegex: editorHtml.find('input[name="substitute_regex"]').prop('checked'),
replaceStrategy: Number(editorHtml.find('select[name="replace_strategy_select"]').find(':selected').val()) ?? 0,
};
const rawTestString = String(editorHtml.find('#regex_test_input').val());
const result = runRegexScript(testScript, rawTestString);
editorHtml.find('#regex_test_output').text(result);
}
editorHtml.find('input, textarea, select').on('input', updateTestResult);
const popupResult = await callPopup(editorHtml, 'confirm', undefined, { okButton: 'Save' });
if (popupResult) {
const newRegexScript = {

View File

@ -21,13 +21,15 @@ export async function getMultimodalCaption(base64Img, prompt) {
}
// OpenRouter has a payload limit of ~2MB. Google is 4MB, but we love democracy.
// Ooba requires all images to be JPEGs.
const isGoogle = extension_settings.caption.multimodal_api === 'google';
const isOllama = extension_settings.caption.multimodal_api === 'ollama';
const isLlamaCpp = extension_settings.caption.multimodal_api === 'llamacpp';
const isCustom = extension_settings.caption.multimodal_api === 'custom';
const isOoba = extension_settings.caption.multimodal_api === 'ooba';
const base64Bytes = base64Img.length * 0.75;
const compressionLimit = 2 * 1024 * 1024;
if (['google', 'openrouter'].includes(extension_settings.caption.multimodal_api) && base64Bytes > compressionLimit) {
if ((['google', 'openrouter'].includes(extension_settings.caption.multimodal_api) && base64Bytes > compressionLimit) || isOoba) {
const maxSide = 1024;
base64Img = await createThumbnail(base64Img, maxSide, maxSide, 'image/jpeg');
@ -69,6 +71,10 @@ export async function getMultimodalCaption(base64Img, prompt) {
requestBody.server_url = textgenerationwebui_settings.server_urls[textgen_types.LLAMACPP];
}
if (isOoba) {
requestBody.server_url = textgenerationwebui_settings.server_urls[textgen_types.OOBA];
}
if (isCustom) {
requestBody.server_url = oai_settings.custom_url;
requestBody.model = oai_settings.custom_model || 'gpt-4-vision-preview';
@ -129,6 +135,10 @@ function throwIfInvalidModel() {
throw new Error('LlamaCPP server URL is not set.');
}
if (extension_settings.caption.multimodal_api === 'ooba' && !textgenerationwebui_settings.server_urls[textgen_types.OOBA]) {
throw new Error('Text Generation WebUI server URL is not set.');
}
if (extension_settings.caption.multimodal_api === 'custom' && !oai_settings.custom_url) {
throw new Error('Custom API URL is not set.');
}

View File

@ -2286,7 +2286,7 @@ async function generateComfyImage(prompt, negativePrompt) {
toastr.error(`Failed to load workflow.\n\n${text}`);
}
let workflow = (await workflowResponse.json()).replace('"%prompt%"', JSON.stringify(prompt));
workflow = (await workflowResponse.json()).replace('"%negative_prompt%"', JSON.stringify(negativePrompt));
workflow = workflow.replace('"%negative_prompt%"', JSON.stringify(negativePrompt));
workflow = workflow.replace('"%seed%"', JSON.stringify(Math.round(Math.random() * Number.MAX_SAFE_INTEGER)));
placeholders.forEach(ph => {
workflow = workflow.replace(`"%${ph}%"`, JSON.stringify(extension_settings.sd[ph]));
@ -2642,7 +2642,7 @@ $('#sd_dropdown [id]').on('click', function () {
jQuery(async () => {
registerSlashCommand('imagine', generatePicture, ['sd', 'img', 'image'], helpString, true, true);
registerSlashCommand('imagine-comfy-workflow', changeComfyWorkflow, ['icw'], '(workflowName) - change the workflow to be used for image generation with ComfyUI, e.g. <tt>/imagine-comfy-workflow MyWorkflow</tt>')
registerSlashCommand('imagine-comfy-workflow', changeComfyWorkflow, ['icw'], '(workflowName) - change the workflow to be used for image generation with ComfyUI, e.g. <tt>/imagine-comfy-workflow MyWorkflow</tt>');
$('#extensions_settings').append(renderExtensionTemplate('stable-diffusion', 'settings', defaultSettings));
$('#sd_source').on('change', onSourceChange);

View File

@ -310,12 +310,12 @@ class CoquiTtsProvider {
modelDict = coquiApiModelsFull;
if (model_setting_language == null & 'languages' in modelDict[model_language][model_dataset][model_label]) {
toastr.error('Model language not selected, please select one.', DEBUG_PREFIX+' voice mapping model language', { timeOut: 10000, extendedTimeOut: 20000, preventDuplicates: true });
toastr.error('Model language not selected, please select one.', DEBUG_PREFIX + ' voice mapping model language', { timeOut: 10000, extendedTimeOut: 20000, preventDuplicates: true });
return;
}
if (model_setting_speaker == null & 'speakers' in modelDict[model_language][model_dataset][model_label]) {
toastr.error('Model speaker not selected, please select one.', DEBUG_PREFIX+' voice mapping model speaker', { timeOut: 10000, extendedTimeOut: 20000, preventDuplicates: true });
toastr.error('Model speaker not selected, please select one.', DEBUG_PREFIX + ' voice mapping model speaker', { timeOut: 10000, extendedTimeOut: 20000, preventDuplicates: true });
return;
}

View File

@ -1,6 +1,6 @@
import { callPopup, cancelTtsPlay, eventSource, event_types, name2, saveSettingsDebounced } from '../../../script.js';
import { ModuleWorkerWrapper, doExtrasFetch, extension_settings, getApiUrl, getContext, modules } from '../../extensions.js';
import { delay, escapeRegex, getStringHash } from '../../utils.js';
import { delay, escapeRegex, getBase64Async, getStringHash, onlyUnique } from '../../utils.js';
import { EdgeTtsProvider } from './edge.js';
import { ElevenLabsTtsProvider } from './elevenlabs.js';
import { SileroTtsProvider } from './silerotts.js';
@ -316,12 +316,14 @@ async function playAudioData(audioBlob) {
if (currentAudioJob == null) {
console.log('Cancelled TTS playback because currentAudioJob was null');
}
const reader = new FileReader();
reader.onload = function (e) {
const srcUrl = e.target.result;
if (audioBlob instanceof Blob) {
const srcUrl = await getBase64Async(audioBlob);
audioElement.src = srcUrl;
};
reader.readAsDataURL(audioBlob);
} else if (typeof audioBlob === 'string') {
audioElement.src = audioBlob;
} else {
throw `TTS received invalid audio data type ${typeof audioBlob}`;
}
audioElement.addEventListener('ended', completeCurrentAudioJob);
audioElement.addEventListener('canplay', () => {
console.debug('Starting TTS playback');
@ -417,11 +419,15 @@ function completeCurrentAudioJob() {
* @param {Response} response
*/
async function addAudioJob(response) {
const audioData = await response.blob();
if (!audioData.type.startsWith('audio/')) {
throw `TTS received HTTP response with invalid data format. Expecting audio/*, got ${audioData.type}`;
if (typeof response === 'string') {
audioJobQueue.push(response);
} else {
const audioData = await response.blob();
if (!audioData.type.startsWith('audio/')) {
throw `TTS received HTTP response with invalid data format. Expecting audio/*, got ${audioData.type}`;
}
audioJobQueue.push(audioData);
}
audioJobQueue.push(audioData);
console.debug('Pushed audio job to queue.');
}
@ -432,7 +438,7 @@ async function processAudioJobQueue() {
}
try {
audioQueueProcessorReady = false;
currentAudioJob = audioJobQueue.pop();
currentAudioJob = audioJobQueue.shift();
playAudioData(currentAudioJob);
talkingAnimation(true);
} catch (error) {
@ -463,13 +469,25 @@ function saveLastValues() {
}
async function tts(text, voiceId, char) {
async function processResponse(response) {
// RVC injection
if (extension_settings.rvc.enabled && typeof window['rvcVoiceConversion'] === 'function')
response = await window['rvcVoiceConversion'](response, char, text);
await addAudioJob(response);
}
let response = await ttsProvider.generateTts(text, voiceId);
// RVC injection
if (extension_settings.rvc.enabled && typeof window['rvcVoiceConversion'] === 'function')
response = await window['rvcVoiceConversion'](response, char, text);
// If async generator, process every chunk as it comes in
if (typeof response[Symbol.asyncIterator] === 'function') {
for await (const chunk of response) {
await processResponse(chunk);
}
} else {
await processResponse(response);
}
addAudioJob(response);
completeTtsJob();
}
@ -733,7 +751,7 @@ function getCharacters(unrestricted) {
if (unrestricted) {
const names = context.characters.map(char => char.name);
names.unshift(DEFAULT_VOICE_MARKER);
return names;
return names.filter(onlyUnique);
}
let characters = [];
@ -748,14 +766,13 @@ function getCharacters(unrestricted) {
characters.push(context.name1);
const group = context.groups.find(group => context.groupId == group.id);
for (let member of group.members) {
// Remove suffix
if (member.endsWith('.png')) {
member = member.slice(0, -4);
const character = context.characters.find(char => char.avatar == member);
if (character) {
characters.push(character.name);
}
characters.push(member);
}
}
return characters;
return characters.filter(onlyUnique);
}
function sanitizeId(input) {

View File

@ -1,4 +1,5 @@
import { getRequestHeaders, callPopup } from '../../../script.js';
import { splitRecursive } from '../../utils.js';
import { getPreviewString, saveTtsProviderSettings } from './index.js';
import { initVoiceMap } from './index.js';
@ -52,7 +53,7 @@ class NovelTtsProvider {
// Add a new Novel custom voice to provider
async addCustomVoice(){
async addCustomVoice() {
const voiceName = await callPopup('<h3>Custom Voice name:</h3>', 'input');
this.settings.customVoices.push(voiceName);
this.populateCustomVoices();
@ -74,7 +75,7 @@ class NovelTtsProvider {
}
// Create the UI dropdown list of voices in provider
populateCustomVoices(){
populateCustomVoices() {
let voiceSelect = $('#tts-novel-custom-voices-select');
voiceSelect.empty();
this.settings.customVoices.forEach(voice => {
@ -88,7 +89,7 @@ class NovelTtsProvider {
console.info('Using default TTS Provider settings');
}
$('#tts-novel-custom-voices-add').on('click', () => (this.addCustomVoice()));
$('#tts-novel-custom-voices-delete').on('click',() => (this.deleteCustomVoice()));
$('#tts-novel-custom-voices-delete').on('click', () => (this.deleteCustomVoice()));
// Only accept keys defined in defaultSettings
this.settings = this.defaultSettings;
@ -108,7 +109,7 @@ class NovelTtsProvider {
// Perform a simple readiness check by trying to fetch voiceIds
// Doesnt really do much for Novel, not seeing a good way to test this at the moment.
async checkReady(){
async checkReady() {
await this.fetchTtsVoiceObjects();
}
@ -179,22 +180,26 @@ class NovelTtsProvider {
this.audioElement.play();
}
async fetchTtsGeneration(inputText, voiceId) {
async* fetchTtsGeneration(inputText, voiceId) {
const MAX_LENGTH = 1000;
console.info(`Generating new TTS for voice_id ${voiceId}`);
const response = await fetch('/api/novelai/generate-voice',
{
method: 'POST',
headers: getRequestHeaders(),
body: JSON.stringify({
'text': inputText,
'voice': voiceId,
}),
},
);
if (!response.ok) {
toastr.error(response.statusText, 'TTS Generation Failed');
throw new Error(`HTTP ${response.status}: ${await response.text()}`);
const chunks = splitRecursive(inputText, MAX_LENGTH);
for (const chunk of chunks) {
const response = await fetch('/api/novelai/generate-voice',
{
method: 'POST',
headers: getRequestHeaders(),
body: JSON.stringify({
'text': chunk,
'voice': voiceId,
}),
},
);
if (!response.ok) {
toastr.error(response.statusText, 'TTS Generation Failed');
throw new Error(`HTTP ${response.status}: ${await response.text()}`);
}
yield response;
}
return response;
}
}

View File

@ -51,7 +51,16 @@ class XTTSTtsProvider {
defaultSettings = {
provider_endpoint: 'http://localhost:8020',
language: 'en',
temperature: 0.75,
length_penalty: 1.0,
repetition_penalty: 5.0,
top_k: 50,
top_p: 0.85,
speed: 1,
enable_text_splitting: true,
stream_chunk_size: 100,
voiceMap: {},
streaming: false,
};
get settingsHtml() {
@ -59,9 +68,7 @@ class XTTSTtsProvider {
<label for="xtts_api_language">Language</label>
<select id="xtts_api_language">`;
for (let language in this.languageLabels) {
if (this.languageLabels[language] == this.settings?.language) {
html += `<option value="${this.languageLabels[language]}" selected="selected">${language}</option>`;
continue;
@ -70,27 +77,73 @@ class XTTSTtsProvider {
html += `<option value="${this.languageLabels[language]}">${language}</option>`;
}
html += `
</select>
<label">XTTS Settings:</label><br/>
<label for="xtts_tts_endpoint">Provider Endpoint:</label>
<input id="xtts_tts_endpoint" type="text" class="text_pole" maxlength="250" value="${this.defaultSettings.provider_endpoint}"/>
`;
html += `
<span>
<span>Use <a target="_blank" href="https://github.com/daswer123/xtts-api-server">XTTSv2 TTS Server</a>.</span>
<label for="xtts_tts_streaming" class="checkbox_label">
<input id="xtts_tts_streaming" type="checkbox" />
<span>Streaming <small>(RVC not supported)</small></span>
</label>
<label for="xtts_speed">Speed: <span id="xtts_tts_speed_output">${this.defaultSettings.speed}</span></label>
<input id="xtts_speed" type="range" value="${this.defaultSettings.speed}" min="0.5" max="2" step="0.01" />
<label for="xtts_temperature">Temperature: <span id="xtts_tts_temperature_output">${this.defaultSettings.temperature}</span></label>
<input id="xtts_temperature" type="range" value="${this.defaultSettings.temperature}" min="0.01" max="1" step="0.01" />
<label for="xtts_length_penalty">Length Penalty: <span id="xtts_length_penalty_output">${this.defaultSettings.length_penalty}</span></label>
<input id="xtts_length_penalty" type="range" value="${this.defaultSettings.length_penalty}" min="0.5" max="2" step="0.1" />
<label for="xtts_repetition_penalty">Repetition Penalty: <span id="xtts_repetition_penalty_output">${this.defaultSettings.repetition_penalty}</span></label>
<input id="xtts_repetition_penalty" type="range" value="${this.defaultSettings.repetition_penalty}" min="1" max="10" step="0.1" />
<label for="xtts_top_k">Top K: <span id="xtts_top_k_output">${this.defaultSettings.top_k}</span></label>
<input id="xtts_top_k" type="range" value="${this.defaultSettings.top_k}" min="0" max="100" step="1" />
<label for="xtts_top_p">Top P: <span id="xtts_top_p_output">${this.defaultSettings.top_p}</span></label>
<input id="xtts_top_p" type="range" value="${this.defaultSettings.top_p}" min="0" max="1" step="0.01" />
<label for="xtts_stream_chunk_size">Stream Chunk Size: <span id="xtts_stream_chunk_size_output">${this.defaultSettings.stream_chunk_size}</span></label>
<input id="xtts_stream_chunk_size" type="range" value="${this.defaultSettings.stream_chunk_size}" min="100" max="400" step="1" />
<label for="xtts_enable_text_splitting" class="checkbox_label">
<input id="xtts_enable_text_splitting" type="checkbox" ${this.defaultSettings.enable_text_splitting ? 'checked' : ''} />
Enable Text Splitting
</label>
`;
return html;
}
onSettingsChange() {
// Used when provider settings are updated from UI
this.settings.provider_endpoint = $('#xtts_tts_endpoint').val();
this.settings.language = $('#xtts_api_language').val();
// Update the default TTS settings based on input fields
this.settings.speed = $('#xtts_speed').val();
this.settings.temperature = $('#xtts_temperature').val();
this.settings.length_penalty = $('#xtts_length_penalty').val();
this.settings.repetition_penalty = $('#xtts_repetition_penalty').val();
this.settings.top_k = $('#xtts_top_k').val();
this.settings.top_p = $('#xtts_top_p').val();
this.settings.stream_chunk_size = $('#xtts_stream_chunk_size').val();
this.settings.enable_text_splitting = $('#xtts_enable_text_splitting').is(':checked');
this.settings.streaming = $('#xtts_tts_streaming').is(':checked');
// Update the UI to reflect changes
$('#xtts_tts_speed_output').text(this.settings.speed);
$('#xtts_tts_temperature_output').text(this.settings.temperature);
$('#xtts_length_penalty_output').text(this.settings.length_penalty);
$('#xtts_repetition_penalty_output').text(this.settings.repetition_penalty);
$('#xtts_top_k_output').text(this.settings.top_k);
$('#xtts_top_p_output').text(this.settings.top_p);
$('#xtts_stream_chunk_size_output').text(this.settings.stream_chunk_size);
saveTtsProviderSettings();
this.changeTTSSettings();
}
async loadSettings(settings) {
@ -121,10 +174,40 @@ class XTTSTtsProvider {
}
}, 2000);
// Set initial values from the settings
$('#xtts_tts_endpoint').val(this.settings.provider_endpoint);
$('#xtts_tts_endpoint').on('input', () => { this.onSettingsChange(); });
$('#xtts_api_language').val(this.settings.language);
$('#xtts_speed').val(this.settings.speed);
$('#xtts_temperature').val(this.settings.temperature);
$('#xtts_length_penalty').val(this.settings.length_penalty);
$('#xtts_repetition_penalty').val(this.settings.repetition_penalty);
$('#xtts_top_k').val(this.settings.top_k);
$('#xtts_top_p').val(this.settings.top_p);
$('#xtts_enable_text_splitting').prop('checked', this.settings.enable_text_splitting);
$('#xtts_stream_chunk_size').val(this.settings.stream_chunk_size);
$('#xtts_tts_streaming').prop('checked', this.settings.streaming);
// Update the UI to reflect changes
$('#xtts_tts_speed_output').text(this.settings.speed);
$('#xtts_tts_temperature_output').text(this.settings.temperature);
$('#xtts_length_penalty_output').text(this.settings.length_penalty);
$('#xtts_repetition_penalty_output').text(this.settings.repetition_penalty);
$('#xtts_top_k_output').text(this.settings.top_k);
$('#xtts_top_p_output').text(this.settings.top_p);
$('#xtts_stream_chunk_size_output').text(this.settings.stream_chunk_size);
// Register input/change event listeners to update settings on user interaction
$('#xtts_tts_endpoint').on('input', () => { this.onSettingsChange(); });
$('#xtts_api_language').on('change', () => { this.onSettingsChange(); });
$('#xtts_speed').on('input', () => { this.onSettingsChange(); });
$('#xtts_temperature').on('input', () => { this.onSettingsChange(); });
$('#xtts_length_penalty').on('input', () => { this.onSettingsChange(); });
$('#xtts_repetition_penalty').on('input', () => { this.onSettingsChange(); });
$('#xtts_top_k').on('input', () => { this.onSettingsChange(); });
$('#xtts_top_p').on('input', () => { this.onSettingsChange(); });
$('#xtts_enable_text_splitting').on('change', () => { this.onSettingsChange(); });
$('#xtts_stream_chunk_size').on('input', () => { this.onSettingsChange(); });
$('#xtts_tts_streaming').on('change', () => { this.onSettingsChange(); });
await this.checkReady();
@ -133,7 +216,7 @@ class XTTSTtsProvider {
// Perform a simple readiness check by trying to fetch voiceIds
async checkReady() {
await this.fetchTtsVoiceObjects();
await Promise.allSettled([this.fetchTtsVoiceObjects(), this.changeTTSSettings()]);
}
async onRefreshClick() {
@ -174,8 +257,46 @@ class XTTSTtsProvider {
return responseJson;
}
// Each time a parameter is changed, we change the configuration
async changeTTSSettings() {
if (!this.settings.provider_endpoint) {
return;
}
const response = await doExtrasFetch(
`${this.settings.provider_endpoint}/set_tts_settings`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Cache-Control': 'no-cache',
},
body: JSON.stringify({
'temperature': this.settings.temperature,
'speed': this.settings.speed,
'length_penalty': this.settings.length_penalty,
'repetition_penalty': this.settings.repetition_penalty,
'top_p': this.settings.top_p,
'top_k': this.settings.top_k,
'enable_text_splitting': this.settings.enable_text_splitting,
'stream_chunk_size': this.settings.stream_chunk_size,
}),
},
);
return response;
}
async fetchTtsGeneration(inputText, voiceId) {
console.info(`Generating new TTS for voice_id ${voiceId}`);
if (this.settings.streaming) {
const params = new URLSearchParams();
params.append('text', inputText);
params.append('speaker_wav', voiceId);
params.append('language', this.settings.language);
return `${this.settings.provider_endpoint}/tts_stream/?${params.toString()}`;
}
const response = await doExtrasFetch(
`${this.settings.provider_endpoint}/tts_to_audio/`,
{

View File

@ -1,6 +1,6 @@
import { eventSource, event_types, extension_prompt_types, getCurrentChatId, getRequestHeaders, is_send_press, saveSettingsDebounced, setExtensionPrompt, substituteParams } from '../../../script.js';
import { ModuleWorkerWrapper, extension_settings, getContext, renderExtensionTemplate } from '../../extensions.js';
import { collapseNewlines, power_user, ui_mode } from '../../power-user.js';
import { collapseNewlines } from '../../power-user.js';
import { SECRET_KEYS, secret_state } from '../../secrets.js';
import { debounce, getStringHash as calculateHash, waitUntilCondition, onlyUnique, splitRecursive } from '../../utils.js';
@ -21,6 +21,7 @@ const settings = {
protect: 5,
insert: 3,
query: 2,
message_chunk_size: 400,
// For files
enabled_files: false,
@ -87,6 +88,29 @@ async function onVectorizeAllClick() {
let syncBlocked = false;
/**
* Splits messages into chunks before inserting them into the vector index.
* @param {object[]} items Array of vector items
* @returns {object[]} Array of vector items (possibly chunked)
*/
function splitByChunks(items) {
if (settings.message_chunk_size <= 0) {
return items;
}
const chunkedItems = [];
for (const item of items) {
const chunks = splitRecursive(item.text, settings.message_chunk_size);
for (const chunk of chunks) {
const chunkedItem = { ...item, text: chunk };
chunkedItems.push(chunkedItem);
}
}
return chunkedItems;
}
async function synchronizeChat(batchSize = 5) {
if (!settings.enabled_chats) {
return -1;
@ -116,8 +140,9 @@ async function synchronizeChat(batchSize = 5) {
const deletedHashes = hashesInCollection.filter(x => !hashedMessages.some(y => y.hash === x));
if (newVectorItems.length > 0) {
const chunkedBatch = splitByChunks(newVectorItems.slice(0, batchSize));
console.log(`Vectors: Found ${newVectorItems.length} new items. Processing ${batchSize}...`);
await insertVectorItems(chatId, newVectorItems.slice(0, batchSize));
await insertVectorItems(chatId, chunkedBatch);
}
if (deletedHashes.length > 0) {
@ -492,6 +517,43 @@ function toggleSettings() {
$('#vectors_chats_settings').toggle(!!settings.enabled_chats);
}
async function onPurgeClick() {
const chatId = getCurrentChatId();
if (!chatId) {
toastr.info('No chat selected', 'Purge aborted');
return;
}
await purgeVectorIndex(chatId);
toastr.success('Vector index purged', 'Purge successful');
}
async function onViewStatsClick() {
const chatId = getCurrentChatId();
if (!chatId) {
toastr.info('No chat selected');
return;
}
const hashesInCollection = await getSavedHashes(chatId);
const totalHashes = hashesInCollection.length;
const uniqueHashes = hashesInCollection.filter(onlyUnique).length;
toastr.info(`Total hashes: <b>${totalHashes}</b><br>
Unique hashes: <b>${uniqueHashes}</b><br><br>
I'll mark collected messages with a green circle.`,
`Stats for chat ${chatId}`,
{ timeOut: 10000, escapeHtml: false });
const chat = getContext().chat;
for (const message of chat) {
if (hashesInCollection.includes(getStringHash(message.mes))) {
const messageElement = $(`.mes[mesid="${chat.indexOf(message)}"]`);
messageElement.addClass('vectorized');
}
}
}
jQuery(async () => {
if (!extension_settings.vectors) {
extension_settings.vectors = settings;
@ -554,9 +616,9 @@ jQuery(async () => {
Object.assign(extension_settings.vectors, settings);
saveSettingsDebounced();
});
$('#vectors_advanced_settings').toggleClass('displayNone', power_user.ui_mode === ui_mode.SIMPLE);
$('#vectors_vectorize_all').on('click', onVectorizeAllClick);
$('#vectors_purge').on('click', onPurgeClick);
$('#vectors_view_stats').on('click', onViewStatsClick);
$('#vectors_size_threshold').val(settings.size_threshold).on('input', () => {
settings.size_threshold = Number($('#vectors_size_threshold').val());
@ -582,6 +644,12 @@ jQuery(async () => {
saveSettingsDebounced();
});
$('#vectors_message_chunk_size').val(settings.message_chunk_size).on('input', () => {
settings.message_chunk_size = Number($('#vectors_message_chunk_size').val());
Object.assign(extension_settings.vectors, settings);
saveSettingsDebounced();
});
toggleSettings();
eventSource.on(event_types.MESSAGE_DELETED, onChatEvent);
eventSource.on(event_types.MESSAGE_EDITED, onChatEvent);

View File

@ -5,7 +5,7 @@
"optional": [],
"generate_interceptor": "vectors_rearrangeChat",
"js": "index.js",
"css": "",
"css": "style.css",
"author": "Cohee#1207",
"version": "1.0.0",
"homePage": "https://github.com/SillyTavern/SillyTavern"

View File

@ -75,7 +75,7 @@
</label>
<div id="vectors_chats_settings">
<div id="vectors_advanced_settings" data-newbie-hidden>
<div id="vectors_advanced_settings">
<label for="vectors_template">
Insertion Template
</label>
@ -97,17 +97,23 @@
</label>
</div>
<div class="flex-container">
<div class="flex1" title="Can increase the retrieval quality for the cost of processing. 0 = disabled.">
<label for="vectors_message_chunk_size">
<small>Chunk size (chars)</small>
</label>
<input id="vectors_message_chunk_size" type="number" class="text_pole widthUnset" min="0" max="9999" />
</div>
<div class="flex1" title="Prevents last N messages from being placed out of order.">
<label for="vectors_protect">
<small>Retain#</small>
</label>
<input type="number" id="vectors_protect" class="text_pole widthUnset" min="1" max="99" />
<input type="number" id="vectors_protect" class="text_pole widthUnset" min="1" max="9999" />
</div>
<div class="flex1" title="How many past messages to insert as memories.">
<label for="vectors_insert">
<small>Insert#</small>
</label>
<input type="number" id="vectors_insert" class="text_pole widthUnset" min="1" max="99" />
<input type="number" id="vectors_insert" class="text_pole widthUnset" min="1" max="9999" />
</div>
</div>
</div>
@ -115,8 +121,16 @@
Old messages are vectorized gradually as you chat.
To process all previous messages, click the button below.
</small>
<div id="vectors_vectorize_all" class="menu_button menu_button_icon">
Vectorize All
<div class="flex-container">
<div id="vectors_vectorize_all" class="menu_button menu_button_icon">
Vectorize All
</div>
<div id="vectors_purge" class="menu_button menu_button_icon">
Purge Vectors
</div>
<div id="vectors_view_stats" class="menu_button menu_button_icon">
View Stats
</div>
</div>
<div id="vectorize_progress" style="display: none;">
<small>

View File

@ -0,0 +1,4 @@
.mes.vectorized .name_text::after {
content: '🟢';
margin-left: 5px;
}

View File

@ -331,7 +331,7 @@ export function formatInstructModeExamples(mesExamples, name1, name2) {
* @returns {string} Formatted instruct mode last prompt line.
*/
export function formatInstructModePrompt(name, isImpersonate, promptBias, name1, name2) {
const includeNames = power_user.instruct.names || (!!selected_group && power_user.instruct.names_force_groups);
const includeNames = name && (power_user.instruct.names || (!!selected_group && power_user.instruct.names_force_groups));
const getOutputSequence = () => power_user.instruct.last_output_sequence || power_user.instruct.output_sequence;
let sequence = isImpersonate ? power_user.instruct.input_sequence : getOutputSequence();

View File

@ -4,7 +4,6 @@ import {
getStoppingStrings,
substituteParams,
api_server,
main_api,
} from '../script.js';
import {
@ -142,7 +141,6 @@ export function getKoboldGenerationData(finalPrompt, settings, maxLength, maxCon
sampler_seed: kai_settings.seed >= 0 ? kai_settings.seed : undefined,
api_server,
main_api,
};
return generate_data;
}
@ -310,6 +308,11 @@ const sliders = [
},
];
/**
* Sets the supported feature flags for the KoboldAI backend.
* @param {string} koboldUnitedVersion Kobold United version
* @param {string} koboldCppVersion KoboldCPP version
*/
export function setKoboldFlags(koboldUnitedVersion, koboldCppVersion) {
kai_flags.can_use_stop_sequence = versionCompare(koboldUnitedVersion, MIN_STOP_SEQUENCE_VERSION);
kai_flags.can_use_streaming = versionCompare(koboldCppVersion, MIN_STREAMING_KCPPVERSION);
@ -318,6 +321,8 @@ export function setKoboldFlags(koboldUnitedVersion, koboldCppVersion) {
kai_flags.can_use_mirostat = versionCompare(koboldCppVersion, MIN_MIROSTAT_KCPPVERSION);
kai_flags.can_use_grammar = versionCompare(koboldCppVersion, MIN_GRAMMAR_KCPPVERSION);
kai_flags.can_use_min_p = versionCompare(koboldCppVersion, MIN_MIN_P_KCPPVERSION);
const isKoboldCpp = versionCompare(koboldCppVersion, '1.0.0');
$('#koboldcpp_hint').toggleClass('displayNone', !isKoboldCpp);
}
/**

276
public/scripts/macros.js Normal file
View File

@ -0,0 +1,276 @@
import { chat, main_api, getMaxContextSize, getCharacterCardFields } from '../script.js';
import { timestampToMoment, isDigitsOnly } from './utils.js';
import { textgenerationwebui_banned_in_macros } from './textgen-settings.js';
import { replaceInstructMacros } from './instruct-mode.js';
import { replaceVariableMacros } from './variables.js';
/**
* Returns the ID of the last message in the chat.
* @returns {string} The ID of the last message in the chat.
*/
function getLastMessageId() {
const index = chat?.length - 1;
if (!isNaN(index) && index >= 0) {
return String(index);
}
return '';
}
/**
* Returns the ID of the first message included in the context.
* @returns {string} The ID of the first message in the context.
*/
function getFirstIncludedMessageId() {
const index = document.querySelector('.lastInContext')?.getAttribute('mesid');
if (!isNaN(index) && index >= 0) {
return String(index);
}
return '';
}
/**
* Returns the last message in the chat.
* @returns {string} The last message in the chat.
*/
function getLastMessage() {
const index = chat?.length - 1;
if (!isNaN(index) && index >= 0) {
return chat[index].mes;
}
return '';
}
/**
* Returns the ID of the last swipe.
* @returns {string} The 1-based ID of the last swipe
*/
function getLastSwipeId() {
const index = chat?.length - 1;
if (!isNaN(index) && index >= 0) {
const swipes = chat[index].swipes;
if (!Array.isArray(swipes) || swipes.length === 0) {
return '';
}
return String(swipes.length);
}
return '';
}
/**
* Returns the ID of the current swipe.
* @returns {string} The 1-based ID of the current swipe.
*/
function getCurrentSwipeId() {
const index = chat?.length - 1;
if (!isNaN(index) && index >= 0) {
const swipeId = chat[index].swipe_id;
if (swipeId === undefined || isNaN(swipeId)) {
return '';
}
return String(swipeId + 1);
}
return '';
}
/**
* Replaces banned words in macros with an empty string.
* Adds them to textgenerationwebui ban list.
* @param {string} inText Text to replace banned words in
* @returns {string} Text without the "banned" macro
*/
function bannedWordsReplace(inText) {
if (!inText) {
return '';
}
const banPattern = /{{banned "(.*)"}}/gi;
if (main_api == 'textgenerationwebui') {
const bans = inText.matchAll(banPattern);
if (bans) {
for (const banCase of bans) {
console.log('Found banned words in macros: ' + banCase[1]);
textgenerationwebui_banned_in_macros.push(banCase[1]);
}
}
}
inText = inText.replaceAll(banPattern, '');
return inText;
}
function getTimeSinceLastMessage() {
const now = moment();
if (Array.isArray(chat) && chat.length > 0) {
let lastMessage;
let takeNext = false;
for (let i = chat.length - 1; i >= 0; i--) {
const message = chat[i];
if (message.is_system) {
continue;
}
if (message.is_user && takeNext) {
lastMessage = message;
break;
}
takeNext = true;
}
if (lastMessage?.send_date) {
const lastMessageDate = timestampToMoment(lastMessage.send_date);
const duration = moment.duration(now.diff(lastMessageDate));
return duration.humanize();
}
}
return 'just now';
}
function randomReplace(input, emptyListPlaceholder = '') {
const randomPatternNew = /{{random\s?::\s?([^}]+)}}/gi;
const randomPatternOld = /{{random\s?:\s?([^}]+)}}/gi;
if (randomPatternNew.test(input)) {
return input.replace(randomPatternNew, (match, listString) => {
//split on double colons instead of commas to allow for commas inside random items
const list = listString.split('::').filter(item => item.length > 0);
if (list.length === 0) {
return emptyListPlaceholder;
}
var rng = new Math.seedrandom('added entropy.', { entropy: true });
const randomIndex = Math.floor(rng() * list.length);
//trim() at the end to allow for empty random values
return list[randomIndex].trim();
});
} else if (randomPatternOld.test(input)) {
return input.replace(randomPatternOld, (match, listString) => {
const list = listString.split(',').map(item => item.trim()).filter(item => item.length > 0);
if (list.length === 0) {
return emptyListPlaceholder;
}
var rng = new Math.seedrandom('added entropy.', { entropy: true });
const randomIndex = Math.floor(rng() * list.length);
return list[randomIndex];
});
} else {
return input;
}
}
function diceRollReplace(input, invalidRollPlaceholder = '') {
const rollPattern = /{{roll[ : ]([^}]+)}}/gi;
return input.replace(rollPattern, (match, matchValue) => {
let formula = matchValue.trim();
if (isDigitsOnly(formula)) {
formula = `1d${formula}`;
}
const isValid = droll.validate(formula);
if (!isValid) {
console.debug(`Invalid roll formula: ${formula}`);
return invalidRollPlaceholder;
}
const result = droll.roll(formula);
return new String(result.total);
});
}
/**
* Substitutes {{macro}} parameters in a string.
* @param {string} content - The string to substitute parameters in.
* @param {*} _name1 - The name of the user.
* @param {*} _name2 - The name of the character.
* @param {*} _original - The original message for {{original}} substitution.
* @param {*} _group - The group members list for {{group}} substitution.
* @param {boolean} _replaceCharacterCard - Whether to replace character card macros.
* @returns {string} The string with substituted parameters.
*/
export function evaluateMacros(content, _name1, _name2, _original, _group, _replaceCharacterCard = true) {
if (!content) {
return '';
}
// Replace {{original}} with the original message
// Note: only replace the first instance of {{original}}
// This will hopefully prevent the abuse
if (typeof _original === 'string') {
content = content.replace(/{{original}}/i, _original);
}
content = diceRollReplace(content);
content = replaceInstructMacros(content);
content = replaceVariableMacros(content);
content = content.replace(/{{newline}}/gi, '\n');
content = content.replace(/{{input}}/gi, String($('#send_textarea').val()));
if (_replaceCharacterCard) {
const fields = getCharacterCardFields();
content = content.replace(/{{charPrompt}}/gi, fields.system || '');
content = content.replace(/{{charJailbreak}}/gi, fields.jailbreak || '');
content = content.replace(/{{description}}/gi, fields.description || '');
content = content.replace(/{{personality}}/gi, fields.personality || '');
content = content.replace(/{{scenario}}/gi, fields.scenario || '');
content = content.replace(/{{persona}}/gi, fields.persona || '');
content = content.replace(/{{mesExamples}}/gi, fields.mesExamples || '');
}
content = content.replace(/{{maxPrompt}}/gi, () => String(getMaxContextSize()));
content = content.replace(/{{user}}/gi, _name1);
content = content.replace(/{{char}}/gi, _name2);
content = content.replace(/{{charIfNotGroup}}/gi, _group);
content = content.replace(/{{group}}/gi, _group);
content = content.replace(/{{lastMessage}}/gi, getLastMessage());
content = content.replace(/{{lastMessageId}}/gi, getLastMessageId());
content = content.replace(/{{firstIncludedMessageId}}/gi, getFirstIncludedMessageId());
content = content.replace(/{{lastSwipeId}}/gi, getLastSwipeId());
content = content.replace(/{{currentSwipeId}}/gi, getCurrentSwipeId());
content = content.replace(/<USER>/gi, _name1);
content = content.replace(/<BOT>/gi, _name2);
content = content.replace(/<CHARIFNOTGROUP>/gi, _group);
content = content.replace(/<GROUP>/gi, _group);
content = content.replace(/\{\{\/\/([\s\S]*?)\}\}/gm, '');
content = content.replace(/{{time}}/gi, moment().format('LT'));
content = content.replace(/{{date}}/gi, moment().format('LL'));
content = content.replace(/{{weekday}}/gi, moment().format('dddd'));
content = content.replace(/{{isotime}}/gi, moment().format('HH:mm'));
content = content.replace(/{{isodate}}/gi, moment().format('YYYY-MM-DD'));
content = content.replace(/{{datetimeformat +([^}]*)}}/gi, (_, format) => {
const formattedTime = moment().format(format);
return formattedTime;
});
content = content.replace(/{{idle_duration}}/gi, () => getTimeSinceLastMessage());
content = content.replace(/{{time_UTC([-+]\d+)}}/gi, (_, offset) => {
const utcOffset = parseInt(offset, 10);
const utcTime = moment().utc().utcOffset(utcOffset).format('LT');
return utcTime;
});
content = bannedWordsReplace(content);
content = randomReplace(content);
return content;
}

View File

@ -62,6 +62,7 @@ import {
formatInstructModePrompt,
formatInstructModeSystemPrompt,
} from './instruct-mode.js';
import { isMobile } from './RossAscends-mods.js';
export {
openai_messages_count,
@ -187,6 +188,8 @@ const default_settings = {
count_pen: 0.0,
top_p_openai: 1.0,
top_k_openai: 0,
min_p_openai: 0,
top_a_openai: 1,
stream_openai: false,
openai_max_context: max_4k,
openai_max_tokens: 300,
@ -235,6 +238,7 @@ const default_settings = {
use_google_tokenizer: false,
exclude_assistant: false,
claude_use_sysprompt: false,
claude_exclude_prefixes: false,
use_alt_scale: false,
squash_system_messages: false,
image_inlining: false,
@ -251,6 +255,8 @@ const oai_settings = {
count_pen: 0.0,
top_p_openai: 1.0,
top_k_openai: 0,
min_p_openai: 0,
top_a_openai: 1,
stream_openai: false,
openai_max_context: max_4k,
openai_max_tokens: 300,
@ -299,6 +305,7 @@ const oai_settings = {
use_google_tokenizer: false,
exclude_assistant: false,
claude_use_sysprompt: false,
claude_exclude_prefixes: false,
use_alt_scale: false,
squash_system_messages: false,
image_inlining: false,
@ -482,7 +489,10 @@ function setOpenAIMessageExamples(mesExamplesArray) {
*/
function setupChatCompletionPromptManager(openAiSettings) {
// Do not set up prompt manager more than once
if (promptManager) return promptManager;
if (promptManager) {
promptManager.render(false);
return promptManager;
}
promptManager = new PromptManager();
@ -1031,9 +1041,6 @@ function preparePromptsForChatCompletion({ Scenario, charPersonality, name2, wor
prompts.set(jbReplacement, prompts.index('jailbreak'));
}
// Allow subscribers to manipulate the prompts object
eventSource.emit(event_types.OAI_BEFORE_CHATCOMPLETION, prompts);
return prompts;
}
@ -1296,6 +1303,25 @@ function getChatCompletionModel() {
}
}
function getOpenRouterModelTemplate(option) {
const model = model_list.find(x => x.id === option?.element?.value);
if (!option.id || !model) {
return option.text;
}
let tokens_dollar = Number(1 / (1000 * model.pricing?.prompt));
let tokens_rounded = (Math.round(tokens_dollar * 1000) / 1000).toFixed(0);
const price = 0 === Number(model.pricing?.prompt) ? 'Free' : `${tokens_rounded}k t/$ `;
return $((`
<div class="flex-container flexFlowColumn" title="${DOMPurify.sanitize(model.id)}">
<div><strong>${DOMPurify.sanitize(model.name)}</strong> | ${model.context_length} ctx | <small>${price}</small></div>
</div>
`));
}
function calculateOpenRouterCost() {
if (oai_settings.chat_completion_source !== chat_completion_sources.OPENROUTER) {
return;
@ -1319,7 +1345,7 @@ function calculateOpenRouterCost() {
}
function saveModelList(data) {
model_list = data.map((model) => ({ id: model.id, context_length: model.context_length, pricing: model.pricing, architecture: model.architecture }));
model_list = data.map((model) => ({ ...model }));
model_list.sort((a, b) => a?.id && b?.id && a.id.localeCompare(b.id));
if (oai_settings.chat_completion_source == chat_completion_sources.OPENROUTER) {
@ -1374,16 +1400,10 @@ function appendOpenRouterOptions(model_list, groupModels = false, sort = false)
$('#model_openrouter_select').append($('<option>', { value: openrouter_website_model, text: 'Use OpenRouter website setting' }));
const appendOption = (model, parent = null) => {
let tokens_dollar = Number(1 / (1000 * model.pricing?.prompt));
let tokens_rounded = (Math.round(tokens_dollar * 1000) / 1000).toFixed(0);
const price = 0 === Number(model.pricing?.prompt) ? 'Free' : `${tokens_rounded}k t/$ `;
let model_description = `${model.id} | ${price} | ${model.context_length} ctx`;
(parent || $('#model_openrouter_select')).append(
$('<option>', {
value: model.id,
text: model_description,
text: model.name,
}));
};
@ -1412,7 +1432,7 @@ const openRouterSortBy = (data, property = 'alphabetically') => {
return parseFloat(a.pricing.prompt) - parseFloat(b.pricing.prompt);
} else {
// Alphabetically
return a?.id && b?.id && a.id.localeCompare(b.id);
return a?.name && b?.name && a.name.localeCompare(b.name);
}
});
};
@ -1556,9 +1576,10 @@ async function sendOpenAIRequest(type, messages, signal) {
delete generate_data.stop;
}
// Vision models don't support logit bias
if (isImageInliningSupported()) {
// Remove logit bias and stop strings if it's not supported by the model
if (isOAI && oai_settings.openai_model.includes('vision') || isOpenRouter && oai_settings.openrouter_model.includes('vision')) {
delete generate_data.logit_bias;
delete generate_data.stop;
}
// Proxy is only supported for Claude and OpenAI
@ -1572,6 +1593,7 @@ async function sendOpenAIRequest(type, messages, signal) {
generate_data['top_k'] = Number(oai_settings.top_k_openai);
generate_data['exclude_assistant'] = oai_settings.exclude_assistant;
generate_data['claude_use_sysprompt'] = oai_settings.claude_use_sysprompt;
generate_data['claude_exclude_prefixes'] = oai_settings.claude_exclude_prefixes;
generate_data['stop'] = getCustomStoppingStrings(); // Claude shouldn't have limits on stop strings.
generate_data['human_sysprompt_message'] = substituteParams(oai_settings.human_sysprompt_message);
// Don't add a prefill on quiet gens (summarization)
@ -1582,6 +1604,8 @@ async function sendOpenAIRequest(type, messages, signal) {
if (isOpenRouter) {
generate_data['top_k'] = Number(oai_settings.top_k_openai);
generate_data['min_p'] = Number(oai_settings.min_p_openai);
generate_data['top_a'] = Number(oai_settings.top_a_openai);
generate_data['use_fallback'] = oai_settings.openrouter_use_fallback;
if (isTextCompletion) {
@ -1607,7 +1631,7 @@ async function sendOpenAIRequest(type, messages, signal) {
}
if (isMistral) {
generate_data['safe_mode'] = false; // already defaults to false, but just incase they change that in the future.
generate_data['safe_prompt'] = false; // already defaults to false, but just incase they change that in the future.
}
if (isCustom) {
@ -2343,6 +2367,8 @@ function loadOpenAISettings(data, settings) {
oai_settings.count_pen = settings.count_pen ?? default_settings.count_pen;
oai_settings.top_p_openai = settings.top_p_openai ?? default_settings.top_p_openai;
oai_settings.top_k_openai = settings.top_k_openai ?? default_settings.top_k_openai;
oai_settings.top_a_openai = settings.top_a_openai ?? default_settings.top_a_openai;
oai_settings.min_p_openai = settings.min_p_openai ?? default_settings.min_p_openai;
oai_settings.stream_openai = settings.stream_openai ?? default_settings.stream_openai;
oai_settings.openai_max_context = settings.openai_max_context ?? default_settings.openai_max_context;
oai_settings.openai_max_tokens = settings.openai_max_tokens ?? default_settings.openai_max_tokens;
@ -2395,6 +2421,7 @@ function loadOpenAISettings(data, settings) {
if (settings.use_google_tokenizer !== undefined) oai_settings.use_google_tokenizer = !!settings.use_google_tokenizer;
if (settings.exclude_assistant !== undefined) oai_settings.exclude_assistant = !!settings.exclude_assistant;
if (settings.claude_use_sysprompt !== undefined) oai_settings.claude_use_sysprompt = !!settings.claude_use_sysprompt;
if (settings.claude_exclude_prefixes !== undefined) oai_settings.claude_exclude_prefixes = !!settings.claude_exclude_prefixes;
if (settings.use_alt_scale !== undefined) { oai_settings.use_alt_scale = !!settings.use_alt_scale; updateScaleForm(); }
$('#stream_toggle').prop('checked', oai_settings.stream_openai);
$('#api_url_scale').val(oai_settings.api_url_scale);
@ -2434,6 +2461,7 @@ function loadOpenAISettings(data, settings) {
$('#use_google_tokenizer').prop('checked', oai_settings.use_google_tokenizer);
$('#exclude_assistant').prop('checked', oai_settings.exclude_assistant);
$('#claude_use_sysprompt').prop('checked', oai_settings.claude_use_sysprompt);
$('#claude_exclude_prefixes').prop('checked', oai_settings.claude_exclude_prefixes);
$('#scale-alt').prop('checked', oai_settings.use_alt_scale);
$('#openrouter_use_fallback').prop('checked', oai_settings.openrouter_use_fallback);
$('#openrouter_force_instruct').prop('checked', oai_settings.openrouter_force_instruct);
@ -2472,6 +2500,10 @@ function loadOpenAISettings(data, settings) {
$('#top_k_openai').val(oai_settings.top_k_openai);
$('#top_k_counter_openai').val(Number(oai_settings.top_k_openai).toFixed(0));
$('#top_a_openai').val(oai_settings.top_a_openai);
$('#top_a_counter_openai').val(Number(oai_settings.top_a_openai));
$('#min_p_openai').val(oai_settings.min_p_openai);
$('#min_p_counter_openai').val(Number(oai_settings.min_p_openai));
$('#seed_openai').val(oai_settings.seed);
if (settings.reverse_proxy !== undefined) oai_settings.reverse_proxy = settings.reverse_proxy;
@ -2616,6 +2648,8 @@ async function saveOpenAIPreset(name, settings, triggerUi = true) {
count_penalty: settings.count_pen,
top_p: settings.top_p_openai,
top_k: settings.top_k_openai,
top_a: settings.top_a_openai,
min_p: settings.min_p_openai,
openai_max_context: settings.openai_max_context,
openai_max_tokens: settings.openai_max_tokens,
wrap_in_quotes: settings.wrap_in_quotes,
@ -2647,6 +2681,7 @@ async function saveOpenAIPreset(name, settings, triggerUi = true) {
use_google_tokenizer: settings.use_google_tokenizer,
exclude_assistant: settings.exclude_assistant,
claude_use_sysprompt: settings.claude_use_sysprompt,
claude_exclude_prefixes: settings.claude_exclude_prefixes,
use_alt_scale: settings.use_alt_scale,
squash_system_messages: settings.squash_system_messages,
image_inlining: settings.image_inlining,
@ -2974,6 +3009,8 @@ function onSettingsPresetChange() {
count_penalty: ['#count_pen', 'count_pen', false],
top_p: ['#top_p_openai', 'top_p_openai', false],
top_k: ['#top_k_openai', 'top_k_openai', false],
top_a: ['#top_a_openai', 'top_a_openai', false],
min_p: ['#min_p_openai', 'min_p_openai', false],
max_context_unlocked: ['#oai_max_context_unlocked', 'max_context_unlocked', true],
openai_model: ['#model_openai_select', 'openai_model', false],
claude_model: ['#model_claude_select', 'claude_model', false],
@ -3019,6 +3056,7 @@ function onSettingsPresetChange() {
use_google_tokenizer: ['#use_google_tokenizer', 'use_google_tokenizer', true],
exclude_assistant: ['#exclude_assistant', 'exclude_assistant', true],
claude_use_sysprompt: ['#claude_use_sysprompt', 'claude_use_sysprompt', true],
claude_exclude_prefixes: ['#claude_exclude_prefixes', 'claude_exclude_prefixes', true],
use_alt_scale: ['#use_alt_scale', 'use_alt_scale', true],
squash_system_messages: ['#squash_system_messages', 'squash_system_messages', true],
image_inlining: ['#openai_image_inlining', 'image_inlining', true],
@ -3661,50 +3699,62 @@ $(document).ready(async function () {
updateScaleForm();
});
$(document).on('input', '#temp_openai', function () {
$('#temp_openai').on('input', function () {
oai_settings.temp_openai = Number($(this).val());
$('#temp_counter_openai').val(Number($(this).val()).toFixed(2));
saveSettingsDebounced();
});
$(document).on('input', '#freq_pen_openai', function () {
$('#freq_pen_openai').on('input', function () {
oai_settings.freq_pen_openai = Number($(this).val());
$('#freq_pen_counter_openai').val(Number($(this).val()).toFixed(2));
saveSettingsDebounced();
});
$(document).on('input', '#pres_pen_openai', function () {
$('#pres_pen_openai').on('input', function () {
oai_settings.pres_pen_openai = Number($(this).val());
$('#pres_pen_counter_openai').val(Number($(this).val()).toFixed(2));
saveSettingsDebounced();
});
$(document).on('input', '#count_pen', function () {
$('#count_pen').on('input', function () {
oai_settings.count_pen = Number($(this).val());
$('#count_pen_counter').val(Number($(this).val()).toFixed(2));
saveSettingsDebounced();
});
$(document).on('input', '#top_p_openai', function () {
$('#top_p_openai').on('input', function () {
oai_settings.top_p_openai = Number($(this).val());
$('#top_p_counter_openai').val(Number($(this).val()).toFixed(2));
saveSettingsDebounced();
});
$(document).on('input', '#top_k_openai', function () {
$('#top_k_openai').on('input', function () {
oai_settings.top_k_openai = Number($(this).val());
$('#top_k_counter_openai').val(Number($(this).val()).toFixed(0));
saveSettingsDebounced();
});
$(document).on('input', '#openai_max_context', function () {
$('#top_a_openai').on('input', function () {
oai_settings.top_a_openai = Number($(this).val());
$('#top_a_counter_openai').val(Number($(this).val()));
saveSettingsDebounced();
});
$('#min_p_openai').on('input', function () {
oai_settings.min_p_openai = Number($(this).val());
$('#min_p_counter_openai').val(Number($(this).val()));
saveSettingsDebounced();
});
$('#openai_max_context').on('input', function () {
oai_settings.openai_max_context = Number($(this).val());
$('#openai_max_context_counter').val(`${$(this).val()}`);
calculateOpenRouterCost();
saveSettingsDebounced();
});
$(document).on('input', '#openai_max_tokens', function () {
$('#openai_max_tokens').on('input', function () {
oai_settings.openai_max_tokens = Number($(this).val());
calculateOpenRouterCost();
saveSettingsDebounced();
@ -3746,6 +3796,11 @@ $(document).ready(async function () {
saveSettingsDebounced();
});
$('#claude_exclude_prefixes').on('change', function () {
oai_settings.claude_exclude_prefixes = !!$('#claude_exclude_prefixes').prop('checked');
saveSettingsDebounced();
});
$('#names_in_completion').on('change', function () {
oai_settings.names_in_completion = !!$('#names_in_completion').prop('checked');
saveSettingsDebounced();
@ -3971,6 +4026,16 @@ $(document).ready(async function () {
resetScrollHeight($(this));
});
if (!isMobile()) {
$('#model_openrouter_select').select2({
placeholder: 'Select a model',
searchInputPlaceholder: 'Search models...',
searchInputCssClass: 'text_pole',
width: '100%',
templateResult: getOpenRouterModelTemplate,
});
}
$('#api_button_openai').on('click', onConnectButtonClick);
$('#openai_reverse_proxy').on('input', onReverseProxyInput);
$('#model_openai_select').on('change', onModelChange);

View File

@ -235,6 +235,8 @@ let power_user = {
restore_user_input: true,
reduced_motion: false,
compact_input_area: true,
auto_connect: false,
auto_load_chat: false,
};
let themes = [];
@ -277,6 +279,8 @@ const storage_keys = {
enableLabMode: 'enableLabMode',
reduced_motion: 'reduced_motion',
compact_input_area: 'compact_input_area',
auto_connect_legacy: 'AutoConnectEnabled',
auto_load_chat_legacy: 'AutoLoadChatEnabled',
};
const contextControls = [
@ -520,7 +524,7 @@ async function switchZenSliders() {
$('#clickSlidersTips').hide();
$('#pro-settings-block input[type=\'number\']').hide();
//hide number inputs that are not 'seed' inputs
$(`#textgenerationwebui_api-settings :input[type='number']:not([id^='seed']),
$(`#textgenerationwebui_api-settings :input[type='number']:not([id^='seed']):not([id^='n_']),
#kobold_api-settings :input[type='number']:not([id^='seed'])`).hide();
//hide original sliders
$(`#textgenerationwebui_api-settings input[type='range'],
@ -604,6 +608,10 @@ async function CreateZenSliders(elmnt) {
sliderID == 'rep_pen_range') {
decimals = 0;
}
if (sliderID == 'min_temp_textgenerationwebui' ||
sliderID == 'max_temp_textgenerationwebui') {
decimals = 2;
}
if (sliderID == 'eta_cutoff_textgenerationwebui' ||
sliderID == 'epsilon_cutoff_textgenerationwebui') {
numSteps = 50;
@ -633,7 +641,9 @@ async function CreateZenSliders(elmnt) {
}
if (sliderID == 'mirostat_eta_textgenerationwebui' ||
sliderID == 'penalty_alpha_textgenerationwebui' ||
sliderID == 'length_penalty_textgenerationwebui') {
sliderID == 'length_penalty_textgenerationwebui' ||
sliderID == 'min_temp_textgenerationwebui' ||
sliderID == 'max_temp_textgenerationwebui') {
numSteps = 50;
}
//customize off values
@ -1377,6 +1387,19 @@ function loadPowerUserSettings(settings, data) {
const expandMessageActions = localStorage.getItem(storage_keys.expand_message_actions);
const enableZenSliders = localStorage.getItem(storage_keys.enableZenSliders);
const enableLabMode = localStorage.getItem(storage_keys.enableLabMode);
const autoLoadChat = localStorage.getItem(storage_keys.auto_load_chat_legacy);
const autoConnect = localStorage.getItem(storage_keys.auto_connect_legacy);
if (autoLoadChat) {
power_user.auto_load_chat = autoLoadChat === 'true';
localStorage.removeItem(storage_keys.auto_load_chat_legacy);
}
if (autoConnect) {
power_user.auto_connect = autoConnect === 'true';
localStorage.removeItem(storage_keys.auto_connect_legacy);
}
power_user.fast_ui_mode = fastUi === null ? true : fastUi == 'true';
power_user.movingUI = movingUI === null ? false : movingUI == 'true';
power_user.noShadows = noShadows === null ? false : noShadows == 'true';
@ -1504,6 +1527,8 @@ function loadPowerUserSettings(settings, data) {
$('#border-color-picker').attr('color', power_user.border_color);
$('#ui_mode_select').val(power_user.ui_mode).find(`option[value="${power_user.ui_mode}"]`).attr('selected', true);
$('#reduced_motion').prop('checked', power_user.reduced_motion);
$('#auto-connect-checkbox').prop('checked', power_user.auto_connect);
$('#auto-load-chat-checkbox').prop('checked', power_user.auto_load_chat);
for (const theme of themes) {
const option = document.createElement('option');
@ -1848,8 +1873,8 @@ export function renderStoryString(params) {
// substitute {{macro}} params that are not defined in the story string
output = substituteParams(output, params.user, params.char);
// remove leading whitespace
output = output.trimStart();
// remove leading newlines
output = output.replace(/^\n+/, '');
// add a newline to the end of the story string if it doesn't have one
if (output.length > 0 && !output.endsWith('\n')) {
@ -3199,6 +3224,16 @@ $(document).ready(() => {
saveSettingsDebounced();
});
$('#auto-connect-checkbox').on('input', function () {
power_user.auto_connect = !!$(this).prop('checked');
saveSettingsDebounced();
});
$('#auto-load-chat-checkbox').on('input', function () {
power_user.auto_load_chat = !!$(this).prop('checked');
saveSettingsDebounced();
});
$(document).on('click', '#debug_table [data-debug-function]', function () {
const functionId = $(this).data('debug-function');
const functionRecord = debug_functions.find(f => f.functionId === functionId);

View File

@ -311,6 +311,8 @@ class PresetManager {
'ollama_model',
'server_urls',
'type',
'custom_model',
'bypass_status_check',
];
const settings = Object.assign({}, getSettingsByApiId(this.apiId));

View File

@ -37,7 +37,7 @@ import { getMessageTimeStamp } from './RossAscends-mods.js';
import { hideChatMessage, unhideChatMessage } from './chats.js';
import { getContext, saveMetadataDebounced } from './extensions.js';
import { getRegexedString, regex_placement } from './extensions/regex/engine.js';
import { findGroupMemberId, groups, is_group_generating, resetSelectedGroup, saveGroupChat, selected_group } from './group-chats.js';
import { findGroupMemberId, groups, is_group_generating, openGroupById, resetSelectedGroup, saveGroupChat, selected_group } from './group-chats.js';
import { autoSelectPersona } from './personas.js';
import { addEphemeralStoppingString, chat_styles, flushEphemeralStoppingStrings, power_user } from './power-user.js';
import { decodeTextTokens, getFriendlyTokenizerName, getTextTokens, getTokenCount } from './tokenizers.js';
@ -149,7 +149,7 @@ parser.addCommand('single', setStoryModeCallback, ['story'], ' sets the mess
parser.addCommand('bubble', setBubbleModeCallback, ['bubbles'], ' sets the message style to bubble chat mode', true, true);
parser.addCommand('flat', setFlatModeCallback, ['default'], ' sets the message style to flat chat mode', true, true);
parser.addCommand('continue', continueChatCallback, ['cont'], ' continues the last message in the chat', true, true);
parser.addCommand('go', goToCharacterCallback, ['char'], '<span class="monospace">(name)</span> opens up a chat with the character by its name', true, true);
parser.addCommand('go', goToCharacterCallback, ['char'], '<span class="monospace">(name)</span> opens up a chat with the character or group by its name', true, true);
parser.addCommand('sysgen', generateSystemMessage, [], '<span class="monospace">(prompt)</span> generates a system message using a specified prompt', true, true);
parser.addCommand('ask', askCharacter, [], '<span class="monospace">(prompt)</span> asks a specified character card a prompt', true, true);
parser.addCommand('delname', deleteMessagesByNameCallback, ['cancel'], '<span class="monospace">(name)</span> deletes all messages attributed to a specified name', true, true);
@ -167,7 +167,7 @@ parser.addCommand('peek', peekCallback, [], '<span class="monospace">(message in
parser.addCommand('delswipe', deleteSwipeCallback, ['swipedel'], '<span class="monospace">(optional 1-based id)</span> deletes a swipe from the last chat message. If swipe id not provided - deletes the current swipe.', true, true);
parser.addCommand('echo', echoCallback, [], '<span class="monospace">(title=string severity=info/warning/error/success [text])</span> echoes the text to toast message. Useful for pipes debugging.', true, true);
//parser.addCommand('#', (_, value) => '', [], ' a comment, does nothing, e.g. <tt>/# the next three commands switch variables a and b</tt>', true, true);
parser.addCommand('gen', generateCallback, [], '<span class="monospace">(lock=on/off [prompt])</span> generates text using the provided prompt and passes it to the next command through the pipe, optionally locking user input while generating.', true, true);
parser.addCommand('gen', generateCallback, [], '<span class="monospace">(lock=on/off name="System" [prompt])</span> generates text using the provided prompt and passes it to the next command through the pipe, optionally locking user input while generating and allowing to configure the in-prompt name for instruct mode (default = "System").', true, true);
parser.addCommand('genraw', generateRawCallback, [], '<span class="monospace">(lock=on/off [prompt])</span> generates text using the provided prompt and passes it to the next command through the pipe, optionally locking user input while generating. Does not include chat history or character card. Use instruct=off to skip instruct formatting, e.g. <tt>/genraw instruct=off Why is the sky blue?</tt>. Use stop=... with a JSON-serialized array to add one-time custom stop strings, e.g. <tt>/genraw stop=["\\n"] Say hi</tt>', true, true);
parser.addCommand('addswipe', addSwipeCallback, ['swipeadd'], '<span class="monospace">(text)</span> adds a swipe to the last chat message.', true, true);
parser.addCommand('abort', abortCallback, [], ' aborts the slash command batch execution', true, true);
@ -175,7 +175,7 @@ parser.addCommand('fuzzy', fuzzyCallback, [], 'list=["a","b","c"] (search value)
parser.addCommand('pass', (_, arg) => arg, ['return'], '<span class="monospace">(text)</span> passes the text to the next command through the pipe.', true, true);
parser.addCommand('delay', delayCallback, ['wait', 'sleep'], '<span class="monospace">(milliseconds)</span> delays the next command in the pipe by the specified number of milliseconds.', true, true);
parser.addCommand('input', inputCallback, ['prompt'], '<span class="monospace">(default="string" large=on/off wide=on/off okButton="string" rows=number [text])</span> Shows a popup with the provided text and an input field. The default argument is the default value of the input field, and the text argument is the text to display.', true, true);
parser.addCommand('run', runCallback, ['call', 'exec'], '<span class="monospace">(QR label)</span> runs a Quick Reply with the specified name from the current preset.', true, true);
parser.addCommand('run', runCallback, ['call', 'exec'], '<span class="monospace">[key1=value key2=value ...] ([qrSet.]qrLabel)</span> runs a Quick Reply with the specified name from a currently active preset or from another preset, named arguments can be referenced in a QR with {{arg::key}}.', true, true);
parser.addCommand('messages', getMessagesCallback, ['message'], '<span class="monospace">(names=off/on [message index or range])</span> returns the specified message or range of messages as a string.', true, true);
parser.addCommand('setinput', setInputCallback, [], '<span class="monospace">(text)</span> sets the user input to the specified text and passes it to the next command through the pipe.', true, true);
parser.addCommand('popup', popupCallback, [], '<span class="monospace">(large=on/off wide=on/off okButton="string" text)</span> shows a blocking popup with the specified text and buttons. Returns the input value into the pipe or empty string if canceled.', true, true);
@ -445,7 +445,7 @@ function getMessagesCallback(args, value) {
return messages.join('\n\n');
}
async function runCallback(_, name) {
async function runCallback(args, name) {
if (!name) {
toastr.warning('No name provided for /run command');
return '';
@ -458,7 +458,7 @@ async function runCallback(_, name) {
try {
name = name.trim();
return await window['executeQuickReplyByName'](name);
return await window['executeQuickReplyByName'](name, args);
} catch (error) {
toastr.error(`Error running Quick Reply "${name}": ${error.message}`, 'Error');
return '';
@ -587,7 +587,8 @@ async function generateCallback(args, value) {
}
setEphemeralStopStrings(resolveVariable(args?.stop));
const result = await generateQuietPrompt(value, false, false, '');
const name = args?.name;
const result = await generateQuietPrompt(value, false, false, '', name);
return result;
} finally {
if (lock) {
@ -1135,8 +1136,14 @@ async function goToCharacterCallback(_, name) {
await openChat(new String(characterIndex));
return characters[characterIndex]?.name;
} else {
console.warn(`No matches found for name "${name}"`);
return '';
const group = groups.find(it => it.name.toLowerCase() == name.toLowerCase());
if (group) {
await openGroupById(group.id);
return group.name;
} else {
console.warn(`No matches found for name "${name}"`);
return '';
}
}
}

View File

@ -13,6 +13,7 @@
<li><tt>&lcub;&lcub;scenario&rcub;&rcub;</tt> the Character's Scenario</li>
<li><tt>&lcub;&lcub;persona&rcub;&rcub;</tt> your current Persona Description</li>
<li><tt>&lcub;&lcub;mesExamples&rcub;&rcub;</tt> the Character's Dialogue Examples</li>
<li><tt>&lcub;&lcub;mesExamplesRaw&rcub;&rcub;</tt> unformatted Dialogue Examples <b>(only for Story String)</b></li>
<li><tt>&lcub;&lcub;user&rcub;&rcub;</tt> your current Persona username</li>
<li><tt>&lcub;&lcub;char&rcub;&rcub;</tt> the Character's name</li>
<li><tt>&lcub;&lcub;lastMessage&rcub;&rcub;</tt> - the text of the latest chat message.</li>

View File

@ -36,7 +36,7 @@
<h3>Still have questions?</h3>
<ul>
<li>
<a target="_blank" href="https://discord.gg/RZdyAEUPvj">
<a target="_blank" href="https://discord.gg/sillytavern">
Join the SillyTavern Discord
</a>
</li>

View File

@ -15,7 +15,7 @@ import {
registerDebugFunction,
} from './power-user.js';
import EventSourceStream from './sse-stream.js';
import { SENTENCEPIECE_TOKENIZERS, getTextTokens, tokenizers } from './tokenizers.js';
import { SENTENCEPIECE_TOKENIZERS, TEXTGEN_TOKENIZERS, getTextTokens, tokenizers } from './tokenizers.js';
import { getSortableDelay, onlyUnique } from './utils.js';
export {
@ -47,7 +47,7 @@ let MANCER_SERVER = localStorage.getItem(MANCER_SERVER_KEY) ?? MANCER_SERVER_DEF
let TOGETHERAI_SERVER = 'https://api.together.xyz';
const SERVER_INPUTS = {
[textgen_types.OOBA]: '#textgenerationwebui_api_url_text',
[textgen_types.OOBA]: '#textgenerationwebui_api_url_text',
[textgen_types.APHRODITE]: '#aphrodite_api_url_text',
[textgen_types.TABBY]: '#tabby_api_url_text',
[textgen_types.KOBOLDCPP]: '#koboldcpp_api_url_text',
@ -79,6 +79,9 @@ const settings = {
presence_pen: 0,
do_sample: true,
early_stopping: false,
dynatemp: false,
min_temp: 0,
max_temp: 2.0,
seed: -1,
preset: 'Default',
add_bos_token: true,
@ -110,6 +113,8 @@ const settings = {
logit_bias: [],
n: 1,
server_urls: {},
custom_model: '',
bypass_status_check: false,
};
export let textgenerationwebui_banned_in_macros = [];
@ -135,6 +140,9 @@ const setting_names = [
'num_beams',
'length_penalty',
'min_length',
'dynatemp',
'min_temp',
'max_temp',
'encoder_rep_pen',
'freq_pen',
'presence_pen',
@ -163,6 +171,8 @@ const setting_names = [
'sampler_order',
'n',
'logit_bias',
'custom_model',
'bypass_status_check',
];
export function validateTextGenUrl() {
@ -241,6 +251,18 @@ function convertPresets(presets) {
return Array.isArray(presets) ? presets.map((p) => JSON.parse(p)) : [];
}
function getTokenizerForTokenIds() {
if (power_user.tokenizer === tokenizers.API_CURRENT && TEXTGEN_TOKENIZERS.includes(settings.type)) {
return tokenizers.API_CURRENT;
}
if (SENTENCEPIECE_TOKENIZERS.includes(power_user.tokenizer)) {
return power_user.tokenizer;
}
return tokenizers.LLAMA;
}
/**
* @returns {string} String with comma-separated banned token IDs
*/
@ -249,7 +271,7 @@ function getCustomTokenBans() {
return '';
}
const tokenizer = SENTENCEPIECE_TOKENIZERS.includes(power_user.tokenizer) ? power_user.tokenizer : tokenizers.LLAMA;
const tokenizer = getTokenizerForTokenIds();
const result = [];
const sequences = settings.banned_tokens
.split('\n')
@ -301,7 +323,7 @@ function calculateLogitBias() {
return {};
}
const tokenizer = SENTENCEPIECE_TOKENIZERS.includes(power_user.tokenizer) ? power_user.tokenizer : tokenizers.LLAMA;
const tokenizer = getTokenizerForTokenIds();
const result = {};
/**
@ -374,12 +396,14 @@ function loadTextGenSettings(data, loadedSettings) {
displayLogitBias(settings.logit_bias, BIAS_KEY);
//this is needed because showTypeSpecificControls() does not handle NOT declarations
if (settings.type === textgen_types.APHRODITE) {
$('[data-forAphro=False]').each(function () {
$('[data-forAphro="False"]').each(function () {
$(this).hide();
});
} else {
$('[data-forAphro=False]').each(function () {
$(this).show();
$('[data-forAphro="False"]').each(function () {
if ($(this).css('display') !== 'none') { //if it wasn't already hidden by showTypeSpecificControls
$(this).show();
}
});
}
@ -434,7 +458,7 @@ jQuery(function () {
if (settings.type === textgen_types.APHRODITE) {
//this is needed because showTypeSpecificControls() does not handle NOT declarations
$('[data-forAphro=False]').each(function () {
$('[data-forAphro="False"]').each(function () {
$(this).hide();
});
$('#mirostat_mode_textgenerationwebui').attr('step', 2); //Aphro disallows mode 1
@ -448,7 +472,7 @@ jQuery(function () {
}
} else {
//this is needed because showTypeSpecificControls() does not handle NOT declarations
$('[data-forAphro=False]').each(function () {
$('[data-forAphro="False"]').each(function () {
$(this).show();
});
$('#mirostat_mode_textgenerationwebui').attr('step', 1);
@ -478,6 +502,63 @@ jQuery(function () {
selectPreset(presetName);
});
$('#samplerResetButton').off('click').on('click', function () {
const inputs = {
'temp_textgenerationwebui': 1,
'top_k_textgenerationwebui': 0,
'top_p_textgenerationwebui': 1,
'min_p_textgenerationwebui': 0,
'rep_pen_textgenerationwebui': 1,
'rep_pen_range_textgenerationwebui': 0,
'dynatemp_textgenerationwebui': false,
'seed_textgenerationwebui': 1,
'ban_eos_token_textgenerationwebui': false,
'do_sample_textgenerationwebui': true,
'add_bos_token_textgenerationwebui': true,
'temperature_last_textgenerationwebui': true,
'skip_special_tokens_textgenerationwebui': true,
'top_a_textgenerationwebui': 0,
'top_a_counter_textgenerationwebui': 0,
'mirostat_mode_textgenerationwebui': 0,
'mirostat_tau_textgenerationwebui': 5,
'mirostat_eta_textgenerationwebui': 0.1,
'tfs_textgenerationwebui': 1,
'epsilon_cutoff_textgenerationwebui': 0,
'eta_cutoff_textgenerationwebui': 0,
'encoder_rep_pen_textgenerationwebui': 1,
'freq_pen_textgenerationwebui': 0,
'presence_pen_textgenerationwebui': 0,
'no_repeat_ngram_size_textgenerationwebui': 0,
'min_length_textgenerationwebui': 0,
'num_beams_textgenerationwebui': 1,
'length_penalty_textgenerationwebui': 0,
'penalty_alpha_textgenerationwebui': 0,
'typical_p_textgenerationwebui': 1, // Added entry
'guidance_scale_textgenerationwebui': 1,
};
for (const [id, value] of Object.entries(inputs)) {
const inputElement = $(`#${id}`);
if (inputElement.prop('type') === 'checkbox') {
inputElement.prop('checked', value);
} else if (inputElement.prop('type') === 'number') {
inputElement.val(value).trigger('input');
} else {
inputElement.val(value).trigger('input');
if (power_user.enableZenSliders) {
let masterElementID = inputElement.prop('id');
console.log(masterElementID)
let zenSlider = $(`#${masterElementID}_zenslider`).slider();
zenSlider.slider('option', 'value', value);
zenSlider.slider('option', 'slide')
.call(zenSlider, null, {
handle: $('.ui-slider-handle', zenSlider), value: value,
});
}
}
}
});
for (const i of setting_names) {
$(`#${i}_textgenerationwebui`).attr('x-setting-id', i);
$(document).on('input', `#${i}_textgenerationwebui`, function () {
@ -653,6 +734,10 @@ function toIntArray(string) {
}
function getModel() {
if (settings.type === OOBA && settings.custom_model) {
return settings.custom_model;
}
if (settings.type === MANCER) {
return settings.mancer_model;
}
@ -684,7 +769,7 @@ export function getTextGenGenerationData(finalPrompt, maxTokens, isImpersonate,
'model': getModel(),
'max_new_tokens': maxTokens,
'max_tokens': maxTokens,
'temperature': settings.temp,
'temperature': settings.dynatemp ? (settings.min_temp + settings.max_temp) / 2 : settings.temp,
'top_p': settings.top_p,
'typical_p': settings.typical_p,
'min_p': settings.min_p,
@ -692,12 +777,16 @@ export function getTextGenGenerationData(finalPrompt, maxTokens, isImpersonate,
'frequency_penalty': settings.freq_pen,
'presence_penalty': settings.presence_pen,
'top_k': settings.top_k,
'min_length': settings.min_length,
'min_length': settings.type === OOBA ? settings.min_length : undefined,
'min_tokens': settings.min_length,
'num_beams': settings.num_beams,
'num_beams': settings.type === OOBA ? settings.num_beams : undefined,
'length_penalty': settings.length_penalty,
'early_stopping': settings.early_stopping,
'add_bos_token': settings.add_bos_token,
'dynamic_temperature': settings.dynatemp,
'dynatemp_low': settings.min_temp,
'dynatemp_high': settings.max_temp,
'dynatemp_range': settings.dynatemp ? (settings.max_temp - settings.min_temp) / 2 : 0,
'stopping_strings': getStoppingStrings(isImpersonate, isContinue),
'stop': getStoppingStrings(isImpersonate, isContinue),
'truncation_length': max_context,
@ -705,8 +794,8 @@ export function getTextGenGenerationData(finalPrompt, maxTokens, isImpersonate,
'skip_special_tokens': settings.skip_special_tokens,
'top_a': settings.top_a,
'tfs': settings.tfs,
'epsilon_cutoff': settings.epsilon_cutoff,
'eta_cutoff': settings.eta_cutoff,
'epsilon_cutoff': settings.type === OOBA ? settings.epsilon_cutoff : undefined,
'eta_cutoff': settings.type === OOBA ? settings.eta_cutoff : undefined,
'mirostat_mode': settings.mirostat_mode,
'mirostat_tau': settings.mirostat_tau,
'mirostat_eta': settings.mirostat_eta,
@ -719,12 +808,14 @@ export function getTextGenGenerationData(finalPrompt, maxTokens, isImpersonate,
'sampler_order': settings.type === textgen_types.KOBOLDCPP ? settings.sampler_order : undefined,
};
const nonAphroditeParams = {
'rep_pen': settings.rep_pen,
'rep_pen_range': settings.rep_pen_range,
'repetition_penalty_range': settings.rep_pen_range,
'encoder_repetition_penalty': settings.encoder_rep_pen,
'no_repeat_ngram_size': settings.no_repeat_ngram_size,
'penalty_alpha': settings.penalty_alpha,
'temperature_last': settings.temperature_last,
'do_sample': settings.do_sample,
'encoder_repetition_penalty': settings.type === OOBA ? settings.encoder_rep_pen : undefined,
'no_repeat_ngram_size': settings.type === OOBA ? settings.no_repeat_ngram_size : undefined,
'penalty_alpha': settings.type === OOBA ? settings.penalty_alpha : undefined,
'temperature_last': (settings.type === OOBA || settings.type === APHRODITE) ? settings.temperature_last : undefined,
'do_sample': settings.type === OOBA ? settings.do_sample : undefined,
'seed': settings.seed,
'guidance_scale': cfgValues?.guidanceScale?.value ?? settings.guidance_scale ?? 1,
'negative_prompt': cfgValues?.negativePrompt ?? substituteParams(settings.negative_prompt) ?? '',
@ -733,7 +824,7 @@ export function getTextGenGenerationData(finalPrompt, maxTokens, isImpersonate,
'repeat_penalty': settings.rep_pen,
'tfs_z': settings.tfs,
'repeat_last_n': settings.rep_pen_range,
'n_predict': settings.maxTokens,
'n_predict': maxTokens,
'mirostat': settings.mirostat_mode,
'ignore_eos': settings.ban_eos_token,
};
@ -769,6 +860,7 @@ export function getTextGenGenerationData(finalPrompt, maxTokens, isImpersonate,
'logit_bias': logitBiasArray,
// Conflicts with ooba's grammar_string
'grammar': settings.grammar_string,
'cache_prompt': true,
};
params = Object.assign(params, llamaCppParams);
}

View File

@ -35,6 +35,8 @@ export const SENTENCEPIECE_TOKENIZERS = [
//tokenizers.NERD2,
];
export const TEXTGEN_TOKENIZERS = [OOBA, TABBY, KOBOLDCPP, LLAMACPP];
const TOKENIZER_URLS = {
[tokenizers.GPT2]: {
encode: '/api/tokenizers/gpt2/encode',
@ -190,7 +192,7 @@ export function getTokenizerBestMatch(forApi) {
// - Tokenizer haven't reported an error previously
const hasTokenizerError = sessionStorage.getItem(TOKENIZER_WARNING_KEY);
const isConnected = online_status !== 'no_connection';
const isTokenizerSupported = [OOBA, TABBY, KOBOLDCPP, LLAMACPP].includes(textgen_settings.type);
const isTokenizerSupported = TEXTGEN_TOKENIZERS.includes(textgen_settings.type);
if (!hasTokenizerError && isConnected) {
if (forApi === 'kobold' && kai_flags.can_use_tokenization) {

View File

@ -39,6 +39,7 @@ const world_info_logic = {
AND_ANY: 0,
NOT_ALL: 1,
NOT_ANY: 2,
AND_ALL: 3,
};
let world_info = {};
@ -359,6 +360,8 @@ function registerWorldInfoSlashCommands() {
return '';
}
value = value.replace(/\\([{}|])/g, '$1');
const data = await loadWorldInfoData(file);
if (!data || !('entries' in data)) {
@ -555,6 +558,7 @@ function displayWorldEntries(name, data, navigation = navigation_option.none) {
$('#world_popup_name_button').off('click').on('click', nullWorldInfo);
$('#world_popup_export').off('click').on('click', nullWorldInfo);
$('#world_popup_delete').off('click').on('click', nullWorldInfo);
$('#world_duplicate').off('click').on('click', nullWorldInfo);
$('#world_popup_entries_list').hide();
$('#world_info_pagination').html('');
return;
@ -692,6 +696,23 @@ function displayWorldEntries(name, data, navigation = navigation_option.none) {
}
});
$('#world_duplicate').off('click').on('click', async () => {
const tempName = getFreeWorldName();
const finalName = await callPopup('<h3>Create a new World Info?</h3>Enter a name for the new file:', 'input', tempName);
if (finalName) {
await saveWorldInfo(finalName, data, true);
await updateWorldInfoList();
const selectedIndex = world_names.indexOf(finalName);
if (selectedIndex !== -1) {
$('#world_editor_select').val(selectedIndex).trigger('change');
} else {
hideWorldEditor();
}
}
});
$('#world_popup_delete').off('click').on('click', async () => {
const confirmation = await callPopup(`<h3>Delete the World/Lorebook: "${name}"?</h3>This action is irreversible!`, 'confirm');
@ -756,6 +777,7 @@ function displayWorldEntries(name, data, navigation = navigation_option.none) {
const originalDataKeyMap = {
'displayIndex': 'extensions.display_index',
'excludeRecursion': 'extensions.exclude_recursion',
'preventRecursion': 'extensions.prevent_recursion',
'selectiveLogic': 'selectiveLogic',
'comment': 'comment',
'constant': 'constant',
@ -1325,6 +1347,18 @@ function getWorldEntry(name, data, entry) {
});
excludeRecursionInput.prop('checked', entry.excludeRecursion).trigger('input');
// prevent recursion
const preventRecursionInput = template.find('input[name="prevent_recursion"]');
preventRecursionInput.data('uid', entry.uid);
preventRecursionInput.on('input', function () {
const uid = $(this).data('uid');
const value = $(this).prop('checked');
data.entries[uid].preventRecursion = value;
setOriginalDataValue(data, uid, 'extensions.prevent_recursion', data.entries[uid].preventRecursion);
saveWorldInfo(name, data);
});
preventRecursionInput.prop('checked', entry.preventRecursion).trigger('input');
// delete button
const deleteButton = template.find('.delete_entry_button');
deleteButton.data('uid', entry.uid);
@ -1420,6 +1454,7 @@ async function _save(name, data) {
headers: getRequestHeaders(),
body: JSON.stringify({ name: name, data: data }),
});
eventSource.emit(event_types.WORLDINFO_UPDATED, name, data);
}
async function saveWorldInfo(name, data, immediately) {
@ -1788,6 +1823,7 @@ async function checkWorldInfo(chat, maxContext) {
) {
console.debug(`WI UID:${entry.uid} found. Checking logic: ${entry.selectiveLogic}`);
let hasAnyMatch = false;
let hasAllMatch = true;
secondary: for (let keysecondary of entry.keysecondary) {
const secondarySubstituted = substituteParams(keysecondary);
const hasSecondaryMatch = secondarySubstituted && matchKeys(textToScan, secondarySubstituted.trim());
@ -1797,6 +1833,10 @@ async function checkWorldInfo(chat, maxContext) {
hasAnyMatch = true;
}
if (!hasSecondaryMatch) {
hasAllMatch = false;
}
// Simplified AND ANY / NOT ALL if statement. (Proper fix for PR#1356 by Bronya)
// If AND ANY logic and the main checks pass OR if NOT ALL logic and the main checks do not pass
if ((selectiveLogic === world_info_logic.AND_ANY && hasSecondaryMatch) || (selectiveLogic === world_info_logic.NOT_ALL && !hasSecondaryMatch)) {
@ -1816,6 +1856,12 @@ async function checkWorldInfo(chat, maxContext) {
console.debug(`(NOT ANY Check) Activating WI Entry ${entry.uid}, no secondary keywords found.`);
activatedNow.add(entry);
}
// Handle AND ALL logic
if (selectiveLogic === world_info_logic.AND_ALL && hasAllMatch) {
console.debug(`(AND ALL Check) Activating WI Entry ${entry.uid}, all secondary keywords found.`);
activatedNow.add(entry);
}
} else {
// Handle cases where secondary is empty
console.debug(`WI UID ${entry.uid}: Activated without filter logic.`);
@ -1870,9 +1916,15 @@ async function checkWorldInfo(chat, maxContext) {
needsToScan = false;
}
if (newEntries.length === 0) {
console.debug('No new entries activated, stopping');
needsToScan = false;
}
if (needsToScan) {
const text = newEntries
.filter(x => !failedProbabilityChecks.has(x))
.filter(x => !x.preventRecursion)
.map(x => x.content).join('\n');
const currentlyActivatedText = transformString(text);
textToScan = (currentlyActivatedText + '\n' + textToScan);
@ -1970,13 +2022,17 @@ function filterByInclusionGroups(newEntries, allActivatedEntries) {
for (const [key, group] of Object.entries(grouped)) {
console.debug(`Checking inclusion group '${key}' with ${group.length} entries`, group);
if (!Array.isArray(group) || group.length <= 1) {
console.debug('Skipping inclusion group check, only one entry');
if (Array.from(allActivatedEntries).some(x => x.group === key)) {
console.debug(`Skipping inclusion group check, group already activated '${key}'`);
// We need to forcefully deactivate all other entries in the group
for (const entry of group) {
newEntries.splice(newEntries.indexOf(entry), 1);
}
continue;
}
if (Array.from(allActivatedEntries).some(x => x.group === key)) {
console.debug(`Skipping inclusion group check, group already activated '${key}'`);
if (!Array.isArray(group) || group.length <= 1) {
console.debug('Skipping inclusion group check, only one entry');
continue;
}
@ -2145,6 +2201,7 @@ function convertCharacterBook(characterBook) {
order: entry.insertion_order,
position: entry.extensions?.position ?? (entry.position === 'before_char' ? world_info_position.before : world_info_position.after),
excludeRecursion: entry.extensions?.exclude_recursion ?? false,
preventRecursion: entry.extensions?.prevent_recursion ?? false,
disable: !entry.enabled,
addMemo: entry.comment ? true : false,
displayIndex: entry.extensions?.display_index ?? index,
@ -2252,24 +2309,52 @@ export async function importEmbeddedWorldInfo(skipPopup = false) {
setWorldInfoButtonClass(chid, true);
}
function onWorldInfoChange(_, text) {
if (_ !== '__notSlashCommand__') { // if it's a slash command
function onWorldInfoChange(args, text) {
if (args !== '__notSlashCommand__') { // if it's a slash command
const silent = isTrueBoolean(args.silent);
if (text.trim() !== '') { // and args are provided
const slashInputSplitText = text.trim().toLowerCase().split(',');
slashInputSplitText.forEach((worldName) => {
const wiElement = getWIElement(worldName);
if (wiElement.length > 0) {
selected_world_info.push(wiElement.text());
wiElement.prop('selected', true);
toastr.success(`Activated world: ${wiElement.text()}`);
const name = wiElement.text();
switch (args.state) {
case 'off': {
if (selected_world_info.includes(name)) {
selected_world_info.splice(selected_world_info.indexOf(name), 1);
wiElement.prop('selected', false);
if (!silent) toastr.success(`Deactivated world: ${name}`);
} else {
if (!silent) toastr.error(`World was not active: ${name}`);
}
break;
}
case 'toggle': {
if (selected_world_info.includes(name)) {
selected_world_info.splice(selected_world_info.indexOf(name), 1);
wiElement.prop('selected', false);
if (!silent) toastr.success(`Deactivated world: ${name}`);
} else {
selected_world_info.push(name);
wiElement.prop('selected', true);
if (!silent) toastr.success(`Activated world: ${name}`);
}
break;
}
default: {
selected_world_info.push(name);
wiElement.prop('selected', true);
if (!silent) toastr.success(`Activated world: ${name}`);
}
}
} else {
toastr.error(`No world found named: ${worldName}`);
if (!silent) toastr.error(`No world found named: ${worldName}`);
}
});
$('#world_info').trigger('change');
} else { // if no args, unset all worlds
toastr.success('Deactivated all worlds');
if (!silent) toastr.success('Deactivated all worlds');
selected_world_info = [];
$('#world_info').val(null).trigger('change');
}
@ -2401,7 +2486,7 @@ function assignLorebookToChat() {
jQuery(() => {
$(document).ready(function () {
registerSlashCommand('world', onWorldInfoChange, [], '<span class="monospace">(optional name)</span> sets active World, or unsets if no args provided', true, true);
registerSlashCommand('world', onWorldInfoChange, [], '<span class="monospace">[optional state=off|toggle] [optional silent=true] (optional name)</span> sets active World, or unsets if no args provided, use <code>state=off</code> and <code>state=toggle</code> to deactivate or toggle a World, use <code>silent=true</code> to suppress toast messages', true, true);
});

View File

@ -294,6 +294,19 @@ app.post('/savequickreply', jsonParser, (request, response) => {
return response.sendStatus(200);
});
app.post('/deletequickreply', jsonParser, (request, response) => {
if (!request.body || !request.body.name) {
return response.sendStatus(400);
}
const filename = path.join(DIRECTORIES.quickreplies, sanitize(request.body.name) + '.json');
if (fs.existsSync(filename)) {
fs.unlinkSync(filename);
}
return response.sendStatus(200);
});
app.post('/uploaduseravatar', urlencodedParser, async (request, response) => {
if (!request.file) return response.sendStatus(400);

View File

@ -36,9 +36,10 @@ async function sendClaudeRequest(request, response) {
});
const isSysPromptSupported = request.body.model === 'claude-2' || request.body.model === 'claude-2.1';
const requestPrompt = convertClaudePrompt(request.body.messages, !request.body.exclude_assistant, request.body.assistant_prefill, isSysPromptSupported, request.body.claude_use_sysprompt, request.body.human_sysprompt_message);
const requestPrompt = convertClaudePrompt(request.body.messages, !request.body.exclude_assistant, request.body.assistant_prefill, isSysPromptSupported, request.body.claude_use_sysprompt, request.body.human_sysprompt_message, request.body.claude_exclude_prefixes);
// Check Claude messages sequence and prefixes presence.
let sequenceError = [];
const sequence = requestPrompt.split('\n').filter(x => x.startsWith('Human:') || x.startsWith('Assistant:'));
const humanFound = sequence.some(line => line.startsWith('Human:'));
const assistantFound = sequence.some(line => line.startsWith('Assistant:'));
@ -56,20 +57,20 @@ async function sendClaudeRequest(request, response) {
}
if (!humanFound) {
console.log(color.red(`${divider}\nWarning: No 'Human:' prefix found in the prompt.\n${divider}`));
sequenceError.push(`${divider}\nWarning: No 'Human:' prefix found in the prompt.\n${divider}`);
}
if (!assistantFound) {
console.log(color.red(`${divider}\nWarning: No 'Assistant: ' prefix found in the prompt.\n${divider}`));
sequenceError.push(`${divider}\nWarning: No 'Assistant: ' prefix found in the prompt.\n${divider}`);
}
if (!sequence[0].startsWith('Human:')) {
console.log(color.red(`${divider}\nWarning: The messages sequence should start with 'Human:' prefix.\nMake sure you have 'Human:' prefix at the very beggining of the prompt, or after the system prompt.\n${divider}`));
if (sequence[0] && !sequence[0].startsWith('Human:')) {
sequenceError.push(`${divider}\nWarning: The messages sequence should start with 'Human:' prefix.\nMake sure you have '\\n\\nHuman:' prefix at the very beggining of the prompt, or after the system prompt.\n${divider}`);
}
if (humanErrorCount > 0 || assistantErrorCount > 0) {
console.log(color.red(`${divider}\nWarning: Detected incorrect Prefix sequence(s).`));
console.log(color.red(`Incorrect "Human:" prefix(es): ${humanErrorCount}.\nIncorrect "Assistant: " prefix(es): ${assistantErrorCount}.`));
console.log(color.red('Check the prompt above and fix it in the SillyTavern.'));
console.log(color.red('\nThe correct sequence should look like this:\nSystem prompt <-(for the sysprompt format only, else have 2 empty lines above the first human\'s message.)'));
console.log(color.red(` <-----(Each message beginning with the "Assistant:/Human:" prefix must have one empty line above.)\nHuman:\n\nAssistant:\n...\n\nHuman:\n\nAssistant:\n${divider}`));
sequenceError.push(`${divider}\nWarning: Detected incorrect Prefix sequence(s).`);
sequenceError.push(`Incorrect "Human:" prefix(es): ${humanErrorCount}.\nIncorrect "Assistant: " prefix(es): ${assistantErrorCount}.`);
sequenceError.push('Check the prompt above and fix it in the SillyTavern.');
sequenceError.push('\nThe correct sequence in the console should look like this:\n(System prompt msg) <-(for the sysprompt format only, else have \\n\\n above the first human\'s message.)');
sequenceError.push(`\\n + <-----(Each message beginning with the "Assistant:/Human:" prefix must have \\n\\n before it.)\n\\n +\nHuman: \\n +\n\\n +\nAssistant: \\n +\n...\n\\n +\nHuman: \\n +\n\\n +\nAssistant: \n${divider}`);
}
// Add custom stop sequences
@ -91,6 +92,10 @@ async function sendClaudeRequest(request, response) {
console.log('Claude request:', requestBody);
sequenceError.forEach(sequenceError => {
console.log(color.red(sequenceError));
});
const generateResponse = await fetch(apiUrl + '/complete', {
method: 'POST',
signal: controller.signal,
@ -329,7 +334,7 @@ async function sendMakerSuiteRequest(request, response) {
}
const responseContent = candidates[0].content ?? candidates[0].output;
const responseText = typeof responseContent === 'string' ? responseContent : responseContent.parts?.[0]?.text;
const responseText = typeof responseContent === 'string' ? responseContent : responseContent?.parts?.[0]?.text;
if (!responseText) {
let message = 'MakerSuite Candidate text empty';
console.log(message, generateResponseJson);
@ -443,7 +448,7 @@ async function sendMistralAIRequest(request, response) {
const messages = Array.isArray(request.body.messages) ? request.body.messages : [];
const lastMsg = messages[messages.length - 1];
if (messages.length > 0 && lastMsg && (lastMsg.role === 'system' || lastMsg.role === 'assistant')) {
if (lastMsg.role === 'assistant') {
if (lastMsg.role === 'assistant' && lastMsg.name) {
lastMsg.content = lastMsg.name + ': ' + lastMsg.content;
} else if (lastMsg.role === 'system') {
lastMsg.content = '[INST] ' + lastMsg.content + ' [/INST]';
@ -478,7 +483,7 @@ async function sendMistralAIRequest(request, response) {
'top_p': request.body.top_p,
'max_tokens': request.body.max_tokens,
'stream': request.body.stream,
//'safe_mode': request.body.safe_mode,
'safe_prompt': request.body.safe_prompt,
'random_seed': request.body.seed === -1 ? undefined : request.body.seed,
};
@ -540,7 +545,7 @@ router.post('/status', jsonParser, async function (request, response_getstatus_o
} else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.MISTRALAI) {
api_url = 'https://api.mistral.ai/v1';
api_key_openai = readSecret(SECRET_KEYS.MISTRALAI);
headers = { 'Content-Length': '0' }; // WTF?
headers = {};
} else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.CUSTOM) {
api_url = request.body.custom_url;
api_key_openai = readSecret(SECRET_KEYS.CUSTOM);
@ -717,6 +722,14 @@ router.post('/generate', jsonParser, function (request, response) {
headers = { 'HTTP-Referer': request.headers.referer };
bodyParams = { 'transforms': ['middle-out'] };
if (request.body.min_p !== undefined) {
bodyParams['min_p'] = request.body.min_p;
}
if (request.body.top_a !== undefined) {
bodyParams['top_a'] = request.body.top_a;
}
if (request.body.use_fallback) {
bodyParams['route'] = 'fallback';
}

View File

@ -336,7 +336,7 @@ function charaFormatData(data) {
if (data.world) {
try {
const file = readWorldInfoFile(data.world);
const file = readWorldInfoFile(data.world, false);
// File was imported - save it to the character book
if (file && file.originalData) {
@ -387,6 +387,7 @@ function convertWorldInfoToCharacterBook(name, entries) {
depth: entry.depth ?? 4,
selectiveLogic: entry.selectiveLogic ?? 0,
group: entry.group ?? '',
prevent_recursion: entry.preventRecursion ?? false,
},
};

View File

@ -10,7 +10,7 @@ const API_NOVELAI = 'https://api.novelai.net';
// Ban bracket generation, plus defaults
const badWordsList = [
[3], [49356], [1431], [31715], [34387], [20765], [30702], [10691], [49333], [1266],
[19438], [43145], [26523], [41471], [2936], [85, 85], [49332], [7286], [1115],
[19438], [43145], [26523], [41471], [2936], [85, 85], [49332], [7286], [1115], [24],
];
const hypeBotBadWordsList = [
@ -175,6 +175,13 @@ router.post('/generate', jsonParser, async function (req, res) {
},
};
// Tells the model to stop generation at '>'
if ('theme_textadventure' === req.body.prefix &&
(true === req.body.model.includes('clio') ||
true === req.body.model.includes('kayra'))) {
data.parameters.eos_token_id = 49405;
}
console.log(util.inspect(data, { depth: 4 }));
const args = {
@ -342,7 +349,9 @@ router.post('/generate-voice', jsonParser, async (request, response) => {
});
if (!result.ok) {
return response.sendStatus(result.status);
const errorText = await result.text();
console.log('NovelAI returned an error.', result.statusText, errorText);
return response.sendStatus(500);
}
const chunks = await readAllChunks(result.body);

View File

@ -4,7 +4,7 @@ const express = require('express');
const FormData = require('form-data');
const fs = require('fs');
const { jsonParser, urlencodedParser } = require('../express-common');
const { getConfigValue, mergeObjectWithYaml, excludeKeysByYaml } = require('../util');
const { getConfigValue, mergeObjectWithYaml, excludeKeysByYaml, trimV1 } = require('../util');
const router = express.Router();
@ -32,7 +32,11 @@ router.post('/caption-image', jsonParser, async (request, response) => {
mergeObjectWithYaml(headers, request.body.custom_include_headers);
}
if (!key && !request.body.reverse_proxy && request.body.api !== 'custom') {
if (request.body.api === 'ooba') {
bodyParams.temperature = 0.1;
}
if (!key && !request.body.reverse_proxy && request.body.api !== 'custom' && request.body.api !== 'ooba') {
console.log('No key found for API', request.body.api);
return response.sendStatus(400);
}
@ -85,6 +89,20 @@ router.post('/caption-image', jsonParser, async (request, response) => {
apiUrl = `${request.body.server_url}/chat/completions`;
}
if (request.body.api === 'ooba') {
apiUrl = `${trimV1(request.body.server_url)}/v1/chat/completions`;
const imgMessage = body.messages.pop();
body.messages.push({
role: 'user',
content: imgMessage?.content?.[0]?.text,
});
body.messages.push({
role: 'user',
content: [],
image_url: imgMessage?.content?.[1]?.image_url?.url,
});
}
const result = await fetch(apiUrl, {
method: 'POST',
headers: {

View File

@ -5,15 +5,21 @@
* @param {string} addAssistantPrefill Add Assistant prefill after the assistant postfix.
* @param {boolean} withSysPromptSupport Indicates if the Claude model supports the system prompt format.
* @param {boolean} useSystemPrompt Indicates if the system prompt format should be used.
* @param {boolean} excludePrefixes Exlude Human/Assistant prefixes.
* @param {string} addSysHumanMsg Add Human message between system prompt and assistant.
* @returns {string} Prompt for Claude
* @copyright Prompt Conversion script taken from RisuAI by kwaroran (GPLv3).
*/
function convertClaudePrompt(messages, addAssistantPostfix, addAssistantPrefill, withSysPromptSupport, useSystemPrompt, addSysHumanMsg) {
function convertClaudePrompt(messages, addAssistantPostfix, addAssistantPrefill, withSysPromptSupport, useSystemPrompt, addSysHumanMsg, excludePrefixes) {
//Prepare messages for claude.
//When 'Exclude Human/Assistant prefixes' checked, setting messages role to the 'system'(last message is exception).
if (messages.length > 0) {
messages[0].role = 'system';
if (excludePrefixes) {
messages.slice(0, -1).forEach(message => message.role = 'system');
} else {
messages[0].role = 'system';
}
//Add the assistant's message to the end of messages.
if (addAssistantPostfix) {
messages.push({
@ -29,7 +35,7 @@ function convertClaudePrompt(messages, addAssistantPostfix, addAssistantPrefill,
}
return message.role === 'assistant' && i > 0;
});
// When 2.1+ and 'Use system prompt" checked, switches to the system prompt format by setting the first message's role to the 'system'.
// When 2.1+ and 'Use system prompt' checked, switches to the system prompt format by setting the first message's role to the 'system'.
// Inserts the human's message before the first the assistant one, if there are no such message or prefix found.
if (withSysPromptSupport && useSystemPrompt) {
messages[0].role = 'system';
@ -43,7 +49,7 @@ function convertClaudePrompt(messages, addAssistantPostfix, addAssistantPrefill,
// Otherwise, use the default message format by setting the first message's role to 'user'(compatible with all claude models including 2.1.)
messages[0].role = 'user';
// Fix messages order for default message format when(messages > Context Size) by merging two messages with "\n\nHuman: " prefixes into one, before the first Assistant's message.
if (firstAssistantIndex > 0) {
if (firstAssistantIndex > 0 && !excludePrefixes) {
messages[firstAssistantIndex - 1].role = firstAssistantIndex - 1 !== 0 && messages[firstAssistantIndex - 1].role === 'user' ? 'FixHumMsg' : messages[firstAssistantIndex - 1].role;
}
}
@ -51,11 +57,11 @@ function convertClaudePrompt(messages, addAssistantPostfix, addAssistantPrefill,
// Convert messages to the prompt.
let requestPrompt = messages.map((v, i) => {
// Set prefix according to the role.
// Set prefix according to the role. Also, when "Exclude Human/Assistant prefixes" is checked, names are added via the system prefix.
let prefix = {
'assistant': '\n\nAssistant: ',
'user': '\n\nHuman: ',
'system': i === 0 ? '' : v.name === 'example_assistant' ? '\n\nA: ' : v.name === 'example_user' ? '\n\nH: ' : '\n\n',
'system': i === 0 ? '' : v.name === 'example_assistant' ? '\n\nA: ' : v.name === 'example_user' ? '\n\nH: ' : excludePrefixes && v.name ? `\n\n${v.name}: ` : '\n\n',
'FixHumMsg': '\n\nFirst message: ',
}[v.role] ?? '';
// Claude doesn't support message names, so we'll just add them to the message content.

View File

@ -7,8 +7,14 @@ const writeFileAtomicSync = require('write-file-atomic').sync;
const { jsonParser, urlencodedParser } = require('../express-common');
const { DIRECTORIES, UPLOADS_PATH } = require('../constants');
function readWorldInfoFile(worldInfoName) {
const dummyObject = { entries: {} };
/**
* Reads a World Info file and returns its contents
* @param {string} worldInfoName Name of the World Info file
* @param {boolean} allowDummy If true, returns an empty object if the file doesn't exist
* @returns {object} World Info file contents
*/
function readWorldInfoFile(worldInfoName, allowDummy) {
const dummyObject = allowDummy ? { entries: {} } : null;
if (!worldInfoName) {
return dummyObject;
@ -34,7 +40,7 @@ router.post('/get', jsonParser, (request, response) => {
return response.sendStatus(400);
}
const file = readWorldInfoFile(request.body.name);
const file = readWorldInfoFile(request.body.name, true);
return response.send(file);
});

View File

@ -5,6 +5,12 @@ const express = require('express');
const { getConfigValue } = require('./util');
const enableServerPlugins = getConfigValue('enableServerPlugins', false);
/**
* Map of loaded plugins.
* @type {Map<string, any>}
*/
const loadedPlugins = new Map();
/**
* Determine if a file is a CommonJS module.
* @param {string} file Path to file
@ -186,11 +192,18 @@ async function initPlugin(app, plugin, exitHooks) {
return false;
}
if (loadedPlugins.has(id)) {
console.error(`Failed to load plugin module; plugin ID '${id}' is already in use`);
return false;
}
// Allow the plugin to register API routes under /api/plugins/[plugin ID] via a router
const router = express.Router();
await plugin.init(router);
loadedPlugins.set(id, plugin);
// Add API routes to the app if the plugin registered any
if (router.stack.length > 0) {
app.use(`/api/plugins/${id}`, router);

View File

@ -31,4 +31,4 @@ echo "Installing Node Modules..."
npm i --no-audit
echo "Entering SillyTavern..."
node "$(dirname "$0")/server.js"
node "$(dirname "$0")/server.js" "$@"